当前位置: 首页 > 后端技术 > Node.js

AloftVue-SSR

时间:2023-04-03 23:41:33 Node.js

在服务器端渲染与HTML字符串相同的组件,将它们直接发送到浏览器,最后在客户端将静态标记“混合”成一个完全交互的应用程序。SSR的目的解决首屏渲染问题SEO问题项目结构vue-ssr├──build(webapck编译配置)├──components(vue页面)├──dist(编译静态资源目录)├──api.js(请求接口,模拟异步请求)├──app.js(创建Vue实例入口)├──App.vue(Vue页面入口)├──entry-client.js(前端执行入口)├──entry-server.js(后端执行入口)├──index.template.html(前端渲染模板)├──router.js(Vue路由配置)├──server.js(Koa服务)├──store.js(Vuex数据状态中心配置)原理概述这张图相信很多大佬都看过N遍了。每个人的理解都不一样。我说说我个人的理解。如有误会请见谅。首先看Source部分。Source部分首先由app.js引入Vue全家桶。如何配置Vue全家桶,后面会讲解。app.js其实就是创建一个注册了各种依赖的Vue对象实例。在SPA单页环境下,我们只需要获取这个Vue实例,然后指定挂载到模板特定的dom节点,然后丢给webpack处理即可。但是这里SSR分为两个部分,一个是前端单页,一个是后端直出。所以Client入口的作用就是挂载Vue对象实例,通过webpack编译打包,最终渲染到浏览器中。Server入口的作用是获取Vue对象实例,对集合页面中的asynData进行处理,获取对应的数据上下文,然后由webpack解析处理。最后在NodeServer端使用两个weback编译好的bundle文件(服务端需要“serverbundle”然后用于服务端渲染(SSR),而“clientbundle”会发送给浏览器混合静态tags.),当用户请求一个页面时,服务端会先使用SSR生成对应的页面文档结构,用户切换路由时使用SPA模式。搭建环境项目依赖说明Koa2+Vue2+Vue-router+Vuex一切从路由开始。首先配置vue-router,生成router.jsimportVuefrom'vue'importRouterfrom'vue-router'importBarfrom'./components/Bar.vue'importBazfrom'./components/Baz.vue'importFoofrom'./components/Foo.vue'从'./components/Item.vue'Vue.use(Router)exportconstcreateRouter=()=>{returnnewRouter({mode:'history',routes:[{path:'/item/:id',component:Item},{path:'/bar',component:Bar},{path:'/baz',component:Baz},{path:'/foo',component:Foo}]})}为每个请求创建一个新的Vue实例,路由也是如此,通过一个工厂函数来保证每次都新创建一个新的Vue路由实例。Vuex配置配置Vuex,生成store.jsimportVuefrom'vue'importVuexfrom'vuex'import{fetchItem}from'./api'Vue.use(Vuex)exportconstcreateStore=()=>{returnnewVuex.Store({state:{items:{}},actions:{fetchItem({commit},id){returnfetchItem(id).then(item=>{commit('setItem',{id,item})})}},mutations:{setItem(state,{id,item}){Vue.set(state.items,id,item)}}})}也通过一个工厂函数创建一个新的Vuex实例,并暴露该方法生成一个Vue的根实例创建Vue实例并生成app.jsimportVuefrom'vue'importAppfrom'./App.vue'import{createRouter}from'./router'import{createStore}from'./store'import{sync}from'vuex-router-sync'exportconstcreateApp=ssrContext=>{constrouter=createRouter()conststore=createStore()sync(store,router)constapp=newVue({router,store,ssrContext,render:h=>h(App)})通过使用return{app,store,router}}使用我们写的createRouter和createStore每次都创建新的Vue-router和Vuex实例,保证像Vue实例一样重新创建,然后挂载注册router和store到Vue实例,提供createApp通过回车对应服务端渲染的数据上下文至此我们基本完成了源码部分的工作。那么我们就要考虑如何编译打包这些文件,让浏览器和Node服务器运行解析。从前端入口文件开始。前端打包入口文件:entry-client.jsimport{createApp}from'./app'const{app,store,router}=createApp()if(window.__INITIAL_STATE__){store.replaceState(window.__INITIAL_STATE__)}router.onReady(()=>{router.beforeResolve((to,from,next)=>{constmatched=router.getMatchedComponents(to)constprevMatched=router.getMatchedComponents(from)letdiffed=falseconstactivated=matched.filter((c,i)=>{returndiffed||(diffed=(prevMatched[i]!==c))})if(!activated.length){returnnext()}Promise.all(activated.map(c=>{if(c.asyncData){returnc.asyncData({store,route:to})}})).then(()=>{next()}).catch(next)})应用程序。$mount('#app')})客户端的入口只需要创建应用程序并将其挂载到DOM中即可。需要注意的是router.onReady仍然需要在应用挂载之前调用。因为路由器必须提前解析路由配置中的异步组件(如果使用异步组件,本项目不使用异步组件,后期考虑添加),才能正确调用组件中可能存在的路由钩子。通过添加处理asyncData的路由钩子函数,在初始路由解析后执行,这样我们就不会预取(double-fetch)现有数据两次。使用router.beforeResolve()保证所有异步组件都被解析,对比之前没有渲染过的组件,找出两个匹配列表的区别。如果没有区别,说明不需要处理直接下一个输出。查看服务端渲染解析入口文件服务端渲染的执行入口文件:entry-server.jsimport{createApp}from'./app'exportdefaultcontext=>{returnnewPromise((resolve,reject)=>{const{app,store,router}=createApp(context)router.push(context.url)router.onReady(()=>{constmatchedComponents=router.getMatchedComponents()if(!matchedComponents.length){返回拒绝({code:404})}Promise.all(matchedComponents.map(Component=>{if(Component.asyncData){returnComponent.asyncData({store,route:router.currentRoute})}})).then(()=>{context.state=store.stateresolve(app)}).catch(reject)},reject)})}服务器入口使用默认导出导出函数,并在每次渲染时重复调用该函数。此时,除了应用实例的创建和返回,服务端的路由匹配和数据预取逻辑也在这里进行。在所有preFetch挂钩解析之后,我们的商店现在填充了呈现应用程序所需的状态。当我们将状态附加到上下文,并在渲染器中使用模板选项时,状态会自动序列化到window.__INITIAL_STATE__并注入到HTML中。直接用webpack4.x版本编写webpack并上手真是令人兴奋。webpack配置分为3个配置,公共配置,客户端配置,服务端配置。三个配置文件如下:baseconfig:constpath=require('path')constwebpack=require('webpack')constExtractTextPlugin=require('extract-text-webpack-plugin')module.exports={devtool:'#cheap-module-source-map',output:{path:path.resolve(__dirname,'../dist'),publicPath:'/',filename:'[name]-[chunkhash].js'},resolve:{别名:{'public':path.resolve(__dirname,'../public'),'components':path.resolve(__dirname,'../components')},extensions:['.js','.vue']},模块:{规则:[{test:/\.vue$/,使用:{loader:'vue-loader'}},{test:/\.js$/,使用:'babel-loader',exclude:/node_modules/},{test:/\.css$/,use:'css-loader'}]},performance:{maxEntrypointSize:300000,hints:'warning'},plugins:[newExtractTextPlugin({filename:'common.[chunkhash].css'})]}修改配置只是简单的配置vue,css,使用babel等loader,然后ExtractTextPlugin提取css资源文件,指定输出目录,入口文件分别在client和serverconfig中配置。客户端配置constwebpack=require('webpack')constmerge=require('webpack-merge')constpath=require('path')constbaseConfig=require('./webpack.base.config.js')constVueSSRClientPlugin=require('vue-server-renderer/client-plugin')module.exports=merge(baseConfig,{entry:path.resolve(__dirname,'../entry-client.js'),插件:[newVueSSRClientPlugin()],优化:{splitChunks:{cacheGroups:{commons:{chunks:'initial',minChunks:2,maxInitialRequests:5,minSize:0},vendor:{test:/node_modules/,chunks:'initial',name:'vendor',priority:10,enforce:true}}},runtimeChunk:true}})客户端的入口文件,使用VueSSRClientPlugin生成对应的vue-ssr-client-manifest.json映射文件,然后加入vendor的chunk分离。服务器配置constmerge=require('webpack-merge')constpath=require('path')constnodeExternals=require('webpack-node-externals')constbaseConfig=require('./webpack.base.config.js')constVueSSRServerPlugin=require('vue-server-renderer/server-plugin')module.exports=merge(baseConfig,{//将入口指向应用程序的服务器入口文件入口:path.resolve(__dirname,'../entry-server.js'),//允许webpackNode处理动态导入(适合Node的方式),target:'node',//提供sourcemap支持devtool:'source-map',//使用Node-styleexports(Node-styleexports)output:{filename:'server-bundle.js',libraryTarget:'commonjs2'},externals:nodeExternals({//不要外部化webpack需要处理的依赖模块。//你可以在这里添加更多的文件类型,例如未处理的*.vue原始文件,//你还应该将修改`global`的依赖模块列入白名单(例如polyfills)whitelist:/\.css$/}),//这是将服务器的整个输出构建成单个JSON文件的插件。//默认文件名为`vue-ssr-server-bundle.json`plugins:[newVueSSRServerPlugin()]})打包过程到此结束,服务端配置参考官网注释。使用Koa2const{createBundleRenderer}=require('vue-server-renderer')constserverBundle=require('./dist/vue-ssr-server-bundle.json')constclientManifest=require('./dist/vue-ssr-client-manifest.json')constfs=require('fs')constpath=require('path')constKoa=require('koa')constKoaRuoter=require('koa-router')constserve=require('koa-static')constapp=newKoa()constrouter=newKoaRuoter()consttemplate=fs.readFileSync(path.resolve('./index.template.html'),'utf-8')constrenderer=createBundleRenderer(serverBundle,{//推荐runInNewContext:false,//(可选)页面模板模板,//(可选)客户端构建清单clientManifest})app.use(serve(path.resolve(__dirname,'./dist')))router.get('*',(ctx,next)=>{ctx.set('Content-Type','text/html')returnnewPromise((resolve,reject)=>{consthandleError=err=>{if(err&&err.code===404){ctx.status=404ctx.body='404|找不到页面'}else{ctx.status=500ctx.body='500|内部服务器错误'console.error(`errorduringrender:${ctx.url}`)console.error(err.stack)}resolve()}console.log(ctx.url)constcontext={url:ctx.url,title:'VueSSR'}//这里不需要传入应用,因为在执行bundle的时候已经自动创建好了//现在我们的服务器和应用已经解耦了!renderer.renderToString(context,(err,html)=>{//处理异常...if(err){handleError(err)}ctx.body=htmlresolve()})})})app.use(router.routes()).use(router.allowedMethods())constport=3000app.listen(port,'127.0.0.1',()=>{console.log(`serverrunningatlocalhost:${port}`)})最后的效果当然是这样的:参考文档:vue-ssr官方文档代码仓库:github链接