“學(xué)而不思則惘,思而不學(xué)則殆”
# 服務(wù)端渲染(SSR)
> SSR意為 server-side rendering(服務(wù)端渲染),目的是為了解決單頁(yè)面應(yīng)用的SEO的問(wèn)題。
## 服務(wù)器端渲染(SSR)和客戶端渲染(CSR)

### 服務(wù)器端渲染(SSR)
瀏覽器先請(qǐng)求HTML文檔,服務(wù)器端先將html頁(yè)面(或頁(yè)面組件),生成為html字符串,再返回給瀏覽器,最后直接渲染到頁(yè)面上。
### 客戶端渲染(CSR)
? 瀏覽器先請(qǐng)求HTML文檔,在瀏覽器端加載html頁(yè)面中的JS腳本。通過(guò)JS(vue/react)的能力,將虛擬DOM最終渲染填充到頁(yè)面中。
### 兩者本質(zhì)的區(qū)別是什么?
#### SSR
服務(wù)端生成好的html頁(yè)面內(nèi)容,直接返回給瀏覽器渲染。
server
```
const express = require('express')
const app = express()
app.get('/ssr', (req, res) => {
? res.send(`
? ? <html>
? ? ? <head>
? ? ? ? <meta charset='utf-8'>
? ? ? ? <title>SSR 服務(wù)端渲染</title>
? ? ? </head>
? ? ? <body>
? ? ? ? <h3>SSR 服務(wù)端渲染</h3>
? ? ? ? <p>SSR意為 Server Side Rendering(服務(wù)端渲染),目的是為了解決單頁(yè)面應(yīng)用的SEO的問(wèn)題。</p>
? ? ? </body>
? ? </html>
? `)
})
app.listen(7200)
```
客戶端訪問(wèn)頁(yè)面localhost:7200/ssr
```
? <html>
? ? ? <head>
? ? ? ? <meta charset='utf-8'>
? ? ? ? <title>SSR 服務(wù)端渲染</title>
? ? ? </head>
? ? ? <body>
? ? ? ? <h3>SSR 服務(wù)端渲染</h3>
? ? ? ? <p>SSR意為 Server Side Rendering(服務(wù)端渲染),目的是為了解決單頁(yè)面應(yīng)用的SEO的問(wèn)題。</p>
? ? ? </body>
? ? </html>
```
#### CSR
頁(yè)面首先直接輸出一個(gè)空的div#root,再由客戶端加載編譯打包好的react代碼(bundle.js chunk.js等js腳本),最終將頁(yè)面組件渲染到頁(yè)面中。
```
npx create-react-app my-app
cd my-app
yarn start
```
瀏覽器訪問(wèn)http://localhost:3000/
```
? <body>
? ? <noscript>You need to enable JavaScript to run this app.</noscript>
? ? <div id="root"></div>
? <script src="/static/js/bundle.js"></script><script src="/static/js/0.chunk.js"></script><script src="/static/js/main.chunk.js"></script></body>
```
客戶端渲染和服務(wù)器端渲染的最重要的區(qū)別就是究竟是誰(shuí)來(lái)完成html文件的完整拼接,如果是在服務(wù)器端完成的,然后返回給客戶端,就是服務(wù)器端渲染,而如果是前端做了更多的工作完成了html的拼接,則就是客戶端渲染。
#### 前后端拆分核心理念
前后端拆分,后端專注于數(shù)據(jù)接口服務(wù),前端專注接口調(diào)用,頁(yè)面渲染,雙劍合璧,相得益彰。
#### 服務(wù)器端渲染的優(yōu)缺點(diǎn)是怎樣的?
優(yōu)點(diǎn):
? - **更好的SEO 首屏加載快**
? 由于搜索引擎爬蟲(chóng)抓取工具可以直接查看完全渲染的頁(yè)面。
? - **首屏加載快**
? ? 快速地看到完整渲染的頁(yè)面,從而提高用戶體驗(yàn)。
? - **后端生成靜態(tài)化文件**
? ? 即解析模板的工作完全交由后端來(lái)做,客戶端只要解析標(biāo)準(zhǔn)的html頁(yè)面即可。
缺點(diǎn):
? - **開(kāi)發(fā)條件受限**
? 在服務(wù)端渲染中,created和beforeCreate之外的生命周期鉤子不可用
? - **占用服務(wù)端資源**
? - **學(xué)習(xí)成本相對(duì)較高**
? 除了對(duì)webpack、Vue要熟悉,還需要掌握node、Express相關(guān)技術(shù)。相對(duì)于客戶端渲染,項(xiàng)目構(gòu)建、部署過(guò)程更加復(fù)雜。
#### 客戶端渲染的優(yōu)缺點(diǎn)是怎樣的?
優(yōu)點(diǎn):
? - **前后端分離**
? 前端專注于前端UI,后端專注于api開(kāi)發(fā),且前端有更多的選擇性,而不需要遵循后端特定的模板。
? - **體驗(yàn)更好**
缺點(diǎn):
? - **首屏加載緩慢**
? - **不利于SEO**
? 除了 Google 和 Bing 比較完美地實(shí)現(xiàn)了對(duì)于 SPA(Single-Page Application)的爬蟲(chóng)渲染及內(nèi)容抓取,大多數(shù)搜索引擎包括百度都沒(méi)有支持。因而,包含豐富內(nèi)容的產(chǎn)品并需要 SEO 流量的產(chǎn)品也就自然需要 SSR 實(shí)現(xiàn)。
### 是否應(yīng)該使用服務(wù)端渲染
- **首屏加載慢**
針對(duì)于首屏加載,可以做服務(wù)端渲染。但要有覺(jué)悟,一旦這樣做,后期維護(hù)是個(gè)很痛苦的事情。相比于做服務(wù)端渲染,更推薦通過(guò)應(yīng)用拆分、code spliting 來(lái)完成優(yōu)化首屏加載的過(guò)程(先前做過(guò)一次首屏優(yōu)化,優(yōu)化前首屏加載每次都在 5s+,code spliting 之后直接變成 2s+,性價(jià)比高)。
- **SEO優(yōu)化**
如果是為了主頁(yè)網(wǎng)站被搜索引擎收錄,可以使用服務(wù)端渲染。但更好的建議新開(kāi)引導(dǎo)項(xiàng)目,在該項(xiàng)目上靜態(tài)資源或服務(wù)端渲染顯示頁(yè)面,作為主要網(wǎng)站的搜索引擎引流作用。
## Vue服務(wù)端渲染
這里我們先從Vue的vue-server-renderer來(lái)聊聊服務(wù)端渲染,暫先不說(shuō)那些ssr框架(Nuxt.js Next.js)
vue-server-renderer 是官方提供給我們用來(lái)實(shí)現(xiàn)服務(wù)端渲染的npm包
### 基本用法
安裝
```
npm install vue vue-server-renderer --save
```
### 渲染一個(gè)Vue實(shí)例
1.創(chuàng)建一個(gè) Vue 實(shí)例
```
const Vue = require('vue')
const app = new Vue({
? template: `<div>hello zhufeng</div>`
})
```
2.創(chuàng)建一個(gè) renderer對(duì)象
```
const renderer = require('vue-server-renderer').createRenderer()
```
3.將 Vue 實(shí)例渲染為 HTML
```
renderer.renderToString(app, (err, html) => {
? if (err) throw err
? console.log(html)
? // <div data-server-rendered="true">hello zhufeng</div>
})
// 在 2.5.0+,如果沒(méi)有傳入回調(diào)函數(shù),則會(huì)返回 Promise:
renderer.renderToString(app).then(html => {
? console.log(html)
}).catch(err => {
? console.error(err)
})
```
#### Vue實(shí)例渲染 完整示例代碼
Node.js 服務(wù)器作為中間層
```
npm i express --save
```
```
const Vue = require('vue')
const server = require('express')()
// 創(chuàng)建一個(gè) renderer對(duì)象
const renderer = require('vue-server-renderer').createRenderer()
// 創(chuàng)建一個(gè)后端路由
server.get('/', (req, res) => {
? // 創(chuàng)建一個(gè) Vue 實(shí)例
? const app = new Vue({
? ? data: {
? ? ? title: 'hello zhufeng'
? ? },
? ? template: `<h3>{{ title }}</h3>`
? })
? // 通過(guò)renderToString方法 將Vue實(shí)例轉(zhuǎn)換成HTML
? renderer
? ? .renderToString(app)
? ? .then(html => {
? ? ? console.log(html)
? ? ? // '<h3 data-server-rendered="true">hello zhufeng</h3>'
? ? ? // 最終將拼接好的html頁(yè)面內(nèi)容 返回給瀏覽器
? ? ? res.send(`
? ? ? ? <!DOCTYPE html>
? ? ? ? <html lang="en">
? ? ? ? ? <head><title>Hello</title></head>
? ? ? ? ? <body>${html}</body>
? ? ? ? </html>
? ? ? `)
? ? })
? ? .catch(err => {
? ? ? res.status(500).end('Internal Server Error')
? ? })
})
server.listen(7300)
```
#### 使用html頁(yè)面模板
創(chuàng)建一個(gè)html模板頁(yè)面,用一個(gè)額外的HTML頁(yè)面包裹容器,來(lái)包裹生成的HTML標(biāo)記(markup)。
html模板
```
<!DOCTYPE html>
<html lang="en">
<head>
? <meta charset="UTF-8">
? <meta name="viewport" content="width=device-width, initial-scale=1.0">
? <meta http-equiv="X-UA-Compatible" content="ie=edge">
? <title>服務(wù)端渲染SSR</title>
</head>
<body>
? <!--vue-ssr-outlet-->
</body>
</html>
```
> 注意 \<!--vue-ssr-outlet--> 注釋 -- 這里將是應(yīng)用程序 HTML 標(biāo)記注入的地方。
server端
```
const Vue = require('vue')
const fs = require('fs')
const server = require('express')()
const { createRenderer } = require('vue-server-renderer')
// 創(chuàng)建一個(gè) renderer對(duì)象 并指定渲染模板
const renderer = createRenderer({
? template: fs.readFileSync('./template/index1.html', 'utf-8')
})
// 創(chuàng)建一個(gè)后端路由
server.get('/', (req, res) => {
? // 創(chuàng)建一個(gè) Vue 實(shí)例
? const app = new Vue({
? ? data: {
? ? ? title: 'hello zhufeng'
? ? },
? ? template: `<h3>{{ title }}</h3>`
? })
? // 通過(guò)renderToString方法 將Vue實(shí)例轉(zhuǎn)換成HTML
? renderer
? ? .renderToString(app)
? ? .then(html => {
? ? ? // 最終將拼接好的html頁(yè)面內(nèi)容 返回給瀏覽器
? ? ? res.send(html)
? ? })
? ? .catch(err => {
? ? ? res.status(500).end('Internal Server Error')
? ? })
})
server.listen(7300)
```
#### 動(dòng)態(tài)注入title 和 meta標(biāo)簽
html模板設(shè)置插值變量
```
<!DOCTYPE html>
<html lang="en">
<head>
? {{{meta}}}
? <title>{{title}}</title>
</head>
<body>
? <!--vue-ssr-outlet-->
</body>
</html>
```
我們可以通過(guò)傳入一個(gè)"渲染上下文對(duì)象",作為 renderToString 函數(shù)的第二個(gè)參數(shù),來(lái)提供插值數(shù)據(jù):
```
? // 動(dòng)態(tài)注入title 和 meta標(biāo)簽
? const context = {
? ? title: '珠峰前端培訓(xùn)',
? ? meta: `
? ? <meta charset="UTF-8">
? ? <meta name="viewport" content="width=device-width, initial-scale=1.0">
? ? <meta http-equiv="X-UA-Compatible" content="ie=edge">
? ? <meta name="keywords" content="HTML, CSS, Vue, React, Node, JavaScript" />
? ? `
? }
? // 通過(guò)renderToString方法 將Vue實(shí)例轉(zhuǎn)換成HTML
? renderer
? ? .renderToString(app, context)
? ? .then(html => {
? ? ? // 最終將拼接好的html頁(yè)面內(nèi)容 返回給瀏覽器
? ? ? res.send(html)
? ? })
? ? .catch(err => {
? ? ? res.status(500).end('Internal Server Error')
? ? })
```
### 參考源碼
https://github.com/Lwenli1224/ssr_csr
## 編寫(xiě)通用代碼
> "通用"代碼 - 即運(yùn)行在服務(wù)器和客戶端的代碼。由于用例和平臺(tái) API 的差異,當(dāng)運(yùn)行在不同環(huán)境中時(shí),我們的代碼將不會(huì)完全相同。所以這里我們將會(huì)闡述你需要理解的關(guān)鍵事項(xiàng)。
### SSR開(kāi)發(fā)需要注意的問(wèn)題
- 服務(wù)端渲染只會(huì)執(zhí)行 vue 的兩個(gè)鉤子函數(shù) beforeCreate 和 created
- 服務(wù)端渲染無(wú)法訪問(wèn) window 和 document等只有瀏覽器才有的全局對(duì)象。
通用API
例如,axios 是一個(gè) HTTP 客戶端,可以向服務(wù)器和客戶端都暴露相同的 API。
## webpack工程構(gòu)建

服務(wù)端和客戶端各自都需要提供Vue應(yīng)用程序。為了做到這一點(diǎn),我們需要使用 webpack 來(lái)打包我們的 Vue 應(yīng)用程序。事實(shí)上,我們可能需要在服務(wù)器上使用 webpack 打包 Vue 應(yīng)用程序,因?yàn)椋?/p>
- 通常 Vue 應(yīng)用程序是由 webpack 和 vue-loader 構(gòu)建,并且許多 webpack 特定功能不能直接在 Node.js 中運(yùn)行(例如通過(guò) file-loader 導(dǎo)入文件,通過(guò) css-loader 導(dǎo)入 CSS)。
- 盡管 Node.js 最新版本能夠完全支持 ES2015 特性,我們還是需要轉(zhuǎn)譯客戶端代碼以適應(yīng)老版瀏覽器。這也會(huì)涉及到構(gòu)建步驟
然后我們的服務(wù)端代碼和客戶端代碼通過(guò)webpack分別打包,生成Server Bundle和Client Bundle
? - 服務(wù)器需要「服務(wù)器 bundle」然后用于服務(wù)器端渲染(SSR)
? - 客戶端 bundle」會(huì)發(fā)送給瀏覽器,用于混合靜態(tài)標(biāo)記。
## 從零搭建Vue開(kāi)發(fā)環(huán)境(SSR)
利用vue-server-renderer 搭建Vue SSR開(kāi)發(fā)環(huán)境
#### Git倉(cāng)庫(kù)源碼
從零大家vue ssr環(huán)境比較費(fèi)勁
https://github.com/Lwenli1224/webpack4-vue-loader/tree/vue-ssr1.0
從零搭建vue環(huán)境 可以參考我這篇文章(從零搭建Vue開(kāi)發(fā)環(huán)境:webpack4 + vue-loader + babel-loader v8 + Babel v7 + eslint + git hooks + editorconfig)
https://juejin.im/post/5c48981ee51d4567680e44a9
## 服務(wù)端渲染應(yīng)用框架-Nuxt.js
> Nuxt.js 是一個(gè)基于 Vue.js 的服務(wù)端渲染應(yīng)用框架。
你可以基于它初始化新項(xiàng)目的基礎(chǔ)結(jié)構(gòu)代碼,或者在已有 Node.js 項(xiàng)目中使用 Nuxt.js。
Nuxt.js 預(yù)設(shè)了利用Vue.js開(kāi)發(fā)服務(wù)端渲染的應(yīng)用所需要的各種配置。
#### create-nuxt-app
Nuxt.js團(tuán)隊(duì)創(chuàng)建的腳手架工具
創(chuàng)建一個(gè)nuxt工程
```
npx create-nuxt-app nuxt-app
```
它會(huì)讓你進(jìn)行一些集成選擇, 如服務(wù)器端框架(express koa)和 UI框架。
啟動(dòng)項(xiàng)目
```
npm run dev
```
現(xiàn)在我們的應(yīng)用運(yùn)行在 http://localhost:3000 上運(yùn)行。
> 注意:Nuxt.js 會(huì)監(jiān)聽(tīng) pages 目錄中的文件更改,因此在添加新頁(yè)面時(shí)無(wú)需重新啟動(dòng)應(yīng)用程序。
目錄結(jié)構(gòu)
```
├── README.md? ? ? ? # 說(shuō)明文檔
├── assets? ? ? ? ? ? # 資源目錄 用于組織未編譯的靜態(tài)資源如 LESS、SASS 或 JavaScript
├── components? ? ? ? # 組件目錄 用于組織應(yīng)用的 Vue.js 組件
├── layouts? ? ? ? ? # 布局目錄 用于組織應(yīng)用的布局組件
├── middleware? ? ? ? # 中間件目錄 用于存放應(yīng)用的中間件。
├── nuxt.config.js? ? # nuxt配置文件 用于組織Nuxt.js 應(yīng)用的個(gè)性化配置,以便覆蓋默認(rèn)配置。
├── pages? ? ? ? ? ? # 頁(yè)面目錄 用于組織應(yīng)用的路由及視圖
├── plugins? ? ? ? ? # 插件目錄 用于組織那些需要 在根vue.js應(yīng)用實(shí)例化之前需要運(yùn)行的Javascript插件
├── server? ? ? ? ? ? # 服務(wù)端 用于組織node中間層服務(wù)代碼
├── static? ? ? ? ? ? # 靜態(tài)文件目錄 用于存放應(yīng)用的靜態(tài)文件
├── store? ? ? ? ? ? # store目錄 用于組織應(yīng)用的 Vuex 狀態(tài)樹(shù) 文件。
```
目錄結(jié)構(gòu)詳情說(shuō)明
https://zh.nuxtjs.org/guide/directory-structure
### 異步數(shù)據(jù)
#### asyncData
asyncData方法會(huì)在組件(限于頁(yè)面組件)每次加載之前被調(diào)用。它可以在服務(wù)端或路由更新之前被調(diào)用。你可以利用 asyncData方法來(lái)獲取數(shù)據(jù),Nuxt.js 會(huì)將 asyncData 返回的數(shù)據(jù)融合組件 data 方法返回的數(shù)據(jù)一并返回給當(dāng)前組件。
#### fetch
fetch 方法用于在渲染頁(yè)面前填充應(yīng)用的狀態(tài)樹(shù)(store)數(shù)據(jù), 與 asyncData 方法類(lèi)似,不同的是它不會(huì)設(shè)置組件的數(shù)據(jù)。
### Nuxt.js開(kāi)發(fā)實(shí)戰(zhàn)(打造CNode社區(qū))

#### 路由創(chuàng)建
> Nuxt.js 依據(jù) pages 目錄結(jié)構(gòu)自動(dòng)生成 vue-router 模塊的路由配置。

#### Vuex配置
> Nuxt.js 會(huì)嘗試找到應(yīng)用根目錄下的 store 目錄,如果該目錄存在,它將做以下的事情:
> - 引用vuex模塊
> - 將vuex模塊加到vendors構(gòu)建配置中去
> - 設(shè)置Vue根實(shí)例的store配置項(xiàng)

#### middleware中間件配置
> 每一個(gè)中間件應(yīng)放置在 middleware/ 目錄。文件名的名稱將成為中間件名稱(middleware/auth.js將成為 auth 中間件)。

#### node服務(wù)配置

#### nuxt.config.js
> Nuxt.js 默認(rèn)的配置涵蓋了大部分使用情形,可通過(guò) nuxt.config.js 來(lái)覆蓋默認(rèn)的配置。
https://zh.nuxtjs.org/api/configuration-build
https://github.com/Lwenli1224/Nuxt.js-CNode/blob/master/nuxt.config.js
### Nuxt.js CNode社區(qū)項(xiàng)目源碼
更多詳情請(qǐng)看我倉(cāng)庫(kù)源碼
https://github.com/Lwenli1224/Nuxt.js-CNode
未完待續(xù) 持續(xù)更新。。。
## 論文檔資料的重要性
越來(lái)越覺(jué)得平時(shí)學(xué)習(xí)工作 看文檔很重要,特別是官方文檔 一手的資料,英語(yǔ)好的話最好是看英文文檔一手資料比較多。
- 看官方文檔 如:
? Nuxt.js https://zh.nuxtjs.org/?
? webpack https://webpack.js.org/?
? babel https://babeljs.io/
- 通過(guò)npm搜包了解
? https://www.npmjs.com/
- 去github看源碼?
? https://github.com/
- 查問(wèn)題去stackoverflow (科學(xué)上網(wǎng))
? https://stackoverflow.com/
? https://segmentfault.com/
? https://www.google.com/?gws_rd=ssl
- 逛技術(shù)社區(qū) MDN 掘金 CSDN 博客園等
