VUE SEO方案二 - SSR服務端渲染
在上一章中,我們分享了預渲染的方案來解決SEO問題,個人還是很中意此方案的,既簡單又能解決大部分問題。但是也有着一定的缺陷,所以我們繼續來看下一個方案--服務端渲染。
1.概述
服務端渲染的配置相比預渲染就復雜多了,要做到同構,還要保證服務端和客服端的組件狀態一致,我們需要對整個項目進行改造。大部分的內容官方文檔中都說明的比較清楚,這里就不重復講述了,需要各位花費一些時間照着文檔一步步改造項目。
本人一開始也是這樣照着文檔做的,但是改造到最后,發現文檔一點不走心啊,本人用官方的@vue/cli創建的項目,一步步走到最后根本跑不起來,要么服務端各種報錯,要么客服端各種渲染失敗。
所以這邊主要講述在按照文檔改造之后還要做的一些配置和其他問題。若有不實之處請大家指正。
2.簡單原理
原理很簡單,同一套代碼,根據服務端和客戶端的需求不同,分為兩個入口,分別打包出兩套邏輯一樣的代碼包。分別在服務器與客戶端運行。
本例中,服務端使用node的express
框架搭建。
3.開始配置
首先,需要保證服務端同樣安裝和開發環境同樣的node_modules,因為服務端打包,並沒有將所有的第三方包與組件打包到一起,也沒有那個必要。在服務器端安裝好就行了,服務端會自行調用,否則服務端會報找不到第三方插件的錯誤,如:
1Error: Cannot find module 'vuex'
服務端渲染的核心插件vue-server-renderer
,安裝:
1npm i -D vue-server-renderer
2npm i -g cross-env
全局安裝cross-env
使得我們可以在package.json
中添加服務端入口打包命令
1"scripts": {
2 "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"
3}
這樣我們可以將參數WEBPACK_TARGET=node代入打包進程中,用以識別打包目標,也可以將客戶端打包與此命令使用&&連接,一起執行。
打包配置大致如下
1// vue.config.js
2const path = require('path')
3const resolve = dir => path.join(__dirname, dir)
4const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
5const target = TARGET_NODE ? 'server' : 'client'
6const nodeExternals = require('webpack-node-externals')
7const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
8const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
9
10module.exports = {
11 // 將server-bundle.json與模板一起放在服務器,靜態資源則打包到cdn中
12 outputDir: TARGET_NODE? resolve('dist') : resolve('../../www/statics/m/first-aid/'),
13 ...
14 chainWebpack: config => {
15 ...
16 // 兩個入口
17 config.entry('app')
18 .clear()
19 .add(`./src/entry-${target}.js`)
20 config.target(TARGET_NODE ? 'node' : 'web')
21 config.output
22 .libraryTarget(TARGET_NODE ? 'commonjs2' : undefined)
23 if(TARGET_NODE){
24 config.externals(nodeExternals({
25 allowlist: [/\.css$/]
26 }))
27 config.optimization
28 .splitChunks(false)
29 config.module
30 .rule('vue')
31 .use('vue-loader')
32 .tap(options => {
33 options.optimizeSSR = false
34 return options
35 })
36 }
37 config.plugin('vue-server-renderer')
38 .use(TARGET_NODE ? VueSSRServerPlugin : VueSSRClientPlugin)
39 ...
40 },
41 css: {
42 sourceMap: true
43 }
44 ...
45}
其中與官方文檔不同的幾處:
nodeExternals
配置的whitelist屬性是舊版的,新版要用allowlist- 我們需要關閉
vue-loader
的optimizeSSR屬性,否則你將遇到下面這樣的錯誤,具體原因可查詢 vue-loader文檔 的相關部分
- 服務端打包的時候,你可能也會遇到這樣的錯誤:
1(node:18696) UnhandledPromiseRejectionWarning: Error: Server-side bundle should have one single entry file.
2Avoid using CommonsChunkPlugin in the server config.
這時還需要關閉config.optimization.splitChunks
,看來服務端渲染的時候對分包不是很友好,不過這也沒有必要,所有的包都在服務器本地,當然不需要分包來優化下載。
- 關於
vue-server-renderer/client-plugin
插件還有一個bug, 查看gitHub issues,需要設置css: {sourceMap: true}
,否則服務端將會報以下錯誤導致不能渲染成功。

4. 服務器配置
1// server.js
2const express = require("express")
3const path = require("path")
4const resolve = dir => path.join(__dirname, dir)
5const fs = require("fs")
6const jsdom = require('jsdom')
7const { JSDOM } = jsdom
8
9const { createBundleRenderer } = require('vue-server-renderer')
10const serverBundle = require(resolve('dist/vue-ssr-server-bundle.json'))
11const clientManifest = require('../../www/statics/m/first-aid/vue-ssr-client-manifest.json')
12const renderer = createBundleRenderer(serverBundle, {
13 runInNewContext: false,
14 template: fs.readFileSync(resolve('template.html'), 'utf-8'),
15 clientManifest
16})
17
18const app = express()
19// 靜態資源
20app.use(express.static(resolve('../../www')))
21
22app.use((req, res) => {
23 const context = { url: req.url }
24 const resourceLoader = new jsdom.ResourceLoader({
25 userAgent: req.headers['user-agent'],
26 });
27 const dom = new JSDOM('', {
28 url: req.protocol+'://'+req.hostname,
29 resources: resourceLoader
30 });
31 global.window = dom.window
32 global.document = window.document
33 global.navigator = window.navigator
34 window.nodeis = true
35 window.scrollTo = (x, y) => {
36 document.documentElement.scrollTop = y;
37 }
38 renderer.renderToString(context, (err, html) => {
39 if (err) {
40 console.log(err)
41 if (err.code === 404) res.status(404).end('Page not found')
42 else res.status(500).end('Internal Server Error')
43 } else res.end(html)
44 })
45})
46
47app.listen(80)
這邊與官方文檔差別不大,但是使用jsdom插件解決了不存在document全局變量的問題。當項目中用到像window這樣的頂層對象時,服務器因為不存在此變量報錯
document is not defined
錯誤,導致渲染失敗安裝jsdom插件,使用請求來源客服端的UA創建虛擬DOM,從而模擬出頂級對象下的各屬性與方法
5.最后
經過官方文檔的改造,再通過這些重新配置后,一個服務端渲染的@vue/cli項目總算是運行正常了,再通過與vue-meta
的配合,設置返回頁面的title與description等,就達到我們解決SEO的目的。邊角細節的配置就需要各位再慢慢摸索了。
