搭建自己的伺服器端渲染 (SSR)
一、渲染一個Vue執行個體
-
mkdir vue-ssr
-
cd vue-ssr
-
npm init -y
-
npm i vue vue-server-renderder
- server.js
const Vue = require('vue') const renderer = require('vue-server-renderer').createRenderer() const app = new Vue({ template: ` <div id="app"> <h1>{{message}}</h1> </div> `, data: { message: '拉鈎教育' } }) renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) })
-
,運作結果:node server.js
這個屬性是為了将來用戶端渲染激活接管的接口data-server-rendered="true"
二、結合到Web伺服器中
server.js
const Vue = require('vue')
const express = require('express')
const renderer = require('vue-server-renderer').createRenderer()
const server = express()
server.get('/', (req, res) => {
const app = new Vue({
template: `
<div id="app">
<h1>{{message}}</h1>
</div>
`,
data: {
message: '拉鈎教育'
}
})
renderer.renderToString(app, (err, html) => {
if (err) {
return res.status(500).end('Internal Server Error.')
}
res.setHeader('Content-Type', 'text/html; charset=utf8') // 設定編碼,防止亂碼
res.end(`
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
${html}
</body>
</html>
`)
})
})
server.listen(3000, () => {
console.log('server running at port 3000...')
})
三、使用HTML模闆
1. 建立HTML模闆檔案
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
<!--vue-ssr-outlet-->
是占位符,為了接收将來要渲染的變量,不能寫錯,不能有多餘的空格
2. js代碼中的createRenderer方法指定模闆檔案
server.js
const Vue = require('vue')
const express = require('express')
const fs = require('fs')
const renderer = require('vue-server-renderer').createRenderer({
// 這裡指定模闆檔案
template: fs.readFileSync('./index.template.html', 'utf-8')
})
const server = express()
server.get('/', (req, res) => {
const app = new Vue({
template: `
<div id="app">
<h1>{{message}}</h1>
</div>
`,
data: {
message: '拉鈎教育'
}
})
renderer.renderToString(app, (err, html) => { // 此處的html參數是被模闆檔案處理過了的,可以直接輸出到使用者的頁面上
if (err) {
return res.status(500).end('Internal Server Error.')
}
res.setHeader('Content-Type', 'text/html; charset=utf8') // 設定編碼,防止亂碼
res.end(html)
})
})
server.listen(3000, () => {
console.log('server running at port 3000...')
})
四、在模闆中使用外部資料
Index.template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{{ meta }}}
<title>{{ title }}</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
使用兩個花括号可以資料外部資料變量,而标簽也會進行轉義後輸出在頁面上。此時可以使用三個花括号原樣輸出資料,不會對标簽進行轉義處理
在js代碼中給
renderer.renderToString
增加第二個參數為外部資料對象
renderer.renderToString(app, {
title: '拉勾教育',
meta: `
<meta name="description" content="拉勾教育" >
`
}, (err, html) => {
if (err) {
return res.status(500).end('Internal Server Error.')
}
res.setHeader('Content-Type', 'text/html; charset=utf8') // 設定編碼,防止亂碼
res.end(html)
})
五、建構配置
1. 基本思路
2. 源碼結構
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 僅運作于浏覽器
└── entry-server.js # 僅運作于伺服器
App.vue
<template>
<div id="app">
<h1>{{message}}</h1>
<h2>用戶端動态互動</h2>
<div>
<input v-model="message">
</div>
<div>
<button @click="onClick">點選測試</button>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data: function () {
return {
message: '拉勾教育'
}
},
methods: {
onClick () {
console.log('Hello World!')
}
}
}
</script>
app.js
是我們應用程式的「通用 entry」。在純用戶端應用程式中,我們将在此檔案中建立根 Vue 執行個體,并直接挂載到 DOM。但是,對于伺服器端渲染(SSR),責任轉移到純用戶端 entry 檔案。
app.js
簡單地使用 export 導出一個
createApp
函數:
import Vue from 'vue'
import App from './App.vue'
// 導出一個工廠函數,用于建立新的
// 應用程式、router 和 store 執行個體
export function createApp () {
const app = new Vue({
// 根執行個體簡單的渲染應用程式元件。
render: h => h(App)
})
return { app }
}
entry-client.js
用戶端 entry 隻需建立應用程式,并且将其挂載到 DOM 中:
import { createApp } from './app'
// 用戶端特定引導邏輯……
const { app } = createApp()
// 這裡假定 App.vue 模闆中根元素具有 `id="app"`
app.$mount('#app')
entry-server.js
伺服器 entry 使用 default export 導出函數,并在每次渲染中重複調用此函數。此時,除了建立和傳回應用程式執行個體之外,它不會做太多事情 - 但是稍後我們将在此執行伺服器端路由比對 (server-side route matching) 和資料預取邏輯 (data pre-fetching logic)。
import { createApp } from './app'
export default context => {
const { app } = createApp()
return app
}
3. 安裝依賴
(1) 安裝生産依賴
npm i vue vue-server-renderer express cross-env
包 | 說明 |
---|---|
vue | Vue.js核心庫 |
vue-server-renderer | Vue服務端渲染工具 |
express | 基于Node的webpack服務架構 |
cross-env | 通過npm scripts設定跨平台環境變量 |
(2) 安裝開發依賴
npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler friendly-errors-webpack-plugin
包 | 說明 |
---|---|
webpack | webpack核心包 |
webpack-cli | webpack的指令行工具 |
webpack-merge | webpack配置資訊合并工具 |
webpack-node-externals | 排除webpack中的Node子產品 |
rimraf | 基于Node封裝的一個跨平台rm -rf工具 |
friendly-errors-webpack-plugin | 友好的webpack錯誤提示 |
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader | Babel相關工具 |
vue-loader vue-template-compiler | 處理.vue資源 |
file-loader | 處理字型資源 |
css-loader | 處理CSS資源 |
url-loader | 處理圖檔資源 |
4. webpack配置檔案及打包指令
(1) 初始化webpack打包配置檔案
build
|---webpack.base.config.js # 公共配置
|---webpack.client.config.js # 用戶端打包配置檔案
|---webpack.server.config.js # 服務端打包配置檔案
webpack.base.config.js
/**
* 公共配置
*/
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
mode: isProd ? 'production' : 'development',
output: {
path: resolve('../dist/'),
publicPath: '/dist/',
filename: '[name].[chunkhash].js'
},
resolve: {
alias: {
// 路徑别名,@ 指向 src
'@': resolve('../src/')
},
// 可以省略的擴充名
// 當省略擴充名的時候,按照從前往後的順序依次解析
extensions: ['.js', '.vue', '.json']
},
devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
module: {
rules: [
// 處理圖檔資源
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
// 處理字型資源
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader',
],
},
// 處理 .vue 資源
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 處理 CSS 資源
// 它會應用到普通的 `.css` 檔案
// 以及 `.vue` 檔案中的 `<style>` 塊
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
},
// CSS 預處理器,參考:https://vue-loader.vuejs.org/zh/guide/pre-processors.html
// 例如處理 Less 資源
// {
// test: /\.less$/,
// use: [
// 'vue-style-loader',
// 'css-loader',
// 'less-loader'
// ]
// },
]
},
plugins: [
new VueLoaderPlugin(),
new FriendlyErrorsWebpackPlugin()
]
}
webpack.client.config.js
/**
* 用戶端打包配置
*/
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
entry: {
app: './src/entry-client.js'
},
module: {
rules: [
// ES6 轉 ES5
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true,
plugins: ['@babel/plugin-transform-runtime']
}
}
},
]
},
// 重要資訊:這将 webpack 運作時分離到一個引導 chunk 中,
// 以便可以在之後正确注入異步 chunk。
optimization: {
splitChunks: {
name: "manifest",
minChunks: Infinity
}
},
plugins: [
// 此插件在輸出目錄中生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
webpack.server.config.js
/**
* 服務端打包配置
*/
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
// 将 entry 指向應用程式的 server entry 檔案
entry: './src/entry-server.js',
// 這允許 webpack 以 Node 适用方式處理子產品加載
// 并且還會在編譯 Vue 元件時,
// 告知 `vue-loader` 輸送面向伺服器代碼(server-oriented code)。
target: 'node',
output: {
filename: 'server-bundle.js',
// 此處告知 server bundle 使用 Node 風格導出子產品(Node-style exports)
libraryTarget: 'commonjs2'
},
// 不打包 node_modules 第三方包,而是保留 require 方式直接加載
externals: [nodeExternals({
// 白名單中的資源依然正常打包
allowlist: [/\.css$/]
})],
plugins: [
// 這是将伺服器的整個輸出建構為單個 JSON 檔案的插件。
// 預設檔案名為 `vue-ssr-server-bundle.json`
new VueSSRServerPlugin()
]
})
5. 配置建構指令
"scripts": {
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
"build": "rimraf dist && npm run build:client && npm run build:server"
}
6. 啟動應用
erver.js
const Vue = require('vue')
const express = require('express')
const fs = require('fs')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const { static } = require('express')
const template = fs.readFileSync('./index.template.html', 'utf-8')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
template,
clientManifest
})
const server = express()
// 請求字首,使用express中間件的static處理
server.use('/dist', express.static('./dist'))
server.get('/', (req, res) => {
renderer.renderToString({
title: '拉勾教育',
meta: `
<meta name="description" content="拉勾教育" >
`
}, (err, html) => {
if (err) {
return res.status(500).end('Internal Server Error.')
}
res.setHeader('Content-Type', 'text/html; charset=utf8') // 設定編碼,防止亂碼
res.end(html)
})
})
server.listen(3001, () => {
console.log('server running at port 3001...')
})
7. 解析渲染流程
六、建構配置開發模式
1. 基本思路
生産模式直接渲染,開發模式監視打包建構,重新生成Renderer渲染器
2. 提取處理子產品
server.js
const Vue = require('vue')
const express = require('express')
const fs = require('fs')
const createBundleRenderer = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')
const server = express()
// 請求字首,使用express中間件的static處理
server.use('/dist', express.static('./dist'))
const isProd = process.env.NODE_ENV === 'production'
let renderer
let onReady
if (isProd) {
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const { static } = require('express')
const template = fs.readFileSync('./index.template.html', 'utf-8')
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
})
} else {
// 開發模式 -> 監視打包建構 -> 重新生成Renderer渲染器
onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
})
})
}
// render 是路由函數
const render = (req, res) => {
// renderer是Vue SSR的渲染器
renderer.renderToString({
title: '拉勾教育',
meta: `
<meta name="description" content="拉勾教育" >
`
}, (err, html) => {
if (err) {
return res.status(500).end('Internal Server Error.')
}
res.setHeader('Content-Type', 'text/html; charset=utf8') // 設定編碼,防止亂碼
res.end(html)
})
}
server.get('/', isProd ? render : async (req, res) => {
// 等待有了Renderer渲染器以後,調用render進行渲染
await onReady
render()
})
server.listen(3001, () => {
console.log('server running at port 3001...')
})
build/setup-dev-server.js
module.exports = (server, callback) => {
let ready // ready就是promise中的resolve
const onReady = new Promise(r => ready = r)
// 監視建構 -> 更新 Renderer
let template
let serverBundle
let clientManifest
return onReady
}
3. update更新函數
const update = () => {
if (template && serverBundle && clientManifest) {
ready()
callback(serverBundle, template, clientManifest)
}
}
4. 處理模闆檔案
// 監視建構 template -> 調用 update -> 更新 Renderer 渲染器
const templatePath = path.resolve(__dirname, '../index.template.html')
template = fs.readFileSync(templatePath, 'utf-8')
update()
// fs.watch、fs.watchFile
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8')
update()
})
5. 服務端監視打包
// 監視建構 serverBundle -> 調用 update -> 更新 Renderer 渲染器
const serverConfig = require('./webpack.server.config')
// serverCompiler是一個webpack編譯器,直接監聽資源改變,進行打包建構
const serverCompiler = webpack(serverConfig)
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
if (stats.hasErrors()) return
serverBundle = JSON.parse(
fs.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
)
console.log(serverBundle)
update()
})
6. 把資料寫到記憶體中
// 監視建構 serverBundle -> 調用 update -> 更新 Renderer 渲染器
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
const serverDevMiddleware = devMiddleware(serverCompiler, {
logLevel: 'silent' // 關閉日志輸出,由 FriendlyErrorsWebpackPlugin 處理
})
serverCompiler.hooks.done.tap('server', () => {
serverBundle = JSON.parse(
serverDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
)
update()
})
7. 用戶端建構
// 監視建構 clientManifest -> 調用 update -> 更新 Renderer 渲染器
const clientConfig = require('./webpack.client.config')
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
clientConfig.entry.app = [
'webpack-hot-middleware/client?quiet=true&reload=true', // 和服務端互動處理熱更新一個用戶端腳本
clientConfig.entry.app
]
clientConfig.output.filename = '[name].js' // 熱更新模式下確定一緻的 hash
const clientCompiler = webpack(clientConfig)
const clientDevMiddleware = devMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
logLevel: 'silent' // 關閉日志輸出,由 FriendlyErrorsWebpackPlugin 處理
})
clientCompiler.hooks.done.tap('client', () => {
clientManifest = JSON.parse(
clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')
)
update()
})
8. 熱更新
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
clientConfig.entry.app = [
'webpack-hot-middleware/client?quiet=true&reload=true', // 和服務端互動處理熱更新一個用戶端腳本
clientConfig.entry.app
]
clientConfig.output.filename = '[name].js' // 熱更新模式下確定一緻的 hash
const hotMiddleware = require('webpack-hot-middleware')
server.use(hotMiddleware(clientCompiler, {
log: false // 關閉它本身的日志輸出
}))
七、編寫通用應用注意事項
八、路由處理
1. 配置Vue-Router
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/src/pages/Home'
Vue.use(VueRouter)
export const createRouter = () => {
const router = new VueRouter({
mode: 'history', // 相容前後端,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: () => import('@/src/pages/About')
},
{
path: '*',
name: 'error404',
component: () => import('@/src/pages/404')
}
]
})
return router // 千萬别忘了傳回router
}
2. 将路由注冊到根執行個體
app.js
/**
* 同構應用通用啟動入口
*/
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router/'
// 導出一個工廠函數,用于建立新的
// 應用程式、router 和 store 執行個體
export function createApp () {
const router = createRouter()
const app = new Vue({
router, // 把路由挂載到Vue根執行個體當中
// 根執行個體簡單的渲染應用程式元件。
render: h => h(App)
})
return { app, router }
}
3. 适配服務端入口
拷貝官網上提供的entry-server.js
// entry-server.js
import { createApp } from './app'
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) {
return reject({ code: 404 })
}
// Promise 應該 resolve 應用程式執行個體,以便它可以渲染
resolve(app)
}, reject)
})
}
路由表裡已經配置過404頁面了,是以不用額外判斷404,然後将Promise改成async/await的形式,最終如下:
// entry-server.js
import { createApp } from './app'
export default async context => {
// 因為有可能會是異步路由鈎子函數或元件,是以我們将傳回一個 Promise,
// 以便伺服器能夠等待所有的内容在渲染前,
// 就已經準備就緒。
const { app, router } = createApp()
// 設定伺服器端 router 的位置
router.push(context.url)
// 等到 router 将可能的異步元件和鈎子函數解析完
await new Promise(router.onReady.bind(router))
return app
}
4. 服務端server适配
我們的伺服器代碼使用了一個
*
處理程式,它接受任意 URL。這允許我們将通路的 URL 傳遞到我們的 Vue 應用程式中,然後對用戶端和伺服器複用相同的路由配置!
server.js處理
// ...
// render 是路由函數
const render =async (req, res) => {
// renderer是Vue SSR的渲染器
try {
const html = await renderer.renderToString({
title: '拉勾教育',
meta: `
<meta name="description" content="拉勾教育" >
`,
url: req.url
})
res.setHeader('Content-Type', 'text/html; charset=utf8') // 設定編碼,防止亂碼
res.end(html)
}catch(err) {
res.status(500).end('Internal Server Error.')
}
}
// 服務端路由比對為*,意味着所有的路由都會進入這裡
server.get('*', isProd ? render : async (req, res) => {
// 等待有了Renderer渲染器以後,調用render進行渲染
await onReady
render(req, res)
})
// ...
5. 适配用戶端入口
需要注意的是,你仍然需要在挂載 app 之前調用
router.onReady
,因為路由器必須要提前解析路由配置中的異步元件,才能正确地調用元件中可能存在的路由鈎子。這一步我們已經在我們的伺服器入口 (server entry) 中實作過了,現在我們隻需要更新用戶端入口 (client entry):
// entry-client.js
import { createApp } from './app'
const { app, router } = createApp()
router.onReady(() => {
app.$mount('#app')
})
6. 處理完成
路由出口:
App.vue
<div id="app">
<ul>
<li>
<router-link to="/">Home</router-link>
</li>
<li>
<router-link to="/about">About</router-link>
</li>
</ul>
<!-- 路由出口 -->
<router-view/>
</div>
八、管理頁面
1. Head 内容
npm install vue-meta
在src/app.js裡面,增加代碼
import VueMeta from 'vue-meta'
Vue.use(VueMeta)
Vue.mixin({
metaInfo: {
titleTemplate: '%s - 拉勾教育'
}
})
在entry-server.js的導出函數裡,增加代碼:
const meta = app.$meta()
context.meta = meta
将meta資料注入到模闆頁面index.template.html中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{{ meta.inject().title.text() }}}
{{{ meta.inject().meta.text() }}}
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
在vue頁面中的應用:
export default {
name: 'Home',
metaInfo: {
title: '首頁'
}
}
export default {
name: 'About',
metaInfo: {
title: '關于'
}
}
九、資料預取和狀态管理
1. 思路分析
在伺服器端渲染(SSR)期間,我們本質上是在渲染我們應用程式的"快照",是以如果應用程式依賴于一些異步資料,那麼在開始渲染過程之前,需要先預取和解析好這些資料。
另一個需要關注的問題是在用戶端,在挂載 (mount) 到用戶端應用程式之前,需要擷取到與伺服器端應用程式完全相同的資料 - 否則,用戶端應用程式會因為使用與伺服器端應用程式不同的狀态,然後導緻混合失敗。
為了解決這個問題,擷取的資料需要位于視圖元件之外,即放置在專門的資料預取存儲容器(data store)或"狀态容器(state container))"中。首先,在伺服器端,我們可以在渲染之前預取資料,并将資料填充到 store 中。此外,我們将在 HTML 中序列化(serialize)和内聯預置(inline)狀态。這樣,在挂載(mount)到用戶端應用程式之前,可以直接從 store 擷取到内聯預置(inline)狀态。
2. 資料預取
npm install vuex
建立src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export const createStore = () => {
return new Vuex.Store({
state: () => ({
posts: []
}),
mutations: {
setPosts (state, data) {
state.posts = data
}
},
actions: {
// 在服務端渲染期間,務必讓action傳回一個promise
async getPosts ({commit}) { // async預設傳回Promise
// return new Promise()
const { data } = await axios.get('https://cnodejs.org/api/v1/topics')
commit('setPosts', data.data)
}
}
})
}
将容器注入到入口檔案src/app.js
/**
* 同構應用通用啟動入口
*/
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router/'
import VueMeat from 'vue-meta'
import { createStore } from './store'
Vue.use(VueMeta)
Vue.mixin({
metaInfo: {
titleTemplate: '%s - 拉勾教育'
}
})
// 導出一個工廠函數,用于建立新的
// 應用程式、router 和 store 執行個體
export function createApp () {
const router = createRouter()
const store = createStore()
const app = new Vue({
router, // 把路由挂載到Vue根執行個體當中
store, // 把容器挂載到Vue根執行個體中
// 根執行個體簡單的渲染應用程式元件。
render: h => h(App)
})
return { app, router, store }
}
頁面pages/Posts.vue,使用serverPrefetch方法在服務端發起異步請求。
<template>
<div>
<h1>Post List</h1>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
<script>
// import axios from 'axios'
import { mapState, mapActions } from 'vuex'
export default {
name: 'PostList',
metaInfo: {
title: 'Posts'
},
data () {
return {
// posts: []
}
},
computed: {
...mapState(['posts'])
},
// Vue SSR 特殊為服務端渲染提供的一個生命周期鈎子函數
serverPrefetch () {
// 發起 action,傳回 Promise
// this.$store.dispatch('getPosts')
return this.getPosts()
},
methods: {
...mapActions(['getPosts'])
}
// 服務端渲染
// 隻支援 beforeCreate 和 created
// 不會等待 beforeCreate 和 created 中的異步操作
// 不支援響應式資料
// 所有這種做法在服務端渲染中是不會工作的!!!
// async created () {
// console.log('Posts Created Start')
// const { data } = await axios({
// method: 'GET',
// url: 'https://cnodejs.org/api/v1/topics'
// })
// this.posts = data.data
// console.log('Posts Created End')
// }
}
</script>
<style>
</style>
3. 将資料預取同步到用戶端
entry-server.js
// entry-server.js
import { createApp } from './app'
export default async context => {
// 因為有可能會是異步路由鈎子函數或元件,是以我們将傳回一個 Promise,
// 以便伺服器能夠等待所有的内容在渲染前,
// 就已經準備就緒。
const { app, router, store } = createApp()
const meta = app.$meta()
// 設定伺服器端 router 的位置
router.push(context.url)
context.meta = meta
// 等到 router 将可能的異步元件和鈎子函數解析完
await new Promise(router.onReady.bind(router))
// 這個rendered函數會在服務端渲染完畢之後被調用
context.rendered = () => {
// Renderer會把 context.state 資料對象内聯到頁面模闆中
// 最終發送到用戶端的頁面中會包含一段腳本:window.__INITIAL_STATE__ = context.state
// 用戶端就要把頁面中的 window.__INITIAL_STATE__ 拿出來填充到用戶端 store 容器中
context.state = store.state
}
return app
}
entry-client.js
// entry-client.js
import { createApp } from './app'
const { app, router, store } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
app.$mount('#app')
})