日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

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

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

服務端渲染的定義

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

  1. 瀏覽器通過請求得到一個html文本
  2. 渲染進程解析HTML文本,構建DOM樹
  3. 解析HTML的同時,如果遇到內聯樣式或者樣式腳本,則下載并構建樣式規則(stytle rules),若遇到JAVAScript腳本,則會下載執行腳本。
  4. DOM樹和樣式規則構建完成之后,渲染進程將兩者合并成渲染樹(render tree)
  5. 渲染進程開始對渲染樹進行布局,生成布局樹(layout tree)
  6. 渲染進程對布局樹進行繪制,生成繪制記錄
  7. 渲染進程的對布局樹進行分層,分別柵格化每一層,并得到合成幀
  8. 渲染進程將合成幀信息發送給GPU進程顯示到頁面中
徹底理解服務端渲染 - SSR原理

 

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

那么什么是服務端渲染(server side render)?顧名思義,服務端渲染就是在瀏覽器請求頁面URL的時候,服務端將我們需要的HTML文本組裝好,并返回給瀏覽器,這個HTML文本被瀏覽器解析之后,不需要經過 JavaScript 腳本的執行,即可直接構建出希望的 DOM 樹并展示到頁面中。這個服務端組裝HTML的過程,叫做服務端渲染。

徹底理解服務端渲染 - SSR原理

 

服務端渲染的由來

Web1.0

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

徹底理解服務端渲染 - SSR原理

 

客戶端渲染

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

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

而且那個時候,根本就沒有前端工程師這一職位,前端js的活一般都由后端同學 jQuery 一把梭。但是隨著前端頁面漸漸地復雜了之后,后端開始發現js好麻煩,雖然很簡單,但是坑太多了,于是讓公司招聘了一些專門寫js的人,也就是前端,這個時候,前后端的鄙視鏈就出現了,后端鄙視前端,因為后端覺得js太簡單,無非就是寫寫頁面的特效(JS),切切圖(css),根本算不上是真正的程序員。

隨之 nodejs 的出現,前端看到了翻身的契機,為了擺脫后端的指指點點,前端開啟了一場前后端分離的運動,希望可以脫離后端獨立發展。前后端分離,表面上看上去是代碼分離,實際上是為了前后端人員分離,也就是前后端分家,前端不再歸屬于后端團隊。

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

徹底理解服務端渲染 - SSR原理

 

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

服務端渲染

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

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

徹底理解服務端渲染 - SSR原理

 

服務端渲染的利弊

相比于客戶端渲染,服務端渲染有什么優勢?

利于SEO

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

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

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

白屏時間更短

相對于客戶端渲染,服務端渲染在瀏覽器請求URL之后已經得到了一個帶有數據的HTML文本,瀏覽器只需要解析HTML,直接構建DOM樹就可以。而客戶端渲染,需要先得到一個空的HTML頁面,這個時候頁面已經進入白屏,之后還需要經過加載并執行 JavaScript、請求后端服務器獲取數據、JavaScript 渲染頁面幾個過程才可以看到最后的頁面。特別是在復雜應用中,由于需要加載 JavaScript 腳本,越是復雜的應用,需要加載的 JavaScript 腳本就越多、越大,這會導致應用的首屏加載時間非常長,進而降低了體驗感。

徹底理解服務端渲染 - SSR原理

 

服務端渲染缺點

并不是所有的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 中, 模板可以理解成組件,數據可以理解為數據模型,即響應式數據。所以,對于同構應用來說,我們必須實現客戶端與服務端的路由、模型組件、數據模型的共享。

徹底理解服務端渲染 - SSR原理

 

實踐

知道了服務端渲染、同構的原理之后,下面從頭開始,一步一步完成一次同構,通過實踐來了解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。而且打開網頁源代碼:

徹底理解服務端渲染 - SSR原理

 

也就是說,當瀏覽器拿到服務器返回的這一段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,但是打開我們開網頁源代碼:

徹底理解服務端渲染 - SSR原理

 

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

同構VUE項目

構建配置

模板組件的共享,其實就是使用同一套組件代碼,為了實現 Vue 組件可以在服務端中運行,首先我們需要解決代碼編譯問題。一般情況,vue項目使用的是webpack進行代碼構建,同樣,服務端代碼的構建,也可以使用webpack,借用官方的一張。

徹底理解服務端渲染 - SSR原理

 

第一步:構建服務端代碼

由前面的圖可以看到,在服務端代碼構建結束后,需要將構建結果運行在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-renderer 的createBundleRenderer創建一個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樣式表標簽,而占位符將會被替換成模板組件被渲染后的具體的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,打開頁面源代碼,

徹底理解服務端渲染 - SSR原理

 

看起來是符合預期的,但是發現控制臺有報錯,加載不到客戶端構建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>

編譯,運行,查看源代碼

徹底理解服務端渲染 - SSR原理

 

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

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

前面我們簡單的實現了服務端渲染,但是實際情況下,我們在訪問頁面的時候,還需要獲取需要渲染的數據,并且渲染成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,查看頁面源代碼

徹底理解服務端渲染 - SSR原理

 

可以看到,狀態已經被序列化到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。

個人覺得了解服務端渲染,有助于提升前端工程師的綜合能力,因為它的內容除了前端框架,還有前端構建和后端內容,是一個性價比還挺高的知識,不學白不學,加油!

分享到:
標簽:原理 SSR
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定