前言
書接上文,昨天簡(jiǎn)單的說到了 SSR 服務(wù)端渲染的相關(guān)內(nèi)容《二十五║初探SSR服務(wù)端渲染》,主要說明了相關(guān)概念,以及為什么使用等,昨天的一個(gè)小栗子因?yàn)闀r(shí)間問題,沒有好好的給大家鋪開來講,今天呢,咱們就繼續(xù)說一下這個(gè) SSR 服務(wù)端渲染,并結(jié)合著 Client 客戶端渲染,一起說一說相關(guān)的內(nèi)容,當(dāng)然還是圍繞著原理來的,并不是要搭建項(xiàng)目,項(xiàng)目我會(huì)在下一個(gè)系列說到,經(jīng)過和群里小伙伴的商量,并采納大家的意見,我初步考慮了下,下一個(gè)系列我會(huì)說下** Nuxt.js** 相關(guān)內(nèi)容(我感覺這個(gè)很有必要的說,現(xiàn)在網(wǎng)站SEO是灰常重要滴 ),然后再下一個(gè)系列就是搭建一個(gè)功能豐富的 后臺(tái)管理系統(tǒng) 作為開源項(xiàng)目,手里有貨的小伙伴來群里,咱們一起開源吧哈哈哈。
這個(gè)時(shí)候細(xì)心的小伙伴會(huì)發(fā)現(xiàn),每天的那個(gè)腦圖不見了,哈哈,并沒有,而是在最下邊,看文末就知道了。
一、Client 瀏覽器端渲染是怎樣運(yùn)行的
為了介紹瀏覽器渲染是怎么回事,我們運(yùn)行一下npm run build 看看我們之前的項(xiàng)目——就是我們的個(gè)人博客第一版,大家應(yīng)該還記得《 二十二║Vue實(shí)戰(zhàn):個(gè)人博客第一版(axios+router)》,發(fā)布版本的文件,到底有哪些東西,
執(zhí)行
npm run build
這里我們通過 Webpack 打包,將我們的項(xiàng)目打包,生成一個(gè) dist 目錄 ,我們可以看到里面有 css+fonts+js 文件夾,還有一個(gè) index.html 靜態(tài)頁(yè)面,我們打開這個(gè)靜態(tài)頁(yè)面,可以看到下面內(nèi)容:
<!DOCTYPE html>
<html lang=en>
<head>
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="width=device-width,initial-scale=1">
<link rel=icon href=/favicon.ico>
<title>blogvue3</title>
<link href=/js/about.143cb27a.js rel=prefetch>
<link href=/css/app.51e9ecbc.css rel=preload as= style>
<link href=/css/chunk-vendors.5aa02cc7.css rel=preload as= style>
<link href=/js/app.16d68887.js rel=preload as=script>
<link href=/js/chunk-vendors.1c001ffe.js rel=preload as=script>
<link href=/css/chunk-vendors.5aa02cc7.css rel=stylesheet>
<link href=/css/app.51e9ecbc.css rel=stylesheet>//全部都是樣式文件,可忽略研究
</head>
<body>
<noscript>
<strong>We're sorry but blogvue3 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id=app />//頁(yè)面掛載入口
<script src=/js/chunk-vendors.1c001ffe.js />//vue 用到的區(qū)塊文件,vue-cli全家桶默認(rèn)配置里面這個(gè)chunk就是將所有從node_modules/里require(import)的依賴都打包到這里
<script src=/js/app.16d68887.js />//這個(gè)就是我們項(xiàng)目的核心內(nèi)容,主要就是 app.vue 的內(nèi)容,封裝了所有方法,包括路由和頁(yè)面渲染之類的
</body>
</html>

大家觀察生成的文件,只有一個(gè)div掛載入口,并沒有多余的dom元素,那么頁(yè)面要怎么呈現(xiàn)呢?答案是js append拼接,對(duì),下面的那些 js 會(huì)負(fù)責(zé)innerHTML。而js是由瀏覽器解釋執(zhí)行的,所以呢,我們稱之為瀏覽器渲染,相信這里大家應(yīng)該很明白這個(gè)原理了,和我們平時(shí)用 jQuery 寫局部異步加載是一樣的,但是,這有幾個(gè)致命的缺點(diǎn):
- js放在dom結(jié)尾,如果js文件過大,那么必然造成頁(yè)面阻塞。
- 隨著我們的業(yè)務(wù)需求增大,打包后的 js 文件愈來愈大,頁(yè)面白屏更加明顯,用戶體驗(yàn)明顯不好,特別是首頁(yè),幾個(gè),幾十個(gè)組件一起渲染,天訥!不敢相信
- 不利于SEO
- 客戶端運(yùn)行在老的JavaScript引擎上

這個(gè)時(shí)候,我們就想其他的一些辦法,比如會(huì)單獨(dú)給我們的首頁(yè)寫一個(gè)靜態(tài)處理,為了應(yīng)對(duì)相應(yīng)速度,但是這個(gè)并不是一個(gè)好的辦法,我們需要處理兩套邏輯,基于以上的一些問題,服務(wù)端渲染呼之欲出....
總結(jié):相信大家看到這里應(yīng)該都能明白,客戶端渲染的工作原理了,其實(shí)就是開發(fā)的時(shí)候組件化,然后通過 webpack 打包工具,將我們的邏輯處理 js ,打包成文件,然后和前端頁(yè)面一起部署,這樣就能講數(shù)據(jù)在 DOM 上展示出來了。
二、Server 服務(wù)端渲染是怎樣運(yùn)行的
上邊咱們看了客戶端瀏覽器渲染,明白了原理和弊端,咱們這個(gè)時(shí)候就需要用到服務(wù)器渲染,SSR , Server Side Render的簡(jiǎn)稱, 服務(wù)端渲染. 首先服務(wù)端渲染的思想由來已久, 在 ajax 興起之前, 所有 web 應(yīng)用都是服務(wù)端渲染, 服務(wù)器直接返回 html 文本給瀏覽器, 用戶操作比如在登陸頁(yè)面提交表單, 成功后跳轉(zhuǎn)到首頁(yè), 服務(wù)器需要返回兩個(gè)頁(yè)面. 這樣的弊端顯而易見, 加大了服務(wù)器的消耗,到了 vue 時(shí)代,咱們雖然是通過 api 返回的Json,但是需要 node 服務(wù)器, 很耗費(fèi)性能, 需要做好緩存和優(yōu)化, 相當(dāng)于空間換時(shí)間。
這里咱們先說下原理

從這個(gè)圖里大家應(yīng)該也能看到,我們的SSR打包流程變化了,在客戶端渲染的時(shí)候,我們 webpack 是打包成js約束文件,直接發(fā)給瀏覽器,然后再獲取數(shù)據(jù)渲染DOM,
網(wǎng)絡(luò)解釋有點(diǎn)兒羞澀難懂:ssr 有兩個(gè)入口文件,client.js 和 server.js, 都包含了應(yīng)用代碼,webpack 通過兩個(gè)入口文件分別打包成給服務(wù)端用的 server bundle 和給客戶端用的 client bundle. 當(dāng)服務(wù)器接收到了來自客戶端的請(qǐng)求之后,會(huì)創(chuàng)建一個(gè)渲染器 bundleRenderer,這個(gè) bundleRenderer 會(huì)讀取上面生成的 server bundle 文件,并且執(zhí)行它的代碼, 然后發(fā)送一個(gè)生成好的 html 到瀏覽器,等到客戶端加載了 client bundle 之后,會(huì)和服務(wù)端生成的DOM 進(jìn)行 Hydration(判斷這個(gè)DOM 和自己即將生成的DOM 是否相同,如果相同就將客戶端的vue實(shí)例掛載到這個(gè)DOM上, 否則會(huì)提示警告)。
可以看出來,我們?cè)黾恿艘粋€(gè)步驟:就是之前我們是在瀏覽器里,通過JavaScript框架來渲染數(shù)據(jù)的,但是現(xiàn)在我們的請(qǐng)求中間走了一遍 node 服務(wù)器,然后 node 服務(wù)器幫我們生成相應(yīng)的 Html 片段,直接發(fā)送給瀏覽器,那瀏覽器肯定是認(rèn)識(shí)html的,所以不用再通過 js 去獲取數(shù)據(jù)渲染了,直接就渲染了,嗯大概就是這樣,就好像多了一個(gè)中間件。
相信大家看內(nèi)容可能不是很清楚,關(guān)鍵時(shí)候還是得上代碼才能說的更清晰。
三、通過代碼實(shí)現(xiàn)服務(wù)端渲染
客戶端渲染咱們就不寫代碼了吧,這些天都寫了很多了
1、首先我們新建一個(gè)文件夾 Vue_SSR_Demo 并對(duì)其 node 服務(wù)初始化
執(zhí)行
npm install vue vue-server-renderer --save
會(huì)看到生成一個(gè) node_modules 文件夾 和 package-lock.json 文件。
然后執(zhí)行
npm install express --save
安裝 express 的node服務(wù)。
2、然后創(chuàng)建一個(gè) index.html 頁(yè)面,作為一個(gè)承載頁(yè)面,類似我們 vue-cli 腳手架中的 index.html
<!-- 如同vue-cli創(chuàng)建項(xiàng)目中的index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title> {{{meta}}} </head>
<body>
<!--vue-ssr-outlet-->
<!--↑↑↑↑↑ 注意上邊的格式一定要有,并且不能帶空格 ↑↑↑↑↑-->
</body>
</html>
3、新建一個(gè) server.js 文件,用作我們的啟服務(wù)入口
const Vue = require('vue')//引入 vue
const server = require('express')()//引入 express 服務(wù)框架
const fs = require('fs') //讀取 html 模版
const renderer = require('vue-server-renderer').createRenderer({
template: fs.readFileSync('./index.html', 'utf-8')//文件地址路徑
}) // 此參數(shù)是vue 生成Dom之外位置的數(shù)據(jù) 如vue生成的dom一般位于body中的某個(gè)元素容器中, //此數(shù)據(jù)可在header標(biāo)簽等位置渲染,是renderer.renderToString()的第二個(gè)參數(shù), //第一個(gè)參數(shù)是vue實(shí)例,第三個(gè)參數(shù)是一個(gè)回調(diào)函數(shù)。
const context = {
title: '老張的哲學(xué)',
meta:` <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="vue-ssr">
<meta name="generator" content="GitBook 3.2.3"> `
} //定義服務(wù)
server.get('*', (req, res) => { //創(chuàng)建vue實(shí)例 主要用于替換index.html中body注釋地方的內(nèi)容, //index.html中 <!--vue-ssr-outlet-->的地方 ,約定熟成
const app = new Vue({
data: {
url: req.url,
data: ['C#', 'SQL', '.NET', '.NET CORE', 'VUE'],
title: '我的技能列表' }, //template 中的文本最外層一定要有容器包裹, 和vue的組件中是一樣的, //只能有一個(gè)父級(jí)元素,這里是div!
template: ` <div>
<p>{{title}}</p>
<p v-for='item in data'>{{item}}</p>
</div> `
}) //將 Vue app實(shí)例渲染為字符串 (其他的API自己看用法是一樣的)
renderer.renderToString(app, context, (err, html) => { if (err) {
res.status(500).end('err:' + err) return } //將模版發(fā)送給瀏覽器
res.end(html) //每次請(qǐng)求 都在node 服務(wù)器中打印
console.log('success')
})
}) //服務(wù)端口開啟并監(jiān)聽
server.listen(8060, () => {
console.log('server success!')
})
文檔中的解釋已經(jīng)很詳細(xì)了,大家可以自行看一看,這樣我們就定義好了一個(gè) node 服務(wù),并通過 express 框架,將我們的 vue 實(shí)例通過 renderer.renderToString() 方法生成字符串,返回到瀏覽器。
4、開啟 node 服務(wù)
執(zhí)行
node server
注意,這里的 server 是我們的文件名,你也可以用其他的,比如 node aaa.js,或者 node aaa

這個(gè)時(shí)候,我們就發(fā)現(xiàn)我們已經(jīng)成功的把我們的頁(yè)面內(nèi)容返回到了瀏覽器,為什么呢?因?yàn)槲覀兊捻?yè)面源代碼已經(jīng)有內(nèi)容了,證明不是通過 js 后期渲染的。binggo!
大家有沒有對(duì) SSR 服務(wù)端渲染有一定的任何和了解,是不是品出來一點(diǎn)兒感覺了,這個(gè)還是最簡(jiǎn)單的一個(gè) node 服務(wù)器渲染。
代碼就不上傳了,大家粘貼復(fù)制就行,全部結(jié)構(gòu)文件

四、通過 webpack 打包,來深入了解服務(wù)器渲染
dang dang dang,如果大家看到這里不費(fèi)勁,或者看懂前邊的了,好滴,你可以看這一塊了,如果上邊的不是很清晰,或者很難懂,好吧,這一塊可能更羞澀了,不過沒關(guān)系,慢慢來!
1、這個(gè)代碼是昨天的,咱們這里重新說一下
結(jié)構(gòu)如下:
├── dist // 保存我們的打包后的文件
├── node_modules // 依賴包文件夾
├── entry // 打包入口文件夾
│ └── entry-server.js // 服務(wù)端 打包入口文件
├── src // 我們的項(xiàng)目的源碼編寫文件
│ ├── views // view存放目錄
│ │ ├── about.vue //about 頁(yè)面
│ │ ├── like.vue //like 頁(yè)面
│ │ └── Home.vue //Home 頁(yè)面
│ └── App.vue // App入口文件
│ └── main.js // 主配置文件
│ └── router.js // 路由配置文件
└── .babelrc // babel 配置文件
└── package.json // 項(xiàng)目依賴包配置文件
└── package-lock.json // npm5 新增文件,優(yōu)化性能
└── server.js // server 文件
└── README.md // 說明文檔

咱們分塊的說一說
2、普通的app代碼塊

這一塊,就是對(duì)應(yīng)的我們 src 文件夾下的模板,這些內(nèi)容大家一定很熟悉了,就不多說了,就是 組件的定義、路由定義、app入口和 main.js 主方法,這里重點(diǎn)說下 main.js

在之前的 main.js 我們是直接實(shí)例化 vue() ,然后對(duì) #appp 進(jìn)行掛載的,但是現(xiàn)在咱們變成了 服務(wù)器渲染,這里就不能掛載了,而是把創(chuàng)建的vue實(shí)例返回出去。
//main.js
import Vue from 'vue' import createRouter from './router' import App from './App.vue'
// 導(dǎo)出一個(gè)工廠函數(shù),用于創(chuàng)建新的vue實(shí)例
export function createApp() { const router = createRouter() const app = new Vue({
router,
render: h => h(App)
}) return app
}
你會(huì)問了,但是返回給誰(shuí)呢,欸?!這個(gè)問題好,請(qǐng)往下看。
3、講我們的 vue實(shí)例封裝到 promise

網(wǎng)友總結(jié):所謂Promise,簡(jiǎn)單說就是一個(gè)容器,里面保存著某個(gè)未來才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果。從語(yǔ)法上說,Promise 是一個(gè)對(duì)象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。
Promise對(duì)象有以下兩個(gè)特點(diǎn)。
(1)對(duì)象的狀態(tài)不受外界影響。Promise對(duì)象代表一個(gè)異步操作,有三種狀態(tài):Pending(進(jìn)行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失?。V挥挟惒讲僮鞯慕Y(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無法改變這個(gè)狀態(tài)。這也是Promise這個(gè)名字的由來,它的英語(yǔ)意思就是“承諾”,表示其他手段無法改變。
(2)一旦狀態(tài)改變,就不會(huì)再變,任何時(shí)候都可以得到這個(gè)結(jié)果。Promise對(duì)象的狀態(tài)改變,只有兩種可能:從Pending變?yōu)镽esolved和從Pending變?yōu)镽ejected。只要這兩種情況發(fā)生,狀態(tài)就凝固了,不會(huì)再變了,會(huì)一直保持這個(gè)結(jié)果。就算改變已經(jīng)發(fā)生了,你再對(duì)Promise對(duì)象添加回調(diào)函數(shù),也會(huì)立即得到這個(gè)結(jié)果。這與事件(Event)完全不同,事件的特點(diǎn)是,如果你錯(cuò)過了它,再去監(jiān)聽,是得不到結(jié)果的。
有了Promise對(duì)象,就可以將異步操作以同步操作的流程表達(dá)出來,避免了層層嵌套的回調(diào)函數(shù)。此外,Promise對(duì)象提供統(tǒng)一的接口,使得控制異步操作更加容易。
簡(jiǎn)單來說,就是把我們 main入口文件中的vue實(shí)例,都封裝到 promise,就像增加一個(gè)外衣,方便我們 webpack打包。對(duì),重點(diǎn)來了
4、通過 Webpack 服務(wù)器打包

/* 5、webpack.server.js 服務(wù)端打包 */
const path = require('path');//獲取路徑對(duì)象
const projectRoot = path.resolve(__dirname, '..');//根路徑 //定義模塊
module.exports = { // 此處告知 server bundle 使用 Node 風(fēng)格導(dǎo)出模塊(Node-style exports) // 這里必須是node,因?yàn)榇虬瓿傻倪\(yùn)行環(huán)境是node,在node端運(yùn)行的,不是在瀏覽器端運(yùn)行。
target: 'node', // entry需要提供一個(gè)單獨(dú)的入口文件
entry: ['babel-polyfill', path.join(projectRoot, 'entry/entry-server.js')], // 輸出
output: { //指定libraryTarget的類型為commonjs2,用來指定代碼export出去的入口的形式。 // 在node.js中模塊是module.exports = {...},commonjs2打包出來的代碼出口形式就類似于此。
libraryTarget: 'commonjs2',
path: path.join(projectRoot, 'dist'), // 打包出的路徑
filename: 'bundle.server.js',// 打包最終的文件名,這個(gè)文件是給 node 服務(wù)器使用的
},
module: { // 因?yàn)槭褂脀ebpack2,這里必須是rules,如果使用use, // 會(huì)報(bào)個(gè)錯(cuò):vue this._init is not a function
rules: [ //規(guī)則1、vue規(guī)則定義
{
test: /\.vue$/,
loader: 'vue-loader',
},//js規(guī)則定義
{
test: /\.js$/,
loader: 'babel-loader',
include: projectRoot, // 這里會(huì)把node_modules里面的東西排除在外,提高打包效率
exclude: /node_modules/, // ES6 語(yǔ)法
options: {
presets: ['es2015']
}
},//css定義
{
test: /\.less$/,
loader: "style-loader!css-loader!less-loader" }
]
},
plugins: [],
resolve: {
alias: { 'vue$': 'vue/dist/vue.runtime.esm.js' }
}
}
基本的內(nèi)容就是上邊這些,注釋已經(jīng)很清楚了,大家可以看一看,這個(gè)時(shí)候我們的準(zhǔn)備工作就已經(jīng)做好了,下一步就改打包了
5、執(zhí)行打包命令,生成服務(wù)端約束文件 bundle.server.js
npm run server
這個(gè)時(shí)候,你會(huì)發(fā)現(xiàn),我們的dist 文件夾內(nèi),多了一個(gè) bundle.server.js 文件

我們看一下生成的文件,部分截圖,會(huì)發(fā)現(xiàn),我們的這個(gè)文件包含了所有頁(yè)面內(nèi)的內(nèi)容和方法,但是這個(gè) bundle.server.js 并不是直接返回給前端的,而且在 node 服務(wù)器使用的

6、配置 node 服務(wù)器啟動(dòng)文件,這個(gè)更類似我們上文中提到的 server.js 文件
/*7、 server.js */
const express = require('express')()//引入express 服務(wù)框架
const renderer = require('vue-server-renderer').createRenderer() const createApp = require('./dist/bundle.server.js')['default']//引入我們剛剛打包文件 // 響應(yīng)路由請(qǐng)求
express.get('*', (req, res) => { const context = { url: req.url } // 創(chuàng)建vue實(shí)例,傳入請(qǐng)求路由信息
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => { if (err) { return res.state(500).end('運(yùn)行時(shí)錯(cuò)誤') }
res.send(` <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue2.0 SSR渲染頁(yè)面</title>
</head>
<body> ${html} </body>
</html> `)
})
}, err => { if(err.code === 404) { res.status(404).end('所請(qǐng)求的頁(yè)面不存在') }
})
}) // 服務(wù)器監(jiān)聽地址
express.listen(8089, () => {
console.log('服務(wù)器已啟動(dòng)!')
})
7、啟動(dòng)服務(wù)
node server
這個(gè)時(shí)候我們就可以看到效果了
好啦,這個(gè)就是 SSR 服務(wù)端渲染的整個(gè)過程。
番外
哈嘍大家好,在這里忙碌的日子又和大家見面了,咱們的前后端系列入門篇已經(jīng) 26 篇了,按照我的計(jì)劃,基本的講解已經(jīng)到這里了,相信如果大家按照我寫的系列,能搭建自己的博客系統(tǒng)了,甚至如果你比較厲害,已經(jīng)開始開發(fā)中型項(xiàng)目了哈哈,咱們這里先回顧下知識(shí),包括 API ,Swagger 文檔,Sugar 數(shù)據(jù)持久層的ORM,Repository倉(cāng)儲(chǔ)架構(gòu),Asyn/Await 異步編程,AOP面向切面編程,IoC控制反轉(zhuǎn)和DI依賴注入,Dto數(shù)據(jù)傳輸對(duì)象,Redis緩存等后端知識(shí),還有Vue 基礎(chǔ)語(yǔ)法、JS高級(jí)、ES6、Vue 組件 、生命周期、數(shù)據(jù)綁定、開發(fā)環(huán)境搭建、Vue-Cli 腳手架、axios Http請(qǐng)求、vue-router 路由協(xié)議、webpack 打包、Vuex 狀態(tài)管理等前端知識(shí)。雖然都是簡(jiǎn)單的說了下皮毛,也是都涵蓋了這個(gè)框架內(nèi)容,咱們可以看看咱們的結(jié)構(gòu)樹,這個(gè)每天都會(huì)出現(xiàn)的哈哈,這個(gè)就是這一個(gè)月咱們的辛苦,也是很有回報(bào)滴,群里的小伙伴都破50了,這是個(gè)大圖,大家可以看看:

本來想著要換其他的系列,但是在群里小伙伴的建議下,還是在把Vue好好說說吧,思考了下,在國(guó)慶前的時(shí)間再說下 SSR 框架——Nuxt.js 吧,感覺這一塊應(yīng)該是要用到的,也是自學(xué)的一個(gè)吧,至于國(guó)慶之后,再慢慢考慮寫其他的吧。
QQ群:
867095512 (blod.core)