一、 CSR vs SSR
不同於傳統拉取JS進行解析渲染的CSR(JS負責進行頁面渲染),SSR實現了服務器端直接返回Html代碼讓瀏覽器進行渲染。
由此,我們就很容易理解以下代碼實現了一個頁面SSR:
// server.js
var express = require('express')
var app = express()
app.get('/', (req, res) => {
res.send(
`
<html>
<head>
<title>hello</title>
</head>
<body>
<h1>hello, this is SSR content.</h1>
<p>now, let's begin to learn SSR.</p>
</body>
</html>
`
)
})
app.listen(3001, () => {
console.log('listen:3001')
})
SSR優點:縮短首屏加載時間,便於SEO。
二、Vue項目的SSR實現
剛剛我們僅僅做到了讓服務器返回一段html字符串,那么,要如何實現Vue項目的服務端渲染呢?
由於整個項目的復雜性,我們可能一時無從下手,但沒關系,我們可以先從實現一個Vue組件的服務端渲染開始,在此之前,我們先從Vue實例的SSR開始吧!
1. 實現一個Vue實例的SSR
首先實現一個Vue實例,這個倒是簡單,那么問題來了:“我們要如何將它轉換成html代碼返回給瀏覽器呢?
大家都學過vue,當然知道Vue的標簽是基於虛擬DOM(JS對象)的,在客戶端渲染中也是采用一定方法將虛擬DOM渲染為真實DOM的,那么服務端的渲染流程也是通過虛擬DOM的編譯來完成的,編譯虛擬DOM的方法是renderToString。在Vue中,vue-server-renderer 提供一個名為 createBundleRenderer 的 API,這個API用於創建一個 render,並且自帶renderToString方法。
官網實現代碼如下:
// server.js
const Vue = require('vue')
const server = require('express')()
// 創建一個 renderer
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
// 第 1 步:創建一個 Vue 實例
const app = new Vue({
data: {
url: req.url
},
template: `<div>訪問的 URL 是: {{ url }}</div>`
})
// 第 2 步:將 Vue 實例渲染為 HTML
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head>
// <meta charset="utf-8"> // 加上這一行就不會出現亂碼了
<title>Hello</title>
</head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
啟動express服務,再瀏覽器上打開對應端口,頁面就能顯示出你在Home組件中編寫的內容了。但不出意外的話,大家看到頁面渲染出來的是一段亂碼,這是因為官網提供的示例代碼返回的html字符串里沒有帶 ,加上它就OK了,在上面代碼中直接取消這一行注釋, 至此,我們初步實現了一個Vue實例的服務端渲染。
TODO:(modify)
當你在渲染 Vue 應用程序時,renderer 只從應用程序生成 HTML 標記 (markup)。在這個示例中,我們必須用一個額外的 HTML 頁面包裹容器,來包裹生成的 HTML 標記。
為了簡化這些,你可以直接在創建 renderer 時提供一個頁面模板。多數時候,我們會將頁面模板放在特有的文件中,例如 index.template.html:
<!-- index.template.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- 使用雙花括號(double-mustache)進行 HTML 轉義插值(HTML-escaped interpolation) -->
<title>{{ title }}</title>
<!-- 使用三花括號(triple-mustache)進行 HTML 不轉義插值(non-HTML-escaped interpolation) -->
{{{ metas }}}
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
注意 注釋 -- 這里將是應用程序 HTML 標記注入的地方。
然后,我們可以讀取和傳輸文件到 Vue renderer 中:
// server.js
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
console.log(html) // html 將是注入應用程序內容的完整頁面
})
我們可以通過傳入一個"渲染上下文對象",作為 renderToString 函數的第二個參數,來提供模板插值數據,也可以與 Vue 應用程序實例共享 context 對象,允許模板插值中的組件動態地注冊數據。
完整代碼示例:
// server.js
const Vue = require('vue');
const server = require('express')();
const template = require('fs').readFileSync('./index.template.html', 'utf-8');
const renderer = require('vue-server-renderer').createRenderer({
template,
});
const context = {
title: 'vue ssr',
metas: `
<meta name="keyword" content="vue,ssr">
<meta name="description" content="vue srr demo">
`,
};
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `<div>訪問的 URL 是: {{ url }}</div>`,
});
renderer.renderToString(app, context, (err, html) => {
console.log(html);
if (err) {
res.status(500).end('Internal Server Error')
return;
}
res.end(html);
});
})
server.listen(8080);
TODO:(個人理解)
以下是個人對SSR的理解:服務端渲染實際是一套代碼的兩次應用,所謂的一套代碼就是拿出server.js外面去的vm實例,上面之所以簡單是因為我們在server內部創建的vm實例,一旦將vm拿出去,在server.js外部引入,那么涉及的就麻煩了。
這里分兩條線說,一個是在server.js外面創建一個app.js;結果是無法引入到server中,而這個也不是關注的重點;
另一條線是使用vue-loader創建一個vm實例,然后引入到server中,整個vue渲染就在解決這個問題,解決引入的問題,解決引入之后與前端混合的問題。下面貼上簡單案例的實現代碼。
因為不能直接應用.vue文件以及外部的js文件,所以需要借助webpack,借助webpack將vue實例,轉譯為node可用代碼,以及對前端代碼進行轉譯。
以vue init webpack-simple vuessr0 為基礎的vue-ssr案例
官網中有提到:每個請求應該都是全新的、獨立的應用程序實例,以便不會有交叉請求造成的狀態污染。
意思就是:每次服務端渲染都要渲染一個新的app,不能再用上一次渲染過的app對象去進行下一次渲染,這是由於app已經包含上一次渲染過的狀態會影響我們渲染內容,所以每次都要創建新的app,即為每個請求創建一個新的根Vue實例。因此,我們不應該直接創建一個應用程序實例,而是應該暴露一個可以重復執行的工廠函數,為每個請求創建新的應用程序實例:
// app.js
const Vue = require('vue')
module.exports= function createApp (context) {
return new Vue({
data: {
title: context.title,
url: context.url
},
template: `
<div>
<h2>{{ title }}</h2>
<div>訪問的 URL 是:{{ url }}</div>
</div>
`
})
}
服務器代碼只需要更改下vue實例的生成方式就可以了:
// server.js
const createApp = require('./app')
const server = require('express')()
// 創建一個 renderer
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
server.get('*', (req, res) => {
// 創建一個"渲染上下文對象"
const context = {
title: 'vue ssr',
metas: `
<meta name="keyword" content="vue,ssr">
<meta name="description" content="vue srr demo">
`,
url: req.url
}
const app = createApp(context)
// 第 2 步:將 Vue 實例渲染為 HTML
renderer.renderToString(app, context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
console.log(html)
res.end(html)
})
})
server.listen(8080)
2. 實現一個Vue組件的SSR
既然涉及到組件,那我們就必須加入路由了,官方建議使用 vue-router。同時,需要webpack來構建項目。
首先創建router.js,實現給每個請求創建一個新的 router 實例,我們在router.js中導出一個 createRouter 函數,然后更新app.js。
// router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter () {
return new Router({
mode: 'history',
routes: [
// ...
]
})
}
// app.js
const Vue = require('vue')
module.exports= function createApp (context) {
return new Vue({
data: {
title: context.title,
url: context.url
},
template: `
<div>
<h2>{{ title }}</h2>
<div>訪問的 URL 是:{{ url }}</div>
</div>
`
})
}
import App from './App.vue'
import { createRouter } from './router'
export function createApp () {
// 創建 router 實例
const router = createRouter()
const app = new Vue({
// 注入 router 到根 Vue 實例
router,
render: h => h(App)
})
// 返回 app 和 router
return { app, router }
}
首先實現一個Vue組件 src/views/home/index.vue,那么接下來我們仍舊是通過renderToString方法編譯Vue組件來實現服務端渲染。
const Vue = require('vue');
const server = require('express')();
// 引入Vue組件
import Home from './containers/Home';
const template = require('fs').readFileSync('./index.template.html', 'utf-8');
const renderer = require('vue-server-renderer').createRenderer({
template,
});
const context = {
title: 'vue ssr',
metas: `
<meta name="keyword" content="vue,ssr">
<meta name="description" content="vue srr demo">
`,
};
server.get('*', (req, res) => {
// 替換為Vue組件
const app = new Vue({
data: {
url: req.url
},
template: `<div>訪問的 URL 是: {{ url }}</div>`,
});
renderer.renderToString(app, context, (err, html) => {
console.log(html);
if (err) {
res.status(500).end('Internal Server Error')
return;
}
res.end(html);
});
})
server.listen(8080);
3. 初識同構
首先我們來了解下什么是重構?--->
前提:VUE框架用於構建客戶端應用,在瀏覽器中輸出Vue組件,生成並操作DOM。將同一個組件渲染為服務器端的HTML字符串發送到瀏覽器,然后將這些靜態標記“激活”為客戶端上可交互的應用程序。
服務器渲染的Vue.js應用程序為"同構"或"通用",應為應用程序的大部分代碼都可以在服務端和客戶端運行。
通俗的講,就是一套Vue代碼在服務器上運行一遍,到達瀏覽器又運行一遍。服務端渲染完成頁面結構,瀏覽器端渲染完成事件綁定,這個完成事件綁定的過程就是靜態標記“激活”為客戶端上可交互應用程序的過程。
那么如何進行瀏覽器端的事件綁定呢?
首先我們要明白:
只要開啟express的靜態文件服務,前端的script就能拿到控制瀏覽器的JS代碼啦!
4. node作中間層及請求代碼優化
作用:解決前后端協作問題
場景:
在不用中間層的前后端分離開發模式下,前端直接請求后端接口獲得返回的數據,但這個返回數據的數據格式也許並非是前端需要的,但出於性能原因或其他因素無法更改接口,就需要前端來做一些數據處理操作,這無疑會產生前端性能損耗,尤其當前端處理數據量很大的時候,甚至會影響用戶體驗。於是引入node中間層,用於替代前端做數據處理操作,中間層的工作流:前端發送請求--->請求node層的接口--->node對於相應的前端請求做轉發,用node去請求真正的后端接口獲取數據--->獲取后再由node層做對應的數據計算等處理操作--->返回給前端。