一、什么是服务端渲染
客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回Promise (官方是asyncData方法)来将需要的数据拿到。最后再通过window.__initial_state=data将其写入网页,最后将服务端渲染好的网页返回回去。接下来客户端将用新的store状态把原来的store状态替换掉,保证客户端和服务端的数据同步。遇到没被服务端渲染的组件,再去发异步请求拿数据。
服务端渲染的环境搭建
这是vue官网的服务端渲染的示意图,ssr有两个入口文件,分别是客户端的入后文件和服务端的入口文件,webpack通过两个入口文件分别打包成给服务端用的server bundle和给客户端用的client bundle.当服务器接收到了来自客户端的请求之后,会创建一个渲染器bundleRenderer,这个bundleRenderer会读取上面生成的server bundle文件,并且执行它的代码, 然后发送一个生成好的html到浏览器,等到客户端加载了client bundle之后,会和服务端生成的DOM进行Hydration(判断这个DOM和自己即将生成的DOM是否相同,如果相同就将客户端的vue实例挂载到这个DOM上)
实现步骤:
1、创建vue实例(main.js)
importVuefrom'vue' importAppfrom'./App.vue' importiViewfrom'iview'; import{createStore}from'./store' import{createRouter}from'./router' import{sync}from'vuex-router-sync' Vue.use(iView); export functioncreateApp() { conststore = createStore() constrouter = createRouter() sync(store,router) constapp =newVue({ router, store, render: h => h(App) }) return{app,router,store} }
因为要做服务端渲染,所以这里不需要再用el去挂载,现将app、router、store导出
2、服务端入口文件(entry-server.js)
import{ createApp }from'./main' constisDev = process.env.NODE_ENV !=='production' const{ app,router,store } = createApp() constgetAllAsyncData=function(component){ letstores = [] functionloopComponent(component) { if(typeofcomponent.asyncData !=='undefined') { for(letaofcomponent.asyncData({store,route: router.currentRoute})) { stores.push(a) } } if(typeofcomponent.components !=='undefined') { for(letcincomponent.components){ loopComponent(component.components[c]) } } } loopComponent(component) returnstores } export defaultcontext => { return newPromise((resolve,reject) => { consts = isDev && Date.now() const{url} = context constfullPath = router.resolve(url).route.fullPath if(fullPath !== url) { reject({url: fullPath }) } router.push(url) router.onReady(() => { constmatchedComponents = router.getMatchedComponents() if(!matchedComponents.length) { reject({code:404}) } letallAsyncData = getAllAsyncData(matchedComponents[0]) Promise.all(allAsyncData).then(() => { isDev && console.log(`data pre-fetch:${Date.now() - s}ms`) context.state = store.state resolve(app) }).catch(reject) },reject) }) }
这个文件的主要工作是接受从服务端传递过来的context参数,context包含当前页面的url,用getMatchedComponents方法获取当前url下的组件,返回一个数组,遍历这个数组中的组件,如果组件有asyncData钩子函数,则传递store获取数据,最后返回一个promise对象。
store.state的作用是将服务端获取到的数据挂载到context对象上,后面在server.js文件里会把这些数据直接发送到浏览器端与客户端的vue实例进行数据(状态)同步。
3、客户端入口文件(entry-client.js)
importVuefrom'vue' import'es6-promise/auto' import{ createApp }from'./main' importProgressBarfrom'./components/ProgressBar.vue' // global progress bar constbar = Vue.prototype.$bar =newVue(ProgressBar).$mount() document.body.appendChild(bar.$el) Vue.mixin({ beforeRouteUpdate(to,from,next) { const{ asyncData } =this.$options if(asyncData) { Promise.all(asyncData({ store:this.$store, route: to })).then(next).catch(next) }else{ next() } } }) const{ app,router,store } = 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 =false constactivated = matched.filter((c,i) => { returndiffed || (diffed = (prevMatched[i] !== c)) }) constasyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) if(!asyncDataHooks.length) { returnnext() } bar.start() Promise.all(asyncDataHooks.map(hook => hook({ store,route: to }))) .then(() => { bar.finish() next() }) .catch(next) }) app.$mount('#app') }) if('https:'=== location.protocol && navigator.serviceWorker) { navigator.serviceWorker.register('/service-worker.js') }
if(window.INITIAL_STATE) { store.replaceState(window.INITIAL_STATE) }
这句的作用是如果服务端的vuex数据发生改变,就将客户端的数据替换掉,保证客户端和服务端的数据同步