1. 程式人生 > >從壹開始前後端分離 [ Vue2.0+.NetCore2.1] 二十六║Client渲染、Server渲染知多少{補充}

從壹開始前後端分離 [ Vue2.0+.NetCore2.1] 二十六║Client渲染、Server渲染知多少{補充}

前言

書接上文,昨天簡單的說到了 SSR 服務端渲染的相關內容《二十五║初探SSR服務端渲染》,主要說明了相關概念,以及為什麼使用等,昨天的一個小栗子因為時間問題,沒有好好的給大家鋪開來講,今天呢,咱們就繼續說一下這個 SSR 服務端渲染,並結合著 Client 客戶端渲染,一起說一說相關的內容,當然還是圍繞著原理來的,並不是要搭建專案,專案我會在下一個系列說到,經過和群裡小夥伴的商量,並採納大家的意見,我初步考慮了下,下一個系列我會說下 Nuxt.js 相關內容(我感覺這個很有必要的說,現在網站SEO是灰常重要滴 ),然後再下一個系列就是搭建一個功能豐富的 後臺管理系統 作為開源專案,手裡有貨的小夥伴來群裡,咱們一起開源吧哈哈哈。

 這個時候細心的小夥伴會發現,每天的那個腦圖不見了,哈哈,並沒有,而是在最下邊,看文末就知道了。

一、Client 瀏覽器端渲染是怎樣執行的

為了介紹瀏覽器渲染是怎麼回事,我們執行一下npm run build 看看我們之前的專案——就是我們的個人部落格第一版,大家應該還記得《 二十二║Vue實戰:個人部落格第一版(axios+router)》,釋出版本的檔案,到底有哪些東西,

執行 

npm run build

這裡我們通過 Webpack 打包,將我們的專案打包,生成一個 dist 目錄 ,我們可以看到裡面有 css+fonts+js 資料夾,還有一個 index.html 靜態頁面,我們開啟這個靜態頁面,可以看到下面內容:

<!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 />//頁面掛載入口 <script src=/js/chunk-vendors.1c001ffe.js />//vue 用到的區塊檔案,vue-cli全家桶預設配置裡面這個chunk就是將所有從node_modules/裡require(import)的依賴都打包到這裡 <script src=/js/app.16d68887.js />//這個就是我們專案的核心內容,主要就是 app.vue 的內容,封裝了所有方法,包括路由和頁面渲染之類的 </body> </html>

大家觀察生成的檔案,只有一個div掛載入口,並沒有多餘的dom元素,那麼頁面要怎麼呈現呢?答案是js append拼接,對,下面的那些 js 會負責innerHTML。而js是由瀏覽器解釋執行的,所以呢,我們稱之為瀏覽器渲染,相信這裡大家應該很明白這個原理了,和我們平時用 jQuery 寫區域性非同步載入是一樣的,但是,這有幾個致命的缺點:

  1. js放在dom結尾,如果js檔案過大,那麼必然造成頁面阻塞。
  2. 隨著我們的業務需求增大,打包後的 js 檔案愈來愈大,頁面白屏更加明顯,使用者體驗明顯不好,特別是首頁,幾個,幾十個元件一起渲染,天訥!不敢相信
  3. 不利於SEO
  4. 客戶端執行在老的JavaScript引擎上

這個時候,我們就想其他的一些辦法,比如會單獨給我們的首頁寫一個靜態處理,為了應對相應速度,但是這個並不是一個好的辦法,我們需要處理兩套邏輯,基於以上的一些問題,服務端渲染呼之欲出....

總結:相信大家看到這裡應該都能明白,客戶端渲染的工作原理了,其實就是開發的時候元件化,然後通過 webpack 打包工具,將我們的邏輯處理 js ,打包成檔案,然後和前端頁面一起部署,這樣就能講資料在 DOM 上展示出來了。

二、Server 服務端渲染是怎樣執行的

上邊咱們看了客戶端瀏覽器渲染,明白了原理和弊端,咱們這個時候就需要用到伺服器渲染,SSR , Server Side Render的簡稱, 服務端渲染. 首先服務端渲染的思想由來已久, 在 ajax 興起之前, 所有 web 應用都是服務端渲染, 伺服器直接返回 html 文字給瀏覽器, 使用者操作比如在登陸頁面提交表單, 成功後跳轉到首頁, 伺服器需要返回兩個頁面. 這樣的弊端顯而易見, 加大了伺服器的消耗,到了 vue 時代,咱們雖然是通過 api 返回的Json,但是需要 node 伺服器, 很耗費效能, 需要做好快取和優化, 相當於空間換時間。

這裡咱們先說下原理

從這個圖裡大家應該也能看到,我們的SSR打包流程變化了,在客戶端渲染的時候,我們 webpack 是打包成js約束檔案,直接發給瀏覽器,然後再獲取資料渲染DOM,

網路解釋有點兒羞澀難懂:ssr 有兩個入口檔案,client.js 和 server.js, 都包含了應用程式碼,webpack 通過兩個入口檔案分別打包成給服務端用的 server bundle 和給客戶端用的 client bundle. 當伺服器接收到了來自客戶端的請求之後,會建立一個渲染器 bundleRenderer,這個 bundleRenderer 會讀取上面生成的 server bundle 檔案,並且執行它的程式碼, 然後傳送一個生成好的 html 到瀏覽器,等到客戶端載入了 client bundle 之後,會和服務端生成的DOM 進行 Hydration(判斷這個DOM 和自己即將生成的DOM 是否相同,如果相同就將客戶端的vue例項掛載到這個DOM上, 否則會提示警告)。

可以看出來,我們增加了一個步驟:就是之前我們是在瀏覽器裡,通過JavaScript框架來渲染資料的,但是現在我們的請求中間走了一遍 node 伺服器,然後 node 伺服器幫我們生成相應的 Html 片段,直接傳送給瀏覽器,那瀏覽器肯定是認識html的,所以不用再通過 js 去獲取資料渲染了,直接就渲染了,嗯大概就是這樣,就好像多了一箇中間件。

相信大家看內容可能不是很清楚,關鍵時候還是得上程式碼才能說的更清晰。

三、通過程式碼實現服務端渲染

客戶端渲染咱們就不寫程式碼了吧,這些天都寫了很多了

1、首先我們新建一個資料夾 Vue_SSR_Demo 並對其 node 服務初始化

執行

 npm install vue vue-server-renderer --save

會看到生成一個 node_modules 資料夾 和 package-lock.json 檔案。

然後執行

 npm install express --save

安裝 express 的node服務。

2、然後建立一個 index.html 頁面,作為一個承載頁面,類似我們 vue-cli 腳手架中的 index.html

<!-- 如同vue-cli建立專案中的index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
        {{{meta}}}
</head>
<body>
    <!--vue-ssr-outlet-->
    <!--↑↑↑↑↑ 注意上邊的格式一定要有,並且不能帶空格 ↑↑↑↑↑-->
</body>
</html>

3、新建一個 server.js 檔案,用作我們的啟服務入口

const Vue = require('vue')//引入 vue
const server = require('express')()//引入 express 服務框架
const fs = require('fs')

//讀取 html 模版
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./index.html', 'utf-8')//檔案地址路徑
})  
// 此引數是vue 生成Dom之外位置的資料  如vue生成的dom一般位於body中的某個元素容器中,
//此資料可在header標籤等位置渲染,是renderer.renderToString()的第二個引數,
//第一個引數是vue例項,第三個引數是一個回撥函式。
const context = {
      title: '老張的哲學',
      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">
      `
}
//定義服務
server.get('*', (req, res) => {
      //建立vue例項   主要用於替換index.html中body註釋地方的內容,
    //index.html中 <!--vue-ssr-outlet-->的地方 ,約定熟成
    const app = new Vue({
        data: {
            url: req.url,
            data: ['C#', 'SQL', '.NET', '.NET CORE', 'VUE'],
            title: '我的技能列表'
        },
        //template 中的文字最外層一定要有容器包裹, 和vue的元件中是一樣的,
      //只能有一個父級元素,這裡是div!
        template: `
            <div>
                <p>{{title}}</p>
                <p v-for='item in data'>{{item}}</p>
            </div>
        `
    })

//將 Vue app例項渲染為字串  (其他的API自己看用法是一樣的)
    renderer.renderToString(app, context,  (err, html) => {
        if (err) {
            res.status(500).end('err:' + err) 
            return 
        }
    //將模版傳送給瀏覽器
        res.end(html)
//每次請求 都在node 伺服器中列印
        console.log('success')
    })
})
//服務埠開啟並監聽
server.listen(8060, () => {
    console.log('server success!')
})

文件中的解釋已經很詳細了,大家可以自行看一看,這樣我們就定義好了一個 node 服務,並通過 express 框架,將我們的 vue 例項通過 renderer.renderToString() 方法生成字串,返回到瀏覽器。

4、開啟 node 服務

執行

node server

注意,這裡的 server 是我們的檔名,你也可以用其他的,比如 node aaa.js,或者 node aaa

這個時候,我們就發現我們已經成功的把我們的頁面內容返回到了瀏覽器,為什麼呢?因為我們的頁面原始碼已經有內容了,證明不是通過 js 後期渲染的。binggo!

大家有沒有對 SSR 服務端渲染有一定的任何和了解,是不是品出來一點兒感覺了,這個還是最簡單的一個 node 伺服器渲染。

 程式碼就不上傳了,大家貼上複製就行,全部結構檔案

四、通過 webpack 打包,來深入瞭解伺服器渲染

 dang dang dang,如果大家看到這裡不費勁,或者看懂前邊的了,好滴,你可以看這一塊了,如果上邊的不是很清晰,或者很難懂,好吧,這一塊可能更羞澀了,不過沒關係,慢慢來!

1、這個程式碼是昨天的,咱們這裡重新說一下

結構如下:

├── dist                               // 儲存我們的打包後的檔案
├── node_modules                        // 依賴包資料夾
├── entry                              // 打包入口資料夾
│   └── entry-server.js                 // 服務端 打包入口檔案
├── src                              // 我們的專案的原始碼編寫檔案
│   ├── views                           // view存放目錄
│   │   ├── about.vue                //about 頁面
│   │   ├── like.vue                //like 頁面
│   │   └── Home.vue                   //Home 頁面
│   └── App.vue                     // App入口檔案
│   └── main.js                      // 主配置檔案
│   └── router.js                    // 路由配置檔案
└── .babelrc                              // babel 配置檔案
└── package.json                          // 專案依賴包配置檔案
└── package-lock.json                     // npm5 新增檔案,優化效能
└── server.js                            // server 檔案
└── README.md                             // 說明文件

 咱們分塊的說一說

2、普通的app程式碼塊

這一塊,就是對應的我們 src 資料夾下的模板,這些內容大家一定很熟悉了,就不多說了,就是 元件的定義、路由定義、app入口和 main.js 主方法,這裡重點說下 main.js

在之前的 main.js 我們是直接例項化 vue() ,然後對 #appp 進行掛載的,但是現在咱們變成了 伺服器渲染,這裡就不能掛載了,而是把建立的vue例項返回出去。

//main.js
import Vue from 'vue'
import createRouter from './router'
import App from './App.vue'

// 匯出一個工廠函式,用於建立新的vue例項
export function createApp() {
    const router = createRouter()
    const app = new Vue({
        router,
        render: h => h(App)
    })

    return app
}

你會問了,但是返回給誰呢,欸?!這個問題好,請往下看。

3、講我們的 vue例項封裝到 promise

網友總結:所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。

Promise物件有以下兩個特點。
(1)物件的狀態不受外界影響。Promise物件代表一個非同步操作,有三種狀態:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。
(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise物件的狀態改變,只有兩種可能:從Pending變為Resolved和從Pending變為Rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對Promise物件添加回調函式,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise物件提供統一的介面,使得控制非同步操作更加容易。

 簡單來說,就是把我們 main入口檔案中的vue例項,都封裝到 promise,就像增加一個外衣,方便我們 webpack打包。對,重點來了

4、通過 Webpack 伺服器打包

 

/* 5、webpack.server.js 服務端打包 */
const path = require('path');//獲取路徑物件
const projectRoot = path.resolve(__dirname, '..');//根路徑

//定義模組
module.exports = {
    // 此處告知 server bundle 使用 Node 風格匯出模組(Node-style exports)
    // 這裡必須是node,因為打包完成的執行環境是node,在node端執行的,不是在瀏覽器端執行。
    target: 'node',
    // entry需要提供一個單獨的入口檔案
    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',// 打包最終的檔名,這個檔案是給 node 伺服器使用的
    },
    module: {
        // 因為使用webpack2,這裡必須是rules,如果使用use,
        // 會報個錯:vue this._init is not a function
        rules: [
            //規則1、vue規則定義
            {
            test: /\.vue$/,
            loader: 'vue-loader',
            },//js規則定義
            {
                test: /\.js$/,
                loader: 'babel-loader',
                include: projectRoot,
                // 這裡會把node_modules裡面的東西排除在外,提高打包效率
                exclude: /node_modules/,
                // ES6 語法
                options: {
                    presets: ['es2015']
                }
            },//css定義
            {
                test: /\.less$/,
                loader: "style-loader!css-loader!less-loader"
            }
        ]
    },
    plugins: [],
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.runtime.esm.js'
        }
    }
}

基本的內容就是上邊這些,註釋已經很清楚了,大家可以看一看,這個時候我們的準備工作就已經做好了,下一步就改打包了

5、執行打包命令,生成服務端約束檔案 bundle.server.js

npm run server

這個時候,你會發現,我們的dist 資料夾內,多了一個 bundle.server.js 檔案

 我們看一下生成的檔案,部分截圖,會發現,我們的這個檔案包含了所有頁面內的內容和方法,但是這個 bundle.server.js 並不是直接返回給前端的,而且在 node 伺服器使用的

6、配置 node 伺服器啟動檔案,這個更類似我們上文中提到的 server.js 檔案

/*7、 server.js */
const express = require('express')()//引入express 服務框架
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/bundle.server.js')['default']//引入我們剛剛打包檔案

// 響應路由請求
express.get('*', (req, res) => {
    const context = { url: req.url }

    // 建立vue例項,傳入請求路由資訊
    createApp(context).then(app => {
        renderer.renderToString(app, (err, html) => {
            if (err) { return res.state(500).end('執行時錯誤') }
            res.send(`
                <!DOCTYPE html>
                <html lang="en">
                    <head>
                        <meta charset="UTF-8">
                        <title>Vue2.0 SSR渲染頁面</title>
                    </head>
                    <body>
                        ${html}
                    </body>
                </html>
            `)
        })
    }, err => {
        if(err.code === 404) { res.status(404).end('所請求的頁面不存在') }
    })
})


// 伺服器監聽地址
express.listen(8089, () => {
    console.log('伺服器已啟動!')
})

7、啟動服務

 node server

這個時候我們就可以看到效果了

好啦,這個就是 SSR 服務端渲染的整個過程。

番外

哈嘍大家好,在這裡忙碌的日子又和大家見面了,咱們的前後端系列入門篇已經 26 篇了,按照我的計劃,基本的講解已經到這裡了,相信如果大家按照我寫的系列,能搭建自己的部落格系統了,甚至如果你比較厲害,已經開始開發中型專案了哈哈,咱們這裡先回顧下知識,包括 API ,Swagger 文件,Sugar 資料持久層的ORM,Repository倉儲架構,Asyn/Await 非同步程式設計,AOP面向切面程式設計,IoC控制反轉和DI依賴注入,Dto資料傳輸物件,Redis快取等後端知識,還有Vue 基礎語法、JS高階、ES6、Vue 元件 、生命週期、資料繫結、開發環境搭建、Vue-Cli 腳手架、axios Http請求、vue-router 路由協議、webpack 打包、Vuex 狀態管理等前端知識。雖然都是簡單的說了下皮毛,也是都涵蓋了這個框架內容,咱們可以看看咱們的結構樹,這個每天都會出現的哈哈,這個就是這一個月咱們的辛苦,也是很有回報滴,群裡的小夥伴都破50了,這是個大圖,大家可以看看:


本來想著要換其他的系列,但是在群裡小夥伴的建議下,還是在把Vue好好說說吧,思考了下,在國慶前的時間再說下 SSR 框架——Nuxt.js 吧,感覺這一塊應該是要用到的,也是自學的一個吧,至於國慶之後,再慢慢考慮寫其他的吧。