当前位置: 移动技术网 > IT编程>脚本编程>vue.js > vue服务端渲染的实例代码

vue服务端渲染的实例代码

2017年12月12日  | 移动技术网IT编程  | 我要评论

宏源生态园,福5鼠之战国烽烟,好色的外科大夫

一、什么是服务端渲染

客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回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"
})

七、例子

这是一个服务端渲的例子

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网