SSR 服務器端渲染

前端的SSR 方案最近幾年比較熱門,React的 next框架 , Vue 的nuxt 框架等等css

簡介:

使用 Vue.js 構建客戶端應用程序時,默認狀況下是在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操做 DOM。而使用 SSR 能夠將同一個組件渲染爲服務器端的 HTML 字符串,而後將它們直接發送到瀏覽器,最後將靜態標記"混合"爲客戶端上徹底交互的應用程序。html

瀏覽器端渲染:指的是用 JS 去生成 HTML,例如 React, Vue 等前端框架作的路由 前端

服務器端渲染:指的是用後臺語言經過一些模版引擎生成 HTML,例如 Java 配合 VM 模版引擎、NodeJS配合 Jade 等,將數據與視圖整理輸出爲完整的 HTML 文檔發送給瀏覽器。vue

1、入門配置

1.起步

新建項目,安裝 Vue 與 SSR 依賴包 vue-server-renderernode

新建空文件夾
$ mkdir testSSR
進入 testSSR 目錄
$ cd testSSR
初始化,生成 package.json
$ npm init
安裝依賴
npm install vue vue-server-renderer --save-dev
先使用 vue-server-renderer 渲染一個簡單的 Vue 組件
// test.js
const Vue = require('vue')
const app = new Vue({ // 建立一個 Vue 實例
    template: `<div>Hello World</div>`
})
const vueRenderer = require('vue-server-renderer')
const renderer = vueRenderer.createRenderer() // 建立一個 renderer
// 經過 renderToString 將 Vue 實例渲染爲 HTML
// 函數簽名: renderer.renderToString(vm, context?, callback?): ?Promise<string>
renderer.renderToString(app, (err, doc) => {
    if (err) throw err
    console.log(doc)
})
運行 test.js,輸出渲染後的 HTML

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

2.引入模板

上例只是渲染一個 vue 組件,一般應用程序都會抽象出一個或多個模板來嵌入不一樣的組件。
Render 的 template 選項爲整個頁面的 HTML 提供一個模板。此模板應包含註釋 <!--vue-ssr-outlet-->,做爲渲染應用程序內容的佔位符。git

首先建立一個 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--> 註釋就是應用程序 HTML 標記注入的地方。
將此模板經過 fs 讀取, 而後在 createRenderer( ) 時注入,修改 test.js 以下:github

運行 test.js 能夠看到以前定義的 hello world 組件已嵌入模板中

2、服務器端整合

選取基於 node.js 的 express 做爲服務器,示例 vue ssr 在服務器端的工做。web

1.啓動 express server
進入項目
$ cd testSSR
安裝 express
$ npm install express --save-dev
新建 server.js
const express = require('express')
const server = express()
server.get('/mytest', (request, response) => {
    response.send("hello world "+request.url)
})
server.listen(8000)

運行$ node server.js 後打開瀏覽器訪問 http://localhost:8000/mytestvue-router

服務器啓動成功。

2.Renderer 渲染

首先建立一個能夠重複執行的工廠函數,爲每一個請求建立新的 Vue 實例,若是建立一個單例對象,它將在每一個傳入的請求之間共享,很容易致使交叉請求狀態污染。

進入項目
$ cd testSSR
新建 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 建立實例,並配置路由與請求渲染
// 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)
    })
})

運行$ node server.js 後打開瀏覽器訪問 http://localhost:8000/ssr?sadas=2222

3.插入模板

增長頁面模板,使用以前定義的 index.template.html 做爲模板,注入到一個新的 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)
    })
})

運行$ node server.js 後打開瀏覽器訪問 http://localhost:8000/template

能夠看到一個簡單的服務器端渲染已經完成。

3、項目工程化

1.SSR 項目結構

一般 Vue 應用程序是由 webpack 和 vue-loader 構建,而且許多 webpack 特定功能不能直接在 Node.js 中運行(例如經過 file-loader 導入文件,經過 css-loader 導入 CSS)。
對於客戶端應用程序和服務器應用程序,咱們都要使用 webpack 打包 - 服務器須要「服務器 bundle」而後用於服務器端渲染(SSR),而「客戶端 bundle」會發送給瀏覽器,用於混合靜態標記。基本流程以下圖。

因此一個基本的項目目錄可能以下:

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 # 根實例
├── app.js # 通用 entry
├── entry-client.js # 配置 僅運行於瀏覽器
├── entry-server.js # 配置 僅運行於服務器
├── server.js # 服務器
├── webpack.config.js
└── package.json
2.配置路由
使用 vue-router
$ npm intall vue-router --save-dev
新建router.js

相似於 createApp,咱們也須要給每一個請求一個新的 router 實例,因此文件導出一個 createRouter 函數

// 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,添加路由
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
export function createApp ( ) {
    // 建立 router 實例
    const router = createRouter()
    const app = new Vue({
        // 注入 router 到根 Vue 實例
        router,
        render: h => h(App)
    })
    // 返回 app 和 router
    return { app, router }
}
3.配置 webpack
新建 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)
    })
}
服務器端配置 webpack.server.config.js,是用於生成傳遞給 createBundleRenderer 的 server bundle
// 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: '/path/to/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$/
    }),
    // 這是將服務器的整個輸出
    // 構建爲單個 JSON 文件的插件。
    // 默認文件名爲 `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 的其餘選項
})

除了 server bundle 以外,咱們還能夠生成客戶端構建清單(client build manifest)。使用客戶端清單(client manifest)和服務器 bundle(server bundle),renderer 如今具備了服務器和客戶端的構建信息,所以它能夠自動推斷和注入資源預加載 / 數據預取指令(preload / prefetch directive),以及 css 連接 / script 標籤到所渲染的 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 運行時分離到一個引導 chunk 中,
        // 以即可以在以後正確注入異步 chunk。
        // 這也爲你的 應用程序/vendor 代碼提供了更好的緩存。
        new webpack.optimize.CommonsChunkPlugin({
        name: "manifest",
        minChunks: Infinity
        }),
        // 此插件在輸出目錄中
        // 生成 `vue-ssr-client-manifest.json`。
        new VueSSRClientPlugin()
    ]
})

這樣就能夠生成客戶端構建清單(client build manifest)。

4.Bundle Renderer

到目前爲止,咱們假設打包的服務器端代碼,將由服務器經過 require 直接使用:

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

然而在每次編輯過應用程序源代碼以後,都必須中止並重啓服務。這在開發過程當中會影響開發效率。此外,Node.js 自己不支持 source map。
vue-server-renderer 提供一個名爲 createBundleRenderer 的 API,用於處理此問題,經過使用 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, // (可選)頁面模板
    clientManifest // (可選)客戶端構建 manifest
})
// 在服務器處理函數中……
server.get('/', (req, res) => {
    const context = { url: req.url }
    // 這裏無需傳入一個應用程序,由於在執行 bundle 時已經自動建立過。
    // 如今咱們的服務器與應用程序已經解耦!
    renderer.renderToString(context, (err, html) => {
        // 處理異常……
        res.end(html)
    })
})

4、其餘

此外,vue SSR 提供 css 管理、緩存管理、流式渲染等,期待之後繼續整理。
Vue SSR 指南:https://ssr.vuejs.org/zh/
API 參考:https://ssr.vuejs.org/zh/api/

相關文章
相關標籤/搜索