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

前言

書接上文,昨天簡單的說到了 SSR 服務端渲染的相關內容《二十五║初探SSR服務端渲染》,主要說明了相關概念,以及爲何使用等,昨天的一個小栗子由於時間問題,沒有好好的給你們鋪開來說,今天呢,我們就繼續說一下這個 SSR 服務端渲染,並結合着 Client 客戶端渲染,一塊兒說一說相關的內容,固然仍是圍繞着原理來的,並非要搭建項目,項目我會在下一個系列說到,通過和羣裏小夥伴的商量,並採納你們的意見,我初步考慮了下,下一個系列我會說下 Nuxt.js 相關內容(我感受這個頗有必要的說,如今網站SEO是灰常重要滴 ),而後再下一個系列就是搭建一個功能豐富的 後臺管理系統 做爲開源項目,手裏有貨的小夥伴來羣裏,我們一塊兒開源吧哈哈哈。css

 

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

1、Client 瀏覽器端渲染是怎樣運行的

爲了介紹瀏覽器渲染是怎麼回事,咱們運行一下npm run build 看看咱們以前的項目——就是咱們的我的博客初版,你們應該還記得《 二十二║Vue實戰:我的博客初版(axios+router)》,發佈版本的文件,到底有哪些東西,前端

執行 vue

npm run build

這裏咱們經過 Webpack 打包,將咱們的項目打包,生成一個 dist 目錄 ,咱們能夠看到裏面有 css+fonts+js 文件夾,還有一個 index.html 靜態頁面,咱們打開這個靜態頁面,能夠看到下面內容:node

<!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 寫局部異步加載是同樣的,可是,這有幾個致命的缺點:webpack

  1. js放在dom結尾,若是js文件過大,那麼必然形成頁面阻塞。
  2. 隨着咱們的業務需求增大,打包後的 js 文件越來越大,頁面白屏更加明顯,用戶體驗明顯很差,特別是首頁,幾個,幾十個組件一塊兒渲染,天訥!不敢相信
  3. 不利於SEO
  4. 客戶端運行在老的JavaScript引擎上

 

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

總結:相信你們看到這裏應該都能明白,客戶端渲染的工做原理了,其實就是開發的時候組件化,而後經過 webpack 打包工具,將咱們的邏輯處理 js ,打包成文件,而後和前端頁面一塊兒部署,這樣就能講數據在 DOM 上展現出來了。web

 

2、Server 服務端渲染是怎樣運行的

上邊我們看了客戶端瀏覽器渲染,明白了原理和弊端,我們這個時候就須要用到服務器渲染,SSR , Server Side Render的簡稱, 服務端渲染. 首先服務端渲染的思想由來已久, 在 ajax 興起以前, 全部 web 應用都是服務端渲染, 服務器直接返回 html 文本給瀏覽器, 用戶操做好比在登陸頁面提交表單, 成功後跳轉到首頁, 服務器須要返回兩個頁面. 這樣的弊端顯而易見, 加大了服務器的消耗,到了 vue 時代,我們雖然是經過 api 返回的Json,可是須要 node 服務器, 很耗費性能, 須要作好緩存和優化, 至關於空間換時間。ajax

這裏我們先說下原理vue-router

 

從這個圖裏你們應該也能看到,咱們的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 去獲取數據渲染了,直接就渲染了,嗯大概就是這樣,就好像多了一箇中間件。

相信你們看內容可能不是很清楚,關鍵時候仍是得上代碼才能說的更清晰。

 

3、經過代碼實現服務端渲染

客戶端渲染我們就不寫代碼了吧,這些天都寫了不少了

一、首先咱們新建一個文件夾 Vue_SSR_Demo 並對其 node 服務初始化

執行

 npm install vue vue-server-renderer --save

會看到生成一個 node_modules 文件夾 和 package-lock.json 文件。

而後執行

 npm install express --save

安裝 express 的node服務。

 

二、而後建立一個 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>

 

三、新建一個 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() 方法生成字符串,返回到瀏覽器。

 

四、開啓 node 服務

執行

node server

注意,這裏的 server 是咱們的文件名,你也能夠用其餘的,好比 node aaa.js,或者 node aaa

 

 

這個時候,咱們就發現咱們已經成功的把咱們的頁面內容返回到了瀏覽器,爲何呢?由於咱們的頁面源代碼已經有內容了,證實不是經過 js 後期渲染的。binggo!

你們有沒有對 SSR 服務端渲染有必定的任何和了解,是否是品出來一點兒感受了,這個仍是最簡單的一個 node 服務器渲染。

 代碼就不上傳了,你們粘貼複製就行,所有結構文件

 

 

4、經過 webpack 打包,來深刻了解服務器渲染

 dang dang dang,若是你們看到這裏不費勁,或者看懂前邊的了,好滴,你能夠看這一塊了,若是上邊的不是很清晰,或者很難懂,好吧,這一塊可能更羞澀了,不過不要緊,慢慢來!

一、這個代碼是昨天的,我們這裏從新說一下

結構以下:

├── 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                             // 說明文檔

 我們分塊的說一說

二、普通的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
}

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

三、講咱們的 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打包。對,重點來了

 

四、經過 Webpack 服務器打包

 

 

/* 五、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: [
            //規則一、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'
        }
    }
}

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

 

五、執行打包命令,生成服務端約束文件 bundle.server.js

npm run server

這個時候,你會發現,咱們的dist 文件夾內,多了一個 bundle.server.js 文件

 咱們看一下生成的文件,部分截圖,會發現,咱們的這個文件包含了全部頁面內的內容和方法,可是這個 bundle.server.js 並非直接返回給前端的,並且在 node 服務器使用的

六、配置 node 服務器啓動文件,這個更相似咱們上文中提到的 server.js 文件

/*七、 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('服務器已啓動!')
})

 

七、啓動服務

 node server

這個時候咱們就能夠看到效果了

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

 

番外

哈嘍你們好,在這裏忙碌的日子又和你們見面了,我們的先後端系列入門篇已經 26 篇了,按照個人計劃,基本的講解已經到這裏了,相信若是你們按照我寫的系列,能搭建本身的博客系統了,甚至若是你比較厲害,已經開始開發中型項目了哈哈,我們這裏先回顧下知識,包括 API ,Swagger 文檔,Sugar 數據持久層的ORM,Repository倉儲架構,Asyn/Await 異步編程,AOP面向切面編程,IoC控制反轉和DI依賴注入,Dto數據傳輸對象,Redis緩存等後端知識,還有Vue 基礎語法、JS高級、ES六、Vue 組件 、生命週期、數據綁定、開發環境搭建、Vue-Cli 腳手架、axios Http請求、vue-router 路由協議、webpack 打包、Vuex 狀態管理等前端知識。雖然都是簡單的說了下皮毛,也是都涵蓋了這個框架內容,我們能夠看看我們的結構樹,這個天天都會出現的哈哈,這個就是這一個月我們的辛苦,也是頗有回報滴,羣裏的小夥伴都破50了,這是個大圖,你們能夠看看:


原本想着要換其餘的系列,可是在羣裏小夥伴的建議下,仍是在把Vue好好說說吧,思考了下,在國慶前的時間再說下 SSR 框架——Nuxt.js 吧,感受這一塊應該是要用到的,也是自學的一個吧,至於國慶以後,再慢慢考慮寫其餘的吧。

相關文章
相關標籤/搜索