第一部分 基本介绍
1、前言
服务端渲染实现原理机制:在服务端拿数据进行解析渲染,直接生成html片段返回给前端。然后前端可以通过解析后端返回的html片段到前端页面,大致有以下两种形式:
1、服务器通过模版引擎直接渲染整个页面,例如java后端的vm模版引擎,PHP后端的smarty模版引擎。 2、服务渲染生成HTML代码块,前端通过AJAX获取然后使用js动态添加。
2、服务端渲染的优劣
服务端渲染能够解决两大问题:
1、SEO问题,有利于搜索引擎蜘蛛抓取网站内容,利于网站的收录和排名。 2、首屏加载过慢问题,例如现在成熟的SPA项目中,打开首页需要加载很多资源,通过服务端渲染可以加速首屏渲染。 同样服务端渲染也会有弊端,主要是根据自己的业务场景来选择适合方式,由于服务端渲染前端页面,必将会给服务器增加压力。
3、SSR的实现原理
客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回 Promise (官方是preFetch方法)来将需要的数据拿到。最后再通过
将其写入网页,最后将服务端渲染好的网页返回回去。
接下来客户端会将vuex将写入的 initial_state 替换为当前的全局状态树,再用这个状态树去检查服务端渲染好的数据有没有问题。遇到没被服务端渲染的组件,再去发异步请求拿数据。说白了就是一个类似React的 shouldComponentUpdate 的Diff操作。
Vue2使用的是单向数据流,用了它,就可以通过 SSR 返回唯一一个全局状态, 并确认某个组件是否已经SSR过了。
4、vue后端渲染主要插件:vue-server-renderer
由于virtual dom的引入,使得vue的服务端渲染成为了可能,下面是官方 vue-server-renderer提供的渲染流程图:
可以看出vue的后端渲染分三个部分组成:页面的源码(source),node层的渲染部分和浏览器端的渲染部分。
source分为两种entry point,一个是前端页面的入口client entry,主要是实例化Vue对象,将其挂载到页面中;另外一个是后端渲染服务入口server entry,主要是控服务端渲染模块回调,返回一个Promise对象,最终返回一个Vue对象(经过测试,直接返回Vue对象也是可以的);
前面的source部分就是业务开发的代码,开发完成之后通过 webpack 进行构建,生成对应的bundle,这里不再赘述client bundle,就是一个可在浏览器端执行的打包文件;这里说下server bundle,vue2提供 vue-server-renderer模块,模块可以提供两种render: rendererer/bundleRenderer,下面分别介绍下这两种render。
renderer接收一个vue对象 ,然后进行渲染,这种对于简单的vue对象,可以这么去做,但是对于复杂的项目,如果使用这种直接require一个vue对象,这个对于服务端代码的结构和逻辑都不太友好,首先模块的状态会一直延续在每个请求渲染请求,我们需要去管理和避免这次渲染请求的状态影响到后面的请求,因此vue-server-renderer提供了另外一种渲染模式,通过一个 bundleRenderer去做渲染。
bundleRenderer是较为复杂项目进行服务端渲染官方推荐的方式,通过webpack以server entry按照一定的要求打包生成一个 server-bundle,它相当于一个可以给服务端用的app的打包压缩文件,每一次调用都会重新初始化 vue对象,保证了每次请求都是独立的,对于开发者来说,只需要专注于当前业务就可以,不用为服务端渲染开发更多的逻辑代码。 renderer生成完成之后,都存在两个接口,分别是renderToString和renderToStream,一个是一次性将页面渲染成字符串文件,另外一个是流式渲染,适用于支持流的web服务器,可以是请求服务的速度更快。
第二部分 从零开始搭建
1、前言
上一节我们大致讲了为什么需要使用vue后端渲染,以及vue后端渲染的基本原理,这节内容我们将从零开始搭建属于自己的vue后端渲染脚手架,当然不能不参考官方页面响应的实例vue-hackernews-2.0,从零开始搭建项目,源码在将在下节与大家共享。
2、前期准备
基本环境要求:node版本6.10.1以上,npm版本3.10.10以上,本机环境是这样的,建议升级到官方最新版本。
使用的技术栈:
1、vue 2.4.2 2、vuex 2.3.1 3、vue-router 2.7.0 4、vue-server-renderer 2.4.2 5、express 4.15.4 6、axios 0.16.2 7、qs 6.5.0 8、q 9、webpack 3.5.0 10、mockjs 1.0.1-beta3 11、babel 相关插件
以上是主要是用的技术栈,在构建过程中会是用相应的插件依赖包来配合进行压缩打包,以下是npm init后package.json文件所要添加的依赖包。
3、项目主目录搭建
基本目录结构如下:
文件目录基本介绍:
- views文件夹下分模块文件,模块文件下下又分模块本身的.vue文件(模版文件),index.js文件(后台数据交互文件),mock.js(本模块的mock假数据),conf.js(配置本模块一些参数,请求路径,模块名称等信息)
- components 公共组件文件夹
- router 主要存放前端路由配置文件,写法规范按照vue-router官方例子即可。
- store 主要是存放共享状态文件,里面包含action.js,getter.js,mutationtype.js等,后期会根据模块再细分这些。
- public 主要存放公共组件代码和项目使用的公共文件代码,例如后期我们将axios封装成公共的api库文件等等
- static文件夹代表静态文件,不会被webpack打包的
- app.js 是项目入口文件
- App.vue 是项目入口文件
- entry-client和entry-server分别是客户端入口文件和服务端的入口文件
- index.template.html是整个项目的模版文件
开始编写app.js项目入口代码
使用vue开发项目入口文件一般都会如下写法:
el: '#app',store,router,render: (h) => h(App)
});
这种写法是程序共享一个vue实例,但是在后端渲染中很容易导致交叉请求状态污染,导致数据流被污染了。
所以,避免状态单例,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例,同样router和store入口文件也需要重新创建一个实例。
为了配合webpack动态加载路由配置,这里会改写常规路由引入写法,这样可以根据路由路径来判断加载相应的组件代码:
以下是路由的基本写法router,对外会抛出一个createRouter方法来创建一个新的路由实例:
以下是store状态管理的基本写法,对外暴露了一个createStore方法,方便每次访问创建一个新的实例:
结合写好的router和store入口文件代码来编写整个项目的入口文件app.js代码内容,同样最终也会对外暴露一个createApp方法,在每次创建app的时候保证router,store,app都是新创建的实例,这里还引入了一个vue路由插件vuex-router-sync,主要作用是同步路由状态(route state)到 store,以下是app.js完整代码:
entry-client.js代码编写:
首页引入从app文件中暴露出来的createApp方法,在每次调用客户端的时候,重新创建一个新的app,router,store,部分代码如下:
这里我们会使用到onReady方法,此方法通常用于等待异步的导航钩子完成,比如在进行服务端渲染的时候,例子代码如下:
我们会调用一个新方法beforeResolve,只有在router2.5.0以上的版本才会有的方法,注册一个类似于全局路由保护router.beforeEach(),除了在导航确认之后,在所有其他保护和异步组件已解决之后调用。基本写法如下:
服务端把要给客户端的 state 放在了 window. INITIAL_STATE 这个全局变量上面。前后端的 HTML 结构应该是一致的。然后要把 store 的状态树写入一个全局变量( INITIAL_STATE ),这样客户端初始化 render 的时候能够校验服务器生成的 HTML 结构,并且同步到初始化状态,然后整个页面被客户端接管。基本代码如下:
接下来贴出来完整的客户端代码,这里的Q也可以不用引入,直接使用babel就能编译es6自带的Promise,因为本人使用习惯了,这里可以根据自身的需求是否安装:
beforeRouteUpdate (to,next) {
const { asyncData } = this.$options
if (asyncData) {
asyncData({
store: this.$store,route: to
}).then(next).catch(next)
} else {
next()
}
}
})
const { app,store } = createApp()
// 将服务端渲染时候的状态写入vuex中
if (window.INITIAL_STATE) {
store.replaceState(window.INITIAL_STATE)
}
router.onReady(() => {
router.beforeResolve((to,next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
// 我们只关心之前没有渲染的组件
// 所以我们对比它们,找出两个匹配列表的差异组件
let diffed = false
const activated = matched.filter((c,i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
// 这里如果有加载指示器(loading indicator),就触发
Q.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store,route: to })
}
})).then(() => {
// 停止加载指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
entry-server.js代码编写:
基本编写和客户端的差不多,因为这是服务端渲染,涉及到与后端数据交互定义的问题,我们需要在这里定义好各组件与后端交互使用的方法名称,这样方便在组件内部直接使用,这里根我们常规在组件直接使用ajax获取数据有些不一样,代码片段如下:
以下是完整的服务端代码:
4、脚手架其他目录介绍:
到这里src下面主要的几个文件代码已经编写完成,接下里介绍下整个项目的目录结构如下:
主要几个文件介绍如下:
- build 主要存放webpack打包配置文件
- dist webpack打包后生成的目录
- log 使用pm2监控进程存放的日志文件目录
- server.js node服务器启动文件
- pmlog.json pm2配置文件
server.js入口文件编写
我们还需要编写在服务端启动服务的代码server.js,我们会使用到部分node原生提供的api,片段代码如下:
大致思路是,引入前端模版页面index.template.html,使用express启动服务,引入webpack打包项目代码的dist文件,引入缓存模块(这里不做深入介绍,后期会单独详细介绍),判断端口是否被占用,自动启动其他接口服务。
引入前端模版文件并且设置环境变量为production,片段代码如下:
vue-server-renderer插件的具体使用,通过读取dist文件夹下的目录文件,来创建createBundleRenderer函数,并且使用LRU来设置缓存的时间,通过判断是生产环境还是开发环境,调用不同的方法,代码片段如下:
使用express启动服务,代码片段如下:
const serve = (path,cache) => express.static(resolve(path),{
maxAge: cache && isProd ? 1000 60 60 24 30 : 0
})
server.use('/dist',serve('./dist',true))
server.get('*',(req,res) => {
const context = {
title: 'hello',url: req.url
}
renderer.renderToString(context,(err,html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(html)
})
})
判断端口是否被占用,片段代码如下:
servers.close()
if (!calledOnce) {
calledOnce = true
callback(true,port)
}
})
servers.on('error',function(err) {
clearTimeout(timeoutRef)
let result = true
if (err.code === 'EADDRINUSE')
result = false
if (!calledOnce) {
calledOnce = true
callback(result,port)
}
})
}
const checkPortPromise = new Promise((resolve) => {
(function serverport(_port) {
let pt = _port || 8080;
probe(pt,function(bl,_pt) {
// 端口被占用 bl 返回false
// _pt:传入的端口号
if (bl === true) {
// console.log("\n Static file server running at" + "\n\n=> http://localhost:" + _pt + '\n');
resolve(_pt);
} else {
serverport(_pt + 1)
}
})
})()
})
checkPortPromise.then(data => {
uri = 'http://localhost:' + data;
console.log('启动服务路径'+uri)
server.listen(data);
});
到这里,基本的代码已经编写完成,webpack打包配置文件基本和官方保持不变,接下来可以尝试启动本地的项目服务,这里简要的使用网易严选首页作为demo示例,结果如下:
第三部分 mockjs和axios配合使用
1、前言
上一节大致介绍了服务端和客户端入口文件代码内容,现在已经可以正常运行你的后端渲染脚手架了,这一节,跟大家分享下如何使用axios做ajax请求,如何使用mockjs做本地假数据,跑通本地基本逻辑,为以后前后端连调做准备。
2、前期准备
需要用npm安装axios,mockjs依赖包,由于mockjs只是代码开发的辅助工具,所以安装的时候我会加--save-dev来区分,具体可以根据自己的需求来定,当然,如果有mock服务平台的话,可以直接走mock平台造假数据,本地直接访问mock平台的接口,例如可以使用阿里的Rap平台管理工具生成。
3、简要介绍axios
其他请求方式,代码示例如下:
api.js完整代码如下:
axios.defaults.withCredentials = true
function ajax(url,type,options) {
return Q.Promise((resolve,reject) => {
axios({
method: type,url: C.HOST + url,params: type === 'get' ? options : null,data: type !== 'get' ? qs.stringify(options) : null
})
.then((result) => {
if (result && result.status === 401) {
// location.href = '/views/401.html'
}
if (result && result.status === 200) {
if (result.data.code === 200) {
resolve(result.data.data);
} else if (result.data.code === 401) {
reject({
nopms: true,msg: result.data.msg
});
} else {
reject({
error: true,msg: result.data.msg
});
}
} else {
reject({
errno: result.errno,msg: result.msg
});
}
})
.catch(function(error) {
console.log(error,url);
});
})
}
const config = {
get(url,options) {
const _self = this;
return Q.Promise((resolve,reject) => {
ajax(url,'get',options)
.then((data) => {
resolve(data);
},(error) => {
reject(error);
});
})
},post(url,'post',put(url,'put',delete(url,'delete',jsonp(url,'jsonp',(error) => {
reject(error);
});
})
}
};
export default config;
mockjs项目基本配置如下:
1、在public下新建conf.js全局定义请求URL地址,代码如下:
2、在views/index根目录下新建conf.js,定义组件mock的请求路径,并且定义是否开始单个组件使用mock数据还是线上接口数据,代码如下:
3、在组件内部定义mockjs来编写mock假数据,代码如下:
以上就是基本的流程,如果有更好更灵活的使用方案,希望能够参与沟通并且分享,项目工作流已经在github上分享,并且会继续维护更新, nofollow" target="_blank" href="https://github.com/wqzwh/ga-vue-ssr">点击查看详情,希望对大家的学习有所帮助,也希望大家多多支持编程之家。
原文链接:https://www.f2er.com/vue/34264.html