解密Vue SSR

做者:百度外卖 耿彩丽 李宗原
转载请标明出处复制代码


引言

最近笔者和小伙伴在研究Vue SSR,可是市面上充斥了太多的从0到1的文章,对你们理解这其中的原理帮助并非很大,所以,本文将从Vue SSR的构建流程、运行流程、SSR的特色和利弊这几方面对Vue SSR有一个较为详细的介绍。最后还将附上一个笔者实现的去除Vue全家桶的Demo案例。css

剖析构建流程

首先咱们镇上一张官网给出的构建图:html

Vue SSR构建流程

app.js入口文件vue

app.js是咱们的通用entry,它的做用就是构建一个Vue的实例以供服务端和客户端使用,注意一下,在纯客户端的程序中咱们的app.js将会挂载实例到dom中,而在ssr中这一部分的功能放到了Client entry中去作了。java


两个entrynode

接下里咱们来看Client entry和Server entry,这二者分别是客户端的入口和服务端的入口。Client entry的功能很简单,就是挂载咱们的Vue实例到指定的dom元素上;Server entry是一个使用export导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。这个函数会在每一次的渲染中重复的调用webpack


webpack打包构建ios

而后咱们的服务端代码和客户端代码经过webpack分别打包,生成Server Bundle和Client Bundle,前者会运行在服务器上经过node生成预渲染的HTML字符串,发送到咱们的客户端以便完成初始化渲染;而客户端bundle就自由了,初始化渲染彻底不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,是其变成由Vue动态管理的DOM,以便响应后续数据的变化。git

剖析运行流程

到这里咱们该谈谈ssr的程序是怎么跑起来的了。首先咱们得去构建一个vue的实例,也就是咱们前面构建流程中说到的app.js作的事情,可是这里不一样于传统的客户端渲染的程序,咱们须要用一个工厂函数去封装它,以便每个用户的请求都可以返回一个新的实例,也就是官网说到的避免交叉污染了github

而后咱们能够暂时移步到服务端的entry中了,这里要作的就是拿到当前路由匹配的组件,调用组件里定义的一个方法(官网取名叫asyncData)拿到初始化渲染的数据,而这个方法要作的也很简单,就是去调用咱们vuex store中的方法去异步获取数据web

接下来node服务器如期启动了,跑的是咱们刚写好的服务端entry里的函数。在这里还要作的就是将咱们刚刚构建好的Vue实例渲染成HTML字符串,而后将拿到的数据混入咱们的HTML字符串中,最后发送到咱们客户端。

打开浏览器的network,咱们看到了初始化渲染的HTML,而且是咱们想要初始化的结构,且彻底不依赖于客户端的js文件了。再仔细研究研究,里面有初始化的dom结构,有css,还有一个script标签。script标签里把咱们在服务端entry拿到的数据挂载了window上。原来只是一个纯静态的HTML页面啊,没有任何的交互逻辑,因此啊,如今知道为啥子须要服务端跑一个vue客户端再跑一个vue了,服务端的vue只是混入了个数据渲染了个静态页面,客户端的vue才是去实现交互的!

chrome network

顺着前面的思路,咱们该看客户端的entry了。在这里客户端拿到存在window中的数据混入咱们客户端的vuex中,而后分析数据去执行咱们熟悉的其他客户端操做了。

SSR独特之处

在SSR中,建立Vue实例、建立store和建立router都是套了一层工厂函数的,目的就是避免数据的交叉污染

在服务端只能执行生命周期中的created和beforeCreate,缘由是在服务端是没法操纵dom的,因此可想而知其余的周期也就是不能执行的了。

服务端渲染和客户端渲染不一样,须要建立两个entry分别跑在服务端和客户端,而且须要webpack对其分别打包

SSR服务端请求不带cookie,须要手动拿到浏览器的cookie传给服务端的请求。实现方式戳这里

SSR要求dom结构规范,由于浏览器会自动给HTML添加一些结构好比tbody,可是客户端进行混淆服务端放回的HTML时,不会添加这些标签,致使混淆后的HTML和浏览器渲染的HTML不匹配。

性能问题须要多加关注。

  • vue.mixin、axios拦截请求使用不当,会内存泄漏。缘由戳这里
  • lru-cache向内存中缓存数据,须要合理缓存改动不频繁的资源。

多是把双刃剑

SSR的优势

  • 更利于SEO。

不一样爬虫工做原理相似,只会爬取源码,不会执行网站的任何脚本(Google除外,听说Googlebot能够运行javaScript)。使用了Vue或者其它MVVM框架以后,页面大多数DOM元素都是在客户端根据js动态生成,可供爬虫抓取分析的内容大大减小。另外,浏览器爬虫不会等待咱们的数据完成以后再去抓取咱们的页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行JavaScript脚本的最终HTML,网络爬中就能够抓取到完整页面的信息。

  • 更利于首屏渲染

首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤为是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载全部所需文件时间较长,首页就会有一个很长的白屏等待时间。


SSR的局限

  • 服务端压力较大

原本是经过客户端完成渲染,如今统一到服务端node服务去作。尤为是高并发访问的状况,会大量占用服务端CPU资源;

  • 开发条件受限

在服务端渲染中,created和beforeCreate以外的生命周期钩子不可用,所以项目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制;

  • 学习成本相对较高

除了对webpack、Vue要熟悉,还须要掌握node、Express相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。

去除VUEX的SSR实践

先附上demo地址,戳这里

说在前面:

  • vue-router不是必须的,不用router其实作个vue的preRender就能够了,彻底不必作ssr;
  • vuex不是必须的,vuex是实现咱们客户端和服务端的状态共享的关键,咱们能够不使用vuex,可是咱们得去实现一套数据预取的逻辑;

官网的demo大而全,集成了vue-router和vuex,想一想咱们的项目若是没有使用到这二者,光引入就又须要改形成本,这并非咱们想搞的“丝滑般”过渡,接下来笔者将带领你们一步一步的作个“啥都没有的”demo。

在此笔者的思路是:构造一个Vue的实例,那么咱们能够用这个实例的data来存储咱们的预取数据,而用methods中的方法去作数据的异步获取,这样咱们只在须要预取数据的组件中去调用这个方法就能够了

首先咱们须要让咱们的组件“共享”这个EventBus,为此笔者简单的封装了一个plugin:

export default {
 install (Vue) {
   const EventBus = new Vue({
     data () {
       return {
	      list: [],
	      nav: []
       }
     },
     methods: {
       getList () {
	      // get list
		},
       getNav () {
         // get nav
       }
     }
   })
   
   Vue.prototype.$events = EventBus
   Vue.$events = EventBus
 }
}
复制代码

而后咱们须要在main.js中export出咱们的EventBus以便两个entry使用。这样咱们的main.js就像下面这样:

import Vue from 'vue'
import App from './App'
import EventBus from './event'

Vue.use(EventBus)
Vue.config.devtools = true

export function createApp () {
 const app = new Vue({
   // 注入 router 到根 Vue 实例
   router,
   render: h => h(App)
 })
 
 return { app, router, eventBus: app.$events }
}
复制代码

接下来是咱们的两个entry了。server用来匹配咱们的组件并调用组件的asyncData方法去获取数据,client用来将预渲染的数据存储到咱们eventBus中的data中。

// server
import { createApp } from './main'

export default context => {
 return new Promise((resolve, reject) => {
   const { app, eventBus, App } = createApp()
   // 这里笔者的demo比较简单,仅app组件须要预取数据,复杂业务能够递归遍历哈;
   const matchedComponents = [App]

   Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
     eventBus
   }))).then(() => {
     context.state = eventBus._data
     resolve(app)
   }).catch(reject)
 })
}


// client
import Vue from 'vue'
import { createApp } from './main'
const { app, eventBus } = createApp()

if (window.__INITIAL_STATE__) {
 eventBus._data = window.__INITIAL_STATE__
}

app.$mount('#app')
复制代码

而后咱们须要改造咱们的组件了,只须要定义一个async方法去调用EventBus中的方法获取,考虑到服务端只会执行beforeCreate和created两个生命周期而beforeCreate不能拿到data,因此咱们须要在created中去作数据的获取。

// 服务端渲染数据预取;
asyncData ({ store, eventBus }) {
 return eventBus.getNav()
}
// 将服务端拿到的数据混入vue组件中;
created () {
 this.nav = this.$events.nav
}
复制代码


而后是webpack的改造了,webpack的配置其实和纯客户端应用相似,为了区分客户端和服务端两个环境咱们将配置分为base、client和server三部分,base就是咱们的通用基础配置,而client和server分别用来打包咱们的客户端和服务端代码。

首先是webpack.server.conf.js,用于生成server bundle来传递给createBundleRenderer函数在node服务器上调用,入口文件是咱们的entry-server:


const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.conf.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
// 去除打包css的配置
baseConfig.module.rules[1].options = ''

module.exports = merge(baseConfig, {
 entry: './src/entry-server.js',
 // 以 Node 适用方式导入
 target: 'node',
 // 对 bundle renderer 提供 source map 支持
 devtool: '#source-map',
 output: {
   filename: 'server-bundle.js',
   libraryTarget: 'commonjs2'
 },
 externals: nodeExternals({
   whitelist: /\.css$/
 }),
 plugins: [
   new webpack.DefinePlugin({
     'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
     'process.env.VUE_ENV': '"server"'
   }),
   // 这是将服务器的整个输出
   // 构建为单个 JSON 文件的插件。
   // 默认文件名为 `vue-ssr-server-bundle.json`
   new VueSSRServerPlugin()
 ]
})
复制代码

其次是webpack.client.conf.js,这里咱们能够根据官方的配置生成clientManifest,自动推断和注入资源预加载,以及 css 连接 / script 标签到所渲染的 HTML。入口是咱们的client-server:


const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.conf')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

const config = merge(base, {
 entry: {
   app: './src/entry-client.js'
 },
 plugins: [
   new webpack.DefinePlugin({
     'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
     'process.env.VUE_ENV': '"client"'
   }),
   new webpack.optimize.CommonsChunkPlugin({
     name: 'vendor',
     minChunks: function (module) {
       return (
         /node_modules/.test(module.context) &&
         !/\.css$/.test(module.request)
       )
     }
   }),
   // 这将 webpack 运行时分离到一个引导 chunk 中,
   // 以即可以在以后正确注入异步 chunk。
   // 这也为你的 应用程序/vendor 代码提供了更好的缓存。
   new webpack.optimize.CommonsChunkPlugin({
     name: 'manifest'
   }),
   new VueSSRClientPlugin()
 ]
})
复制代码


从localhost中咱们看到ssr预取的数据已经成功出来了,大功告成!

结语

本文介绍了Vue的SSR的构建和运行流程,也分析了SSR的特色和利弊,但愿对你们了解SSR有必定的帮助。最后针对不使用vuex的SSR实现方案进行了介绍,若是感兴趣或者有疑问,欢迎你们留言交流。

相关文章
相关标签/搜索