博客全站升级为https。如果遇到无法访问,请手动添加https://前缀。先来看“疗效”。大家可以打开我的博客u3xyz.com查看源码,看看SSR的效果。我的博客已经上线快一年了,但不想炸了,流量很小,一直在努力增加流量(包括在sf写文章,哈哈)。当然,在PC端,搜索引擎一直是重要的流量来源。这里不得不提到SEO。下图是我之前在百度的博客截图:细心的朋友会发现这张截图非常简单,简单到几乎什么都没有。这也是不可能的。博客基于VueSPA页面,整个项目就是一个“空架子”。这个快照从2月份博客上线一直到现在SSR上线都是一样的。搜索引擎蜘蛛每次来爬你的网站,看起来都一样,慢慢的,就不会再来了。相应的,网站的权重和排名肯定不会好。至此,不搜索网址是找不到我的博客的。推出SSR后,再加上一些SEO优化,终于更新了百度快照:为什么要做SSR?文章基本回答了为什么要做SSR的问题。当然还有一个原因就是SSR的概念现在在前端霍很重要,但是我在实际项目中没有机会,只能借助博客来实践。下面将详细介绍本博客项目SSR的全过程。SSR变身实战一般来说,SSR变身还是比较容易的。建议在动手之前先看懂官方文档和官方的VueSSRDemo,会让我们事半功倍。一、构建与改造上图是Vue的SSR原理官方介绍图。从这张图我们可以知道,我们需要通过Webpack打包生成两个bundle文件:ClientBundleforbrowsers。类似于纯Vue前端项目Bundle,ServerBundle用于服务端SSR。一个json文件不管你的项目以前长什么样,是不是用vue-cli生成的。会有这个构建转换过程。构建和改造都会用到vue-server-renderer库。这里需要注意的是,vue-server-renderer版本要和vue版本一致。下图是我的构建文件目录:util.js提供了一些公共方法webpack.base.js是公共配置webpack.client.js是生成ClientBundle的配置。内核配置如下:constVueSSRClientPlugin=require('vue-server-renderer/client-plugin')//...constconfig=merge(baseConfig,{target:'web',entry:'./src/entry.client.js',plugins:[newwebpack.DefinePlugin({'process.env.NODE_ENV':JSON.stringify(process.env.NODE_ENV||'development'),'process.env.VUE_ENV':'"client"'}),newwebpack.optimize.CommonsChunkPlugin({name:'vender',minChunks:2}),//提取webpackruntime&manifest以避免vendorchunkhash在每次构建时改变//newwebpack.optimize.CommonsChunkPlugin({name:'manifest'}),newVueSSRClientPlugin()]})webpack.server.js是生成ServerBundle的配置,核心配置如下:constVueSSRServerPlugin=require('vue-server-renderer/server-plugin')//...constconfig=merge(baseConfig,{target:'node',devtool:'#source-map',entry:'./src/entry.server.js',output:{libraryTarget:'commonjs2',文件名:'server-bundle.js'},外部:nodeExternals({//不要外部化CSS文件,以防我们需要从dep白名单中导入它:/\.css$/}),plugins:[newwebpack.DefinePlugin({'process.env.NODE_ENV':JSON.stringify(process.env.NODE_ENV||'development'),'process.env.VUE_ENV':'"server"'}),newVueSSRServerPlugin()]})2、代码改造2.1必须使用VueRouter、Vuexajax库建议使用axios可能你的项目没有使用VueRouter或Vuex。但遗憾的是,Vue-SSR必须基于Vue+VueRouter+Vuex。官方没有提到Vuex,但实际上文档和demo都是基于Vuex的。之前我的博客没有用到Vuex,折腾了好久,还是乖乖加了Vuex。另外,由于代码必须能够在浏览器和Node.js环境下同时运行,所以ajax库推荐使用axios等跨平台的库。2.2两个包装入口(entry),重构app、store、router,为每个对象添加工厂方法createXXX。每个用户通过浏览器访问Vue页面的时候,都是一个全新的上下文,但是在服务端,应用启动之后,一直在运行,每个用户的请求都是在同一个应用上下文中处理的。为了不串数据,需要为每个SSR请求创建一个新的app、store、router。上图是我的工程文件目录。app.js,通用Vue应用代码App.vue,Vue应用根组件entry.client.js,浏览器环境入口entry.server.js,服务端环境入口index.html,html模板下面我们来看看具体的内核实现代码://app.jsimportVuefrom'vue'importAppfrom'./App.vue'//根组件import{createRouter}from'./routers/index'import{createStore}from'./vuex/store'import{sync}from'vuex-router-sync'//将VueRouter的状态同步到Vuex//createApp工厂方法exportfunctioncreateApp(ssrContext){letrouter=createRouter()//创建一个新的路由器实例letstore=createStore()//创建一个新的store实例//将路由状态同步到storesync(store,router)//创建一个Vue应用constapp=newVue({router,store,ssrContext,render:h=>h(App)})return{app,router,store}}//entry.client.jsimportVuefrom'vue'import{createApp}from'./app'const{app,router,store}=createApp()/复制代码/如果有__INITIAL_STATE__变量,则用它替换store的状态if(window.__INITIAL_STATE__){store.replaceState(window.__INITIAL_STATE__)}router.onReady(()=>{//通过路由钩子,执行拉取数据逻辑路由器.beforeResolve((to,from,next)=>{//查找增量组件并拉取数据constmatched=router.getMatchedComponents(to)constprevMatched=router.getMatchedComponents(from)letdiffed=falseconstactivated=matched.filter((c,i)=>{returndiffed||(diffed=(prevMatched[i]!==c))})//通过执行asyncData方法获取组件数据constasyncDataHooks=activated.map(c=>c.asyncData).filter(_=>_)if(!asyncDataHooks.length){returnnext()}//注意asyncData方法必须返回promise,asyncData调用的vuexaction也必须返回promisePromise.all(asyncDataHooks.map(hook=>hook({store,route:to}))).then(()=>{next()}).catch(next)})//将Vue实例挂载到dom中完成浏览器端应用启动app.$mount('#app')})//entry.server.jsimport{createApp}from'./app'exportdefaultcontext=>{returnnewPromise((resolve,reject)=>{const{app,router,store}=createApp(context)//设置路由router.push(context.url)router.onReady(()=>{const匹配dComponents=router.getMatchedComponents()if(!matchedComponents.length){returnreject({code:404})}//执行asyncData方法预取数据Promise.all(matchedComponents.map(Component=>{if(Component.asyncData){returnComponent.asyncData({store:store,route:router.currentRoute})}})).then(()=>{//将store快照挂在ssr上下文context.state=store.stateresolve(app)}).catch(reject)},reject)})}//createStoreimportVuefrom'vue'importVuexfrom'vuex'//...Vue.use(Vuex)//createStore工厂方法导出functioncreateStore(){returnnewVuex.Store({//rootstatestate:{appName:'appName',title:'home'},modules:{//...},strict:process.env.NODE_ENV!=='production'//线上环境关闭storecheck})}//createRouterimportVuefrom'vue'importRouterfrom'vue-router'Vue.use(Router)//createRouter工厂方法exportfunctioncreateRouter(){returnnewRouter({mode:'history',//注意这里使用的是history模式,因为hash不会发送到服务器fallback:false,routes:[{path:'/index',name:'index',component:()=>System.import('./index/index.vue')//代码碎片},{path:'/detail/:aid',name:'detail',component:()=>System.import('./detail/detail.vue')},//...{path:'/',redirect:'/index'}]})}3.重构组件获取数据的方法关于状态管理,需要严格遵守Redux的思想。建议将应用的所有状态都存储在store中,然后组件使用时再mapState,状态变化严格使用action方法。另一件要提到的事情是,该行动应该返回一个承诺。这样,我们就可以使用asyncData方法来获取组件数据了size:state.pagi.itemsPerPage,page:curPageNum}}).then((res)=>{//...})}}//组件asyncData实现exportdefault{asyncData({store}){returnstore.dispatch('getArticleList',1)}}3.SSR服务端实现完成并改造代码后,如果一切顺利。我们可以得到如下包文件:至此,我们可以开始实现SSR服务端代码了。下面是我的博客SSR实现(基于Koa)//server.jsconstKoa=require('koa')constpath=require('path')constlogger=require('./logger')constserver=newKoa()const{createBundleRenderer}=require('vue-server-renderer')consttemplateHtml=require('fs').readFileSync(path.resolve(__dirname,'./index.template.html'),'utf-8')letdistPath='./dist'constrenderer=createBundleRenderer(require(`${distPath}/vue-ssr-server-bundle.json`),{runInNewContext:false,template:templateHtml,clientManifest:require(`${distPath}/vue-ssr-client-manifest.json`)})server.use(function*(next){letctx=thisconstcontext={url:ctx.req.url,pageTitle:'default-title'}//cgi请求,前端资源请求不能走这里。这里可以使用nginx来做if(/\.\w+$/.test(context.url)){returnyieldnext}//注意这里也必须返回promisereturnnewPromise((resolve,reject)=>{renderer.renderToString(context,function(err,html){if(err){logger.error(`[error][ssr-error]:`+err.stack)returnreject(err)}ctx.status=200ctx.type='text/html;charset=utf-8'ctx.body=htmlresolve(html)})})})//错误处理server.on('error',function(err){logger.error('[error][server-error]:'+err.stack)})letport=80server.listen(port,()=>{logger.info(`[info]:服务器部署在端口上:${端口}`)})4.服务器部署服务器部署与您的项目架构有关。比如我的博客项目在服务器端有两个后端服务,一个数据库服务,使用nginx做请求转发:五、遇到的问题及解决方案无法加载组件的JS文件[vue-router]未能解决async组件默认:Error:Cannotfindmodule'js\main1.js'[vue-router]路由导航时未捕获的错误:解决方法:去掉webpack配置中的output.chunkFilename:getFileName('js/main[name]-$hash.js')如果你正在使用CommonsChunkPlugin,请确保只在客户端配置中使用它,因为服务器包需要一个入口块。所以不要为webpack.server.js配置CommonsChunkPlugin,也不要设置output.chunkFilename代码高亮codeMirror使用navigator对象,只能运行在浏览器环境,执行逻辑放在挂载的回调中。如果实现失败,封装一个异步组件,将组件的初始化放到mounted:mounted(){letparagraph=require('./paragraph.vue')Vue.component('paragraph',paragraph)newVue().$mount('#paragraph')},字符串数据派发的动作不返回promise,保证返回路由跳转的promise。router方法或
