前言
在前端开发过程中,经常面对多种业务场景。到目前为止,前端对于不同场景的解决通常会采纳不同的渲染计划来组合解决,常见的渲染计划包含: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-plugin和vue-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 listconst 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.htmlconst addIndexHtml = page_name => { return `<div id="${page_name}" data-server-rendered="true"></div>`};// page_name下的routerconst addRouterIndex = page_name => { return `{ path: '/', component: () => import('../../views/${page_name}/index.vue')},`};// page_name下的views index.vueconst addViewsIndex = page_name => { return `<template> <div> ${page_name} </div></template>`};// page_name下的views main.jsconst 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 = falsenew Vue({ router, store, render: h => h(index), mounted () { document.dispatchEvent(new Event('custom-render-trigger')) }}).$mount('#${page_name}')`};// page_name下的pages.jsconst 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畛域的体验与开发建设。技术计划的抉择不只局限于生态的整合,更重要的是对适合场景的正当利用。
“形而上者谓之道,形而下者谓之器”,各位前端开发者不仅应该只着眼于眼前的业务实现,同时也须要展望未来,站在更高的视线上来仰视技术的走向与演进,共勉!!!
参考
- vue-cli搭建自动化多页面我的项目(vue高阶)
- Vue-cli配置多页面
- vue预渲染之prerender-spa-plugin插件利用
- 预渲染插件prerender-spa-plugin生成多页面
- CSR、SSR、NSR、ESR傻傻分不清楚,一文帮你理清前端渲染计划!
- vue我的项目革新SSR(服务端渲染)
- 什么是SSR/SSG/ISR?如何在AWS上托管它们?
- 卷起来,前端建站SSG,SSR,ISR,Hydration, Island…一网打尽
- 你晓得吗?SSR、SSG、ISR、DPR 有什么区别?
- SSR、ISR、CSR、SSG有什么区别
- 一文看懂Next.js渲染办法:CSR、SSR、SSG和ISR