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 相关文章推荐
Ext JS 4官方文档之三 -- 类体系概述与实践
Dec 16 Javascript
公共js在页面底部加载的注意事项介绍
Jul 18 Javascript
js中array的sort()方法使用介绍
Feb 20 Javascript
JavaScript跨域方法汇总
Oct 16 Javascript
javascript图片预加载完整实例
Dec 10 Javascript
jquery编写Tab选项卡滚动导航切换特效
Jul 17 Javascript
JavaScript预解析及相关技巧分析
Apr 21 Javascript
JS填写银行卡号每隔4位数字加一个空格
Dec 19 Javascript
Vue.js项目中管理每个页面的头部标签的两种方法
Jun 25 Javascript
使用JS实现导航切换时高亮显示的示例讲解
Aug 22 Javascript
微信小程序实现分享朋友圈的图片功能示例
Jan 18 Javascript
toString.call()通用的判断数据类型方法示例
Aug 28 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
php2html php生成静态页函数
2008/12/08 PHP
snoopy 强大的PHP采集类使用实例代码
2010/12/09 PHP
深入PHP购物车模块功能分析(函数讲解,附源码)
2013/06/25 PHP
php文件上传的例子及参数详解
2013/12/12 PHP
深入理解PHP中的global
2014/08/19 PHP
php实现高效获取图片尺寸的方法
2014/12/12 PHP
Smarty模板配置实例简析
2019/07/20 PHP
实用的Jquery选项卡TAB示例代码
2013/08/28 Javascript
Javascript的严格模式strict mode详细介绍
2014/06/06 Javascript
Blocksit插件实现瀑布流数据无限( 异步)加载
2014/06/20 Javascript
一个JavaScript处理textarea中的字符成每一行实例
2014/09/22 Javascript
SuperSlide标签切换、焦点图多种组合插件
2015/03/14 Javascript
基于javascript代码实现通过点击图片显示原图片
2015/11/29 Javascript
jQuery 获取多选框的值及多选框中文的函数
2016/05/16 Javascript
Bootstrap下拉菜单效果实例代码分享
2016/06/30 Javascript
JS中动态创建元素的三种方法总结(推荐)
2016/10/20 Javascript
AngularJS实现给动态生成的元素绑定事件的方法
2016/12/14 Javascript
jQuery与js实现颜色渐变的方法
2016/12/30 Javascript
js实现图片加载淡入淡出效果
2017/04/07 Javascript
JavaScript中this的用法及this在不同应用场景的作用解析
2017/04/13 Javascript
jQuery修改DOM结构_动力节点Java学院整理
2017/07/05 jQuery
Vue实例中生命周期created和mounted的区别详解
2017/08/25 Javascript
vue2.0实现的tab标签切换效果(内容可自定义)示例
2019/02/11 Javascript
Vue项目中如何使用Axios封装http请求详解
2019/10/23 Javascript
浅析我对JS延迟异步脚本的思考
2020/10/12 Javascript
Python 用matplotlib画以时间日期为x轴的图像
2019/08/06 Python
Pandas聚合运算和分组运算的实现示例
2019/10/17 Python
Python xpath表达式如何实现数据处理
2020/06/13 Python
Python sublime安装及配置过程详解
2020/06/29 Python
linux面试题参考答案(10)
2013/11/04 面试题
语文教育专业应届生求职信
2013/11/23 职场文书
竞选村长演讲稿
2014/04/28 职场文书
颐和园的导游词
2015/01/30 职场文书
伊索寓言读书笔记
2015/06/30 职场文书
少先队大队委竞选口号
2015/12/25 职场文书
mybatis-plus模糊查询指定字段
2022/04/28 Java/Android