手把教你搭建SSR(vue/vue-cli + express)

Aaron發表於2019-06-13

Express + vue搭建SSR

最近簡單的研究了一下SSR,對SSR已經有了一個簡單的認知,主要應用於單頁面應用,NuxtSSR很不錯的框架。也有過調研,簡單的用了一下,感覺還是很不錯。但是還是想知道若不依賴於框架又應該如果處理SSR,研究一下做個筆記。

什麼是SSR

Vue元件渲染為伺服器端的HTML字串,將他們直接傳送到瀏覽器,最後將靜態標記混合為客戶端上完全互動的應用程式。

為什麼要使用SSR

  1. 更好的SEO,搜尋引擎爬蟲爬取工具可以直接檢視完全渲染的頁面
  2. 更寬的內容達到時間(time-to-content),當權請求頁面的時候,服務端渲染完資料之後,把渲染好的頁面直接傳送給瀏覽器,並進行渲染。瀏覽器只需要解析html不需要去解析js

SSR弊端

  1. 開發條件受限,Vue元件的某些生命週期鉤子函式不能使用
  2. 開發環境基於Node.js
  3. 會造成服務端更多的負載。在Node.js中渲染完整的應用程式,顯然會比僅僅提供靜態檔案server更加佔用CPU資源,因此如果你在預料在高流量下使用,請準備響應的服務負載,並明智的採用快取策略。

準備工作

在正式開始之前,在vue官網找到了一張這個圖片,圖中詳細的講述了vue中對ssr的實現思路。如下圖簡單的說一下。

下圖中很重要的一點就是webpack,在專案過程中會用到webpack的配置,從最左邊開始就是我們所寫入的原始碼檔案,所有的檔案都有一個公共的入口檔案app.js,然後就進入了server-entry(服務端入口)和client-entry(客戶端入口),兩個入口檔案都要經過webpack,當訪問node端的時候,使用的是服務端渲染,在服務端渲染的時候,會生成一個server-Bender,最後通過server-Bundle可以渲染出HTML頁面,若在客戶端訪問的時候則是使用客戶端渲染,通過client-Bundle在以後渲染出HTML頁面。so~通過這個圖可以很清晰的看出來,接下來會用到兩個檔案,一個server入口,一個client入口,最後由webpack生成server-Bundleclient-Bundle,最終當去請求頁面的時候,node中的server-Bundle會生成HTML介面通過client-Bundle混合到html頁面中即可。

<html>

<img src="https://pic.xiaohuochai.site/blogssr1.png"/>

</html>

對於vue中使用ssr做了一些簡單的瞭解之後,那麼就開始我們要做的第一步吧,首先要建立一個專案,建立一個資料夾,名字不重要,但是最好不要使用中文。

mkdir dome
cd dome
npm init

npm init命令用來初始化package.json檔案:

{
  "name": "dome",   //  專案名稱
  "version": "1.0.0",   //  版本號
  "description": "",    //  描述
  "main": "index.js",   //  入口檔案
  "scripts": {          //  命令列執行命令 如:npm run test
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Aaron",     //  作者
  "license": "ISC"      //  許可證
}

初始化完成之後接下來需要安裝,專案所需要依賴的包,所有依賴項如下:

npm install express --save-dev
npm install vue --save-dev
npm install vue-server-renderer --save-dev
npm install vue-router --save-dev

如上所有依賴項一一安裝即可,安裝完成之後就可以進行下一步了。前面說過SSR是服務端預渲染,所以當然要建立一個Node服務來支撐。在dome資料夾下面建立一個index.js檔案,並使用express建立一個服務。

程式碼如下:

const express = require("express");
const app = express();

app.get('*',(request,respones) => {
    respones.end("ok");
})

app.listen(3000,() => {
    console.log("服務已啟動")
});

完成上述程式碼之後,為了方便我們需要在package.json新增一個命令,方便後續開發啟動專案。

{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  }
}

建立好之後,在命令列直接輸入npm start即可,當控制檯顯示服務已啟動則表示該服務已經啟動成功了。接下來需要開啟瀏覽器看一下渲染的結果。在瀏覽器位址列輸入locahost:3000則可以看到ok兩個字。

SSR渲染手動搭建

前面的準備工作已經做好了,千萬不要完了我們的主要目的不是為了渲染文字,主要的目標是為了渲染*.vue檔案或html所以。接下來就是做我們想要做的事情了。接下來就是要修改index.js檔案,將之前安裝的vuevue-server-renderer引入進來。

由於返回的不再是文字,而是html模板,所以我們要對響應頭進行更改,告訴瀏覽器我們渲染的是什麼,否則瀏覽器是不知道該如何渲染伺服器返回的資料。

index.js中引入了vue-server-renderer之後,在使用的時候,我們需要執行一下vue-server-renderer其中的createRenderer方法,這個方法的作用就是會將vue的例項轉換成html的形式。

既然有了vue-server-renderer的方法,接下來就需要引入主角了vue,引入之後然後接著在下面建立一個vue例項,在web端使用vue的時候需要傳一些引數給Vue然而在服務端也是如此也可以傳遞一些引數給Vue例項,這個例項也就是後續新增的那些*.vue檔案。為了防止使用者訪問的時候頁面資料不會互相干擾,暫時需要把例項放到get請求中,每次有訪問的時候就會建立一個新的例項,渲染新的模板。

creteRender方法能夠把vue的例項轉成html字串傳遞到瀏覽器。那麼接下來由應該怎麼做?在vueServerRender方法下面有一個renderToString方法,這個方法就可以幫助我們完成這步操作。這個方法接受的第一個引數是vue的例項,第二個引數是一個回撥函式,如果不想使用回撥函式的話,這個方法也返回了一個Promise物件,當方法執行成功之後,會在then函式裡面返回html結構。

index.js改動如下:

const express = require("express");
const Vue = require("vue");
const vueServerRender = require("vue-server-renderer").createRenderer();

const app = express();

app.get('*',(request,respones) => {
    const vueApp = new Vue({
        data:{
            message:"Hello,Vue SSR!"
        },
        template:`<h1>{{message}}</h1>` 
    });
    respones.status(200);
    respones.setHeader("Content-Type","text/html;charset-utf-8;");
    vueServerRender.renderToString(vueApp).then((html) => {
        respones.end(html);
    }).catch(error => console.log(error));
})

app.listen(3000,() => {
    console.log("服務已啟動")
});

上述操作完成之後,一定要記得儲存,然後重啟伺服器,繼續訪問一下locahost:3000,就會看到在服務端寫入的HTML結構了。這樣做好像給我們新增了大量的工作,到底與在web端直接使用有什麼區別麼?

接下來見證奇蹟的時刻到了。在網頁中右鍵檢視原始碼就會發現與之前的在web端使用的時候完全不同,可以看到渲染的模板了。如果細心的就會發現一件很有意思的事情,在h1標籤上會有一個data-server-rendered=true這樣的屬性,這個可以告訴我們這個頁面是通過服務端渲染來做的。大家可以去其他各大網站看看哦。沒準會有其他的收穫。

上面的案例中,雖然已經實現了服務端預渲染,但是會有一個很大的缺陷,就是我們所渲染的這個網頁並不完整,沒有文件宣告,head等等等,當然可能會有一個其他的想法,就是使用es6的模板字串做拼接就好了啊。確實,這樣也是行的通的,但是這個仍是飲鴆止渴不能徹底的解決問題,如果做過傳統MVC開發的話,就應該知道,MVC開發模式全是基於模板的,現在這種與MVC有些相似的地方,同理也是可以使用模板的。在dome資料夾下建立index.html,並建立好HTML模板。

模板現在有了該如何使用?在createRenderer函式可以接收一個物件作為配置引數。配置引數中有一項為template,這項配置的就是我們即將使用的Html模板。這個接收的不是一個單純的路徑,我們需要使用fs模組將html模板讀取出來。

其配置如下:

let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({
    template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

現在模板已經有了,在web端進行開發的時候,需要掛在一個el的掛載點,這樣Vue才知道把這些template渲染在哪,服務端渲染也是如此,同樣也需要告訴Vuetemplate渲染到什麼地方。接下來要做的事情就是在index.html中做手腳。來通知createRenderertemplate新增到什麼地方。

更改index.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>Document</title>
</head>
<body>
    <!--vue-ssr-outlet-->
</body>
</html>

可以發現,在htmlbody裡面新增了一段註釋,當將vueServerRender編譯好的html傳到模板當中之後這個地方將被替換成服務端預編譯的模板內容,這樣也算是完成一個簡單的服務端預渲染了。雖然寫入的只是簡單的html渲染,沒有資料互動也沒有頁面互動,也算是一個不小的進展了。

使用SSR搭建專案我們繼續延續上個專案繼續向下開發,大家平時在使用vue-cli搭建專案的時候,都是在src資料夾下面進行開發的,為了和vue專案結構保持一致,同樣需要建立一個src資料夾,並在src資料夾建立conponents,router,utils,view,暫定專案結構就這樣,隨著程式碼的編寫會逐漸向專案裡面新增內容。

└─src
|   ├─components
|   ├─router
|   ├─utils
|   ├─view
|   └─app.js
└─index.js

初始的目錄結構已經搭建好了之後,接下來需要繼續向下進行,首先要做的就是要在router目錄中新增一個index.js檔案,用來建立路由資訊(在使用路由的時候一定要確保路由已經安裝)。路由在專案中所起到的作用應該是重要的,路由會通過路徑把頁面和元件之間建立聯絡,並且一一的對應起來,完成路由的渲染。

接下來在router下面的index.js檔案中寫入如下配置:

const vueRouter = require("vue-router");
const Vue = require("vue");

Vue.use(vueRouter);

module.exports = () => {
    return new vueRouter({
        mode:"history",
        routes:[
            {
                path:"/",
                component:{
                    template:`<h1>這裡是首頁</h1>`
                },
                name:"home"
            },
            {
                path:"/about",
                component:{
                    template:`<h1>這裡是關於我</h1>`
                },
                name:"about"
            }
        ]
    })
}

上面的程式碼中,仔細觀察的話,和平時在vue-cli中所匯出的方式是不一樣的,這裡採用了工廠方法,這裡為什麼要這樣?記得在雛形裡面說過,為了保證使用者每次訪問都要生成一個新的路由,防止使用者與使用者之間相互影響,也就是說Vue例項是新的,我們的vue-router的例項也應該保證它是一個全新的。

現在Vue例項和服務端混在一起,這樣對於專案的維護是很不好的,所以也需要把Vue從服務端單獨抽離出來,放到app.js中去。這裡採用和router同樣的方式使用工廠方式,以保證每次被訪問都是一個全新的vue例項。在app.js匯入剛剛寫好的路由,在每次觸發工廠的時候,建立一個新的路由例項,並繫結到vue例項裡面,這樣使用者在訪問路徑的時候無論是vue例項還是router都是全新的了。

app.js:

const Vue = require("vue");
const createRouter = require("../router")

module.exports = (context) => {
    const router = createRouter();
    return new Vue({
        router,
        data:{
            message:"Hello,Vue SSR!"
        },
        template:`
            <div>
                <div>
                    <h1>{{message}}</h1>
                    <ul>
                        <li>
                            <router-link to="/">首頁</router-link>
                        </li>
                        <li>
                            <router-link to="/about">關於我</router-link>
                        </li>
                    </ul>
                </div>
                <router-view></router-view>
            </div>
        ` 
    });
}

做完這些東西貌似好像就能用了一樣,但是還是不行,仔細想想好像忘了一些什麼操作,剛剛把vue例項從index.js中抽離出來了,但是卻沒有在任何地方使用它,哈哈,好像是一件很尷尬的事情。

修改index.js檔案:

const express = require("express");
const vueApp = require("./src/app.js");
let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({
  template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

const app = express();

app.get('*',(request,respones) => {
    
    //  這裡可以傳遞給vue例項一些引數
    let vm = vueApp({})
    
    respones.status(200);
    respones.setHeader("Content-Type","text/html;charset-utf-8;");
    vueServerRender.renderToString(vm).then((html) => {
        respones.end(html);
    }).catch(error => console.log(error));
})

app.listen(3000,() => {
    console.log("服務已啟動")
});

準備工作都已經做好啦,完事具備只欠東風啦。現在執行一下npm start可以去頁面上看一下效果啦。看到頁面中已經渲染出來了,但是好像是少了什麼?雖然導航內容已經都顯示出來了,但是路由對應的元件好像沒得渲染噻。具體是什麼原因導致的呢,vue-router是由前端控制渲染的,當訪問路由的時候其實,在做首屏渲染的時候並沒有授權給服務端讓其去做渲染路由的工作。(⊙﹏⊙),是的我就是這麼懶...

這個問題解決方案也提供了相對應的操作,不然就知道該怎麼寫下去了。既然在做渲染的時候分為服務端渲染和客戶端渲染兩種,那麼我們就需要兩個入口檔案,分別對應的服務端渲染的入口檔案,另個是客戶端渲染的入口檔案。

src資料夾下面新增兩個.js檔案(當然也可以放到其他地方,這裡只是為了方便),entry-client.js這個檔案使用者客戶端的入口檔案,entry-server.js那麼這個檔案則就作為服務端的入口檔案。既然入口檔案已經確定了,接下來就是要解決剛才的問題了,首先解決的是服務端渲染,在服務端這裡需要把使用者所訪問的路徑傳遞給vue-router,如果不傳遞給vue-router的話,vue-router會一臉懵逼的看著你,你什麼都不給我,我怎麼知道渲染什麼?

entry-server中需要做的事情就是需要把app.js匯入進來,這裡可以向上翻一下app.js中儲存的是建立vue例項的方法。首先在裡面寫入一個函式,至於為什麼就不多說了(同樣也是為了保證每次訪問都有一個新的例項),這個函式接收一個引數([object]),由於這裡考慮到可能會有非同步操作(如懶載入),在這個函式中使用了Promise,在Promise中首先要拿到連個東西,不用猜也是能想到的,很重要的vue例項和router例項,so~但是在app中好像只匯出了vue例項,還要根據當前所需要的去更改app.js

app.js:

const Vue = require("vue");
const createRouter = require("../router")

module.exports = (context) => {
    const router = createRouter();
    const app = new Vue({
        router,
        data:{
            message:"Hello,Vue SSR!"
        },
        template:`
            <div>
                <div>
                    <h1>{{message}}</h1>
                    <ul>
                        <li>
                            <router-link to="/">首頁</router-link>
                        </li>
                        <li>
                            <router-link to="/about">關於我</router-link>
                        </li>
                    </ul>
                </div>
                <router-view></router-view>
            </div>
        ` 
    });
    return {
        app,
        router
    }
}

通過上面的改造之後,就可以在entry-server.js中輕鬆的拿到vuerouter的例項了,現在檢視一下當前entry-server.js中有那些可用引數,vue,router,提及到的URL從哪裡來?既然這個函式是給服務端使用的,那麼當服務端去執行這個函式的時候,就可以通過引數形式傳遞進來,獲取到我們想要的引數,我們假設這個引數叫做url,我們需要讓路由去做的就是跳轉到對應的路由中(這一步很重要),然後再把對router的例項掛載到vue例項中,然後再把vue例項返回出去,供vueServerRender消費。那麼就需要匯出這個函式,以供服務端使用。

由於我們不能預測到使用者所訪問的路由就是在vue-router中所配置的,所以需要在onReady的時候進行處理,我們可以通過routergetMatchedComponents這個方法,獲取到我們所匯入的元件,這些有個我們就可通過判斷元件對匹配結果進行渲染。

entry-server.js

const createApp = require("./app.js");

module.exports = (context) => {
    return new Promise((reslove,reject) => {
        let {url} = context;
        let {app,router} = createApp(context);
        router.push(url);
        //  router回撥函式
        //  當所有非同步請求完成之後就會觸發
        router.onReady(() => {
            let matchedComponents = router.getMatchedComponents();
            if(!matchedComponents.length){
                return reject({
                    code:404,
                });
            }
            reslove(app);
        },reject)
    })
}

既然例項又發生了變化,需要對應發生變化的index.js同樣也需要做出對應的改動。把剛才的引入vue例項的路徑改為entey-server.js,由於這裡返回的是一個Promise物件,這裡使用async/await處理接收一下,並拿到vue例項。不要忘了把router所需要的url引數傳遞進去。

index.js:

const express = require("express");
const App = require("./src/entry-server.js");
let path = require("path");
const vueServerRender = require("vue-server-renderer").createRenderer({
  template:require("fs").readFileSync(path.join(__dirname,"./index.html"),"utf-8")
});

const app = express();

app.get('*',async (request,respones) => {
    respones.status(200);
    respones.setHeader("Content-Type","text/html;charset-utf-8;");

    let {url} = request;
    //  這裡可以傳遞給vue例項一些引數
    let vm = await App({url});
    vueServerRender.renderToString(vm).then((html) => {
        respones.end(html);
    }).catch(error => console.log(error));
})

app.listen(3000,() => {
    console.log("服務已啟動")
});

這下子就完成了,啟動專案吧,當訪問根路徑的時候,就會看到剛才缺少的元件也已經渲染出來了,當然我們也可以切換路由,也是沒有問題的。大功告成。。。好像並沒有emmmmmmmmm,為什麼,細心的話應該會發現,當我們切換路由的時候,位址列旁邊的重新整理按鈕一直在閃動,這也就是說,我們所做出來的並不是一個單頁應用(手動笑哭),出現這樣的問題也是難怪的,畢竟我們沒有配置前端路由,我們把所有路由的控制權都交給了服務端,每次訪問一個路由的時候,都會向服務端傳送一個請求,返回路由對應的頁面。想要解決這個問題,當處於前端的時候我們需要讓服務端把路由的控制權交還給前端路由,讓前端去控制路由的跳轉。

之前在src資料夾下面新增了兩個檔案,只用到了服務端的檔案,為了在客戶端能夠交還路由控制權,要對web端路由進行配置。由於在客戶端在使用vue的時候需要掛載一個document,因為vue的例項已經建立完成了,所以,這裡需要使用$mount這個鉤子函式,來完成客戶端的掛載。同樣為了解決懶載入這種類似的問題so~同樣需要使用onReady裡進行路由的處理,只有當vue-router載入完成以後再去掛載。

在客戶端是使用的時候很簡單,只需要把路由掛載到app裡面就可以了。

entry-client.js

const createApp = require("./app.js");
let {app,router} = createApp({});

router.onReady(() => {
    app.$mount("#app")
});

整個專案的雛形也就這樣了,由於服務端把路由控制權交還給客戶端,需要複雜的webpack配置,so~不再贅述了,下面直接使用vue-cli繼續(做的是使用需要用到上面的程式碼)。

vue-cli專案搭建

在做準備工作的時候簡單講述了vue中使用ssr的執行思路,裡面提及了一個很重要的webpack,因此這裡需要藉助vue-cli腳手架,直接更改原有的webpack就可以了,這樣會方便很多。

這裡建議大家返回頂部再次看一下vue服務端渲染的流程,在介紹中的client-bundleserver-bundle,,所以需要構建兩個配置,分別是服務端配置和客戶端的配置。

如想要實現服務端渲染需要對vue-cli中個js檔案中的配置進行修改。以下只展示更改部分的程式碼,不展示全部。

檔案分別是:

  1. webpack.server.conf.js - 服務端webpack配置
  2. dev-server.js - 獲取服務端bundle
  3. server.js - 建立後端服務
  4. webpack.dev.conf.js - 客戶端的bundle
  5. webpack.base.conf - 修改入口檔案

客戶端配置

客戶端生成一份客戶端構建清單,記錄客戶端的資源,最終會將客戶端構建清單中記錄的檔案,注入到執行的執行的模板中,這個清單與服務端類似,同樣也會生成一份json檔案,這個檔案的名字是vue-ssr-client-manifest.json(專案啟動以後可以通過地址/檔名訪問到),當然必不可少的是,同樣也需要引入一個叫做vue-server-renderer/client-plugin模組,作為webpack的外掛供其使用。

首先要安裝一下vue-server-renderer這個模組,這個是整個服務端渲染的核心,沒有整個ssr是沒有任何靈魂的。

npm install vue-server-renderer -S

安裝完成之後,首先要找到webpack.dev.conf.js,首先要對其進行相關配置。

webpack.dev.conf.js

//  新增引入  vue-server-render/client-plugin  模組
const vueSSRClientPlugin = require("vue-server-renderer/client-plugin");

const devWebpackConfig = merge(baseWebpackConfig,{
    plugins:[
        new vueSSRClientPlugin()
    ] 
});

新增了這個配置以後,重新啟動專案通過地址就可以訪問到vue-ssr-client-manifest.jsonhttp://localhost:8080/vue-ssr-client-manifest.json),頁面中出現的內容就是所需要的client-bundle

服務端配置

服務端會預設生成一個vue-ssr-server-bundle.json檔案,在檔案中會記錄整個服務端整個輸出,怎麼才能生成這個檔案呢?要在這個json檔案,必須要引入vue-server-renderer/server-plugin,並將其作為webpack的外掛。

在開始服務端配置之前,需要在src資料夾下面建立三個檔案,app.jsentry-client.jsentry-server.js,建立完成之後需要對其寫入相關程式碼。

src/router/index.js

import vueRouter from "vue-router";
import Vue from "vue";
import HelloWorld from "@/components/HelloWorld";

Vue.use(vueRouter);
export default () => {
    return new vueRouter({
        mode:"history",
        routes:[
            {
                path:"/",
                component:HelloWorld,
                name:"HelloWorld"
            }
        ]
    })
}

app.js

import Vue from "vue";
import createRouter from "./router";
import App from "./App.vue";

export default (context) => {
    const router = createRouter();
    const app = new Vue({
        router,
        components: { App },
        template: '<App/>'
    });
    return {
        app,
        router
    }
}

entry-server.js

import createApp from "./app.js";

export default (context) => {
    return new Promise((reslove,reject) => {
        let {url} = context;
        let {app,router} = createApp(context);
        router.push(url);
        router.onReady(() => {
            let matchedComponents = router.getMatchedComponents();
            if(!matchedComponents.length){
                return reject({
                    code:404,
                });
            }
            reslove(app);
        },reject)
    })
}

entry-client.js

import createApp from "./app.js";
let {app,router} = createApp();

router.onReady(() => {
    app.$mount("#app");
});

webpack.base.conf.js

module.exports = {
    entry:{
        app:"./src/entry-client.js"
    },
    output:{
        publicPath:"http://localhost:8080/"
    }
};

webpack.server.conf.js(手動建立)

const webpack = require("webpack");
const merge = require("webpack-merge");
const base = require("./webpack.base.conf");
//  手動安裝
//  在服務端渲染中,所需要的檔案都是使用require引入,不需要把node_modules檔案打包
const webapckNodeExternals = require("webpack-node-externals");


const vueSSRServerPlugin = require("vue-server-renderer/server-plugin");

module.exports = merge(base,{
    //  告知webpack,需要在node端執行
    target:"node",
    entry:"./src/entry-server.js",
    devtool:"source-map",
    output:{
        filename:'server-buldle.js',
        libraryTarget: "commonjs2"
    },
    externals:[
        webapckNodeExternals()
    ],
    plugins:[
        new webpack.DefinePlugin({
            'process.env.NODE_ENV':'"devlopment"',
            'process.ent.VUE_ENV': '"server"'
        }),
        new vueSSRServerPlugin()
    ]
});

dev-server.js(手動建立)

const serverConf = require("./webpack.server.conf");
const webpack = require("webpack");
const fs = require("fs");
const path = require("path");
//  讀取記憶體中的.json檔案
//  這個模組需要手動安裝
const Mfs = require("memory-fs");
const axios = require("axios");

module.exports = (cb) => {
    const webpackComplier = webpack(serverConf);
    var mfs = new Mfs();
    
    webpackComplier.outputFileSystem = mfs;
    
    webpackComplier.watch({},async (error,stats) => {
        if(error) return console.log(error);
        stats = stats.toJson();
        stats.errors.forEach(error => console.log(error));
        stats.warnings.forEach(warning => console.log(warning));
        //  獲取server bundle的json檔案
        let serverBundlePath = path.join(serverConf.output.path,'vue-ssr-server-bundle.json');
        let serverBundle = JSON.parse(mfs.readFileSync(serverBundlePath,"utf-8"));
        //  獲取client bundle的json檔案
        let clientBundle = await axios.get("http://localhost:8080/vue-ssr-client-manifest.json");
        //  獲取模板
        let template = fs.readFileSync(path.join(__dirname,"..","index.html"),"utf-8");
        cb && cb(serverBundle,clientBundle,template);
    })
};

根目錄/server.js(手動建立)

const devServer = require("./build/dev-server.js");
const express = require("express");
const app = express();
const vueRender = require("vue-server-renderer");

app.get('*',(request,respones) => {
    respones.status(200);
    respones.setHeader("Content-Type","text/html;charset-utf-8;");
    devServer((serverBundle,clientBundle,template) => {
        let render = vueRender.createBundleRenderer(serverBundle,{
            template,
            clientManifest:clientBundle.data,
            //  每次建立一個獨立的上下文
            renInNewContext:false
        }); 
        render.renderToString({
            url:request.url
        }).then((html) => {
            respones.end(html);
        }).catch(error => console.log(error));
    });
})

app.listen(5000,() => {
    console.log("服務已啟動")
});

index.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>Document</title>
</head>
<body>
    <div id="app">
        <!--vue-ssr-outlet-->
    </div>
    <!-- built files will be auto injected -->
</body>
</html>

以上就是所有要更改和新增的配置項,配置完所有地方就可以完成服務端渲染。此時需要在package.json中的sctipt中新增啟動項:http:node server.js,就可以正常執行專案了。注意一定要去訪問服務端設定的埠,同時要保證你的客戶端也是線上的。

總結

這篇部落格耗時3天才完成,可能讀起來會很費時間,但是卻有很大的幫助,希望大家能夠好好閱讀這篇文章,對大家有所幫助。

感謝大家花費很長時間來閱讀這篇文章,若文章中有錯誤灰常感謝大家提出指正,我會盡快做出修改的。

相關文章