【萬字長文警告】從頭至尾完全理解服務端渲染SSR原理

前言

閒來無事,研究一下SSR,主要緣由在於上週一位後端同窗在一次組內技術分享的時候說,對先後端分離、服務端渲染特別感興趣,在他分享了後端微服務以後,專門點名邀請我下週分享服務端渲染,而後我還沒贊成,領導就內定讓我下週分享了(其實就是下週願意下週分享,我是那個替死鬼)。php

本人主要從我的角度介紹了對服務端渲染的理解,讀完本文後,你將瞭解到:css

  • 什麼是服務端渲染,與客戶端渲染的區別是什麼?
  • 爲何須要服務端渲染,服務端渲染的利弊是什麼?
  • 如何對VUE項目進行同構?

原文地址 歡迎starhtml

服務端渲染的定義

在講服務度渲染以前,咱們先回顧一下頁面的渲染流程:前端

  1. 瀏覽器經過請求獲得一個HTML文本
  2. 渲染進程解析HTML文本,構建DOM樹
  3. 解析HTML的同時,若是遇到內聯樣式或者樣式腳本,則下載並構建樣式規則(stytle rules),若遇到JavaScript腳本,則會下載執行腳本。
  4. DOM樹和樣式規則構建完成以後,渲染進程將二者合併成渲染樹(render tree)
  5. 渲染進程開始對渲染樹進行佈局,生成佈局樹(layout tree)
  6. 渲染進程對佈局樹進行繪製,生成繪製記錄
  7. 渲染進程的對佈局樹進行分層,分別柵格化每一層,並獲得合成幀
  8. 渲染進程將合成幀信息發送給GPU進程顯示到頁面中

渲染流程

能夠看到,頁面的渲染其實就是瀏覽器將HTML文本轉化爲頁面幀的過程。而現在咱們大部分WEB應用都是使用 JavaScript 框架(Vue、React、Angular)進行頁面渲染的,也就是說,在執行 JavaScript 腳本的時候,HTML頁面已經開始解析而且構建DOM樹了,JavaScript 腳本只是動態的改變 DOM 樹的結構,使得頁面成爲但願成爲的樣子,這種渲染方式叫動態渲染,也能夠叫客戶端渲染(client side rende)。vue

那麼什麼是服務端渲染(server side render)?顧名思義,服務端渲染就是在瀏覽器請求頁面URL的時候,服務端將咱們須要的HTML文本組裝好,並返回給瀏覽器,這個HTML文本被瀏覽器解析以後,不須要通過 JavaScript 腳本的執行,便可直接構建出但願的 DOM 樹並展現到頁面中。這個服務端組裝HTML的過程,叫作服務端渲染。node

1

服務端渲染的由來

Web1.0

在沒有AJAX的時候,也就是web1.0時代,幾乎全部應用都是服務端渲染(此時服務器渲染非如今的服務器渲染),那個時候的頁面渲染大概是這樣的,瀏覽器請求頁面URL,而後服務器接收到請求以後,到數據庫查詢數據,將數據丟到後端的組件模板(php、asp、jsp等)中,並渲染成HTML片斷,接着服務器在組裝這些HTML片斷,組成一個完整的HTML,最後返回給瀏覽器,這個時候,瀏覽器已經拿到了一個完整的被服務器動態組裝出來的HTML文本,而後將HTML渲染到頁面中,過程沒有任何JavaScript代碼的參與。webpack

2

客戶端渲染

在WEB1.0時代,服務端渲染看起來是一個當時的最好的渲染方式,可是隨着業務的日益複雜和後續AJAX的出現,也漸漸開始暴露出了WEB1.0服務器渲染的缺點。git

  • 每次更新頁面的一小的模塊,都須要從新請求一次頁面,從新查一次數據庫,從新組裝一次HTML
  • 前端JavaScript代碼和後端(jsp、php、jsp)代碼混雜在一塊兒,使得日益複雜的WEB應用難以維護

並且那個時候,根本就沒有前端工程師這一職位,前端js的活通常都由後端同窗 jQuery 一把梭。可是隨着前端頁面漸漸地複雜了以後,後端開始發現js好麻煩,雖然很簡單,可是坑太多了,因而讓公司招聘了一些專門寫js的人,也就是前端,這個時候,先後端的鄙視鏈就出現了,後端鄙視前端,由於後端以爲js太簡單,無非就是寫寫頁面的特效(JS),切切圖(CSS),根本算不上是真正的程序員。程序員

隨之 nodejs 的出現,前端看到了翻身的契機,爲了擺脫後端的指指點點,前端開啓了一場先後端分離的運動,但願能夠脫離後端獨立發展。先後端分離,表面上看上去是代碼分離,其實是爲了先後端人員分離,也就是先後端分家,前端再也不歸屬於後端團隊。github

先後端分離以後,網頁開始被當成了獨立的應用程序(SPA,Single Page Application),前端團隊接管了全部頁面渲染的事,後端團隊只負責提供全部數據查詢與處理的API,大致流程是這樣的:首先瀏覽器請求URL,前端服務器直接返回一個空的靜態HTML文件(不須要任何查數據庫和模板組裝),這個HTML文件中加載了不少渲染頁面須要的 JavaScript 腳本和 CSS 樣式表,瀏覽器拿到 HTML 文件後開始加載腳本和樣式表,而且執行腳本,這個時候腳本請求後端服務提供的API,獲取數據,獲取完成後將數據經過JavaScript腳本動態的將數據渲染到頁面中,完成頁面顯示。

3

這一個先後端分離的渲染模式,也就是客戶端渲染(CSR)。

服務端渲染

隨着單頁應用(SPA)的發展,程序員們漸漸發現 SEO(Search Engine Optimazition,即搜索引擎優化)出了問題,並且隨着應用的複雜化,JavaScript 腳本也不斷的臃腫起來,使得首屏渲染相比於 Web1.0時候的服務端渲染,也慢了很多。

本身選的路,跪着也要走下去。因而前端團隊選擇了使用 nodejs 在服務器進行頁面的渲染,進而再次出現了服務端渲染。大致流程與客戶端渲染有些類似,首先是瀏覽器請求URL,前端服務器接收到URL請求以後,根據不一樣的URL,前端服務器向後端服務器請求數據,請求完成後,前端服務器會組裝一個攜帶了具體數據的HTML文本,而且返回給瀏覽器,瀏覽器獲得HTML以後開始渲染頁面,同時,瀏覽器加載並執行 JavaScript 腳本,給頁面上的元素綁定事件,讓頁面變得可交互,當用戶與瀏覽器頁面進行交互,如跳轉到下一個頁面時,瀏覽器會執行 JavaScript 腳本,向後端服務器請求數據,獲取完數據以後再次執行 JavaScript 代碼動態渲染頁面。

4

服務端渲染的利弊

相比於客戶端渲染,服務端渲染有什麼優點?

利於SEO

有利於SEO,其實就是有利於爬蟲來爬你的頁面,而後在別人使用搜索引擎搜索相關的內容時,你的網頁排行能靠得更前,這樣你的流量就有越高。那爲何服務端渲染更利於爬蟲爬你的頁面呢?其實,爬蟲也分低級爬蟲和高級爬蟲。

  • 低級爬蟲:只請求URL,URL返回的HTML是什麼內容就爬什麼內容。
  • 高級爬蟲:請求URL,加載並執行JavaScript腳本渲染頁面,爬JavaScript渲染後的內容。

也就是說,低級爬蟲對客戶端渲染的頁面來講,簡直無能爲力,由於返回的HTML是一個空殼,它須要執行 JavaScript 腳本以後纔會渲染真正的頁面。而目前像百度、谷歌、微軟等公司,有一部分年代老舊的爬蟲還屬於低級爬蟲,使用服務端渲染,對這些低級爬蟲更加友好一些。

白屏時間更短

相對於客戶端渲染,服務端渲染在瀏覽器請求URL以後已經獲得了一個帶有數據的HTML文本,瀏覽器只須要解析HTML,直接構建DOM樹就能夠。而客戶端渲染,須要先獲得一個空的HTML頁面,這個時候頁面已經進入白屏,以後還須要通過加載並執行 JavaScript、請求後端服務器獲取數據、JavaScript 渲染頁面幾個過程才能夠看到最後的頁面。特別是在複雜應用中,因爲須要加載 JavaScript 腳本,越是複雜的應用,須要加載的 JavaScript 腳本就越多、越大,這會致使應用的首屏加載時間很是長,進而下降了體驗感。

5

服務端渲染缺點

並非全部的WEB應用都必須使用SSR,這須要開發者本身來權衡,由於服務端渲染會帶來如下問題:

  • 代碼複雜度增長。爲了實現服務端渲染,應用代碼中須要兼容服務端和客戶端兩種運行狀況,而一部分依賴的外部擴展庫卻只能在客戶端運行,須要對其進行特殊處理,才能在服務器渲染應用程序中運行。
  • 須要更多的服務器負載均衡。因爲服務器增長了渲染HTML的需求,使得本來只須要輸出靜態資源文件的nodejs服務,新增了數據獲取的IO和渲染HTML的CPU佔用,若是流量忽然暴增,有可能致使服務器down機,所以須要使用響應的緩存策略和準備相應的服務器負載。
  • 涉及構建設置和部署的更多要求。與能夠部署在任何靜態文件服務器上的徹底靜態單頁面應用程序 (SPA) 不一樣,服務器渲染應用程序,須要處於 Node.js server 運行環境。

因此在使用服務端渲染SSR以前,須要開發者考慮投入產出比,好比大部分應用系統都不須要SEO,並且首屏時間並無很是的慢,若是使用SSR反而小題大作了。

同構

知道了服務器渲染的利弊後,假如咱們須要在項目中使用服務端渲染,咱們須要作什麼呢?那就是同構咱們的項目。

同構的定義

在服務端渲染中,有兩種頁面渲染的方式:

  • 前端服務器經過請求後端服務器獲取數據並組裝HTML返回給瀏覽器,瀏覽器直接解析HTML後渲染頁面
  • 瀏覽器在交互過程當中,請求新的數據並動態更新渲染頁面

這兩種渲染方式有一個不一樣點就是,一個是在服務端中組裝html的,一個是在客戶端中組裝html的,運行環境是不同的。所謂同構,就是讓一份代碼,既能夠在服務端中執行,也能夠在客戶端中執行,而且執行的效果都是同樣的,都是完成這個html的組裝,正確的顯示頁面。也就是說,一份代碼,既能夠客戶端渲染,也能夠服務端渲染。

同構的條件

爲了實現同構,咱們須要知足什麼條件呢?首先,咱們思考一個應用中一個頁面的組成,假如咱們使用的是Vue.js,當咱們打開一個頁面時,首先是打開這個頁面的URL,這個URL,能夠經過應用的路由匹配,找到具體的頁面,不一樣的頁面有不一樣的視圖,那麼,視圖是什麼?從應用的角度來看,視圖 = 模板 + 數據,那麼在 Vue.js 中, 模板能夠理解成組件,數據能夠理解爲數據模型,即響應式數據。因此,對於同構應用來講,咱們必須實現客戶端與服務端的路由、模型組件、數據模型的共享。

6

實踐

知道了服務端渲染、同構的原理以後,下面從頭開始,一步一步完成一次同構,經過實踐來了解SSR。

實現基礎的NODEJS服務端渲染

首先,模擬一個最簡單的服務器渲染,只須要向頁面返回咱們須要的html文件。

const express = require('express');
const app = express();

app.get('/', function(req, res) {
    res.send(`
        <html>
            <head>
                <title>SSR</title>
            </head>
            <body>
                <p>hello world</p>
            </body>
        </html>
    `);
});

app.listen(3001, function() {
    console.log('listen:3001');
});

啓動以後打開localhost:3001能夠看到頁面顯示了hello world。並且打開網頁源代碼:

7

也就是說,當瀏覽器拿到服務器返回的這一段HTML源代碼的時候,不須要加載任何JavaScript腳本,就能夠直接將hello world顯示出來。

實現基礎的VUE客戶端渲染

咱們用 vue-cli新建一個vue項目,修改一個App.vue組件:

<template>
      <div>
            <p>hello world</p>
            <button @click="sayHello">say hello</button>
      </div>
</template>

<script>
export default {
    methods: {
        sayHello() {
              alert('hello ssr');
        }
    }
}
</script>

而後運行npm run serve啓動項目,打開瀏覽器,同樣能夠看到頁面顯示了 hello world,可是打開咱們開網頁源代碼:

8

除了簡單的兼容性處理 noscript 標籤之外,只有一個簡單的id爲app的div標籤,沒有關於hello world的任何字眼,能夠說這是一個空的頁面(白屏),而當加載了下面的 script 標籤的 JavaScript 腳本以後,頁面開始這行這些腳本,執行結束,hello world 正常顯示。也就是說真正渲染 hello world 的是 JavaScript 腳本。

同構VUE項目

構建配置

模板組件的共享,其實就是使用同一套組件代碼,爲了實現 Vue 組件能夠在服務端中運行,首先咱們須要解決代碼編譯問題。通常狀況,vue項目使用的是webpack進行代碼構建,一樣,服務端代碼的構建,也可使用webpack,借用官方的一張。

9

第一步:構建服務端代碼

由前面的圖能夠看到,在服務端代碼構建結束後,須要將構建結果運行在nodejs服務器上,可是,對於服務端代碼的構建,有一下內容須要注意:

  • 不須要編譯CSS,樣式表只有在瀏覽器(客戶端)運行時須要。
  • 構建的目標的運行環境是commonjs,nodejs的模塊化模式爲commonjs
  • 不須要代碼切割,nodejs將全部代碼一次性加載到內存中更有利於運行效率

因而,咱們獲得一個服務端的 webpack 構建配置文件 vue.server.config.js

const nodeExternals = require("webpack-node-externals");
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = {
    css: {
        extract: false // 不提取 CSS
    },
    configureWebpack: () => ({
        entry: `./src/server-entry.js`, // 服務器入口文件
        devtool: 'source-map',
        target: 'node', // 構建目標爲nodejs環境
        output: {
            libraryTarget: 'commonjs2' // 構建目標加載模式 commonjs
        },
        // 跳過 node_mdoules,運行時會自動加載,不須要編譯
        externals: nodeExternals({
            allowlist: [/\.css$/] // 容許css文件,方便css module
        }),
        optimization: {
            splitChunks: false // 關閉代碼切割
        },
          plugins: [
            new VueSSRServerPlugin()
        ]
    })
};

使用 vue-server-renderer提供的server-plugin,這個插件主要配合下面講到的client-plugin使用,做用主要是用來實現nodejs在開發過程當中的熱加載、source-map、生成html文件。

第二步:構建客戶端代碼

在構建客戶端代碼時,使用的是客戶端的執行入口文件,構建結束後,將構建結果在瀏覽器運行便可,可是在服務端渲染中,HTML是由服務端渲染的,也就是說,咱們要加載那些JavaScript腳本,是服務端決定的,由於HTML中的script標籤是由服務端拼接的,因此在客戶端代碼構建的時候,咱們須要使用插件,生成一個構建結果清單,這個清單是用來告訴服務端,當前頁面須要加載哪些JS腳本和CSS樣式表。

因而咱們獲得了客戶端的構建配置,vue.client.config.js

const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = {
    configureWebpack: () => ({
        entry: `./src/client-entry.js`,
        devtool: 'source-map',
        target: 'web',
        plugins: [
            new VueSSRClientPlugin()
        ]
    }),
    chainWebpack: config => {
          // 去除全部關於客戶端生成的html配置,由於已經交給後端生成
        config.plugins.delete('html');
        config.plugins.delete('preload');
        config.plugins.delete('prefetch');
    }
};

使用vue-server-renderer提供的client-server,主要做用是生成構建加過清單vue-ssr-client-manifest.json,服務端在渲染頁面時,根據這個清單來渲染HTML中的script標籤(JavaScript)和link標籤(CSS)。

接下來,咱們須要將vue.client.config.js和vue.server.config.js都交給vue-cli內置的構建配置文件vue.config.js,根據環境變量使用不一樣的配置

// vue.config.js
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
const serverConfig = require('./vue.server.config');
const clientConfig = require('./vue.client.config');

if (TARGET_NODE) {
    module.exports = serverConfig;
} else {
    module.exports = clientConfig;
}

使用cross-env區分環境

{
  "scripts": {
    "server": "babel-node src/server.js",
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server"
  }
}

模板組件共享

第一步:建立VUE實例

爲了實現模板組件共享,咱們須要將獲取 Vue 渲染實例寫成通用代碼,以下 createApp:

import Vue from 'vue';
import App from './App';

export default function createApp (context) {
    const app = new Vue({
        render: h => h(App)
    });
      return {
          app
    };
};
第二步:客戶端實例化VUE

新建客戶端項目的入口文件,client-entry.js

import Vue from 'vue'
import createApp from './createApp';

const {app} = createApp();

app.$mount('#app');

client-entry.js是瀏覽器渲染的入口文件,在瀏覽器加載了客戶端編譯後的代碼後,組件會被渲染到id爲app的元素節點上。

第三步:服務端實例化VUE

新建服務端代碼的入口文件,server-entry.js

import createApp from './createApp'

export default context => {
    const { app } = createApp(context);
    return app;
}

server-entry.js是提供給服務器渲染vue組件的入口文件,在瀏覽器經過URL訪問到服務器後,服務器須要使用server-entry.js提供的函數,將組件渲染成html。

第四步:HTTP服務

全部東西的準備好以後,咱們須要修改nodejs的HTTP服務器的啓動文件。首先,加載服務端代碼server-entry.js的webpack構建結果

const path = require('path');
const serverBundle = path.resolve(process.cwd(), 'serverDist', 'vue-ssr-server-bundle.json');
const {createBundleRenderer} = require('vue-server-renderer');
const serverBundle = path.resolve(process.cwd(), 'serverDist', 'vue-ssr-server-bundle.json');

加載客戶端代碼client-entry.js的webpack構建結果

const clientManifestPath = path.resolve(process.cwd(), 'dist', 'vue-ssr-client-manifest.json');
const clientManifest = require(clientManifestPath);

使用 vue-server-renderercreateBundleRenderer建立一個html渲染器:

const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');
const renderer = createBundleRenderer(serverBundle, {
    template,  // 使用HTML模板
    clientManifest // 將客戶端的構建結果清單傳入
});

建立HTML模板,index.html

<html>
  <head>
    <title>SSR</title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

在HTML模板中,經過傳入的客戶端渲染結果clientManifest,將自動注入全部link樣式表標籤,而佔位符<!--vue-ssr-outlet-->將會被替換成模板組件被渲染後的具體的HTML片斷和script腳本標籤。

HTML準備完成後,咱們在server中掛起全部路由請求

const express = require('express');
const app = express();

/* code todo 實例化渲染器renderer */

app.get('*', function(req, res) {
    renderer.renderToString({}, (err, html) => {
        if (err) {
            res.send('500 server error');
            return;
        }
        res.send(html);
    })
});

接下來,咱們構建客戶端、服務端項目,而後執行 node server.js,打開頁面源代碼,

10

看起來是符合預期的,可是發現控制檯有報錯,加載不到客戶端構建css和js,報404,緣由很明確,咱們沒有把客戶端的構建結果文件掛載到服務器的靜態資源目錄,在掛載路由前加入下面代碼:

app.use(express.static(path.resolve(process.cwd(), 'dist')));

看起來大功告成,點擊say hello也彈出了消息,細心的同窗會發現根節點有一個data-server-rendered屬性,這個屬性有什麼做用呢?

因爲服務器已經渲染好了 HTML,咱們顯然無需將其丟棄再從新建立全部的 DOM 元素。相反,咱們須要"激活"這些靜態的 HTML,而後使他們成爲動態的(可以響應後續的數據變化)。

若是檢查服務器渲染的輸出結果,應用程序的根元素上添加了一個特殊的屬性:

<div id="app" data-server-rendered="true">

data-server-rendered是特殊屬性,讓客戶端 Vue 知道這部分 HTML 是由 Vue 在服務端渲染的,而且應該以激活模式進行掛載。

路由的共享和同步

完成了模板組件的共享以後,下面完成路由的共享,咱們前面服務器使用的路由是*,接受任意URL,這容許全部URL請求交給Vue路由處理,進而完成客戶端路由與服務端路由的複用。

第一步:建立ROUTER實例

爲了實現複用,與createApp同樣,咱們建立一個createRouter.js

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home';
import About from './views/About';
Vue.use(Router)
const routes = [{
    path: '/',
    name: 'Home',
    component: Home
}, {
    path: '/about',
    name: 'About',
    component: About
}];
export default function createRouter() {
    return new Router({
        mode: 'history',
        routes
    })
}

在createApp.js中建立router

import Vue from 'vue';
import App from './App';
import createRouter from './createRouter';

export default function createApp(context) {
    const router = createRouter(); // 建立 router 實例
    const app = new Vue({
        router, // 注入 router 到根 Vue 實例
        render: h => h(App)
    });
    return { router, app };
};
第二步:路由匹配

router準備好了以後,修改server-entry.js,將請求的URL傳遞給router,使得在建立app的時候能夠根據URL匹配到對應的路由,進而可知道須要渲染哪些組件

import createApp from './createApp';

export default context => {
    // 由於有可能會是異步路由鉤子函數或組件,因此咱們將返回一個 Promise,
    // 以便服務器可以等待全部的內容在渲染前就已經準備就緒。
    return new Promise((resolve, reject) => {
        const { app, router } = createApp();
        // 設置服務器端 router 的位置
        router.push(context.url)
        // onReady 等到 router 將可能的異步組件和鉤子函數解析完
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents();
            // 匹配不到的路由,執行 reject 函數,並返回 404
            if (!matchedComponents.length) {
                return reject({
                    code: 404
                });
            }
            // Promise 應該 resolve 應用程序實例,以便它能夠渲染
            resolve(app)
        }, reject)
    })
}

修改server.js的路由,把url傳遞給renderer

app.get('*', function(req, res) {
    const context = {
        url: req.url
    };
    renderer.renderToString(context, (err, html) => {
        if (err) {
            console.log(err);
            res.send('500 server error');
            return;
        }
        res.send(html);
    })
});

爲了測試,咱們將App.vue修改成router-view

<template>
    <div id="app">
        <router-link to="/">Home</router-link>
        <router-link to="/about">About</router-link>
        <router-view />
    </div>
</template>

Home.vue

<template>
    <div>Home Page</div>
</template>

About.vue

<template>
    <div>About Page</div>
</template>

編譯,運行,查看源代碼

11

點擊路由並無刷新頁面,而是客戶端路由跳轉的,一切符合預期。

數據模型的共享與狀態同步

前面咱們簡單的實現了服務端渲染,可是實際狀況下,咱們在訪問頁面的時候,還須要獲取須要渲染的數據,而且渲染成HTML,也就是說,在渲染HTML以前,咱們須要將全部數據都準備好,而後傳遞給renderer。

通常狀況下,在Vue中,咱們將狀態數據交給Vuex進行管理,固然,狀態也能夠保存在組件內部,只不過須要組件實例化的時候本身去同步數據。

第一步:建立STORE實例

首先第一步,與createApp相似,建立一個createStore.js,用來實例化store,同時提供給客戶端和服務端使用

import Vue from 'vue';
import Vuex from 'vuex';
import {fetchItem} from './api';

Vue.use(Vuex);

export default function createStore() {
    return new Vuex.Store({
        state: {
            item: {}
        },
        actions: {
            fetchItem({ commit }, id) {
                return fetchItem(id).then(item => {
                    commit('setItem', item);
                })
            }
        },
        mutations: {
            setItem(state, item) {
                Vue.set(state.item, item);
            }
        }
    })
}

actions封裝了請求數據的函數,mutations用來設置狀態。

將createStore加入到createApp中,並將store注入到vue實例中,讓全部Vue組件能夠獲取到store實例

export default function createApp(context) {
    const router = createRouter();
    const store = createStore();
    const app = new Vue({
        router,
        store, // 注入 store 到根 Vue 實例
        render: h => h(App)
    });
    return { router, store, app };
};

爲了方便測試,咱們mock一個遠程服務函數fetchItem,用於查詢對應item

export function fetchItem(id) {
    const items = [
        { name: 'item1', id: 1 },
        { name: 'item2', id: 2 },
        { name: 'item3', id: 3 }
    ];
    const item = items.find(i => i.id == id);
    return Promise.resolve(item);
}
第二步:STORE鏈接組件

通常狀況下,咱們須要經過訪問路由,來決定獲取哪部分數據,這也決定了哪些組件須要渲染。事實上,給定路由所需的數據,也是在該路由上渲染組件時所需的數據。因此,咱們須要在路由的組件中放置數據預取邏輯函數。

在Home組件中自定義一個靜態函數asyncData,須要注意的是,因爲此函數會在組件實例化以前調用,因此它沒法訪問 this。須要將 store 和路由信息做爲參數傳遞進去

<template>
<div>
    <div>id: {{item.id}}</div>
    <div>name: {{item.name}}</div>
</div>
</template>

<script>
export default {
    asyncData({ store, route }) {
        // 觸發 action 後,會返回 Promise
        return store.dispatch('fetchItems', route.params.id)
    },
    computed: {
        // 從 store 的 state 對象中的獲取 item。
        item() {
            return this.$store.state.item;
        }
    }
}
</script>
第三步:服務端獲取數據

在服務器的入口文件server-entry.js中,咱們經過URL路由匹配 router.getMatchedComponents()獲得了須要渲染的組件,這個時候咱們能夠調用組件內部的asyncData方法,將所須要的全部數據都獲取完後,傳遞給渲染器renderer上下文。

修改createApp,在路由組件匹配到了以後,調用asyncData方法,獲取數據後傳遞給renderer

import createApp from './createApp';

export default context => {
    // 由於有可能會是異步路由鉤子函數或組件,因此咱們將返回一個 Promise,
    // 以便服務器可以等待全部的內容在渲染前就已經準備就緒。
    return new Promise((resolve, reject) => {
        const { app, router, store } = createApp();
        // 設置服務器端 router 的位置
        router.push(context.url)
        // onReady 等到 router 將可能的異步組件和鉤子函數解析完
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents();
            // 匹配不到的路由,執行 reject 函數,並返回 404
            if (!matchedComponents.length) {
                return reject({ code: 404 })
            }
            // 對全部匹配的路由組件調用 `asyncData()`
            Promise.all(matchedComponents.map(Component => {
                if (Component.asyncData) {
                    return Component.asyncData({
                        store,
                        route: router.currentRoute
                    });
                }
            })).then(() => {
                // 狀態傳遞給renderer的上下文,方便後面客戶端激活數據
                context.state = store.state
                resolve(app)
            }).catch(reject);
        }, reject);
    })
}

將state存入context後,在服務端渲染HTML時候,也就是渲染template的時候,context.state會被序列化到window.__INITIAL_STATE__中,方便客戶端激活數據。

第四步:客戶端激活狀態數據

服務端預請求數據以後,經過將數據注入到組件中,渲染組件並轉化成HTML,而後吐給客戶端,那麼客戶端爲了激活後端返回的HTML被解析後的DOM節點,須要將後端渲染組件時用的store的state也同步到瀏覽器的store中,保證在頁面渲染的時候保持與服務器渲染時的數據是一致的,才能完成DOM的激活,也就是咱們前面說到的data-server-rendered標記。

在服務端的渲染中,state已經被序列化到了window.__INITIAL_STATE__,好比咱們訪問 http://localhost:3001?id=1,而後查看頁面源代碼

能夠看到,狀態已經被序列化到window.__INITIAL_STATE__中,咱們須要作的就是將這個window.__INITIAL_STATE__在客戶端渲染以前,同步到客戶端的store中,下面修改client-entry.js

const { app, router, store } = createApp();

if (window.__INITIAL_STATE__) {
      // 激活狀態數據
    store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() => {
    app.$mount('#app', true);
});

經過使用store的replaceState函數,將window.__INITIAL_STATE__同步到store內部,完成數據模型的狀態同步。

總結

當瀏覽器訪問服務端渲染項目時,服務端將URL傳給到預選構建好的VUE應用渲染器,渲染器匹配到對應的路由的組件以後,執行咱們預先在組件內定義的asyncData方法獲取數據,並將獲取完的數據傳遞給渲染器的上下文,利用template組裝成HTML,並將HTML和狀態state一併吐給前端瀏覽器,瀏覽器加載了構建好的客戶端VUE應用後,將state數據同步到前端的store中,並根據數據激活後端返回的被瀏覽器解析爲DOM元素的HTML文本,完成了數據狀態、路由、組件的同步,同時使得頁面獲得直出,較少了白屏時間,有了更好的加載體驗,同時更有利於SEO。

我的以爲了解服務端渲染,有助於提高前端工程師的綜合能力,由於它的內容除了前端框架,還有前端構建和後端內容,是一個性價比還挺高的知識,不學白不學,加油!

參考文獻

相關文章
相關標籤/搜索