Vue SSR服务端渲染-SRR指南构建
Madman 后端工程师

前言

基于Vue-CLI v4版本构建的SPA项目改造成VueSSR服务端渲染项目。

背景

前段时间对网站项目进行了大的架构重构,上线已经半年了。一直想分享Vue-SSR项目架构,但是一直在忙SSR网站的架构优化迭代。
首先说一下做SSR项目重构的项目目的是什么

  1. 更全面的SEO优化
  2. 网站首屏渲染性能优化
  3. 项目可维护性优化
  4. 项目高可用性优化

技术选型

我们这次选用的是Vue SSR指南自定义搭建Vue-SSR项目架构, 为啥不选用Vue开箱即用的SSR框架Nuxt?

  1. 我们需要更多设置和选择的余地,Nuxt是更高级别的抽象,它为开发者做出了很多选择
  2. 我们想更深度的了解SSR的工作原理
  3. Nuxt根据目录生成动态路由,让人感觉有点不自由(虽然有约束不一定是负面的)

准备工作

需要熟悉的技术栈

  • Node.js
  • Vue
  • Webpack
  • Nginx
  • Redis
  • CF动态CDN

事先阅读一遍相关文档

环境准备

  • node v10以上环境
  • Vue-CLI前端工具

创建SSR架构项目

下面构建的服务端渲染架构图如下图
网站SSR项目架构图

Vue-CLI创建项目

使用vue ui(项目管理界面)或者vue create [options] <app-name> Vue命令创建项目(具体Vue命令介绍使用vue -h命令查看)

Vue ui 管理界面创建项目

执行

1
vue ui


进入http://localhost:8000/project/create 图形界面按步骤创建项目

选择Vue版本&选择路由模式等

Vue 命令创建项目

这里就不详细介绍了,执行vue create [options] <app-name>命令根据提示选项选择需要的配置就好了。或查阅Vue-CLI官方文档
创建项目有哪些可选项可通过vue create --help查看

1
vue create --help
1
vue create hello-world

调整项目目录结构

创建完项目,我们现在得到的是一个Vue单页面应用,要把他改造成服务端渲染项目根据以下步骤调整项目结构

在根目录下新建server目录

server目录功能概述

server主要是负责服务端相关代码的配置,包含了之前单页面npm run serve的本地服务器功能。
主要包括以下主要功能

  1. 服务端路由分配管理
  2. 服务端控制中心
  3. 服务端中间件,全局参数初始化
  4. 服务端开发环境热更新
  5. 服务端渲染以及缓存等

在server目录下面新建目录和文件

在server目录下面新建以下文件夹和文件
.
├── controllers
│ └── appController.js
├── middlewares
│ └── initMiddleware.js
├── routes
│ └── index.js
├── render
│ └── index.js
├── tools
│ ├── utill.js
│ └── cache
│ ├── redisCluster.js
│ └── constants.js
├── app.js
└── setup-dev.js

server目录截图

调整src目录

src目录还是主要负责前端业务逻辑/页面路由/页面组件/子组件/全局状态store等
新增文件夹template,utils。新增文件entry-client.js, entry-server.js
修改main.js代码,接下来很多之前spa的new Vue/new Vuex等代码都会改为函数式return
因为在服务端我们要 避免状态单例(点击查看VueSSR指南文档了解更多)

调整public目录

public目前其实没啥调整的,把你需要的静态资源放public目录就好了,唯一重要的就是要把public目录下面的index.html删掉。
因为服务端渲染的时候用不上这个空壳的index.html,服务端渲染会重新按照对应路由渲染出来对应完整内容的index.html。

修改vue.config.js配置

修改vue.config.js 新增vue.config.cilent.js 和 vue.config.server.js 来区分服务端渲染的打包配置,和客户端的打包配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// vue.config.js
const cilentConfig = require('./vue.config.cilent');
const serverConfig = require('./vue.config.server');
const lessToJs = require('less-vars-to-js')
const fs = require('fs')
const paletteLess = fs.readFileSync('./src/styles/variables.less', 'utf8')
const globalVars = lessToJs(paletteLess, { resolveVariables: true, stripPrefix: true })

// const isProd = process.env.NODE_ENV === 'production'

module.exports = {
outputDir: 'server/public',
configureWebpack: process.env.TARGET_ENV === 'server' ? serverConfig : cilentConfig,
chainWebpack: config => {
const lessRule = config.module.rule('less')
console.log(lessRule)
},
css: {
requireModuleExtension: true,
loaderOptions: {
less: {
globalVars,
modules: false,
javascriptEnabled: true
}
}
},
pwa: {
name: 'vue cli4 ssr',
themeColor: '#ff2556',
appleMobileWebAppStatusBarStyle: 'black-translucent',
workboxOptions: {
swDest: 'vue-cli4-ssr-sw.js',
skipWaiting: true,
clientsClaim: true,
include: [
/js\/main.[0-9A-Za-z]{8,8}.js$/,
/js\/vendor.[0-9A-Za-z]{8,8}.js$/,
/css\/main.[0-9A-Za-z]{8,8}.css$/
],
cacheId: 'vue-cli4-ssr-sw-cache'
}
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// vue.config.cilent.js
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
// const isProd = process.env.NODE_ENV === 'production'

module.exports = {
entry: './src/entry-client.js',
optimization: {
runtimeChunk: {
name: 'manifest',
},
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
},
plugins: [
// This plugins generates `vue-ssr-client-manifest.json` in the
// output directory.
new VueSSRClientPlugin(),
],
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// vue.config.server.js
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = {
entry: './src/entry-server.js',
target: 'node',
devtool: 'source-map',
output: {
libraryTarget: 'commonjs2',
},
externals: nodeExternals({
whitelist: [/\.css$/, /vant\/lib/, /ant-design-vue\/lib/],
}),
optimization: {
splitChunks: false,
},
module: {
rules: [
]
},
// This is the plugin that turns the entire output of the server build
// into a single JSON file. The default file name will be
// `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin(),
],
}

服务端vue router客户端router同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/main.js
import { sync } from 'vuex-router-sync'
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
import createStore from './store'
// import { Sticky } from 'vant'

Vue.config.productionTip = false

// Vue.use(Sticky) // 可以在main.js中全局按需引入,也可以在对应子组件中按需引入

export const createApp = (context) => {
// create router and store instances
const router = createRouter()
const store = createStore()

// sync so that route state is available as part of the store
sync(store, router)

const app = new Vue({
data: { url: context ? context.url : '' },
router,
store,
render: (h) => h(App),
})

return { app, router, store }
}

安装依赖库

最核心就是要选择node服务端的框架 Express

  • express // node服务端框架,当然也可以选择其他node框架,例如koa
  • register-service-worker// PWA注册service worker的插件
  • vue-server-renderer // vue服务端渲染指南里面介绍的必须要用到的库,用户服务端渲染vue组件
  • vuex-router-sync // 同步服务端和客户端的路由
  • webpack-hot-middleware // node端打包排查不必要打包的相关依赖

其他相关依赖查看demo的package.json

Vuex 全局状态

服务端vuex store初始化以及挂载

服务端注入全局状态

1
2
3
// src/entry-server.js
// serialized and injected into the HTML as `window.__INITIAL_STATE__`.
context.rendered = () => context.state = store.state

客户端接管服务端的vuex store并动态注册

客户端接管状态

1
2
3
4
5
// src/entry-client.js
if (window.__INITIAL_STATE__) {
// We initialize the store state with the data injected from the server
store.replaceState(window.__INITIAL_STATE__)
}

环境变量配置

服务端的.env配置跟Vue CLI里面的前端打包.env配置不一样。服务端的env配置的值是不公开的,一般情况下只有运维知道产生环境的.env配置只。
因为服务端的.env配置经常会要配置一些私钥或者一些 Redis集群的链接地址和授权,这些都是不对外公开的。
所以项目根目录下面的 .env要被.gitignore忽略,开发可以提交一个.env.example 环境变量名称和变量用途的例子给运维部署的时候参照。开发环境时大家可以在项目根目录本地创建.env单独配置自己本地环境的环境变量,一般都是测试环境的环境变量值。

总结

服务端渲染主要需要注意以下几点概念

  1. 服务端跟客户端渲染环境不同,容易造成客户端能直接获取的全局变量在服务端获取不到如windows、document
  2. 服务端渲染的数据状态会注入到html的window.INITIAL_STATE 客户端接管的时候会用这个状态数据验证客户端虚拟doc的内容是否和服务端一致,不一致会导致重新渲染甚至报错页面交互卡死。需要注意在客户端mounted什么周期前不要轻易对页面渲染的全局状态数据做修改,不然验证容易导致不一致。
  3. 部分重定向逻辑和前置逻辑默认数据,可以在node服务端新建middlewares目录在下面编写服务端前置中间件逻辑
  4. 验证Network里面的Doc请求是否首屏内容是否真的实现了服务端渲染,服务端渲染的数据需要在serverPrefetch生命周期提前加载好渲染页面相关数据,并挂载在全局状态。

GitHub demo链接

vue-cli4-ssr

  • 本文标题:Vue SSR服务端渲染-SRR指南构建
  • 本文作者:Madman
  • 创建时间:2021-03-18 11:31:27
  • 本文链接:https://www.patpat.site/开发/前端/Vue-SSR服务端渲染-SRR指南构建.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论