vue服务端渲染的实例代码


Posted in Javascript onAugust 28, 2017

一、什么是服务端渲染

客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回Promise (官方是asyncData方法)来将需要的数据拿到。最后再通过window.__initial_state=data将其写入网页,最后将服务端渲染好的网页返回回去。接下来客户端将用新的store状态把原来的store状态替换掉,保证客户端和服务端的数据同步。遇到没被服务端渲染的组件,再去发异步请求拿数据。

服务端渲染的环境搭建

vue服务端渲染的实例代码

这是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"
})

七、例子

https://github.com/linmoer/ssr-vue这是一个服务端渲的例子

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
从jQuery.camelCase()学习string.replace() 函数学习
Sep 13 Javascript
自定义的一个简单时尚js下拉选择框
Nov 20 Javascript
纯js实现div内图片自适应大小(已测试,兼容火狐)
Jun 16 Javascript
JavaScript数组Array对象增加和删除元素方法总结
Jan 20 Javascript
JavaScript编程中布尔对象的基本使用
Oct 25 Javascript
js实现滚动条滚动到页面底部继续加载
Dec 19 Javascript
Jquery跨域获得Json的简单实例
May 18 Javascript
JS工作中的小贴士之”闭包“与事件委托的”阻止冒泡“
Jun 16 Javascript
Java  Spring 事务回滚详解
Oct 17 Javascript
layer弹出层中H5播放器全屏出错的解决方法
Feb 21 Javascript
JS学习笔记之闭包小案例分析
May 29 Javascript
简单了解TypeScript中如何继承 Error 类
Jun 21 Javascript
jQuery 1.9版本以上的浏览器判断方法代码分享
Aug 28 #jQuery
使用react-router4.0实现重定向和404功能的方法
Aug 28 #Javascript
vue.js路由跳转详解
Aug 28 #Javascript
jQuery Collapse1.1.0折叠插件简单使用
Aug 28 #jQuery
解决IE7中使用jQuery动态操作name问题
Aug 28 #jQuery
BootStrap入门学习第一篇
Aug 28 #Javascript
[js高手之路]图解javascript的原型(prototype)对象,原型链实例
Aug 28 #Javascript
You might like
php4与php5的区别小结(配置异同)
2011/12/20 PHP
PHP实现服务器状态监控的方法
2014/12/09 PHP
爱恋千雪-US-AscII加密解密工具(网页加密)下载
2007/06/06 Javascript
jQuery EasyUI 中文API Layout(Tabs)
2010/04/27 Javascript
用console.table()调试javascript
2014/09/04 Javascript
jQuery处理json数据返回数组和输出的方法
2015/03/11 Javascript
浅谈jQuery中height与width
2015/07/06 Javascript
基于Jquery实现焦点图淡出淡入效果
2015/11/30 Javascript
Knockout自定义绑定创建方法
2015/12/26 Javascript
jquery获取复选框checkbox的值实现方法
2016/05/30 Javascript
用原生js做单页应用
2017/01/17 Javascript
微信小程序开发之toast等弹框提示使用教程
2017/06/08 Javascript
ZeroClipboard.js使用一个flash复制多个文本框
2017/06/19 Javascript
微信小程序云开发使用方法新手初体验
2019/05/16 Javascript
Nodejs libuv运行原理详解
2019/08/21 NodeJs
JavaScript 判断数据类型的4种方法
2020/09/11 Javascript
vue $router和$route的区别详解
2020/12/02 Vue.js
[07:54]DOTA2-DPC中国联赛 正赛 iG vs VG 选手采访
2021/03/11 DOTA
Python代码调试的几种方法总结
2015/04/15 Python
python3序列化与反序列化用法实例
2015/05/26 Python
Python的Django框架中设置日期和字段可选的方法
2015/07/17 Python
Python的GUI框架PySide的安装配置教程
2016/02/16 Python
浅谈pyhton学习中出现的各种问题(新手必看)
2017/05/17 Python
python计算auc指标实例
2017/07/13 Python
Python中实现switch功能实例解析
2018/01/11 Python
解决PySide+Python子线程更新UI线程的问题
2019/01/11 Python
python文件写入write()的操作
2019/05/14 Python
Python 窗体(tkinter)按钮 位置实例
2019/06/13 Python
python绘制多个子图的实例
2019/07/07 Python
Pytorch抽取网络层的Feature Map(Vgg)实例
2019/08/20 Python
如何一键升级Python所有包
2020/11/05 Python
python脚本定时发送邮件
2020/12/22 Python
猎人靴英国官网:Hunter Boots
2017/02/02 全球购物
高校生生产实习自我鉴定
2013/09/21 职场文书
学校元旦晚会开场白
2014/12/14 职场文书
Python集合的基础操作
2021/11/01 Python