vue脚手架多页自动化生成实践

简介: 本文旨在针对门户类应用的场景下的Vue脚手架构建方案的一些总结和分析,通过自动化的配置脚本来生成模板化的多页应用实践,以期能够给读者提供一个基于Vue全家桶的门户类工程构建方案。

前端 | Vue脚手架多页自动化生成实践.png

前言

在前端开发过程中,常常面对多种业务场景。到目前为止,前端对于不同场景的处理通常会采用不同的渲染方案来组合处理,常见的渲染方案包括:CSR(Client Side Rendering)、SSR(Server Side Rendering)、SSG(Static Site Generation)、ISR(Incremental Site Rendering)、DPR(Distributed Persistent Rendering)、NSR(Native Side Rendering)以及ESR(Edge Side Rendering)等。在目前项目开发过程中,遇到了需要构建门户类应用的需求,而团队主要技术栈以Vue为主,整个技术方案以Vue全家桶进行构建。因此,本文旨在针对门户类应用的场景下的Vue脚手架构建方案的一些总结和分析,通过自动化的配置脚本来生成模板化的多页应用实践,以期能够给读者提供一个基于Vue全家桶的门户类工程构建方案。

架构

对于门户类型的应用,由于其大部分内容变动内容较少,而对于部分关键页面却会有动态更新的要求,因而在通常会采用多页形式的处理配合部分单页应用中的优势进行处理。因而,在技术选型方面,团队采用了预渲染配合多页的方式实现门户类SEO及首屏加载快的需求。同时,结合单页应用的优势,在多页中的部分关键页面中采用单页中的优点,如:路由切换快、用户体验好等。综上,架构风格采用ISR的增量渲染方案,由于项目背景的特殊性,无法配合常规CDN等部署方案特点,但可以使用云原生相关的中间件实现类似效果,整体部署仍以“云+端”的形式为主。

目录

selfService├─portal
├─ build                                // vue cli打包所需的options中内容一些抽离,对其中做了环境区分
|   ├─ demo
|   |    ├─config.json
|   |    ├─configureWebpack.js
|   ├─ dev
|   |    ├─ config.json
|   |    ├─ configureWebpack.js
|   ├─ production
|   |    ├─ config.json
|   |    ├─ configureWebpack.js
|   ├─ chainWebpack.js
|   ├─ configureWebpack.js
|   ├─ devServer.js
|   ├─ pages.js
|   ├─ routes.js
|   ├─ index.js
|   ├─ utils.js
├─ deploy                                 // 不同环境的部署
|   ├─ demo
|   |    ├─ default.conf
|   |    ├─ Dockerfile
|   |    ├─ env.sh
|   ├─ dev
|   |    ├─ default.conf
|   |    ├─ Dockerfile
|   |    ├─ env.sh
|   ├─ production
|   |    ├─ default.conf
|   |    ├─ Dockerfile
|   |    ├─ env.sh
|   ├─ build.sh
├─ public
|   ├─ pageA                              // pageA的html,这里可以存放一些静态资源,非构建状态下的js、css等
|   |    ├─ index.html
|   ├─ pageB                              // pageB的html,这里可以存放一些静态资源,非构建状态下的js、css等
|   |    ├─ index.html
|   ├─ favicon.ico
├─ src
|   ├─ assets                             // 存放小资源,通常为必须,如:logo等,其他静态资源请放入cdn或者public下
|   |    ├─ logo.png
|   ├─ components                         // 公共组件,可抽离多个静态页面的公共组件
|   |    ├─ Header.vue
|   ├─ router
|   |    ├─ pageA                         // pageA的router,使用了history模式
|   |        ├─ index.js
|   |    ├─ pageB                         // pageB的router,使用了history模式
|   |        ├─ index.js
|   ├─ store
|   |    ├─ pageA                         // pageA的Vuex
|   |        ├─ index.js
|   |    ├─ pageB                         // pageB的Vuex
|   |        ├─ index.js
|   ├─ views
|   |    ├─ pageA                         // pageA的页面,写法和之前一个的单页应用一致
|   |        ├─ main.js                   // 注入了mode,挂载到了vue的原型上,使用this可以获取环境变量
|   |        ├─ pageA.vue
|   |    ├─ pageB                         // pageB的页面,写法和之前一个的单页应用一致
|   |        ├─ main.js                   // 注入了mode,挂载到了vue的原型上,使用this可以获取环境变量
|   |        ├─ pageB.vue
├─ scripts                 
├─ babel.config.js                        // 配置es转化语法
├─ vue.config.js                          // vue cli打包相关配置
├─ app.json                               // 存放各个多页应用的public、router、vuex、views入口地址

实践

配置

Vue脚手架中配置多页主要是使用Webpack中的pages入口配置,这里主要是修改vue.config.js中的pages的设置,代码如下:

const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
module.exports = {
    // ...
    pages: {
        page3:{
            entry: "src/views/page3/main.js",
            template: "public/page3/index.html",
            filename: "page3.html",
            title: "page3"
        }
    },
    configureWebpack: config => {
         config.plugins.push(
            new PrerenderSPAPlugin({
                staticDir: path.resolve(__dirname,'../../dist'),
                routes: [
                    '/page3'
                ],
                renderer: new Renderer({
                    less: false,
                    //renderAfterDocumentEvent: 'render-event',
                    //renderAfterTime: 5000,
                    //renderAfterElementExists: 'my-app-element'
                }),
            })
        )
    } 
}

其中,如果配置了pages,@vue/cli-service会先清除原有的entry,如果没有index,则devServer默认入口的根路径'/'仍为index.html;如果有index的key值,则会进行相应的覆盖。在这里,对pages下的key值为对应多页的路径,如:上述代码下的page3,则对应的路径为'/page3.html';pages下的value可以为字符串,也可以为对象,其中:entry为多页的入口(必选项)、template为模板来源、filename为打包后的输出名称以及title会通过html-webpack-plugin的插件对template中的<title><%= htmlWebpackPlugin.options.title %></title>进行替换。

而对于预渲染的应用,这里使用了prerender-spa-pluginvue-meta-info来进行SEO及首屏加载优化,代码如下:

// ...
import MetaInfo from 'vue-meta-info'

Vue.use(MetaInfo)

new Vue({
    router,
    store,
    render: h => h(index),
    mounted () {
        document.dispatchEvent(new Event('custom-render-trigger'))
    }
}).$mount('#page3')

脚本

通过上述的配置,基本就可以实现一个 预渲染+多页 的vue脚手架搭建。但是,除了开发环境的配置,对于生产环境、部署等也需要进行一定的设置,这样频繁的操作就会带来一定的功效降低。因而,在前端工程化领域中,通常会进行一定的脚本化或者说脚手架方案的构建。这里,在目前项目中,团队对多页应用的配置进行了自动化的脚本实现。

生成多页的脚本主要通过page.js进行实现,代码如下:

const inquirer  = require('inquirer');
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const ora = require('ora');
const { transform, compose } = require('./utils');

const spinner = ora();

const PAGE_REG = /[a-zA-Z_0-9]/ig;
const rootDir = path.resolve(process.cwd(), '.');

// 判断dir目录下是否存在name的文件夹
const isExt = (dir, name)  => fs.existsSync(path.join(dir, name));

const APP_JSON_EJS = `{
    "pages": <%= page_name %> 
}`;

const INDEX_HTML_EJS = `<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="../favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <%= page_name %>
  </body>
</html>
`

const INDEX_VUE_EJS = `<%= page_name %>

<script>
export default {
components: {
},
data() {
  return {};
},
};
</script>

<style lang="less">
</style>`

const MAIN_JS_EJS = `<%= page_name %>`

const INDEX_ROUTER_EJS = `import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
    <%= page_name %>
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router`

const INDEX_STORE_EJS = `import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})
`

// inquirer list
const promptList = [
    {
        type: 'input',
        name: 'page_name',
        message: '请输入你想要创建的多页应用名称',
        filter: function (v) {
            return v.match(PAGE_REG).join('')
        }
    }
];

// nginx的default.conf所需添加内容
const addDefaultConf = page_name => {
    return `location /${page_name} {
    root   /usr/share/nginx/html;
    index  ${page_name}.html;
    try_files $uri $uri/ /${page_name}.html;
    gzip_static on;
}`
};

// page_name下的index.html
const addIndexHtml = page_name => {
    return `<div id="${page_name}" data-server-rendered="true"></div>`
};

// page_name下的router
const addRouterIndex = page_name => {
    return `{
    path: '/',
    component: () => import('../../views/${page_name}/index.vue')
},`
};

// page_name下的views index.vue
const addViewsIndex = page_name => {
    return `<template>
    <div>
        ${page_name}
    </div>
</template>`
};

// page_name下的views main.js
const addViewsMain = page_name => {
    return `import Vue from 'vue'
import index from './index.vue'
import router from '../../router/${page_name}/index.js'
import store from '../../store/${page_name}/index.js'
import MetaInfo from 'vue-meta-info'

Vue.use(MetaInfo)

import axios from 'axios'

Vue.prototype.$mode = process.env.VUE_APP_MODE;

Vue.prototype.axios = axios;

Vue.config.productionTip = false

new Vue({
    router,
    store,
    render: h => h(index),
    mounted () {
        document.dispatchEvent(new Event('custom-render-trigger'))
    }
}).$mount('#${page_name}')`
};

// page_name下的pages.js
const addPages = page_name => {
    return JSON.stringify({
        entry: `src/views/${page_name}/main.js`,
        template: `public/${page_name}/index.html`,
        filename: `${page_name}.html`,
        title: `${page_name}`,
    })
}

const updateApp = page_name => {
    // 获取pages的数组
    const pages = require('../app.json')['pages'];
    if(pages.includes(page_name)) return true;
    pages.push(page_name);
    spinner.start()
    fs.writeFile(`${rootDir}/app.json`, transform(/<%= page_name %>/g, JSON.stringify(pages), APP_JSON_EJS), err => {
        spinner.color = 'red';
        spinner.text = 'Loading Update app.json'
        if(err) {
            spinner.fail(chalk.red(`更新app.json失败`))
            return false;
        } else {
            spinner.succeed(chalk.green(`更新app.json成功`))
            return true;
        }
    });
}

// 处理 public 文件夹下的核心逻辑
const processPublic = args => {
    const { page_name } = args;
    if(isExt(`${rootDir}/public`, page_name)) {
        return args;
    } else {
        fs.mkdirSync(`${rootDir}/public/${page_name}`)
    }
    fs.writeFileSync(
        `${rootDir}/public/${page_name}/index.html`, 
        transform(/<%= page_name %>/g, addIndexHtml(page_name), INDEX_HTML_EJS)
    );
    // 处理默认页面的跳转
    const content = require('../app.json')['pages'].map(page => {
        return `<li>
    <a href="/${page}.html">${page}</a>
</li>`
    }).join(`
`);
    const ejs_arr = fs.readFileSync(`${rootDir}/public/index.html`, 'utf-8').split(`<body>`);
    fs.writeFileSync(
        `${rootDir}/public/index.html`, 
        ejs_arr[0] + `<body>
`+`<h1>自服务门户</h1>
<ul>
    ${content}
</ul>` + `
</body>
</html>`
    );
    return args;
};

// 处理 src/views 文件夹下的核心逻辑
const processViews = args => {
    const { page_name } = args;
    if(isExt(`${rootDir}/src/views`, page_name)) {
        return args;
    } else {
        fs.mkdirSync(`${rootDir}/src/views/${page_name}`)
    }
    fs.writeFileSync(
        `${rootDir}/src/views/${page_name}/index.vue`, 
        transform(/<%= page_name %>/g, addViewsIndex(page_name), INDEX_VUE_EJS)
    );
    fs.writeFileSync(
        `${rootDir}/src/views/${page_name}/main.js`, 
        transform(/<%= page_name %>/g, addViewsMain(page_name), MAIN_JS_EJS)
    );
    return args;
};

// 处理 src/router 文件夹下的核心逻辑
const processRouter = args => {
    const { page_name } = args;
    if(isExt(`${rootDir}/src/router`, page_name)) {
        return args;
    } else {
        fs.mkdirSync(`${rootDir}/src/router/${page_name}`)
    }
    fs.writeFileSync(
        `${rootDir}/src/router/${page_name}/index.js`, 
        transform(/<%= page_name %>/g, addRouterIndex(page_name), INDEX_ROUTER_EJS)
    );
    return args;
};

// 处理 src/store 文件夹下的核心逻辑
const processStore = args => {
    const { page_name } = args;
    if(isExt(`${rootDir}/src/store`, page_name)) {
        return args;
    } else {
        fs.mkdirSync(`${rootDir}/src/store/${page_name}`)
    }
    fs.writeFileSync(
        `${rootDir}/src/store/${page_name}/index.js`, 
        INDEX_STORE_EJS
    );
    return args;
};

// 处理 build 文件夹下的核心逻辑
const processBuild = args => {
    const { page_name } = args;
    // 处理 build/page.js
    const pages  = require('../build/pages.js');
    if(Object.keys(pages).includes(page_name)) return args;
    pages[`${page_name}`] = JSON.parse(addPages(page_name));
    const PAGES_JS_EJS =`const pages = ${JSON.stringify(pages)}
module.exports = pages;
    `;
    fs.writeFileSync(
        `${rootDir}/build/pages.js`, 
        PAGES_JS_EJS
    );

    // 处理 build/routes.js
    const routes = require('../build/routes.js');
    if(routes.includes(`/${page_name}`)) return args;
    routes.push(`/${page_name}`);
    const ROUTES_JS_EJS =`const pages = ${JSON.stringify(routes)}
module.exports = pages;
    `;
    fs.writeFileSync(
        `${rootDir}/build/routes.js`, 
        ROUTES_JS_EJS
    );
    return args;
}

// 处理 deploy 文件夹下的核心逻辑
const processDeploy = args => {
    const { page_name } = args;
    const reg = new RegExp(`location /${page_name}`);
     ['demo', 'dev', 'production'].forEach(item => {
        const content = fs.readFileSync(`${rootDir}/deploy/${item}/default.conf`, 'utf-8');
        if(reg.test(content)) return args;
        const ejs_arr = content.split(`location  /api/`)
        fs.writeFileSync(
            `${rootDir}/deploy/${item}/default.conf`, 
            transform(/<%= page_name %>/g, addDefaultConf(page_name), ejs_arr[0] + `<%= page_name %>
location  /api/`+ ejs_arr[1])
        );
    });
    return args;
};

inquirer
    .prompt(promptList)
    .then(answers => {
        const page_name = answers.page_name;
        return updateApp(page_name)
    })
    .then(() => {
        const pages = require('../app.json')['pages'];
        pages.forEach(page => {
            console.log('page', page)
            compose(
                processDeploy,
                processBuild, 
                processStore, 
                processRouter, 
                processViews, 
                processPublic
            )({
                page_name: page
            });
        })
    })
    .catch(err => {
        if(err) {
            console.log(chalk.red(err))
        }
    })

为了更好的实现代码的优雅性,对代码工具进行了抽离,放入到utils.js中,代码如下:

// 将内容替换进ejs占位符
const transform = ($, content, ejs) => ejs.replace($,content);

// 将流程串联
const compose = (...args) => args.reduce((prev,current) => (...values) => prev(current(...values)));

module.exports = {
    transform,
    compose
}

总结

仅管到目前为止,单页应用仍是前端开发中的主流方案。但是,随着各大应用的复杂度提升,多种方案的建设也都有了来自业界不同的声音,诸如:多种渲染方案、Island架构等都是为了能更好的提升Web领域的体验与开发建设。技术方案的选择不只局限于生态的整合,更重要的是对合适场景的合理应用。

“形而上者谓之道,形而下者谓之器”,各位前端开发者不仅应该只着眼于眼前的业务实现,同时也需要展望未来,站在更高的视野上来俯视技术的走向与演进,共勉!!!

参考

相关文章
|
8天前
|
敏捷开发 人工智能 Devops
探索自动化测试的高效策略与实践###
当今软件开发生命周期中,自动化测试已成为提升效率、保障质量的关键工具。本文深入剖析了自动化测试的核心价值,探讨了一系列高效策略,包括选择合适的自动化框架、设计可维护的测试脚本、集成持续集成/持续部署(CI/CD)流程,以及有效管理和维护测试用例库。通过具体案例分析,揭示了这些策略在实际应用中的成效,为软件测试人员提供了宝贵的经验分享和实践指导。 ###
|
7天前
|
机器学习/深度学习 人工智能 jenkins
软件测试中的自动化与持续集成实践
在快速迭代的软件开发过程中,自动化测试和持续集成(CI)是确保代码质量和加速产品上市的关键。本文探讨了自动化测试的重要性、常见的自动化测试工具以及如何将自动化测试整合到持续集成流程中,以提高软件测试的效率和可靠性。通过案例分析,展示了自动化测试和持续集成在实际项目中的应用效果,并提供了实施建议。
|
8天前
|
Java 测试技术 持续交付
探索自动化测试在软件开发中的关键作用与实践
在现代软件开发流程中,自动化测试已成为提升产品质量、加速交付速度的不可或缺的一环。本文深入探讨了自动化测试的重要性,分析了其在不同阶段的应用价值,并结合实际案例阐述了如何有效实施自动化测试策略,以期为读者提供一套可操作的实践指南。
|
22天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
6天前
|
Devops jenkins 测试技术
DevOps实践:自动化部署与持续集成的融合之旅
【10月更文挑战第41天】在软件开发的世界中,快速迭代和高效交付是企业竞争力的关键。本文将带你走进DevOps的核心实践——自动化部署与持续集成,揭示如何通过它们提升开发流程的效率与质量。我们将从DevOps的基本理念出发,逐步深入到具体的技术实现,最终展示一个实际的代码示例,让理论与实践相结合,为你的开发旅程提供清晰的指引。
16 4
|
8天前
|
Web App开发 敏捷开发 测试技术
探索自动化测试的奥秘:从理论到实践
【10月更文挑战第39天】在软件质量保障的战场上,自动化测试是提升效率和准确性的利器。本文将深入浅出地介绍自动化测试的基本概念、必要性以及如何实施自动化测试。我们将通过一个实际案例,展示如何利用流行的自动化测试工具Selenium进行网页测试,并分享一些实用的技巧和最佳实践。无论你是新手还是有经验的测试工程师,这篇文章都将为你提供宝贵的知识,帮助你在自动化测试的道路上更进一步。
|
8天前
|
敏捷开发 Java 测试技术
探索自动化测试:从理论到实践
【10月更文挑战第39天】在软件开发的海洋中,自动化测试是一艘能够带领团队高效航行的船只。本文将作为你的航海图,指引你理解自动化测试的核心概念,并分享一段实际的代码旅程,让你领略自动化测试的魅力和力量。准备好了吗?让我们启航!
|
9天前
|
机器学习/深度学习 数据采集 人工智能
智能运维:从自动化到AIOps的演进与实践####
本文探讨了智能运维(AIOps)的兴起背景、核心组件及其在现代IT运维中的应用。通过对比传统运维模式,阐述了AIOps如何利用机器学习、大数据分析等技术,实现故障预测、根因分析、自动化修复等功能,从而提升系统稳定性和运维效率。文章还深入分析了实施AIOps面临的挑战与解决方案,并展望了其未来发展趋势。 ####
|
10天前
|
数据采集 IDE 测试技术
Python实现自动化办公:从基础到实践###
【10月更文挑战第21天】 本文将探讨如何利用Python编程语言实现自动化办公,从基础概念到实际操作,涵盖常用库、脚本编写技巧及实战案例。通过本文,读者将掌握使用Python提升工作效率的方法,减少重复性劳动,提高工作质量。 ###
24 1
|
13天前
|
测试技术 API Android开发
探索软件测试中的自动化框架选择与实践####
本文深入探讨了软件测试领域内,面对众多自动化测试框架时,如何依据项目特性和团队需求做出明智选择,并分享了实践中的有效策略与技巧。不同于传统摘要的概述方式,本文将直接以一段实践指南的形式,简述在选择自动化测试框架时应考虑的核心要素及推荐路径,旨在为读者提供即时可用的参考。 ####
下一篇
无影云桌面