长春网站建设吉网传媒实力牜,注册网站需要什么程序,个人网站创建平台要多少钱,广告网页前言
在前端开发过程中#xff0c;常常面对多种业务场景。到目前为止#xff0c;前端对于不同场景的处理通常会采用不同的渲染方案来组合处理#xff0c;常见的渲染方案包括#xff1a;CSR(Client Side Rendering)、SSR(Server Side Rendering)、SSG(Static Site Generati…
前言
在前端开发过程中常常面对多种业务场景。到目前为止前端对于不同场景的处理通常会采用不同的渲染方案来组合处理常见的渲染方案包括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}),}))}
}其中如果配置了pagesvue/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-infoVue.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 langheadmeta charsetutf-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width,initial-scale1.0link relicon href../favicon.icotitle% htmlWebpackPlugin.options.title %/title/headbodynoscriptstrongWere sorry but % htmlWebpackPlugin.options.title % doesnt 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 {};
},
};
/scriptstyle langless
/styleconst MAIN_JS_EJS % page_name %const INDEX_ROUTER_EJS import Vue from vue
import VueRouter from vue-routerVue.use(VueRouter)const routes [% page_name %
]const router new VueRouter({mode: history,routes
})export default routerconst INDEX_STORE_EJS import Vue from vue
import Vuex from vuexVue.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-renderedtrue/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 templatediv${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-infoVue.use(MetaInfo)import axios from axiosVue.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.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.jsonif(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 lia 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.jsconst 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.jsconst 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上托管它们卷起来前端建站SSGSSRISRHydration, Island…一网打尽你知道吗SSR、SSG、ISR、DPR 有什么区别SSR、ISR、CSR、SSG有什么区别一文看懂Next.js渲染方法CSR、SSR、SSG和ISR