Vue服務(wù)器端渲染(Vue SSR )

簡(jiǎn)介


使用 Vue.js構(gòu)建客戶端應(yīng)用程序時(shí),默認(rèn)情況下是在瀏覽器中輸出Vue 組件,進(jìn)行生成 DOM 和操作DOM。而使用SSR 可以將同一個(gè)組件渲染為服務(wù)器端的HTML 字符串,然后將它們直接發(fā)送到瀏覽器,最后將靜態(tài)標(biāo)記 "混合" 為客戶端上完全交互的應(yīng)用程序。

如何看一個(gè)網(wǎng)頁(yè)是否是服務(wù)器端渲染?

簡(jiǎn)單的方式是在 Chrome 瀏覽器打開(kāi)控制臺(tái)/開(kāi)發(fā)者工具,查看 Network 中加載的資源,如下圖所示segmentfault 網(wǎng)站,可以看到第一個(gè)文件總是 document 類型,這是服務(wù)器發(fā)送過(guò)來(lái)的完整的 HTML文檔,瀏覽器只需要加載 css/js 進(jìn)行視圖渲染即可。

看 Vue SSR 官網(wǎng)也是服務(wù)器端渲染:

這里說(shuō)的渲染,就是指生成 HTML 文檔的過(guò)程,和之前瀏覽器的 CSS+HTML 渲染沒(méi)有關(guān)系。簡(jiǎn)單來(lái)說(shuō),瀏覽器端渲染,指的是用 JS 去生成 HTML,例如 React, Vue 等前端框架做的路由。服務(wù)器端渲染,指的是用后臺(tái)語(yǔ)言通過(guò)一些模版引擎生成 HTML,例如 Java 配合 VM 模版引擎、NodeJS配合Jade 等,將數(shù)據(jù)與視圖整理輸出為完整的 HTML 文檔發(fā)送給瀏覽器。

一、入門(mén)配置

1. 起步

新建項(xiàng)目,安裝 VueSSR 依賴包 vue-server-renderer

$ mkdir testSSR // 新建空文件夾 vueSSR
$ cd testSSR // 進(jìn)入 vueSSR 目錄
$ npm init // 初始化,生成 package.json
$ npm install vue vue-server-renderer --save-dev // 安裝

先使用 vue-server-renderer 渲染一個(gè)簡(jiǎn)單的 Vue 組件

$ touch test.js // 新建 test.js
// test.js
const Vue = require('vue')
const app = new Vue({ // 創(chuàng)建一個(gè) Vue 實(shí)例
  template: `<div>Hello World</div>`
})
const vueRenderer = require('vue-server-renderer')
const renderer = vueRenderer.createRenderer() // 創(chuàng)建一個(gè) renderer
// 通過(guò) renderToString 將 Vue 實(shí)例渲染為 HTML
// 函數(shù)簽名: renderer.renderToString(vm, context?, callback?): ?Promise<string>
renderer.renderToString(app, (err, doc) => {
  if (err) throw err
  console.log(doc)
})

運(yùn)行 test.js,輸出渲染后的 HTML

注意到應(yīng)用程序的根元素上添加了一個(gè)特殊的屬性 data-server-rendered,這是讓客戶端 Vue 知道這
部分 HTML 是由 Vue 在服務(wù)端渲染的。

2. 引入模板

上例只是渲染一個(gè) vue 組件,通常應(yīng)用程序都會(huì)抽象出一個(gè)或多個(gè)模板來(lái)嵌入不同的組件。
Render 的 template 選項(xiàng)為整個(gè)頁(yè)面的 HTML 提供一個(gè)模板。此模板應(yīng)包含注釋 <\!--vue-ssr-outlet-->作為渲染應(yīng)用程序內(nèi)容的占位符。

首先創(chuàng)建一個(gè) HTML 模板 index.template.html

<!--index.template.html -->
<!doctype html>
<html lang="en">
<head>
  <title></title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>

這里的 <\!--vue-ssr-outlet--> 注釋就是應(yīng)用程序 HTML 標(biāo)記注入的地方。

將此模板通過(guò) fs 讀取, 然后在 createRenderer( ) 時(shí)注入,修改 test.js 如下:

// test.js
//const renderer = vueRenderer.createRenderer()
const fs = require('fs')
const renderer = vueRenderer.createRenderer({
  template: fs.readFileSync('./index.template.html', 'utf-8') // 同步讀取文件
})

運(yùn)行 test.js 可以看到之前定義的 hello world 組件已嵌入模板中。

二、服務(wù)器端整合

選取基于 node.js 的 express 作為服務(wù)器,示例 vue ssr 在服務(wù)器端的工作。

1. 啟動(dòng) express server

$ cd testSSR // 進(jìn)入項(xiàng)目
$ npm install express --save-dev // 安裝 express
$ touch server.js // 新建 server.js

引入 express 并設(shè)置一個(gè)測(cè)試路由

// server.js
const express = require('express')
const server = express()
server.get('/mytest', (request, response) => {
  response.send("hello world "+request.url)
})
server.listen(8000)

運(yùn)行$ node server.js 后打開(kāi)瀏覽器訪問(wèn) http://localhost:8000/mytest

服務(wù)器啟動(dòng)成功。

2. Renderer 渲染

首先創(chuàng)建一個(gè)可以重復(fù)執(zhí)行的工廠函數(shù),為每個(gè)請(qǐng)求創(chuàng)建新的 Vue 實(shí)例,如果創(chuàng)建一個(gè)單例對(duì)象,它將在每個(gè)傳入的請(qǐng)求之間共享,很容易導(dǎo)致交叉請(qǐng)求狀態(tài)污染。

$ cd testSSR // 進(jìn)入項(xiàng)目
$ touch app.js // 新建 app.js
// app.js
const Vue = require('vue')
module.exports = function createApp (context) {
  return new Vue({
    data: {
      url: context.url
    },
    template: `<div>Vue SSR URL: {{ url }}</div>`
  })
}

然后在 server.js 中引入 app.js 創(chuàng)建實(shí)例,并配置路由與請(qǐng)求渲染。

// server.js
const createApp = require('./app')
const vueRenderer = require('vue-server-renderer')
const renderer = vueRenderer.createRenderer()

server.get('/ssr', (request, response) => {
  const context = { url: request.url }
  const app = createApp(context)
  renderer.renderToString(app, (err, doc) => {
    if (err) throw err
    response.send(doc)
  })
})

運(yùn)行$ node server.js 后打開(kāi)瀏覽器訪問(wèn) http://localhost:8000/ssr?sadas=2222

3. 插入模板

增加頁(yè)面模板,使用之前定義的 index.template.html 作為模板,注入到一個(gè)新的 renderer

// server.js
const fs = require('fs')
const rendererTmp = vueRenderer.createRenderer({
  template: fs.readFileSync('./index.template.html', 'utf-8') // 同步讀取文件
})
server.get('/template', (request, response) => {
  const context = { url: request.url }
  const app = createApp(context)
  rendererTmp.renderToString(app, (err, doc) => {
    if (err) throw err
    response.send(doc)
  })
})

運(yùn)行$ node server.js 后打開(kāi)瀏覽器訪問(wèn) http://localhost:8000/template

可以看到一個(gè)簡(jiǎn)單的服務(wù)器端渲染已經(jīng)完成。

三、項(xiàng)目工程化

1. SSR 項(xiàng)目結(jié)構(gòu)

通常 Vue 應(yīng)用程序是由 webpack 和 vue-loader 構(gòu)建,并且許多 webpack 特定功能不能直接在Node.js 中運(yùn)行(例如通過(guò) file-loader 導(dǎo)入文件,通過(guò) css-loader 導(dǎo)入 CSS)。

對(duì)于客戶端應(yīng)用程序和服務(wù)器應(yīng)用程序,我們都要使用 webpack 打包 - 服務(wù)器需要「服務(wù)器bundle」然后用于服務(wù)器端渲染(SSR),而「客戶端 bundle」會(huì)發(fā)送給瀏覽器,用于混合靜態(tài)標(biāo)記?;玖鞒倘缦聢D。

所以一個(gè)基本的項(xiàng)目目錄可能如下:

src
├── config
│ ├── webpack.base.config.js
│ ├── webpack.client.config.js
│ └── webpack.server.config.js
├── components
│ ├── Foo.vue
│ └── xxx.vue
├── build
│ ├── index.js
│ └── xxx.js
├── template
│ ├── index.template.html
│ └── xxx.html
├── route.js # vue-router 路由
├── App.vue # 根實(shí)例
├── app.js # 通用 entry
├── entry-client.js # 配置 僅運(yùn)行于瀏覽器
├── entry-server.js # 配置 僅運(yùn)行于服務(wù)器
├── server.js # 服務(wù)器
├── webpack.config.js
└── package.json

2. 配置路由

使用 vue-router

$ npm intall vue-router --save-dev
$ touch route.js

在新建的 router.js 中創(chuàng)建 router,類似于 createApp,我們也需要給每個(gè)請(qǐng)求一個(gè)新的 router 實(shí)例,
所以文件導(dǎo)出一個(gè) createRouter 函數(shù)

// router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter () {
  return new Router({
    mode: 'history',
    routes: [
      // ...
    ]
  })
}

修改 app.js,添加路由

// app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
export function createApp () {
  // 創(chuàng)建 router 實(shí)例
  const router = createRouter()
  const app = new Vue({
    // 注入 router 到根 Vue 實(shí)例
    router,
    render: h => h(App)
  })
  // 返回 app 和 router
  return { app, router }
}

3. 配置 webpack

新建 entry-server.js,實(shí)現(xiàn)服務(wù)器端路由邏輯:

// entry-server.js
import { createApp } from './app'
export default context => {
  // 因?yàn)橛锌赡軙?huì)是異步路由鉤子函數(shù)或組件,所以我們將返回一個(gè) Promise,
  // 以便服務(wù)器能夠等待所有的內(nèi)容在渲染前,
  // 就已經(jīng)準(zhǔn)備就緒。
  return new Promise((resolve, reject) => {
    const { app, router } = createApp()
    // 設(shè)置服務(wù)器端 router 的位置
    router.push(context.url)
    // 等到 router 將可能的異步組件和鉤子函數(shù)解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // Promise 應(yīng)該 resolve 應(yīng)用程序?qū)嵗员闼梢凿秩?      resolve(app)
    }, reject)
  })
}

在生成 vue-ssr-server-bundle.json 之后,只需將文件路徑傳遞給 createBundleRenderer:

// 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 指向應(yīng)用程序的 server entry 文件
  entry: '/path/to/entry-server.js',
  // 這允許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動(dòng)態(tài)導(dǎo)入(dynamic import),
  // 并且還會(huì)在編譯 Vue 組件時(shí),
  // 告知 `vue-loader` 輸送面向服務(wù)器代碼(server-oriented code)。
  target: 'node',
  // 對(duì) bundle renderer 提供 source map 支持
  devtool: 'source-map',
  // 此處告知 server bundle 使用 Node 風(fēng)格導(dǎo)出模塊(Node-style exports)
  output: {
    libraryTarget: 'commonjs2'
  },
  // https://webpack.js.org/configuration/externals/#function
  // https://github.com/liady/webpack-node-externals
 // 外置化應(yīng)用程序依賴模塊??梢允狗?wù)器構(gòu)建速度更快,
  // 并生成較小的 bundle 文件。
  externals: nodeExternals({
    // 不要外置化 webpack 需要處理的依賴模塊。
    // 你可以在這里添加更多的文件類型。例如,未處理 *.vue 原始文件,
    // 你還應(yīng)該將修改 `global`(例如 polyfill)的依賴模塊列入白名單
    whitelist: /\.css$/
  }),
  // 這是將服務(wù)器的整個(gè)輸出
  // 構(gòu)建為單個(gè) JSON 文件的插件。
  // 默認(rèn)文件名為 `vue-ssr-server-bundle.json`
  plugins: [
    new VueSSRServerPlugin()
  ]
})

在生成 vue-ssr-server-bundle.json 之后,只需將文件路徑傳遞給 createBundleRenderer:

// server.js
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
  // ……renderer 的其他選項(xiàng)
})

除了 server bundle 之外,我們還可以生成客戶端構(gòu)建清單(client build manifest)。使用客戶端清單(client manifest)和服務(wù)器bundle(server bundle),renderer 現(xiàn)在具有了服務(wù)器和客戶端的構(gòu)建信息,因此它可以自動(dòng)推斷和注入資源預(yù)加載 / 數(shù)據(jù)預(yù)取指令(preload / prefetch directive),以及 css 鏈接 / script 標(biāo)簽到所渲染的 HTML。

// webpack.client.config.js
const webpack = require('webpack')
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: '/path/to/entry-client.js',
  plugins: [
    // 重要信息:這將 webpack 運(yùn)行時(shí)分離到一個(gè)引導(dǎo) chunk 中,
    // 以便可以在之后正確注入異步 chunk。
    // 這也為你的 應(yīng)用程序/vendor 代碼提供了更好的緩存。
    new webpack.optimize.CommonsChunkPlugin({
      name: "manifest",
      minChunks: Infinity
    }),
    // 此插件在輸出目錄中
    // 生成 `vue-ssr-client-manifest.json`。
    new VueSSRClientPlugin()
  ]
})

這樣就可以生成客戶端構(gòu)建清單(client build manifest)。

4. Bundle Renderer

到目前為止,我們假設(shè)打包的服務(wù)器端代碼,將由服務(wù)器通過(guò) require 直接使用:

const createApp = require('/path/to/built-server-bundle.js')

然而在每次編輯過(guò)應(yīng)用程序源代碼之后,都必須停止并重啟服務(wù)。這在開(kāi)發(fā)過(guò)程中會(huì)影響開(kāi)發(fā)效率。
此外,Node.js 本身不支持 source map。

vue-server-renderer 提供一個(gè)名為 createBundleRenderer 的 API,用于處理此問(wèn)題,通過(guò)使用
webpack 的自定義插件,server bundle 將生成為可傳遞到 bundle renderer 的特殊 JSON 文件。

// server.js
const { createBundleRenderer } = require('vue-server-renderer')
const template = require('fs').readFileSync('/path/to/template.html', 'utf-8')
const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
const clientManifest = require('/path/to/vue-ssr-client-manifest.json')
const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false, // 推薦
  template, // (可選)頁(yè)面模板
  clientManifest // (可選)客戶端構(gòu)建 manifest
})
// 在服務(wù)器處理函數(shù)中……
server.get('/', (req, res) => {
  const context = { url: req.url }
  // 這里無(wú)需傳入一個(gè)應(yīng)用程序,因?yàn)樵趫?zhí)行 bundle 時(shí)已經(jīng)自動(dòng)創(chuàng)建過(guò)。
  // 現(xiàn)在我們的服務(wù)器與應(yīng)用程序已經(jīng)解耦!
  renderer.renderToString(context, (err, html) => {
    // 處理異?!?    res.end(html)
  })
})
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容