# 服务端渲染

# 介绍

服务端渲染主要解决两个问题:首屏加载和SEO。首屏加载变快其原理就是返回给用户的HTML已包含当前页面的DOM节点,页面只需要加载完成CSS文件就可以进行渲染,而SPA应用中页面的DOM节点必须加载完Vue运行时代码之后才能进行创建,最后再和CSSOM树进行结合,对页面进行渲染,这之中相差的就是JS文件的加载以及Vue创建DOM节点的时间。

# 基本用法

基本用法可以参照Vue SSR指南 (opens new window)。建议对基本用法实现一遍,然后对其他章节做一定了解。

# 构建配置

# 关于externals

以下构建配置主要介绍如何结合第三方UI库如antd进行配置,基于官方文档的构建配置 (opens new window)

服务端配置文件中修改:

  externals: nodeExternals({
    whitelist: /ant-design-vue\/lib\/(.)+\/style\/css/
  })

这种配置是基于使用了ant-design-vue文档中推荐的使用babel-plugin-import插件进行按需加载的情况,因为使用babel-plugin-import按需加载antd时,大概原理是这样:

import { DatePicker } from 'ant-design-vue';

=> 转换
import DatePicker from 'ant-design-vue/lib/date-picker'
import 'ant-design-vue/lib/date-picker/style/css';

这里的'ant-design-vue/lib/date-picker/style/css'是一个js文件,并且这个是node_modules里的,如果whitelist是/\.css$/,那么就不会处理css.js内引入的css文件,导致服务端运行失败。

# 关于CSS提取

对于CSS,最好选用提取,否则antd中的css会全部内联进html中,访问不同页面就会带来冗余,并且内联了antd的css的html文件会很大,提取还能对css文件做出更好的缓存。

需要注意:

  • 服务端不进行提取,否则mini-css-extract-plugin致使服务端运行报错(document在服务端不存在)。
  • 服务端不使用vue-style-loader,否则服务端仍然会将antd的css内联进html中,并且会带上link标签引入antd css,导致重复。
  • 要开启css sourcemap,有了sourcemap,服务端bundle进行renderToString时才能够自动推到css文件的资源注入。

# 开发环境下服务端的处理

开发环境主要的事:文件变动时重新进行服务端webpack的打包。

const axios = require('axios')
const path = require('path')
const MemoryFS = require('memory-fs')
const webpack = require('webpack')
const serverConfig = require('@vue/cli-service/webpack.config')
const Router = require('koa-router')
const { createBundleRenderer } = require('vue-server-renderer')
const {
  resolve
} = require('../utils/utils')

const ssrRouter = new Router()

const template = require('fs').readFileSync(resolve('../../public/index-server.html'), 'utf-8')

const serverCompiler = webpack(serverConfig)
const mfs = new MemoryFS()
serverCompiler.outputFileSystem = mfs

let serverBundle
serverCompiler.watch({}, () => {
  const bundlePath = path.join(
    serverConfig.output.path,
    'vue-ssr-server-bundle.json'
  )
  serverBundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'))
  console.log('new bundle generated')
})

ssrRouter.get('*', async ctx => {
  console.log(ctx.url)
  if (!serverBundle) {
    ctx.body = 'wait a minute'
    return
  }
  const context = { url: ctx.req.url }
  const clientManifestResp = await axios.get(
    'http://localhost:8080/vue-ssr-client-manifest-legacy.json'
  )
  let clientManifest = clientManifestResp.data
  const renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest
  })
  try {
    const html = await renderer.renderToString(context)
    ctx.body = html
  } catch (err) {
    ctx.body = 'no response'
  }
})

module.exports = ssrRouter

serverConfig的require路径下的文件是Vue CLI提供的,具体见Vue CLI webpack配置文件 (opens new window)。这个配置文件就相当于打包时的配置文件,就能生成serverCompiler,这个comipler使用的文件系统是虚拟文件系统,也就是构建出来的文件放在内存中,这个和webpack dev server是一样的。接下来就是路由响应的逻辑,其使用的clientManifest(可见Vue SSR文档中介绍,用于想html模板自动注入资源)是来自webpack dev server中构建出来的clientManifest,这个通过axios来获取,然后服务端下注入的资源都会来自webpack dev server中,并且还有热更新的效果。

上面的clientManifest路径是'http://localhost:8080/vue-ssr-client-manifest-legacy.json',因此客户端配置文件的publicPath需要做出改变:

// vue.config.js

//...
module.exports = {
    publicPath: isProd ? '/' : 'http://localhost:8080/',
    devServer: {
        port: 8080,
        headers: { 'Access-Control-Allow-Origin': '*' },
        historyApiFallback: {
          rewrites: [
            { from: /.*/, to: '/index.html' }
          ]
        }
  	}
    // ...
}

上面还配置了webpack dev server的headers字段,用于处理跨域问题,因为服务端启的端口(假设:3000)和客户端不同(:8080)。

let proxy;
if (isDev) {
  proxy = require('koa-proxy')
  app.use(proxy({
    host: 'http://localhost:8080/',
    match: /(serviceworker)|(static)|(dll)|(workbox)/
  }))
}

服务端在开发环境下还要设置代理,因为服务端通过webpack配置是没有处理serviceWorker、workbox相关文件的,另外static路径的文件也要代理是因为希望serviceWorker预缓存的资源不是cors类型的,关于cors类型对workbox的影响,详见workbox文档 (opens new window)

# 最后

服务端渲染最困难的地方是对其原理的认识、webpack环境的配置,你必须对相关webpack loader和webpack打包有个清楚的认识。