原文鏈接:https://blog.csdn.net/ligang2585116/article/details/78533793
當前 SPA 架構流行的趨勢如日中天,但在 SEO 方面好像一直是個痛點,所以衆多流行的 mv* 等框架也爲此痛點提出瞭解決方案。
vue 官方提供了快速構建項目的工具 vue-cli,其方便快捷性衆所周知。本文章來分享一下使用vue cli構建項目後如何集成 SSR(server side render 服務器端渲染),本文主要說明使用兩種
方式來實現SSR的效果。
兩個示例的git地址:
此文章面向對 vue 較爲熟悉以及對 vue-cli 有些許瞭解的同學。
1. 我的環境
- node v8.5.0
- npm v5.0.0
- vue-cli v2.8.2
- MacOS 10.12.3
以上並不是重要因素~~
2. 方式一:使用prerender-spa-plugin插件獲得SSR的效果。
2.1 說明
插件地址:prerender-spa-plugin
嚴格上來說,此種實現方式並非叫做 SSR,而是預渲染。不過效果上是一樣的,甚至某種程度上來說可能要比 SSR 更好。相比官方提供的 SSR 繁瑣配置,prerender 配置更簡單快捷,如無特殊要求只需在 webpack 中加一個 plugin 的配置即可。 但是此方式只支持h5 history
方式的路由,不支持hash
方式的路由。
prerender 主要是利用phantom js
模擬瀏覽器環境,將指定的路由頁面放在 phantom j s中運行,這樣.vue
便會在 phantom 中工作並完成渲染,prerender再去獲取渲染後的dom結構並將其寫入對應路由的 html 文件。
服務啓動後,在真實瀏覽器環境中輸入對應的路由地址,服務器便會將 prerender 已渲染好生成的 html 返回給瀏覽器,從而達到了 SSR 的效果。
2.2 初始化
確保 vue-cli 安裝成功,執行命令:
vue init webpack vue-prerender-demo //此文章都是在webpack基礎上配置的
- 1
回車之後構建工具會提示一些項目信息的相關設置。這裏的我選擇了vue-router
、代碼檢查ESLint
、Standard
,沒有選擇集成測試與單元測試,安裝包太耗時了。
初始化完畢:
cd vue-prerender-demo
npm install
npm run dev
- 1
- 2
- 3
進入項目,安裝完畢,啓動項目,確保項目正常工作。
2.3 配置
爲了方便測試效果,我對初始化好的 demo 做了以下修改:
- 將路由的
mode
修改爲history
- 增加一個Test組件,與Hello組件評級,作爲一個單獨的路由頁面
- 修改
router/index.js
中的配置,增加/test
路由。 - 在Hello組件中加入了
<router-link to="/test">/test</router-link>
, 在Test組件中加入了<router-link to="/">回到首頁</router-link>
如此之後查看頁面,從首頁->Test->首頁
之間跳轉,路由可正常工作。
2.4 開始
那麼接下來即可開始正式工作:
1. 安裝 prerender-spa-plugin, 因爲依賴phantom js
,phantom 的安裝比較蛋疼,太耗時了~
npm install prerender-spa-plugin -D
- 1
2. 開始 prerender 相關的配置:
修改 webpack.prod.conf.js
,只在生產環境進行預渲染。
//引用
var PrerenderSpaPlugin = require('prerender-spa-plugin')
//...
plugins: {
//....
//配置 prerender-spa-plugin
new PrerenderSpaPlugin(
// 生成文件的路徑,此處與webpack打包地址一致
path.join(config.build.assetsRoot), //config.build.assetsRoot爲vue cli生成的配置,打包後的文件地址
// 配置要做預渲染的路由,只支持h5 history方式
[ '/', '/test']
)
//....
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
3. 編譯
運行命令:
npm run build
- 1
等待命令完成後,可以看到 dist 目錄下的文件結構:
相比原配置打包出來的內容多出了一個test目錄,此目錄對應prerender配置中的/test
路由,那麼配置的/
就是dist/index.html
嗎?對的,是這樣。
打開dist/index.html
查看一下內容,此文件內有很多東西,不再是以前孤單單的一個<div id="app"></div>
,現在body裏面的dom結構其實是與在瀏覽器中渲染的/
路徑頁面dom結構是一致的!test/index.html
便是對應訪問/test
渲染後的dom結構。
4. 驗證
可忽略此步驟。 這裏面使用了python作爲快速啓動的server。
爲了在真實環境中確認最終效果是正確的,我在本地使用 python 啓動了一個 http 服務(沒有使用 webpack 與 node 作爲服務)
cd dist //進入到對應目錄
python -m SimpleHTTPServer 8888 //將dist作爲根目錄,啓動8888端口,
- 1
- 2
在瀏覽器中直接輸入localhost:8888/test
,並右鍵選擇查看顯示網頁源代碼
:
在/test
的 response 內容中我們可以看到返回的是渲染之後的 dom 結構,搜索引擎的小蜘蛛可以順利的獲取到內容,從而達到了 SEO 的效果。
2.5 優缺點
優
簡單、易上手、無需配置即可滿足基本的 SEO 要求
缺
只支持h5 history
不適合頻繁變動的頁面,如果這個頁面希望做SEO優化。因爲預渲染只是類似於快照的概念。
此方式實現的 demo 地址: vue-prerender-demo
3. 方式二:使用官方提供的輪子在node端做SSR
3.1 說明
本示例只說明如何完成一個相對基礎的 SSR,
vuex
以及緩存
等可參考官網說明。
官方文檔地址:https://ssr.vuejs.org/zh/
官方文檔開篇便說明 「vue 與 library 的版本最低要求、爲什麼使用 SSR 以及對比上面提到的 prerender-spa-plugin 插件」。
以及如下:
本指南將會非常深入,並且假設你已經熟悉 Vue.js 本身,並且具有 Node.js 和 webpack 的相當不錯的應用經驗。如果你傾向於使用提供了平滑開箱即用體驗的更高層次解決方案,你應該去嘗試使用 Nuxt.js。它建立在同等的 Vue 技術棧之上,但抽象出很多模板,並提供了一些額外的功能,例如靜態站點生成。但是,如果你需要更直接地控制應用程序的結構,Nuxt.js 並不適合這種使用場景。
官方 vue ssr demo : HackerNews Demo
相對於 prerender 插件來對比,SSR 上手複雜度可以用天差地別來形如了吧~~
跟隨本章節可以實現一個基礎版本的 SSR 構建配置。
3.2 約束
如果你打算爲你的vue
項目在node
使用 SSR,那麼在通用代碼
中,我們有必要並且需要遵守下面的這些約定:
通用代碼: 在客戶端與服務器端都會運行的部分爲通用代碼。
- 注意服務端只調用
beforeCreat與created
兩個鉤子,所以不可以做類似於在created
初始化一個定時器
,然後在mounted或者destroyed
銷燬這個定時器
,不然服務器會慢慢的被這些定時器給榨乾了 - 因單線程的機制,在服務器端渲染時,過程中有類似於
單例
的操作,那麼所有的請求都會共享這個單例的操作,所以應該使用工廠函數
來確保每個請求之間的獨立性。 - 如有在
beforeCreat與created
鉤子中使用第三方的API,需要確保該類API在node
端運行時不會出現錯誤,比如在created
鉤子中初始化一個數據請求的操作,這是正常並且及其合理的做法。但如果只單純的使用XHR
去操作,那在node
端渲染時就出現問題了,所以應該採取axios
這種瀏覽器端與服務器端
都支持的第三方庫。 - 最重要一點: 切勿在通用代碼中使用
document
這種只在瀏覽器端可以運行的API,反過來也不可以使用只在node
端可以運行的API。
3.3 準備工作
使用 vue-cli
再次初始化一個項目:
vue init webpack vue-ssr-demo
- 1
然後,
cd vue-ssr-demo
npm install
npm run dev
- 1
- 2
- 3
確保初始化的項目可正常運行,接下來開始慢慢折騰吧~~
3.4 開始折騰
1. 首先安裝 ssr 支持
npm i -D vue-server-renderer
- 1
重要的是 vue-server-renderer 與 vue 版本必須一致匹配
2. 增加路由test
與頁面
隨便寫了個計數器,以驗證服務端渲染時,vue 的機制會正常工作。
<template>
<div>
Just a test page.
<div>
<router-link to="/">Home</router-link>
</div>
<div><h2>{{mode}}</h2></div>
<div><span>{{count}}</span></div>
<div><button @click="count++">+1</button></div>
</div>
</template>
<script>
export default {
data () {
return {
mode: process.env.VUE_ENV === 'server' ? 'server' : 'client',
count: 2
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
3. 在src
目錄下創建兩個js:
src
├── entry-client.js # 僅運行於瀏覽器
└── entry-server.js # 僅運行於服務器
- 1
- 2
- 3
4. 修改router
配置。
無論什麼系統路由
總是最重要的,服務器端渲染自然也要公用一套路由系統,並且爲了避免產生單例
的影響,這裏主要只爲每一個請求都導出一個新的router
實例:
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export function createRouter () {
return new Router({
mode: 'history', // 注意這裏也是爲history模式
routes: [
{
path: '/',
name: 'Hello',
component: HelloWorld
}, {
path: '/test',
name: 'Test',
component: () => import('@/components/Test') // 異步組件
}
]
})
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
5. 改造main.js
main.js
初始化的只適合在瀏覽器的運行,所以要改造兩端都可以使用的文件,同樣爲了避免產生單例
的影響,這裏將導出一個createApp
的工廠函數:
import Vue from 'vue'
import App from './App'
import { createRouter } from './router'
export function createApp () {
// 創建 router 實例
const router = new createRouter()
const app = new Vue({
// 注入 router 到根 Vue 實例
router,
render: h => h(App)
})
// 返回 app 和 router
return { app, router }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
6. entry-client.js
加入以下內容:
import { createApp } from './main'
const { app, router } = createApp()
// 因爲可能存在異步組件,所以等待router將所有異步組件加載完畢,服務器端配置也需要此操作
router.onReady(() => {
app.$mount('#app')
})
- 1
- 2
- 3
- 4
- 5
- 6
7. entry-server.js
// entry-server.js
import { createApp } from './main'
export default context => {
// 因爲有可能會是異步路由鉤子函數或組件,所以我們將返回一個 Promise,
// 以便服務器能夠等待所有的內容在渲染前,
// 就已經準備就緒。
return new Promise((resolve, reject) => {
const { app, router } = createApp()
// 設置服務器端 router 的位置
router.push(context.url)
// 等到 router 將可能的異步組件和鉤子函數解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,執行 reject 函數,並返回 404
if (!matchedComponents.length) {
// eslint-disable-next-line
return reject({ code: 404 })
}
// Promise 應該 resolve 應用程序實例,以便它可以渲染
resolve(app)
}, reject)
})
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
8. webpack
配置
vue
相關代碼已處理完畢,接下來就需要對webpack
打包配置進行修改了。
官網推薦下面這種配置:
build
├── webpack.base.conf.js # 基礎通用配置
├── webpack.client.conf.js # 客戶端打包配置
└── webpack.server.conf.js # 服務器端打包配置
- 1
- 2
- 3
- 4
但vue-cli
初始化的配置文件也有三個:base、dev、prod
,我們依然保留這三個配置文件,只需要增加webpack.server.conf.js
即可。
9. webpack 客戶端的配置
修改webpack.base.conf.js
的entry
入口配置爲: ./src/entry-client.js
。這樣原 dev 配置與 prod 配置都不會受到影響。
服務器端的配置也會引用base配置,但會將
entry
通過merge
覆蓋爲 server-entry.js。
生成客戶端構建清單client manifest
好處:
- 在生成的文件名中有哈希時,可以取代
html-webpack-plugin
來注入正確的資源 URL。- 在通過 webpack 的按需代碼分割特性渲染 bundle 時,我們可以確保對 chunk 進行最優化的資源預加載/數據預取,並且還可以將所需的異步 chunk 智能地注入爲
<script>
標籤,以避免客戶端的瀑布式請求(waterfall request),以及改善可交互時間(TTI - time-to-interactive)。
其實很簡單,在prod
配置中引入一個插件,並配置到plugin
中即可:
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
// ...
// ...
plugins: [
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"' // 增加process.env.VUE_ENV
}),
//...
// 另外需要將 prod 的HtmlWebpackPlugin 去除,因爲我們有了vue-ssr-client-manifest.json之後,服務器端會幫我們做好這個工作。
// new HtmlWebpackPlugin({
// filename: config.build.index,
// template: 'index.html',
// inject: true,
// minify: {
// removeComments: true,
// collapseWhitespace: true,
// removeAttributeQuotes: true
// // more options:
// // https://github.com/kangax/html-minifier#options-quick-reference
// },
// // necessary to consistently work with multiple chunks via CommonsChunkPlugin
// chunksSortMode: 'dependency'
// }),
// 此插件在輸出目錄中
// 生成 `vue-ssr-client-manifest.json`。
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
- 30
10. webpack 服務器端的配置
server的配置有用到新插件運行安裝:
npm i -D webpack-node-externals
webpack.server.conf.js
配置如下:
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 指向應用程序的 server entry 文件
entry: './src/entry-server.js',
// 這允許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動態導入(dynamic import),
// 並且還會在編譯 Vue 組件時,
// 告知 `vue-loader` 輸送面向服務器代碼(server-oriented code)。
target: 'node',
// 對 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 此處告知 server bundle 使用 Node 風格導出模塊(Node-style exports)
output: {
libraryTarget: 'commonjs2'
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化應用程序依賴模塊。可以使服務器構建速度更快,
// 並生成較小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 需要處理的依賴模塊。
// 你可以在這裏添加更多的文件類型。例如,未處理 *.vue 原始文件,
// 你還應該將修改 `global`(例如 polyfill)的依賴模塊列入白名單
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()
]
})
- 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
注意此處對baseConfig刪除了一個屬性
baseConfig.module.rules[1].options = '' // 去除分離css打包的插件
- 1
11. 配置package.json
增加打包服務器端構建命令並修改原打包命令
"scripts": {
//...
"build:client": "node build/build.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
"build": "rimraf dist && npm run build:client && npm run build:server"
}
- 1
- 2
- 3
- 4
- 5
- 6
如果出現
cross-env
找不到,請安裝npm i -D cross-env
12. 修改index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>vue-ssr-demo</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
原來的<div id="app">
刪掉,只在 body 中保留一個標記即可:<!--vue-ssr-outlet-->
。 服務器端會在這個標記的位置自動生成一個<div id="app" data-server-rendered="true">
,客戶端會通過app.$mount('#app')
掛載到服務端生成的元素上,並變爲響應式的。
注意一下,此處將模板 html 修改爲服務端渲染適用的模板了,但項目中的
dev
模式也適用的這個模板,但會因爲找不到#app
到報錯,可以這樣處理一下:
- 最簡單的辦法,爲
dev
模式單獨建立一個 html 模板。。。- 爲
dev
模式也集成服務端渲染模式,這樣無論生產環境與開發環境共同處於服務端渲染模式下也是相當靠譜的一件事。(官方例子是這樣操作的)
13. 運行構建命令
npm run build
- 1
然後在dist
目錄下可見生成的兩個 json 文件: vue-ssr-server-bundle.json
與vue-ssr-client-manifest.json
。
這兩個文件都會應用在 node 端,進行服務器端渲染與注入靜態資源文件。
14. 構建服務器端(官方例子使用的express
,所以此 demo 將採用koa2
來作爲服務器端,當然,無論是 koa 與 express 都不重要…)
npm i -S koa
- 1
在項目根目錄創建server.js
,內容如下
const Koa = require('koa')
const app = new Koa()
// response
app.use(ctx => {
ctx.body = 'Hello Koa'
})
app.listen(3001)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
運行node server.js
,訪問localhost:3001
,確保瀏覽器得到了Hello Koa
。
15. 編寫服務端代碼
需要安裝koa靜態資源中間件:
npm i -D koa-static
server.js
代碼如下:
const Koa = require('koa')
const app = new Koa()
const fs = require('fs')
const path = require('path')
const { createBundleRenderer } = require('vue-server-renderer')
const resolve = file => path.resolve(__dirname, file)
// 生成服務端渲染函數
const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
// 推薦
runInNewContext: false,
// 模板html文件
template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
// client manifest
clientManifest: require('./dist/vue-ssr-client-manifest.json')
})
function renderToString (context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html)
})
})
}
app.use(require('koa-static')(resolve('./dist')))
// response
app.use(async (ctx, next) => {
try {
const context = {
title: '服務端渲染測試', // {{title}}
url: ctx.url
}
// 將服務器端渲染好的html返回給客戶端
ctx.body = await renderToString(context)
// 設置請求頭
ctx.set('Content-Type', 'text/html')
ctx.set('Server', 'Koa2 server side render')
} catch (e) {
// 如果沒找到,放過請求,繼續運行後面的中間件
next()
}
})
app.listen(3001)
- 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
- 45
- 46
運行啓動服務命令:
node server.js
- 1
16. 大功告成
瀏覽器訪問: localhost:3001/test
,截圖爲服務器渲染成功的頁面
在test.vue
中的 data 屬性便已證明服務器端渲染工作是正常的(mode: process.env.VUE_ENV === 'server' ? 'server' : 'client',
),但在客戶端數據混合的時候,mode 是等於 client 的。
3.4 優缺點
優
可以做到真實數據實時渲染,完全可供SEO
小蜘蛛盡情的爬來爬去
完全前後端同構,路由配置共享,不再影響服務器404
請求
缺
依舊只支持h5 history
的路由模式,(沒辦法,哈希就是提交不到服務器能咋辦呢。。。)
配置比較麻煩、處理流程比較複雜 (比對預渲染插件,複雜太多)
約束較多,不能隨心所欲的亂放大招
對服務器會造成較大的壓力,既然讓瀏覽器更快的渲染了,那就得以佔用服務器的性能來買單了