從零開始搭建一個vue-ssr(上)

從零開始搭建一個vue-ssr

背景

What?SSR是什麼?

SSR全拼是Server-Side Rendering,服務端渲染。
所謂服務端渲染,指的是把vue組件在服務器端渲染爲組裝好的HTML字符串,而後將它們直接發送到瀏覽器,最後須要將這些靜態標記混合在客戶端上徹底可交互的應用程序。html

Why?爲何選擇SSR?

①知足seo需求,傳統的spa數據都是異步加載的,爬蟲引擎沒法加載,須要利用ssr將數據直出渲染在頁面源代碼中。
②更寬的內容達到時間(首屏加載更快),當請求頁面的時候,服務端渲染完數據以後,把渲染好的頁面直接發送給瀏覽器,並進行渲染。瀏覽器只須要解析html不須要去解析js。前端

How?SSR的原理

借用下面的一張圖,咱們來簡單闡述一下vue-ssr的原理。
clipboard.png
咱們能夠看到,左側Source部分就是咱們所編寫的源代碼,全部代碼有一個公共入口,就是app.js,緊接着就是服務端的入口
(entry-server.js)和客戶端的入口(entry-client.js)。當完成全部源代碼的編寫以後,咱們經過webpack的構建,打包出兩個bundle,分別是server bundle和client bundle;當用戶進行頁面訪問的時候,先是通過服務端的入口,將vue組建組裝爲html字符串,並混入客戶端所訪問的html模板中,最終就完成了整個ssr渲染的過程。vue

開始搭建

建立一個空白目錄並初始化

在終端輸入如下命令node

mkdir ssr-demo
cd ssr-demo
npm init

因爲咱們這個只是一個demo項目,能夠直接一路按回車鍵,直接忽略配置。
完成以後咱們能夠看到文件夾裏面有一個package.json的文件,這就是配置表。webpack

安裝依賴

該項目須要四個依賴,依次安裝git

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

其中express使咱們node端的框架,vue用於建立vue實例,vue-router則用於實現路由控制,最後vue-server-renderer尤其關鍵,咱們實現的vue-ssr依靠於這個庫提供的API。
在安裝依賴完畢以後,咱們看到package.json中已經把四個依賴都寫上了。github

"express": "^4.17.1",
"vue": "^2.6.10",
"vue-router": "^3.0.6",
"vue-server-renderer": "^2.6.10"

建立一個node服務

在根目錄下咱們新建一個server.js,用戶搭建node服務web

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

app.get('*', (request, response) => {
    response.end('hello, ssr');
})

app.listen(3001, () => {
    console.log('服務已開啓')
})

接着爲了後續開發的便利,咱們在package.json中添加一個啓動命令:vue-router

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

接着咱們在終端輸入 npm run server,而後再瀏覽器輸入localhost:3001,即可以看到頁面中的文字被成功渲染。vue-cli

渲染html頁面

在上一步咱們已經能成功渲染出一個文字,可是ssr並非主要爲了渲染文字,而是渲染一個html模板。
那麼,接下來,咱們得告知瀏覽器,咱們須要渲染的是html,而不僅是text,所以咱們須要修改響應頭。
同時,引入vue-server-renderer中的createRenderer對象,有一個renderToString的方法,能夠將vue實例轉成html的形式。(renderToString這個方法接受的第一個參數是vue的實例,第二個參數是一個回調函數,若是不想使用回調函數的話,這個方法也返回了一個Promise對象,當方法執行成功以後,會在then函數裏面返回html結構。)
修改server.js以下:

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

app.get('*', (request, response) => {
    const vueApp = new Vue({
        data:{
           message: "hello, ssr"
        },
        template: `<h1>{{message}}</h1>`
    });

    response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");
    vueServerRender.renderToString(vueApp).then((html) => {
        response.end(html);
    }).catch(err => console.log(err))
})

app.listen(3001, () => {
    console.log('服務已開啓')
})

保存代碼,重啓服務,而後從新刷新頁面。咱們發現,頁面好像沒什麼不一樣,就是字體變粗了而已。其實並非,你能夠嘗試查看頁面源代碼,咱們發如今源代碼中,已經存在一個標籤對h1,這就是html模板的雛形。同時,細心的同窗還會發現,h1上面有一個屬性:
data-server-rendered="true",那這個屬性是幹什麼的呢?這個是一個標記,代表這個頁面是由vue-ssr渲染而來的。你們不妨能夠打開一些seo頁面或者一些公司的網站,查看源代碼,你會發現,也是有這個標記。
雖然h1標籤對被成功渲染,可是咱們發現這個html頁面並不完整, 他缺乏了文檔聲明,html標籤,body標籤,title標籤等。

將Vue實例掛載進html模板中

建立一個index.html,用於掛載Vue實例。

<!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>Hello, SSR</title>
</head>
<body>
    <!--vue-ssr-outlet-->
</body>
</html>

注意,body中的註釋不能去掉,這是Vue掛載的佔位符。
而後修改server.js,將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")
});

保存代碼,重啓服務,而後從新刷新頁面。咱們查看源代碼,發現,已經能成功渲染出一個完整的頁面了。

建立一個Vue項目的開發目錄

上面的開發模式,很顯然只是一個demo而已,接下來咱們模擬一下正常的vue開發的目錄結構。
建立一個src文件夾,裏面有一個router文件夾,再有一個index,js用做路由,並建立一個app.js,用做vue的入口,以下圖:
clipboard.png
修改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>this is home page</h1>`
                },
                name:"home"
            },
            {
                path:"/about",
                component:{
                    template:`<h1>this is about page</h1>`
                },
                name:"about"
            }
        ]
    })
}

修改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>
                <h1>{{message}}</h1>
                <ul>
                    <li>
                        <router-link to="/">home</router-link>
                    </li>
                    <li>
                        <router-link to="/about">about</router-link>
                    </li>
                </ul>
                <router-view></router-view>
            </div>
        ` 
    });
}

而後在server.js中,將app.js引入

const express = require("express");
const app = 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")
});

app.get('*', (request, response) => {
    let vm = vueApp({});

    response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");

    vueServerRender.renderToString(vm).then((html) => {
        response.end(html);
    }).catch(err => console.log(err))
})

app.listen(3001, () => {
    console.log('服務已開啓')
})

保存代碼,重啓服務,而後從新刷新頁面。而後咱們能夠看到瀏覽器的路由已經被成功渲染了,可是不管怎麼點擊都沒反應,瀏覽器的url有更改,可是頁面內容不變。
這是由於咱們只是將頁面渲染的工做交給服務端,而頁面路由切換,仍是在前端執行,服務端並未能接收到該指令,所以不管怎麼切換路由,服務端渲染出來的頁面根本沒變化。

實現服務端控制頁面路由

在src中建立一個entry-server.js文件,該文件爲服務端入口文件,接收app和router實例:

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

module.exports = (context) => {
    return new Promise(async (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();
            }
            reslove(app);
        },reject)
    })
}

在src中建立一個entry-client.js文件,該文件爲客戶端入口,負責將路由掛載到app裏面。

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

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

修改app.js,將router和vue實例暴露出去

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>
                <h1>{{message}}</h1>
                <ul>
                    <li>
                        <router-link to="/">home</router-link>
                    </li>
                    <li>
                        <router-link to="/about">about</router-link>
                    </li>
                </ul>
                <router-view></router-view>
            </div>
        ` 
    });
    return {
        app,
        router
    }
}

最終修改server.js

const express = require("express");
const app = 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")
});

app.get('*', async(request, response) => {

    response.status(200);
    response.setHeader("Content-type", "text/html;charset-utf-8");

    let {url} = request;
    let vm;
    vm = await App({url})
    vueServerRender.renderToString(vm).then((html) => {
        response.end(html);
    }).catch(err => console.log(err))
})

app.listen(3001, () => {
    console.log('服務已開啓')
})

保存代碼,重啓服務,而後從新刷新頁面。這時候,咱們發現頁面的路由切換生效了,而且不一樣頁面的源代碼也不同了。

數據傳遞

既然是服務端渲染,數據的接收也是來源於服務端,那怎樣才能把服務端接收到的數據傳輸給前端,而後進行渲染呢?
修改entry-server.js,進行同步或者異步獲取數據

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

const getData = function(){
    return new Promise((reslove, reject) => {
        let str = 'this is a async data!';
        reslove(str);
    })
}

module.exports = (context) => {
    return new Promise(async (reslove,reject) => {
        let {url} = context;

        // 數據傳遞
        context.propsData = 'this is a data from props!'

        context.asyncData = await getData();

        let {app,router} = createApp(context);
        router.push(url);
        //  router回調函數
        //  當全部異步請求完成以後就會觸發
        router.onReady(() => {
            let matchedComponents = router.getMatchedComponents();
            if(!matchedComponents.length){
                return reject();
            }
            reslove(app);
        },reject)
    })
}

修改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!",
            propsData: context.propsData,
            asyncData: context.asyncData
        },
        template:`
            <div>
                <h1>{{message}}</h1>
                <p>{{asyncData}}</p>
                <p>{{propsData}}</p>
                <ul>
                    <li>
                        <router-link to="/">home</router-link>
                    </li>
                    <li>
                        <router-link to="/about">about</router-link>
                    </li>
                </ul>
                <router-view></router-view>
            </div>
        ` 
    });
    return {
        app,
        router
    }
}

最後咱們能夠看到不管是同步仍是異步獲取的數據,都能成功地經過服務端渲染,展現在頁面源代碼中。
另外,你也能夠在server.js中的request中,將數據傳遞下去。

總結

實現了一個簡易版本的vue-ssr,下期咱們會依賴於vue-cli,進行webpack改造,實現一個通用且更實用的vue-ssr框架。從零開始搭建一個vue-ssr(下)

項目源碼

https://github.com/TheWalking...

相關文章
相關標籤/搜索