構建服務器端渲染(SSR)咱們須要藉助vue-server-renderer
,咱們先嚐試一下官方文檔的一段demo,編寫server.jsjavascript
// 第 1 步:建立一個 Vue 實例
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello World</div>`
})
// 第 2 步:建立一個 renderer
const renderer = require('vue-server-renderer').createRenderer()
// 第 3 步:將 Vue 實例渲染爲 HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
// => <div data-server-rendered="true">Hello World</div>
})
複製代碼
執行node server.js
能夠看到控制檯打印<div data-server-rendered="true">Hello World</div>
css
從這段代碼咱們應該能夠明白vue-server-renderer
的做用是拿到vue實例並渲染成html結構,但它不只僅只作着一件事,後面會介紹其餘配置參數和配合webpack進行構建。html
拿到html結構渲染到頁面上是咱們接下來要作的事情,這裏官方事例用的是express搭建服務器,我這裏採用Koa,爲何用Koa?我不會express🤣。Koa起一個服務很是簡單,咱們還須要藉助Koa-router來作路由的處理。修改server.js前端
const Vue = require('vue')
const Koa = require('koa')
const Router = require('koa-router')
const renderer = require('vue-server-renderer').createRenderer()
// 第 1 步:建立koa、koa-router 實例
const app = new Koa()
const router = new Router()
// 第 2 步:路由中間件
router.get('*', async (ctx, next) => {
// 建立Vue實例
const app = new Vue({
data: {
url: ctx.url
},
template: `<div>訪問的 URL 是: {{ url }}</div>`
})
// 有錯誤返回500,無錯誤返回html結構
try {
const html = await renderer.renderToString(app)
ctx.status = 200
ctx.body = ` <!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body> </html> `
} catch (error) {
console.log(error)
ctx.status = 500
ctx.body = 'Internal Server Error'
}
})
app
.use(router.routes())
.use(router.allowedMethods())
// 第 3 步:啓動服務,經過http://localhost:3000/訪問
app.listen(3000, () => {
console.log(`server started at localhost:3000`)
})
複製代碼
從上段代碼咱們就能夠看出服務器端渲染的基本原理了,其實說白了,無服務器端渲染時,前端打包後的html只是包含head部分,body部分都是經過動態插入到id爲#app
的dom中。如圖:vue
而服務器端渲染(SSR)就是服務器來提早編譯Vue生成HTML返回給web瀏覽器,這樣網絡爬蟲爬取的內容就是網站上全部可呈現的內容。🤓java
爲了能夠個性化頁面,咱們能夠把html結構抽成一個模板template,經過雙花括號{{}}
進行傳值,新建index.template.html
按照官網編寫以下代碼node
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 三花括號不進行html轉義 -->
{{{ meta }}}
<title>{{ title }}</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
複製代碼
咱們須要經過Node模塊fs
讀取模板,做爲vue-server-renderer
的template參數傳入,修改代碼:webpack
const renderer = require('vue-server-renderer').createRenderer({
// 讀取傳入template參數
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
// ...忽略無關代碼
router.get('*', async (ctx, next) => {
// title、meta會插入模板中
const context = {
title: ctx.url,
meta: ` <meta charset="UTF-8"> <meta name="descript" content="基於webpack、koa搭建的SSR"> `
}
try {
// 傳入context渲染上下文對象
const html = await renderer.renderToString(app, context)
ctx.status = 200
// 傳入了template, html結構會插入到<!--vue-ssr-outlet-->
ctx.body = html
} catch (error) {
ctx.status = 500
ctx.body = 'Internal Server Error'
}
})
// ...忽略無關代碼
複製代碼
能夠看到咱們的標題和meta都被插入啦!👏👏👏。到這裏,咱們才實現了最基本的用法,接下來咱們終於要使用webpack來構建咱們項目。git
Node.js服務器是一個長期運行的進程、當咱們的代碼進入該進程時,它將進行一次取值並留存在內存中。這意味着若是建立一個單例對象,它將在每一個傳入的請求之間共享,因此咱們須要爲每一個請求建立一個新的根 Vue 實例github
不只vue實例,接下來要用到的vuex、vue-router也是如此。咱們利用webpack須要分別對客戶端代碼和服務器端代碼分別打包, 服務器須要「服務器 bundle」而後用於服務器端渲染(SSR),而「客戶端 bundle」會發送給瀏覽器,用於混合靜態標記。這裏貼一下官方構建圖:
咱們能夠大體的理解爲服務器端、客戶端經過倆個入口Server entry
、 Clinet entry
獲取源代碼,再經過webpack打包變成倆個bundlevue-ssr-server-bundle.json
、vue-ssr-client-manifest.json
,配合生成完成HTML,而app.js
是倆個入口通用的代碼部分,其做用是暴露出vue實例。因此咱們能夠按照官方建議整理文件目錄,並按照官方事例代碼編寫,其中起服務的server.js
咱們用的是Koa,因此能夠先不用改。
上面代碼須要注意的是entry-server.js,它是暴露出一個函數,接受渲染上下文context參數,而後根據url匹配組件。因此說參數須要在咱們調用renderToString
傳入context,幷包括url屬性。
生成的倆個bundle實際上是做爲參數傳入到createBundleRenderer()
函數中,而後在renderToString變成html結構,與createRenderer
不一樣的是前者是經過bundle參數獲取vue組件編譯,後者是須要在renderToString
時傳入vue實例👉文檔。咱們先編寫webpack成功生成bundle後,再去編寫server.js,這樣有利於咱們更好的理解和測試。
首先咱們創建build文件夾,用於存放webpack相關配置,在vue-cli3以前,vue init 初始化後的項目都是有build文件夾的,能夠清楚看到webpack配置。而vue-cli3後,使用webpack4,並將配置隱藏了起來,若是想了解webpack4構建vue單頁面應用能夠去個人github上查看👉地址。咱們能夠模仿vue-cli,建立通用配置webpack.base.conf.js、客戶端配置webpack.client.conf.js、服務端配置webpack.server.conf.js。文件目錄爲
├── build
│ ├── webpack.base.conf.js # 基本webpack配置
│ ├── webpack.client.conf.js # 客戶端webpack配置
│ └── webpack.server.conf.js # 服務器端webpack配置
├── src
├── index.template.html
└── server.js
複製代碼
webpack.base.conf.js
配置主要定義通用的rules,例如vue-loader對.vue文件編譯,對js文件babel編譯,處理圖片、字體等。其基本配置以下:
const path = require('path')
// vue-loader v15版本須要引入此插件
const VueLoaderPlugin = require('vue-loader/lib/plugin')
// 用於返回文件相對於根目錄的絕對路徑
const resolve = dir => path.posix.join(__dirname, '..', dir)
module.exports = {
// 入口暫定客戶端入口,服務端配置須要更改它
entry: resolve('src/entry-client.js'),
// 生成文件路徑、名字、引入公共路徑
output: {
path: resolve('dist'),
filename: '[name].js',
publicPath: '/'
},
resolve: {
// 對於.js、.vue引入不須要寫後綴
extensions: ['.js', '.vue'],
// 引入components、assets能夠簡寫,可根據須要自行更改
alias: {
'components': resolve('src/components'),
'assets': resolve('src/assets')
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
// 配置哪些引入路徑按照模塊方式查找
transformAssetUrls: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
},
{
test: /\.js$/, // 利用babel-loader編譯js,使用更高的特性,排除npm下載的.vue組件
loader: 'babel-loader',
exclude: file => (
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
)
},
{
test: /\.(png|jpe?g|gif|svg)$/, // 處理圖片
use: [
{
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/img/[name].[hash:7].[ext]'
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, // 處理字體
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/fonts/[name].[hash:7].[ext]'
}
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
複製代碼
webpack.client.conf.js
主要是對客戶端代碼進行打包,它是經過webpack-merge
實現對基礎配置的合併,其中要實現對css樣式的處理,此處我用了stylus,同時要下載對應的stylus-loader來處理。在這裏咱們先不考慮開發環境,後面會針對開發環境對webpack進行修改。
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
// css樣式提取單獨文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 服務端渲染用到的插件、默認生成JSON文件(vue-ssr-client-manifest.json)
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseWebpackConfig, {
mode: 'production',
output: {
// chunkhash是根據內容生成的hash, 易於緩存,
// 開發環境不須要生成hash,目前先不考慮開發環境,後面詳細介紹
filename: 'static/js/[name].[chunkhash].js',
chunkFilename: 'static/js/[id].[chunkhash].js'
},
module: {
rules: [
{
test: /\.styl(us)?$/,
// 利用mini-css-extract-plugin提取css, 開發環境也不是必須
use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader']
},
]
},
devtool: false,
plugins: [
// webpack4.0版本以上採用MiniCssExtractPlugin 而不使用extract-text-webpack-plugin
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash].css',
chunkFilename: 'static/css/[name].[contenthash].css'
}),
// 當vendor模塊再也不改變時, 根據模塊的相對路徑生成一個四位數的hash做爲模塊id
new webpack.HashedModuleIdsPlugin(),
new VueSSRClientPlugin()
]
})
複製代碼
編寫完,咱們須要在package.json定義命令來執行webpack打包命令。若是沒有該文件,須要經過npm init
初始化生成
// package.json
"scripts": {
"build:client": "webpack --config build/webpack.client.conf.js", # 打包客戶端代碼
"build:server": "webpack --config build/webpack.server.conf.js", # 打包服務端代碼
"start": "node server.js" # 啓動服務
}
複製代碼
咱們如今能夠經過npm run build:client
執行打包命令,執行命令以前要把依賴的npm包下載好,目前所須要到的依賴見下圖:
當打包命令執行完畢後,咱們會發現多了一個dist文件夾,其中除了靜態文件之外,生成了用於服務端渲染的JSON文件:vue-ssr-client-manifest.json。
同理,咱們須要編寫服務端webpack配置,一樣打包生成vue-ssr-server-bundle.json。配置代碼以下:
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseWebpackConfig = require('./webpack.base.conf')
const VueServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseWebpackConfig, {
mode: 'production',
target: 'node',
devtool: 'source-map',
entry: path.join(__dirname, '../src/entry-server.js'),
output: {
libraryTarget: 'commonjs2',
filename: 'server-bundle.js',
},
// 這裏有個坑... 服務端也須要編譯樣式,但不能使用mini-css-extract-plugin,
// 由於它會使用document,但服務端並沒document,致使打包報錯。詳情見
// https://github.com/webpack-contrib/mini-css-extract-plugin/issues/48#issuecomment-375288454
module: {
rules: [
{
test: /\.styl(us)?$/,
use: ['css-loader/locals', 'stylus-loader']
}
]
},
// 不要外置化 webpack 須要處理的依賴模塊
externals: nodeExternals({
whitelist: /\.css$/
}),
plugins: [
new webpack.DefinePlugin({
'process.env.VUE_ENV': '"server"'
}),
// 默認文件名爲 `vue-ssr-server-bundle.json`
new VueServerPlugin()
]
})
複製代碼
同上,咱們執行命令後發現dist文件下生成vue-ssr-server-bundle.json,咱們能夠新建build
命令來一塊兒執行打包。
好了,如今咱們能夠修改咱們的server.js來實現整個服務器端渲染流程。咱們須要獲取倆個JSON文件、html模板做爲參數傳入createBundleRenderer
,vue實例再也不須要,context須要url,由於服務端端入口(entry-server.js) 須要獲取訪問的路徑來匹配對應的vue組件(上面提到過)。部分改動代碼以下:
/* 將createRenderer替換成createBundleRenderer,不一樣之處在上面提到過... */
const { createBundleRenderer } = require('vue-server-renderer')
// ...忽略無關代碼
// 獲取客戶端、服務器端生成的json文件、html模板文件
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const template = require('fs').readFileSync('./index.template.html', 'utf-8')
// 傳入 json文件和template, 渲染上下文url須要傳入,服務端須要匹配路由
router.get('*', async (ctx, next) => {
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false, // 推薦
template, // 頁面模板
clientManifest // 客戶端構建 manifest
})
const context = {
url: ctx.url,
// ...
}
// ...忽略無關代碼
複製代碼
改動後,咱們運行npm run start
,發現頁面已經成功渲染出來,但這時有個問題,加載的資源都失敗了,文件存在於dist中,很顯然,必定是路徑不對致使的。這時咱們能夠經過koa-send來實現靜態資源的發送。咱們須要在server.js中加入這行代碼:
const send = require('koa-send')
// 引入/static/下的文件都經過koa-send轉發到dist文件目錄下
router.get('/static/*', async (ctx, next) => {
await send(ctx, ctx.path, { root: __dirname + '/dist' });
})
複製代碼
再從新運行,打開控制檯能夠看到資源加載成功,而且加載的doc裏面包含頁面上全部內容。👏
咱們跑通了基本的服務端渲染流程,但尚未涉及到異步數據、緩存等問題。在此以前,咱們須要先實現開發環境的搭建,由於咱們不可能敲的每一行代碼都須要從新打包並起服務。這是不利於調試的。而且很🐷。
想想vue-cli構建出來的項目,咱們能夠經過npm run dev
(vue-cli3使用了npm run serve
)起一個服務,而後更改文件的時候,頁面也會自動的熱加載,不須要手動刷新。咱們也要實現一個相似的開發環境,因此咱們須要利用node來構建webpack配置,而且實時監控文件的改變,當改變時應該從新進行打包,從新生成倆個JSON文件,並從新進行BundleRenderer.renderToString()
方法。咱們除了從新生成JSON文件意外,其餘邏輯和以前實現的邏輯大致相同。因此咱們能夠在server.js基礎上進行修改,在原基礎上進行環境的判斷,作不一樣的render
。咱們須要一個環境變量來決定執行哪一個邏輯。
這裏咱們使用cross-env
來設置process.env.NODE_ENV
變量:
咱們把build、start命令都設置了process.env.NODE_ENV
爲production生產環境,這樣咱們在文件中能夠獲取到該值,若是沒有咱們就默認是development開發環境。那咱們的server.js都須要修改哪裏呢?
BundleRenderer
實例,以前咱們是經過固定路徑(打包後的dist文件夾下)獲取JSON文件// 以前代碼邏輯
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const template = require('fs').readFileSync('./index.template.html', 'utf-8')
//...忽略無關代碼
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template, // 頁面模板
clientManifest // 客戶端構建 manifest
})
複製代碼
咱們須要按照環境變量更改邏輯,若是是生產環境上述代碼不變,若是是開發環境,咱們須要有一個函數來動態的獲取打包的JSON文件而且從新生成BundleRenderer
實例,咱們先定義好這個函數爲setupDevServer
,顧名思義這個函數是構建開發環境的,它的做用是nodeAPI構建webpack配置,而且作到監聽文件。咱們server.js中能夠經過傳遞個回調函數來作從新生成BundleRenderer
實例的操做。而接受的參數就是倆個新生成的JSON文件。
// 假設已經實現
const setupDevServer = require('./build/setup-dev-server')
// 生成實例公共函數,開發、生產環境只是傳入參數不一樣
const createBundle = (bundle, clientManifest) => {
return createBundleRenderer(bundle, {
runInNewContext: false,
template,
clientManifest
})
}
let renderer // 將實例變量提到全局變量,根據環境變量賦值
const template = require('fs').readFileSync('./index.template.html', 'utf-8') // 模板
// 第 2步:根據環境變量生成不一樣BundleRenderer實例
if (process.env.NODE_ENV === 'production') {
// 獲取客戶端、服務器端打包生成的json文件
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
// 賦值
renderer = createBundle(serverBundle, clientManifest)
// 靜態資源,開發環境不須要指定
router.get('/static/*', async (ctx, next) => {
console.log('進來')
await send(ctx, ctx.path, { root: __dirname + '/dist' });
})
} else {
// 假設setupDevServer已經實現,並傳入的回調函數會接受生成的json文件
setupDevServer(app, (bundle, clientManifest) => {
// 賦值
renderer = createBundle(bundle, clientManifest)
})
}
複製代碼
const setupDevServer = require('./build/setup-dev-server')
// 第 2步:根據環境變量生成不一樣BundleRenderer實例
if (process.env.NODE_ENV === 'production') {
// 獲取客戶端、服務器端打包生成的json文件
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
// 賦值
renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest
})
// 靜態資源,開發環境不須要指定
router.get('/static/*', async (ctx, next) => {
console.log('進來')
await send(ctx, ctx.path, { root: __dirname + '/dist' });
})
} else {
// 假設setupDevServer已經實現,並傳入的回調函數會接受生成的json文件
setupDevServer(app, (bundle, clientManifest) => {
// 賦值
renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template,
clientManifest
})
})
}
複製代碼
這裏咱們先假設已實現setupDevServer的功能,後面咱們再來仔細講其中的代碼邏輯。 咱們能夠在判斷生產環境的地方加上log,打印一下是否如咱們所願,針對不一樣的NODE_ENV環境執行不一樣的邏輯。
在以前,咱們實現的webpack配置並無對生產環境與開發環境作區別,但其實,咱們應該像vue-cli同樣針對環境來作不一樣的優化,好比開發環境devtool咱們可使用cheap-module-eval-source-map
編譯會更快,css樣式沒有必要打包單獨文件,使用vue-style-loader
作處理就好,而且由於開發環境須要模塊熱重載,因此不提取文件是必要的。開發環境能夠作更友好的錯誤提示。還有就是生產環境須要作更多的打包優化,好比壓縮,緩存之類。在這個系列文章中,咱們就不對生產環境作更好的優化,由於我本身對這方面知識也是很懵懂😑。咱們先修改webpack.base.conf.js:
// ...
// 定義是不是生產環境的標誌位,用於配置中
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
// 這裏使用對象的格式,由於在setDevServer.js中須要添加一個熱重載的入口
entry: {
app: resolve('src/entry-client.js')
},
// 開發環境啓動sourcemap能夠更好地定位錯誤位置
devtool: isProd
? false
: 'cheap-module-eval-source-map',
// ...... 省略
}
複製代碼
咱們在對webpack.client.conf.js進行修改:
// 定義是不是生產環境的標誌位,用於配置中
const isProd = process.env.NODE_ENV === 'production'
const pordWebpackConfig = merge(baseWebpackConfig, {
mode: process.env.NODE_ENV || 'development',
output: {
// chunkhash是根據內容生成的hash, 易於緩存。
// 開發環境不須要生hash、這個咱們在setDevServer函數裏面改
filename: 'static/js/[name].[chunkhash].js',
chunkFilename: 'static/js/[id].[chunkhash].js'
},
module: {
rules: [
{
test: /\.styl(us)?$/,
// 開發環境不須要提取css單獨文件
use: isProd
? [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader']
: ['vue-style-loader', 'css-loader', 'stylus-loader']
},
]
},
// ... 省略
}
複製代碼
關於服務器端webpack的配置能夠不進行修改,由於它的功能最後只打包出一個JSON文件,並不須要針對環境作一些改變。
好的,接下來咱們要編寫set-dev-server.js,setDevServer函數主要是利用webpack手動構建應用,並實現熱加載。首先咱們須要倆箇中間件koa-webpack-dev-middleware
和koa-webpack-hot-middleware
,前者是經過傳入webpack編譯好的compiler實現熱加載,然後者是實現模塊熱更替,熱加載是監聽文件變化,從而進行刷新網頁,模塊熱更替則在它的基礎上作到不須要刷新頁面。咱們客戶端webpack配置能夠經過前面說的實現自動更新,而服務端compiler,咱們經過watch
API,進行監聽。當倆者其中有一個變化時,咱們就須要調用傳入的回調,將新生成的JSON文件傳入。整個流程大體就是這樣,具體代碼以下:
const fs = require('fs')
const path = require('path')
// memory-fs可使webpack將文件寫入到內存中,而不是寫入到磁盤。
const MFS = require('memory-fs')
const webpack = require('webpack')
const clientConfig = require('./webpack.client.conf')
const serverConfig = require('./webpack.server.conf')
// webpack熱加載須要
const webpackDevMiddleware = require('koa-webpack-dev-middleware')
// 配合熱加載實現模塊熱替換
const webpackHotMiddleware = require('koa-webpack-hot-middleware')
// 讀取vue-ssr-webpack-plugin生成的文件
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
} catch (e) {
console.log('讀取文件錯誤:', e)
}
}
module.exports = function setupDevServer(app, cb) {
let bundle
let clientManifest
// 監聽改變後更新函數
const update = () => {
if (bundle && clientManifest) {
cb(bundle, clientManifest)
}
}
// 修改webpack配合模塊熱替換使用
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
clientConfig.output.filename = '[name].js'
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
// 編譯clinetWebpack 插入Koa中間件
const clientshh = webpack(clientConfig)
const devMiddleware = webpackDevMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
noInfo: true
})
app.use(devMiddleware)
clientCompiler.plugin('done', stats => {
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
if (stats.errors.length) return
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem,
'vue-ssr-client-manifest.json'
))
update()
})
// 插入Koa中間件(模塊熱替換)
app.use(webpackHotMiddleware(clientCompiler))
const serverCompiler = webpack(serverConfig)
const mfs = new MFS()
serverCompiler.outputFileSystem = mfs
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
if (stats.errors.length) return
// vue-ssr-webpack-plugin 生成的bundle
bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
update()
})
}
複製代碼
咱們用到了memory-fs
將生成的JSON文件寫入內存中,而不是磁盤中,是爲了更快的讀寫。客戶端不須要是由於webpack-dev-middleware
已經幫咱們完成了。這就是爲何咱們在開發環境並有dist文件夾生成。咱們如今能夠經過npm run dev
訪問localhost:3000,更改代碼,能夠實現熱加載。
在服務器端渲染(SSR)期間,咱們本質上是在渲染咱們應用程序的"快照",因此若是應用程序依賴於一些異步數據,那麼在開始渲染過程以前,須要先預取和解析好這些數據。
正如官方文檔解釋的,SSR本質上就是先執行應用程序並返回HTML,因此咱們須要服務端處理數據,客戶端與之同步。數據預取官方文檔實例代碼很詳細,咱們照着實現一下便可。這裏不得不說,vue生態的文檔一貫都是很友好。而且都配有中文文檔,對於跟我同樣的小白來講,真是太好不過啦🙈
咱們像官網同樣引入vuex
對編寫示例代碼,並進行修改。修改store/index.js
// ...
export function createStore() {
return new Vuex.Store({
state: {
movie: {}
},
actions: {
// 經過傳入id請求電影數據,這裏咱們模擬一下,先返回id
fetchMovie({ commit }, id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id })
}, 500)
}).then(res => {
commit('setMoive', { res })
})
}
},
mutations: {
// 設置state
setMoive(state, { res }) {
state.movie = res
}
}
})
}
複製代碼
修改A.vue
<template>
<div>
A頁 請求電影數據結果:{{ this.$store.state.movie }}
</div>
</template>
<script>
export default {
name: 'A',
// 定義asyncData, entry-server.js會編譯全部匹配的組件中是否包含,包含則執行
// 將state值掛在到context上,會被序列化爲window.__INITIAL_STATE__
//
asyncData ({ store, route }) {
// 請求電影數據, 傳入 ID : 12345
return store.dispatch('fetchMovie', 12345)
},
}
</script>
<style lang="stylus" scoped>
h1
color blue
</style>
複製代碼
服務端預取的原理就是,經過在組件內定義asyncData函數用於異步請求,在entry-server.js服務端中遍歷全部匹配到的組件,若是包含asyncData則執行,並將state掛載到context上下文,vue-server-renderer
會將state序列化爲window.__ INITIAL_STATE __,這樣,entry-client.js客戶端就能夠替換state,實現同步。咱們運行代碼,打開瀏覽器會看到
由於入口只會在第一次進入應用時執行一次,頁面的跳轉不會再執行服務端數據預取的邏輯,因此說咱們須要客戶端數據預取,官網文檔實現有倆種方式,這裏就只嘗試一種,利用router的導航守衛,原理就是在每次進行跳轉時,執行沒有執行過的asyncData函數,
// 官方代碼
router.onReady(() => {
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
// 咱們只關心非預渲染的組件
// 因此咱們對比它們,找出兩個匹配列表的差別組件
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
// 這裏若是有加載指示器(loading indicator),就觸發
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 中止加載指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
複製代碼
這回,咱們複製粘貼A.vue修改成B.vue做爲B頁面傳入不一樣id(如666666),執行命令,查看結果,能夠看到在跳轉時,state.movie
已經被賦予不一樣的值
咱們作服務端渲染,根據不一樣的頁面會有不一樣的meta、title。因此咱們還須要注入不一樣的Head。能夠用到強大的vue-meta 配合SSR使用。這裏咱們就按照官方文檔來實現一個簡單的title注入,首先你須要在你的template模板中定義<title>{{ title }}</title>
基本原理跟數據預取相似,咱們在特定的時機來獲取組件內title函數,或者字符串,而後將它掛載到context上,這樣,就能夠實現動態改變標題。客戶端直接調用document.title = title
就能夠。咱們將官方示例代碼title-mixin.js放到mixin文件夾下。app.js中引用,調用Vue.mixin(titleMixin)
,在訪問A頁面時,title就變成A頁面
// app.js
import titleMixin from './mixins/title-mixin'
Vue.mixin(titleMixin)
// A.vue
export default {
title: 'A頁面', // 或者是 title () { return 'A頁面' }
// ...
}
複製代碼
緩存的基本原理官方代碼寫的也是一目瞭然。官方代碼以下:
// server.js
// 設置緩存參數
const microCache = LRU({
max: 100, // 最大緩存數
maxAge: 10000 // 10s過時,意味着10s內請求統一路徑,緩存中都有
})
// 判斷是否能夠緩存,這裏先模擬,當訪問B就緩存
const isCacheable = ctx => {
return ctx.url === '/b'
}
const render = async (ctx) => {
// ...忽略無關代碼
// 判斷是否可緩存,若是可緩存則先從緩存中查找
const cacheable = isCacheable(ctx)
if (cacheable) {
const hit = microCache.get(ctx.url)
if (hit) {
console.log('取到緩存') // 便於調試
ctx.body = hit
return
}
}
// 存入緩存, 只有當緩存中沒有 && 能夠緩存
if (cacheable) {
console.log('設置緩存') // 便於調試
microCache.set(ctx.url, html)
}
}
複製代碼
咱們運行代碼,刷新頁面,查看命令行,能夠看到,第一次進入B設置了緩存,10s內不管怎麼刷新頁面,都是取得緩存。反而A頁面不會被緩存。
關於配置Vue服務端渲染到此就結束啦😁。文章主要偏重的仍是利用webpack來構建開發環境和生產環境的SSR,由於就我而言在這地方花費時間比較多。像數據的預取、Head的動態設置、路由的緩存基本上都是按照官方文檔走下來的,理解起來並不難。但真要是作成能夠用於線上項目開發仍是有許多要作的。好比nuxt就已經作到很好了,我所在公司也在使用nuxt。之因此有這篇文章,也是想對服務端渲染有更好的理解。項目的完整代碼👉地址,若是對你有幫助,別忘給個star哈~