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 相关文章推荐
让FireFox支持innerText的实现代码
Dec 01 Javascript
php与js的区别是什么
Aug 05 Javascript
iframe子父页面调用js函数示例
Nov 07 Javascript
JS实现漂亮的淡蓝色滑动门效果代码
Sep 23 Javascript
实例详解jQuery结合GridView控件的使用方法
Jan 04 Javascript
JavaScript学习笔记整理之引用类型
Jan 22 Javascript
Sublime Text新建.vue模板并高亮(图文教程)
Oct 26 Javascript
微信小程序template模板实例详解
Oct 27 Javascript
vue-cli脚手架引入图片的几种方法总结
Mar 13 Javascript
webpack 如何解析代码模块路径的实现
Sep 04 Javascript
基于Vue CSR的微前端实现方案实践
May 27 Javascript
vue项目中使用多选框的实例代码
Jul 22 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
php校验表单检测字段是否为空的方法
2015/03/20 PHP
[原创]php简单防盗链验证实现方法
2016/07/09 PHP
老生常谈PHP中的数据结构:DS扩展
2017/07/17 PHP
JavaScript高级程序设计
2006/12/29 Javascript
Javascript面向对象设计一 工厂模式
2011/12/20 Javascript
javascript针对DOM的应用分析(四)
2012/04/15 Javascript
Javascript代码在页面加载时的执行顺序介绍
2013/05/03 Javascript
node中socket.io的事件使用详解
2014/12/15 Javascript
Javascript基础教程之定义和调用函数
2015/01/18 Javascript
jQuery选择器源码解读(七):elementMatcher函数
2015/03/31 Javascript
简介JavaScript中的italics()方法的使用
2015/06/08 Javascript
jQuery实现宽屏图片轮播实例教程
2015/11/24 Javascript
JS函数的几种定义方式分析
2015/12/17 Javascript
仅9张思维导图帮你轻松学习Javascript 就这么简单
2016/06/01 Javascript
jquery.tableSort.js表格排序插件使用方法详解
2020/08/12 Javascript
javascript 判断一个对象为数组的方法
2017/05/03 Javascript
webpack项目调试以及独立打包配置文件的方法
2018/02/28 Javascript
VUE+Element UI实现简单的表格行内编辑效果的示例的代码
2018/10/31 Javascript
vue实现购物车列表
2020/06/30 Javascript
[01:00:14]DOTA2官方TI8总决赛纪录片 真视界True Sight
2019/01/16 DOTA
Python下rrdtool模块的基本使用方法
2015/11/13 Python
python编程线性回归代码示例
2017/12/07 Python
Python 中的Selenium异常处理实例代码
2018/05/03 Python
使用pandas把某一列的字符值转换为数字的实例
2019/01/29 Python
利用python Selenium实现自动登陆京东签到领金币功能
2019/10/31 Python
Python 获取numpy.array索引值的实例
2019/12/06 Python
python如何设置静态变量
2020/09/07 Python
最新pycharm安装教程
2020/11/18 Python
Matplotlib中rcParams使用方法
2021/01/05 Python
市场部专员岗位职责
2013/11/30 职场文书
仓库主管的岗位职责
2013/12/04 职场文书
英语道歉信范文
2014/01/09 职场文书
写给老师的表扬信
2014/01/21 职场文书
整脏治乱工作简报
2015/07/21 职场文书
李白经典诗之一:全文无一“月”字,却句句有月
2019/07/12 职场文书
SQL 聚合、分组和排序
2021/11/11 MySQL