九色国产,午夜在线视频,新黄色网址,九九色综合,天天做夜夜做久久做狠狠,天天躁夜夜躁狠狠躁2021a,久久不卡一区二区三区

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費(fèi)電子書(shū)等14項(xiàng)超值服

開(kāi)通VIP
徹底理解服務(wù)端渲染 - SSR原理

本人主要從個(gè)人角度介紹了對(duì)服務(wù)端渲染的理解,讀完本文后,你將了解到:

  • 什么是服務(wù)端渲染,與客戶端渲染的區(qū)別是什么?

  • 為什么需要服務(wù)端渲染,服務(wù)端渲染的利弊是什么?

  • 如何對(duì)VUE項(xiàng)目進(jìn)行同構(gòu)?

服務(wù)端渲染的定義

在講服務(wù)度渲染之前,我們先回顧一下頁(yè)面的渲染流程:

  1. 瀏覽器通過(guò)請(qǐng)求得到一個(gè)HTML文本

  2. 渲染進(jìn)程解析HTML文本,構(gòu)建DOM樹(shù)

  3. 解析HTML的同時(shí),如果遇到內(nèi)聯(lián)樣式或者樣式腳本,則下載并構(gòu)建樣式規(guī)則(stytle rules),若遇到JavaScript腳本,則會(huì)下載執(zhí)行腳本。

  4. DOM樹(shù)和樣式規(guī)則構(gòu)建完成之后,渲染進(jìn)程將兩者合并成渲染樹(shù)(render tree)

  5. 渲染進(jìn)程開(kāi)始對(duì)渲染樹(shù)進(jìn)行布局,生成布局樹(shù)(layout tree)

  6. 渲染進(jìn)程對(duì)布局樹(shù)進(jìn)行繪制,生成繪制記錄

  7. 渲染進(jìn)程的對(duì)布局樹(shù)進(jìn)行分層,分別柵格化每一層,并得到合成幀

  8. 渲染進(jìn)程將合成幀信息發(fā)送給GPU進(jìn)程顯示到頁(yè)面中

可以看到,頁(yè)面的渲染其實(shí)就是瀏覽器將HTML文本轉(zhuǎn)化為頁(yè)面幀的過(guò)程。而如今我們大部分WEB應(yīng)用都是使用 JavaScript 框架(Vue、React、Angular)進(jìn)行頁(yè)面渲染的,也就是說(shuō),在執(zhí)行 JavaScript 腳本的時(shí)候,HTML頁(yè)面已經(jīng)開(kāi)始解析并且構(gòu)建DOM樹(shù)了,JavaScript 腳本只是動(dòng)態(tài)的改變 DOM 樹(shù)的結(jié)構(gòu),使得頁(yè)面成為希望成為的樣子,這種渲染方式叫動(dòng)態(tài)渲染,也可以叫客戶端渲染(client side rende)。

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

服務(wù)端渲染的由來(lái)

Web1.0

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

客戶端渲染

在WEB1.0時(shí)代,服務(wù)端渲染看起來(lái)是一個(gè)當(dāng)時(shí)的最好的渲染方式,但是隨著業(yè)務(wù)的日益復(fù)雜和后續(xù)AJAX的出現(xiàn),也漸漸開(kāi)始暴露出了WEB1.0服務(wù)器渲染的缺點(diǎn)。

  • 每次更新頁(yè)面的一小的模塊,都需要重新請(qǐng)求一次頁(yè)面,重新查一次數(shù)據(jù)庫(kù),重新組裝一次HTML

  • 前端JavaScript代碼和后端(jsp、php、jsp)代碼混雜在一起,使得日益復(fù)雜的WEB應(yīng)用難以維護(hù)

而且那個(gè)時(shí)候,根本就沒(méi)有前端工程師這一職位,前端js的活一般都由后端同學(xué) jQuery 一把梭。但是隨著前端頁(yè)面漸漸地復(fù)雜了之后,后端開(kāi)始發(fā)現(xiàn)js好麻煩,雖然很簡(jiǎn)單,但是坑太多了,于是讓公司招聘了一些專門寫js的人,也就是前端,這個(gè)時(shí)候,前后端的鄙視鏈就出現(xiàn)了,后端鄙視前端,因?yàn)楹蠖擞X(jué)得js太簡(jiǎn)單,無(wú)非就是寫寫頁(yè)面的特效(JS),切切圖(CSS),根本算不上是真正的程序員。

隨之 nodejs 的出現(xiàn),前端看到了翻身的契機(jī),為了擺脫后端的指指點(diǎn)點(diǎn),前端開(kāi)啟了一場(chǎng)前后端分離的運(yùn)動(dòng),希望可以脫離后端獨(dú)立發(fā)展。前后端分離,表面上看上去是代碼分離,實(shí)際上是為了前后端人員分離,也就是前后端分家,前端不再歸屬于后端團(tuán)隊(duì)。

前后端分離之后,網(wǎng)頁(yè)開(kāi)始被當(dāng)成了獨(dú)立的應(yīng)用程序(SPA,Single Page Application),前端團(tuán)隊(duì)接管了所有頁(yè)面渲染的事,后端團(tuán)隊(duì)只負(fù)責(zé)提供所有數(shù)據(jù)查詢與處理的API,大體流程是這樣的:首先瀏覽器請(qǐng)求URL,前端服務(wù)器直接返回一個(gè)空的靜態(tài)HTML文件(不需要任何查數(shù)據(jù)庫(kù)和模板組裝),這個(gè)HTML文件中加載了很多渲染頁(yè)面需要的 JavaScript 腳本和 CSS 樣式表,瀏覽器拿到 HTML 文件后開(kāi)始加載腳本和樣式表,并且執(zhí)行腳本,這個(gè)時(shí)候腳本請(qǐng)求后端服務(wù)提供的API,獲取數(shù)據(jù),獲取完成后將數(shù)據(jù)通過(guò)JavaScript腳本動(dòng)態(tài)的將數(shù)據(jù)渲染到頁(yè)面中,完成頁(yè)面顯示。

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

服務(wù)端渲染

隨著單頁(yè)應(yīng)用(SPA)的發(fā)展,程序員們漸漸發(fā)現(xiàn) SEO(Search Engine Optimazition,即搜索引擎優(yōu)化)出了問(wèn)題,而且隨著應(yīng)用的復(fù)雜化,JavaScript 腳本也不斷的臃腫起來(lái),使得首屏渲染相比于 Web1.0時(shí)候的服務(wù)端渲染,也慢了不少。

自己選的路,跪著也要走下去。于是前端團(tuán)隊(duì)選擇了使用 nodejs 在服務(wù)器進(jìn)行頁(yè)面的渲染,進(jìn)而再次出現(xiàn)了服務(wù)端渲染。大體流程與客戶端渲染有些相似,首先是瀏覽器請(qǐng)求URL,前端服務(wù)器接收到URL請(qǐng)求之后,根據(jù)不同的URL,前端服務(wù)器向后端服務(wù)器請(qǐng)求數(shù)據(jù),請(qǐng)求完成后,前端服務(wù)器會(huì)組裝一個(gè)攜帶了具體數(shù)據(jù)的HTML文本,并且返回給瀏覽器,瀏覽器得到HTML之后開(kāi)始渲染頁(yè)面,同時(shí),瀏覽器加載并執(zhí)行 JavaScript 腳本,給頁(yè)面上的元素綁定事件,讓頁(yè)面變得可交互,當(dāng)用戶與瀏覽器頁(yè)面進(jìn)行交互,如跳轉(zhuǎn)到下一個(gè)頁(yè)面時(shí),瀏覽器會(huì)執(zhí)行 JavaScript 腳本,向后端服務(wù)器請(qǐng)求數(shù)據(jù),獲取完數(shù)據(jù)之后再次執(zhí)行 JavaScript 代碼動(dòng)態(tài)渲染頁(yè)面。

服務(wù)端渲染的利弊

相比于客戶端渲染,服務(wù)端渲染有什么優(yōu)勢(shì)?

利于SEO

有利于SEO,其實(shí)就是有利于爬蟲(chóng)來(lái)爬你的頁(yè)面,然后在別人使用搜索引擎搜索相關(guān)的內(nèi)容時(shí),你的網(wǎng)頁(yè)排行能靠得更前,這樣你的流量就有越高。那為什么服務(wù)端渲染更利于爬蟲(chóng)爬你的頁(yè)面呢?其實(shí),爬蟲(chóng)也分低級(jí)爬蟲(chóng)和高級(jí)爬蟲(chóng)。

  • 低級(jí)爬蟲(chóng):只請(qǐng)求URL,URL返回的HTML是什么內(nèi)容就爬什么內(nèi)容。

  • 高級(jí)爬蟲(chóng):請(qǐng)求URL,加載并執(zhí)行JavaScript腳本渲染頁(yè)面,爬JavaScript渲染后的內(nèi)容。

也就是說(shuō),低級(jí)爬蟲(chóng)對(duì)客戶端渲染的頁(yè)面來(lái)說(shuō),簡(jiǎn)直無(wú)能為力,因?yàn)榉祷氐腍TML是一個(gè)空殼,它需要執(zhí)行 JavaScript 腳本之后才會(huì)渲染真正的頁(yè)面。而目前像百度、谷歌、微軟等公司,有一部分年代老舊的爬蟲(chóng)還屬于低級(jí)爬蟲(chóng),使用服務(wù)端渲染,對(duì)這些低級(jí)爬蟲(chóng)更加友好一些。

白屏?xí)r間更短

相對(duì)于客戶端渲染,服務(wù)端渲染在瀏覽器請(qǐng)求URL之后已經(jīng)得到了一個(gè)帶有數(shù)據(jù)的HTML文本,瀏覽器只需要解析HTML,直接構(gòu)建DOM樹(shù)就可以。而客戶端渲染,需要先得到一個(gè)空的HTML頁(yè)面,這個(gè)時(shí)候頁(yè)面已經(jīng)進(jìn)入白屏,之后還需要經(jīng)過(guò)加載并執(zhí)行 JavaScript、請(qǐng)求后端服務(wù)器獲取數(shù)據(jù)、JavaScript 渲染頁(yè)面幾個(gè)過(guò)程才可以看到最后的頁(yè)面。特別是在復(fù)雜應(yīng)用中,由于需要加載 JavaScript 腳本,越是復(fù)雜的應(yīng)用,需要加載的 JavaScript 腳本就越多、越大,這會(huì)導(dǎo)致應(yīng)用的首屏加載時(shí)間非常長(zhǎng),進(jìn)而降低了體驗(yàn)感。

服務(wù)端渲染缺點(diǎn)

并不是所有的WEB應(yīng)用都必須使用SSR,這需要開(kāi)發(fā)者自己來(lái)權(quán)衡,因?yàn)榉?wù)端渲染會(huì)帶來(lái)以下問(wèn)題:

  • 代碼復(fù)雜度增加。為了實(shí)現(xiàn)服務(wù)端渲染,應(yīng)用代碼中需要兼容服務(wù)端和客戶端兩種運(yùn)行情況,而一部分依賴的外部擴(kuò)展庫(kù)卻只能在客戶端運(yùn)行,需要對(duì)其進(jìn)行特殊處理,才能在服務(wù)器渲染應(yīng)用程序中運(yùn)行。

  • 需要更多的服務(wù)器負(fù)載均衡。由于服務(wù)器增加了渲染HTML的需求,使得原本只需要輸出靜態(tài)資源文件的nodejs服務(wù),新增了數(shù)據(jù)獲取的IO和渲染HTML的CPU占用,如果流量突然暴增,有可能導(dǎo)致服務(wù)器down機(jī),因此需要使用響應(yīng)的緩存策略和準(zhǔn)備相應(yīng)的服務(wù)器負(fù)載。

  • 涉及構(gòu)建設(shè)置和部署的更多要求。與可以部署在任何靜態(tài)文件服務(wù)器上的完全靜態(tài)單頁(yè)面應(yīng)用程序 (SPA) 不同,服務(wù)器渲染應(yīng)用程序,需要處于 Node.js server 運(yùn)行環(huán)境。

所以在使用服務(wù)端渲染SSR之前,需要開(kāi)發(fā)者考慮投入產(chǎn)出比,比如大部分應(yīng)用系統(tǒng)都不需要SEO,而且首屏?xí)r間并沒(méi)有非常的慢,如果使用SSR反而小題大做了。

同構(gòu)

知道了服務(wù)器渲染的利弊后,假如我們需要在項(xiàng)目中使用服務(wù)端渲染,我們需要做什么呢?那就是同構(gòu)我們的項(xiàng)目。

同構(gòu)的定義

在服務(wù)端渲染中,有兩種頁(yè)面渲染的方式:

  • 前端服務(wù)器通過(guò)請(qǐng)求后端服務(wù)器獲取數(shù)據(jù)并組裝HTML返回給瀏覽器,瀏覽器直接解析HTML后渲染頁(yè)面

  • 瀏覽器在交互過(guò)程中,請(qǐng)求新的數(shù)據(jù)并動(dòng)態(tài)更新渲染頁(yè)面

這兩種渲染方式有一個(gè)不同點(diǎn)就是,一個(gè)是在服務(wù)端中組裝html的,一個(gè)是在客戶端中組裝html的,運(yùn)行環(huán)境是不一樣的。所謂同構(gòu),就是讓一份代碼,既可以在服務(wù)端中執(zhí)行,也可以在客戶端中執(zhí)行,并且執(zhí)行的效果都是一樣的,都是完成這個(gè)html的組裝,正確的顯示頁(yè)面。也就是說(shuō),一份代碼,既可以客戶端渲染,也可以服務(wù)端渲染。

同構(gòu)的條件

為了實(shí)現(xiàn)同構(gòu),我們需要滿足什么條件呢?首先,我們思考一個(gè)應(yīng)用中一個(gè)頁(yè)面的組成,假如我們使用的是Vue.js,當(dāng)我們打開(kāi)一個(gè)頁(yè)面時(shí),首先是打開(kāi)這個(gè)頁(yè)面的URL,這個(gè)URL,可以通過(guò)應(yīng)用的路由匹配,找到具體的頁(yè)面,不同的頁(yè)面有不同的視圖,那么,視圖是什么?從應(yīng)用的角度來(lái)看,視圖 = 模板 + 數(shù)據(jù),那么在 Vue.js 中, 模板可以理解成組件,數(shù)據(jù)可以理解為數(shù)據(jù)模型,即響應(yīng)式數(shù)據(jù)。所以,對(duì)于同構(gòu)應(yīng)用來(lái)說(shuō),我們必須實(shí)現(xiàn)客戶端與服務(wù)端的路由、模型組件、數(shù)據(jù)模型的共享。

實(shí)踐

知道了服務(wù)端渲染、同構(gòu)的原理之后,下面從頭開(kāi)始,一步一步完成一次同構(gòu),通過(guò)實(shí)踐來(lái)了解SSR。

實(shí)現(xiàn)基礎(chǔ)的NODEJS服務(wù)端渲染

首先,模擬一個(gè)最簡(jiǎn)單的服務(wù)器渲染,只需要向頁(yè)面返回我們需要的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');});

啟動(dòng)之后打開(kāi)localhost:3001可以看到頁(yè)面顯示了hello world。而且打開(kāi)網(wǎng)頁(yè)源代碼:

也就是說(shuō),當(dāng)瀏覽器拿到服務(wù)器返回的這一段HTML源代碼的時(shí)候,不需要加載任何JavaScript腳本,就可以直接將hello world顯示出來(lái)。

實(shí)現(xiàn)基礎(chǔ)的VUE客戶端渲染

我們用 vue-cli新建一個(gè)vue項(xiàng)目,修改一個(gè)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>

然后運(yùn)行npm run serve啟動(dòng)項(xiàng)目,打開(kāi)瀏覽器,一樣可以看到頁(yè)面顯示了 hello world,但是打開(kāi)我們開(kāi)網(wǎng)頁(yè)源代碼:

除了簡(jiǎn)單的兼容性處理 noscript 標(biāo)簽以外,只有一個(gè)簡(jiǎn)單的id為app的div標(biāo)簽,沒(méi)有關(guān)于hello world的任何字眼,可以說(shuō)這是一個(gè)空的頁(yè)面(白屏),而當(dāng)加載了下面的 script 標(biāo)簽的 JavaScript 腳本之后,頁(yè)面開(kāi)始這行這些腳本,執(zhí)行結(jié)束,hello world 正常顯示。也就是說(shuō)真正渲染 hello world 的是 JavaScript 腳本。

同構(gòu)VUE項(xiàng)目

構(gòu)建配置

模板組件的共享,其實(shí)就是使用同一套組件代碼,為了實(shí)現(xiàn) Vue 組件可以在服務(wù)端中運(yùn)行,首先我們需要解決代碼編譯問(wèn)題。一般情況,vue項(xiàng)目使用的是webpack進(jìn)行代碼構(gòu)建,同樣,服務(wù)端代碼的構(gòu)建,也可以使用webpack,借用官方的一張。

第一步:構(gòu)建服務(wù)端代碼

由前面的圖可以看到,在服務(wù)端代碼構(gòu)建結(jié)束后,需要將構(gòu)建結(jié)果運(yùn)行在nodejs服務(wù)器上,但是,對(duì)于服務(wù)端代碼的構(gòu)建,有一下內(nèi)容需要注意:

  • 不需要編譯CSS,樣式表只有在瀏覽器(客戶端)運(yùn)行時(shí)需要。

  • 構(gòu)建的目標(biāo)的運(yùn)行環(huán)境是commonjs,nodejs的模塊化模式為commonjs

  • 不需要代碼切割,nodejs將所有代碼一次性加載到內(nèi)存中更有利于運(yùn)行效率

于是,我們得到一個(gè)服務(wù)端的 webpack 構(gòu)建配置文件 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`, // 服務(wù)器入口文件 devtool: 'source-map', target: 'node', // 構(gòu)建目標(biāo)為nodejs環(huán)境 output: { libraryTarget: 'commonjs2' // 構(gòu)建目標(biāo)加載模式 commonjs }, // 跳過(guò) node_mdoules,運(yùn)行時(shí)會(huì)自動(dòng)加載,不需要編譯 externals: nodeExternals({ allowlist: [/\.css$/] // 允許css文件,方便css module }), optimization: { splitChunks: false // 關(guān)閉代碼切割 }, plugins: [ new VueSSRServerPlugin() ] })};

使用 vue-server-renderer提供的server-plugin,這個(gè)插件主要配合下面講到的client-plugin使用,作用主要是用來(lái)實(shí)現(xiàn)nodejs在開(kāi)發(fā)過(guò)程中的熱加載、source-map、生成html文件。

第二步:構(gòu)建客戶端代碼

在構(gòu)建客戶端代碼時(shí),使用的是客戶端的執(zhí)行入口文件,構(gòu)建結(jié)束后,將構(gòu)建結(jié)果在瀏覽器運(yùn)行即可,但是在服務(wù)端渲染中,HTML是由服務(wù)端渲染的,也就是說(shuō),我們要加載那些JavaScript腳本,是服務(wù)端決定的,因?yàn)镠TML中的script標(biāo)簽是由服務(wù)端拼接的,所以在客戶端代碼構(gòu)建的時(shí)候,我們需要使用插件,生成一個(gè)構(gòu)建結(jié)果清單,這個(gè)清單是用來(lái)告訴服務(wù)端,當(dāng)前頁(yè)面需要加載哪些JS腳本和CSS樣式表。

于是我們得到了客戶端的構(gòu)建配置,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 => {      	// 去除所有關(guān)于客戶端生成的html配置,因?yàn)橐呀?jīng)交給后端生成        config.plugins.delete('html');        config.plugins.delete('preload');        config.plugins.delete('prefetch');    }};

使用vue-server-renderer提供的client-server,主要作用是生成構(gòu)建加過(guò)清單
vue-ssr-client-manifest.json,服務(wù)端在渲染頁(yè)面時(shí),根據(jù)這個(gè)清單來(lái)渲染HTML中的script標(biāo)簽(JavaScript)和link標(biāo)簽(CSS)。

接下來(lái),我們需要將vue.client.config.js和vue.server.config.js都交給vue-cli內(nèi)置的構(gòu)建配置文件vue.config.js,根據(jù)環(huán)境變量使用不同的配置

// vue.config.jsconst 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區(qū)分環(huán)境

{  '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'  }}

模板組件共享

第一步:創(chuàng)建VUE實(shí)例

為了實(shí)現(xiàn)模板組件共享,我們需要將獲取 Vue 渲染實(shí)例寫成通用代碼,如下 createApp:

import Vue from 'vue';import App from './App';export default function createApp (context) { const app = new Vue({ render: h => h(App) }); return { app };};

第二步:客戶端實(shí)例化VUE

新建客戶端項(xiàng)目的入口文件,client-entry.js

import Vue from 'vue'import createApp from './createApp';const {app} = createApp();app.$mount('#app');

client-entry.js是瀏覽器渲染的入口文件,在瀏覽器加載了客戶端編譯后的代碼后,組件會(huì)被渲染到id為app的元素節(jié)點(diǎn)上。

第三步:服務(wù)端實(shí)例化VUE

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

import createApp from './createApp'export default context => { const { app } = createApp(context); return app;}

server-entry.js是提供給服務(wù)器渲染vue組件的入口文件,在瀏覽器通過(guò)URL訪問(wèn)到服務(wù)器后,服務(wù)器需要使用server-entry.js提供的函數(shù),將組件渲染成html。

第四步:HTTP服務(wù)

所有東西的準(zhǔn)備好之后,我們需要修改nodejs的HTTP服務(wù)器的啟動(dòng)文件。首先,加載服務(wù)端代碼server-entry.js的webpack構(gòu)建結(jié)果

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構(gòu)建結(jié)果

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

使用 vue-server-renderer 的createBundleRenderer創(chuàng)建一個(gè)html渲染器:

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

創(chuàng)建HTML模板,index.html

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

在HTML模板中,通過(guò)傳入的客戶端渲染結(jié)果clientManifest,將自動(dòng)注入所有l(wèi)ink樣式表標(biāo)簽,而占位符將會(huì)被替換成模板組件被渲染后的具體的HTML片段和script腳本標(biāo)簽。

HTML準(zhǔn)備完成后,我們?cè)趕erver中掛起所有路由請(qǐng)求

const express = require('express');const app = express();/* code todo 實(shí)例化渲染器renderer */app.get('*', function(req, res) {    renderer.renderToString({}, (err, html) => {        if (err) {            res.send('500 server error');            return;        }        res.send(html);    })});

接下來(lái),我們構(gòu)建客戶端、服務(wù)端項(xiàng)目,然后執(zhí)行 node server.js,打開(kāi)頁(yè)面源代碼,

看起來(lái)是符合預(yù)期的,但是發(fā)現(xiàn)控制臺(tái)有報(bào)錯(cuò),加載不到客戶端構(gòu)建css和js,報(bào)404,原因很明確,我們沒(méi)有把客戶端的構(gòu)建結(jié)果文件掛載到服務(wù)器的靜態(tài)資源目錄,在掛載路由前加入下面代碼:

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

看起來(lái)大功告成,點(diǎn)擊say hello也彈出了消息,細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)根節(jié)點(diǎn)有一個(gè)data-server-rendered屬性,這個(gè)屬性有什么作用呢?

由于服務(wù)器已經(jīng)渲染好了 HTML,我們顯然無(wú)需將其丟棄再重新創(chuàng)建所有的 DOM 元素。相反,我們需要'激活'這些靜態(tài)的 HTML,然后使他們成為動(dòng)態(tài)的(能夠響應(yīng)后續(xù)的數(shù)據(jù)變化)。

如果檢查服務(wù)器渲染的輸出結(jié)果,應(yīng)用程序的根元素上添加了一個(gè)特殊的屬性:

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

data-server-rendered是特殊屬性,讓客戶端 Vue 知道這部分 HTML 是由 Vue 在服務(wù)端渲染的,并且應(yīng)該以激活模式進(jìn)行掛載。

路由的共享和同步

完成了模板組件的共享之后,下面完成路由的共享,我們前面服務(wù)器使用的路由是*,接受任意URL,這允許所有URL請(qǐng)求交給Vue路由處理,進(jìn)而完成客戶端路由與服務(wù)端路由的復(fù)用。

第一步:創(chuàng)建ROUTER實(shí)例

為了實(shí)現(xiàn)復(fù)用,與createApp一樣,我們創(chuàng)建一個(gè)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中創(chuàng)建router

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

第二步:路由匹配

router準(zhǔn)備好了之后,修改server-entry.js,將請(qǐng)求的URL傳遞給router,使得在創(chuàng)建app的時(shí)候可以根據(jù)URL匹配到對(duì)應(yīng)的路由,進(jìn)而可知道需要渲染哪些組件

import createApp from './createApp';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) // onReady 等到 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) })}

修改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);    })});

為了測(cè)試,我們將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>

編譯,運(yùn)行,查看源代碼

點(diǎn)擊路由并沒(méi)有刷新頁(yè)面,而是客戶端路由跳轉(zhuǎn)的,一切符合預(yù)期。

數(shù)據(jù)模型的共享與狀態(tài)同步

前面我們簡(jiǎn)單的實(shí)現(xiàn)了服務(wù)端渲染,但是實(shí)際情況下,我們?cè)谠L問(wèn)頁(yè)面的時(shí)候,還需要獲取需要渲染的數(shù)據(jù),并且渲染成HTML,也就是說(shuō),在渲染HTML之前,我們需要將所有數(shù)據(jù)都準(zhǔn)備好,然后傳遞給renderer。

一般情況下,在Vue中,我們將狀態(tài)數(shù)據(jù)交給Vuex進(jìn)行管理,當(dāng)然,狀態(tài)也可以保存在組件內(nèi)部,只不過(guò)需要組件實(shí)例化的時(shí)候自己去同步數(shù)據(jù)。

第一步:創(chuàng)建STORE實(shí)例

首先第一步,與createApp類似,創(chuàng)建一個(gè)createStore.js,用來(lái)實(shí)例化store,同時(shí)提供給客戶端和服務(wù)端使用

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封裝了請(qǐng)求數(shù)據(jù)的函數(shù),mutations用來(lái)設(shè)置狀態(tài)。

將createStore加入到createApp中,并將store注入到vue實(shí)例中,讓所有Vue組件可以獲取到store實(shí)例

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

為了方便測(cè)試,我們mock一個(gè)遠(yuǎn)程服務(wù)函數(shù)fetchItem,用于查詢對(duì)應(yīng)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連接組件

一般情況下,我們需要通過(guò)訪問(wèn)路由,來(lái)決定獲取哪部分?jǐn)?shù)據(jù),這也決定了哪些組件需要渲染。事實(shí)上,給定路由所需的數(shù)據(jù),也是在該路由上渲染組件時(shí)所需的數(shù)據(jù)。所以,我們需要在路由的組件中放置數(shù)據(jù)預(yù)取邏輯函數(shù)。

在Home組件中自定義一個(gè)靜態(tài)函數(shù)asyncData,需要注意的是,由于此函數(shù)會(huì)在組件實(shí)例化之前調(diào)用,所以它無(wú)法訪問(wèn) this。需要將 store 和路由信息作為參數(shù)傳遞進(jìn)去

<template><div> <div>id: {{item.id}}</div> <div>name: {{item.name}}</div></div></template><script>export default { asyncData({ store, route }) { // 觸發(fā) action 后,會(huì)返回 Promise return store.dispatch('fetchItems', route.params.id) }, computed: { // 從 store 的 state 對(duì)象中的獲取 item。 item() { return this.$store.state.item; } }}</script>

第三步:服務(wù)端獲取數(shù)據(jù)

在服務(wù)器的入口文件server-entry.js中,我們通過(guò)URL路由匹配
router.getMatchedComponents()得到了需要渲染的組件,這個(gè)時(shí)候我們可以調(diào)用組件內(nèi)部的asyncData方法,將所需要的所有數(shù)據(jù)都獲取完后,傳遞給渲染器renderer上下文。

修改createApp,在路由組件匹配到了之后,調(diào)用asyncData方法,獲取數(shù)據(jù)后傳遞給renderer

import createApp from './createApp';export default context => {    // 因?yàn)橛锌赡軙?huì)是異步路由鉤子函數(shù)或組件,所以我們將返回一個(gè) Promise,    // 以便服務(wù)器能夠等待所有的內(nèi)容在渲染前就已經(jīng)準(zhǔn)備就緒。    return new Promise((resolve, reject) => {        const { app, router, store } = createApp();        // 設(shè)置服務(wù)器端 router 的位置        router.push(context.url)        // onReady 等到 router 將可能的異步組件和鉤子函數(shù)解析完        router.onReady(() => {            const matchedComponents = router.getMatchedComponents();            // 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404            if (!matchedComponents.length) {                return reject({ code: 404 })            }            // 對(duì)所有匹配的路由組件調(diào)用 `asyncData()`            Promise.all(matchedComponents.map(Component => {                if (Component.asyncData) {                    return Component.asyncData({                        store,                        route: router.currentRoute                    });                }            })).then(() => {                // 狀態(tài)傳遞給renderer的上下文,方便后面客戶端激活數(shù)據(jù)                context.state = store.state                resolve(app)            }).catch(reject);        }, reject);    })}

將state存入context后,在服務(wù)端渲染HTML時(shí)候,也就是渲染template的時(shí)候,context.state會(huì)被序列化到window.__INITIAL_STATE__中,方便客戶端激活數(shù)據(jù)。

第四步:客戶端激活狀態(tài)數(shù)據(jù)

服務(wù)端預(yù)請(qǐng)求數(shù)據(jù)之后,通過(guò)將數(shù)據(jù)注入到組件中,渲染組件并轉(zhuǎn)化成HTML,然后吐給客戶端,那么客戶端為了激活后端返回的HTML被解析后的DOM節(jié)點(diǎn),需要將后端渲染組件時(shí)用的store的state也同步到瀏覽器的store中,保證在頁(yè)面渲染的時(shí)候保持與服務(wù)器渲染時(shí)的數(shù)據(jù)是一致的,才能完成DOM的激活,也就是我們前面說(shuō)到的data-server-rendered標(biāo)記。

在服務(wù)端的渲染中,state已經(jīng)被序列化到了window.__INITIAL_STATE__,比如我們?cè)L問(wèn)http://localhost:3001?id=1,查看頁(yè)面源代碼

可以看到,狀態(tài)已經(jīng)被序列化到window.__INITIAL_STATE__中,我們需要做的就是將這個(gè)window.__INITIAL_STATE__在客戶端渲染之前,同步到客戶端的store中,下面修改client-entry.js

const { app, router, store } = createApp();if (window.__INITIAL_STATE__) { // 激活狀態(tài)數(shù)據(jù) store.replaceState(window.__INITIAL_STATE__);}router.onReady(() => { app.$mount('#app', true);});

通過(guò)使用store的replaceState函數(shù),將window.__INITIAL_STATE__同步到store內(nèi)部,完成數(shù)據(jù)模型的狀態(tài)同步。

總結(jié)

當(dāng)瀏覽器訪問(wèn)服務(wù)端渲染項(xiàng)目時(shí),服務(wù)端將URL傳給到預(yù)選構(gòu)建好的VUE應(yīng)用渲染器,渲染器匹配到對(duì)應(yīng)的路由的組件之后,執(zhí)行我們預(yù)先在組件內(nèi)定義的asyncData方法獲取數(shù)據(jù),并將獲取完的數(shù)據(jù)傳遞給渲染器的上下文,利用template組裝成HTML,并將HTML和狀態(tài)state一并吐給前端瀏覽器,瀏覽器加載了構(gòu)建好的客戶端VUE應(yīng)用后,將state數(shù)據(jù)同步到前端的store中,并根據(jù)數(shù)據(jù)激活后端返回的被瀏覽器解析為DOM元素的HTML文本,完成了數(shù)據(jù)狀態(tài)、路由、組件的同步,同時(shí)使得頁(yè)面得到直出,較少了白屏?xí)r間,有了更好的加載體驗(yàn),同時(shí)更有利于SEO。

個(gè)人覺(jué)得了解服務(wù)端渲染,有助于提升前端工程師的綜合能力,因?yàn)樗膬?nèi)容除了前端框架,還有前端構(gòu)建和后端內(nèi)容,是一個(gè)性價(jià)比還挺高的知識(shí),不學(xué)白不學(xué),加油!

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開(kāi)APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
Vue 服務(wù)端渲染實(shí)踐 ——Web應(yīng)用首屏耗時(shí)最優(yōu)化方案
Vue基礎(chǔ)知識(shí)總結(jié)(絕對(duì)經(jīng)典)
和chatgpt學(xué)架構(gòu)04-路由開(kāi)發(fā)
vue+vueRouter+seaJS 模仿vue-loader
vue3 Ts 封裝全局函數(shù)式組件教程——確認(rèn)彈窗組件
vue3.0自定義指令(directives)
更多類似文章 >>
生活服務(wù)
熱點(diǎn)新聞
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服