vue服务端渲染的实例代码
一、什么是服务端渲染
客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回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数据发生改变,就将客户端的数据替换掉,保证客户端和服务端的数据同步
service worker主要用于拦截并修改访问和资源请求,细粒度地缓存资源。它运行浏览器在后台,运行环境与普通页面脚本不同,所以不能直接参与页面交互。出于安全考虑,service worker只能运行在https上,防止被人从中攻击。
4、创建服务端渲染器(server.js)
constfs = require('fs') constpath = require('path') constlru = require('lru-cache') constexpress = require('express') constcompression = require('compression') constresolve= file => path.resolve(__dirname,file) const{ createbundlerenderer } = require('vue-server-renderer') constisprod = process.env.node_env ==='production'|| process.env.node_env ==='beta' constusemicrocache = process.env.micro_cache !=='false' constserverinfo = `express/${require('express/package.json').version}`+ `vue-server-renderer/${require('vue-server-renderer/package.json').version}` constapp = express() consttemplate = fs.readfilesync(resolve('./src/index.template.html'),'utf-8') functioncreaterenderer(bundle,options) { returncreatebundlerenderer(bundle,object.assign(options,{ template, cache: lru({ max:1000, maxage:1000*60*15 }), basedir: resolve('./dist'), runinnewcontext:false })) } letrenderer letreadypromise if(isprod) { constbundle = require('./dist/vue-ssr-server-bundle.json') constclientmanifest = require('./dist/vue-ssr-client-manifest.json') renderer = createrenderer(bundle,{ clientmanifest }) }else{ readypromise = require('./build/setup-dev-server')(app,(bundle,options) => { renderer = createrenderer(bundle,options) }) } constserve= (path,cache) => express.static(resolve(path),{ maxage: cache && isprod ?1000*60*60*24*30:0 }) app.use(compression({threshold:0})) app.use('/dist',serve('./dist',true)) app.use('/static',serve('./static',true)) app.use('/service-worker.js',serve('./dist/service-worker.js')) constmicrocache = lru({ max:100, maxage:1000 }) constiscacheable= req => usemicrocache functionrender(req,res) { consts = date.now() res.setheader("content-type","text/html") res.setheader("server",serverinfo) consthandleerror= err => { if(err.url) { res.redirect(err.url) }else if(err.code ===404) { res.status(404).end('404 | page not found') }else{ // render error page or redirect res.status(500).end('500 | internal server error') console.error(`error during render :${req.url}`) console.error(err.stack) } } constcacheable = iscacheable(req) if(cacheable) { consthit = microcache.get(req.url) if(hit) { if(!isprod) { console.log(`cache hit!`) } returnres.end(hit) } } constcontext = { title:'vue db',// default title url: req.url } renderer.rendertostring(context,(err,html) => { if(err) { returnhandleerror(err) } res.end(html) if(cacheable) { microcache.set(req.url,html) } if(!isprod) { console.log(`whole request:${date.now() - s}ms`) } }) } app.get('*',isprod ? render : (req,res) => { readypromise.then(() => render(req,res)) }) constport = process.env.port ||8888 app.listen(port,() => { console.log(`server started at localhost:${port}`) })
5、客户端api文件create-api-client.js
/** * created by lin on 2017/8/25. */ import axios from 'axios'; let api; axios.defaults.baseurl = process.env.api_url; axios.defaults.timeout = 10000; axios.interceptors.response.use((res) => { if (res.status >= 200 && res.status < 300) { return res; } return promise.reject(res); }, (error) => { return promise.reject({message: '网络异常,请刷新重试', err: error}); }); if (process.__api__) { api = process.__api__; } else { api = { get: function(url) { return new promise((resolve, reject) => { axios.get(url).then(res => { resolve(res); }).catch((error) => { reject(error); }); }); }, post: function(target, options = {}) { return new promise((resolve, reject) => { axios.post(target, options).then(res => { resolve(res); }).catch((error) => { reject(error); }); }); } }; } export default api;
6、服务端api文件create-api-server.js
/** * created by lin on 2017/8/25. */ import axios from 'axios'; let cook = process.__cookie__ || ''; let api; axios.defaults.baseurl = 'https://api.douban.com/v2/'; axios.defaults.timeout = 10000; axios.interceptors.response.use((res) => { if (res.status >= 200 && res.status < 300) { return promise.resolve(res); } return promise.reject(res); }, (error) => { // 网络异常 return promise.reject({message: '网络异常,请刷新重试', err: error, type: 1}); }); if (process.__api__) { api = process.__api__; } else { api = { get: function(target) { return new promise((resolve, reject) => { axios.request({ url: encodeuri(target), method: 'get', headers: { 'cookie': cook } }).then(res => { resolve(res); }).catch((error) => { reject(error); }); }); }, post: function(target, options = {}) { return new promise((resolve, reject) => { axios.request({ url: target, method: 'post', headers: { 'cookie': cook }, params: options }).then(res => { resolve(res); }).catch((error) => { reject(error); }); }); } }; } export default api;
六、那些年遇到的那些坑
问题1、window is not defined
答案1:给用到浏览器对象的地方加if (typeof window !== 'undefined') {},有一些插件里也用到了浏览器对象,在使用的地方也加一个条件判断:
if (typeofwindow !== 'undefined') { vue.use(vueanalytics, { id: process.env.ua_tracking_id, router }) }
问题2:用到非vue系列的插件,如hello.all.js(三方登录的插件),需要用的地方才引用,报的错和问题1一样。
答案2:这个时候不能再用import导入,需要使用require,
let hello
if (typeof window !== 'undefined') { hello = require('hello') }
问题3:引用bootstrap
答案3:将bootstrap.css和bootstrap.js加入webpack.base.config.js的entry中的vendor中
问题6:bootstap需要jquery,此时把jquery加在vendor中没用。
答案6:给webpack.base.config.js的plugins添加一个插件,如:
newwebpack.provideplugin({ $ : "jquery", jquery : "jquery", "window.jquery" :"jquery" })
七、例子
这是一个服务端渲的例子
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。