什麼是SSR?vue中怎麼實現SSR伺服器端渲染?

2022-02-25 13:00:20
什麼是SSR?下面本篇文章給大家介紹一下在中實現SSR伺服器端渲染的方法,希望對大家有所幫助!

一、SSR是什麼

Server-Side Rendering 我們稱其為SSR,意為伺服器端渲染

指由服務側完成頁面的 HTML 結構拼接的頁面處理技術,傳送到瀏覽器,然後為其繫結狀態與事件,成為完全可互動頁面的過程。【相關推薦:】

先來看看Web3個階段的發展史:

  • 傳統伺服器端渲染SSR
  • 單頁面應用SPA
  • 伺服器端渲染SSR

傳統web開發

網頁內容在伺服器端渲染完成,⼀次性傳輸到瀏覽器

1.png

開啟頁面檢視原始碼,瀏覽器拿到的是全部的dom結構

單頁應用SPA

單頁應用優秀的使用者體驗,使其逐漸成為主流,頁面內容由JS渲染出來,這種方式稱為使用者端渲染

2.png

開啟頁面檢視原始碼,瀏覽器拿到的僅有宿主元素#app,並沒有內容

伺服器端渲染SSR

SSR解決方案,後端渲染出完整的首屏的dom結構返回,前端拿到的內容包括首屏及完整spa結構,應用啟用後依然按照spa方式執行

3.png

看完前端發展,我們再看看Vue官方對SSR的解釋:

Vue.js 是構建使用者端應用程式的框架。預設情況下,可以在瀏覽器中輸出 Vue 元件,進行生成 DOM 和操作 DOM。然而,也可以將同一個元件渲染為伺服器端的 HTML 字串,將它們直接傳送到瀏覽器,最後將這些靜態標記"啟用"為使用者端上完全可互動的應用程式

伺服器渲染的 Vue.js 應用程式也可以被認為是"同構"或"通用",因為應用程式的大部分程式碼都可以在伺服器和使用者端上執行

我們從上門解釋得到以下結論:

  • Vue SSR是一個在SPA上進行改良的伺服器端渲染
  • 通過Vue SSR渲染的頁面,需要在使用者端啟用才能實現互動
  • Vue SSR將包含兩部分:伺服器端渲染的首屏,包含互動的SPA

二、解決了什麼

SSR主要解決了以下兩種問題:

  • seo:搜尋引擎優先爬取頁面HTML結構,使用ssr時,伺服器端已經生成了和業務想關聯的HTML,有利於seo
  • 首屏呈現渲染:使用者無需等待頁面所有js載入完成就可以看到頁面檢視(壓力來到了伺服器,所以需要權衡哪些用伺服器端渲染,哪些交給使用者端)

但是使用SSR同樣存在以下的缺點:

  • 複雜度:整個專案的複雜度

  • 庫的支援性,程式碼相容

  • 效能問題

    • 每個請求都是n個範例的建立,不然會汙染,消耗會變得很大
    • 快取 node serve nginx判斷當前使用者有沒有過期,如果沒過期的話就快取,用剛剛的結果。
    • 降級:監控cpu、記憶體佔用過多,就spa,返回單個的殼
  • 伺服器負載變大,相對於前後端分離務器只需要提供靜態資源來說,伺服器負載更大,所以要慎重使用

所以在我們選擇是否使用SSR前,我們需要慎重問問自己這些問題:

  • 需要SEO的頁面是否只是少數幾個,這些是否可以使用預渲染(Prerender SPA Plugin)實現

  • 首屏的請求響應邏輯是否複雜,資料返回是否大量且緩慢

三、如何實現

對於同構開發,我們依然使用webpack打包,我們要解決兩個問題:伺服器端首屏渲染和使用者端啟用

這裡需要生成一個伺服器bundle檔案用於伺服器端首屏渲染和一個使用者端bundle檔案用於使用者端啟用

4.png

程式碼結構 除了兩個不同入口之外,其他結構和之前vue應用完全相同

src 
├── router 
├────── index.js # 路由宣告 
├── store 
├────── index.js # 全域性狀態 
├── main.js # ⽤於建立vue範例 
├── entry-client.js # 使用者端⼊⼝,⽤於靜態內容「啟用」 
└── entry-server.js # 伺服器端⼊⼝,⽤於⾸屏內容渲染

路由設定

import Vue from "vue"; import Router from "vue-router"; Vue.use(Router);
//匯出⼯⼚函數
export function createRouter(){
    return new Router({
    mode: 'history',
    routes: [ 
       // 使用者端沒有編譯器,這⾥要寫成渲染函數 
        { path: "/", component: { render: h => h('div', 'index page') } }, 
        { path: "/detail", component: { render: h => h('div', 'detail page') }} 
    ] 
  });
 }

主檔案main.js

跟之前不同,主檔案是負責建立vue範例的工廠,每次請求均會有獨立的vue範例建立

import Vue from "vue";
import App from "./App.vue";
import { createRouter } from "./router"; 
// 匯出Vue範例⼯⼚函數,為每次請求建立獨⽴範例 
// 上下⽂⽤於給vue範例傳遞引數
export function createApp(context) {
    const router = createRouter();
    const app = new Vue({ router, context, render: h => h(App) });
    return { app, router };
  }

編寫伺服器端入口src/entry-server.js

它的任務是建立Vue範例並根據傳入url指定首屏

import { createApp } from "./main";
// 返回⼀個函數,接收請求上下⽂,返回建立的vue範例
export default context => {
// 這⾥返回⼀個Promise,確保路由或元件準備就緒
    return new Promise((resolve, reject) => {
        const { app, router } = createApp(context);
        // 跳轉到⾸屏的地址
        router.push(context.url);
        // 路由就緒,返回結果
        router.onReady(() => {
        resolve(app);
        }, reject);
    }); 
};

編寫使用者端入口entry-client.js

使用者端入口只需建立vue範例並執行掛載,這⼀步稱為啟用

import { createApp } from "./main"; 
// 建立vue、router範例
const { app, router } = createApp();
// 路由就緒,執⾏掛載
router.onReady(() => { app.$mount("#app"); });

webpack進行設定

安裝依賴

npm install webpack-node-externals lodash.merge -D

vue.config.js進行設定

// 兩個外掛分別負責打包使用者端和伺服器端
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");
// 根據傳⼊環境變數決定⼊⼝⽂件和相應設定項
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
    css: {
        extract: false
    },
    outputDir: './dist/'+target,
    configureWebpack: () => ({
        // 將 entry 指向應⽤程式的 server / client ⽂件
        entry: `./src/entry-${target}.js`,
        // 對 bundle renderer 提供 source map ⽀持
        devtool: 'source-map',
        // target設定為node使webpack以Node適⽤的⽅式處理動態導⼊,
        // 並且還會在編譯Vue元件時告知`vue-loader`輸出⾯向伺服器程式碼。
        target: TARGET_NODE ? "node" : "web",
        // 是否模擬node全域性變數
        node: TARGET_NODE ? undefined : false,
        output: {
            // 此處使⽤Node⻛格匯出模組
            libraryTarget: TARGET_NODE ? "commonjs2" : undefined
        },
        // https://webpack.js.org/configuration/externals/#function
        // https://github.com/liady/webpack-node-externals
        // 外接化應⽤程式依賴模組。可以使伺服器構建速度更快,並⽣成較⼩的打包⽂件。
        externals: TARGET_NODE
        ? nodeExternals({
            // 不要外接化webpack需要處理的依賴模組。
            // 可以在這⾥新增更多的⽂件型別。例如,未處理 *.vue 原始⽂件,
            // 還應該將修改`global`(例如polyfill)的依賴模組列⼊⽩名單
            whitelist: [/\.css$/]
        })
        : undefined,
        optimization: {
            splitChunks: undefined
        },
        // 這是將伺服器的整個輸出構建為單個 JSON ⽂件的外掛。
        // 伺服器端預設⽂件名為 `vue-ssr-server-bundle.json`
        // 使用者端預設⽂件名為 `vue-ssr-client-manifest.json`。
        plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new
                  VueSSRClientPlugin()]
    }),
    chainWebpack: config => {
        // cli4項⽬新增
        if (TARGET_NODE) {
            config.optimization.delete('splitChunks')
        }

        config.module
            .rule("vue")
            .use("vue-loader")
            .tap(options => {
            merge(options, {
                optimizeSSR: false
            });
        });
    }
};

對指令碼進行設定,安裝依賴

npm i cross-env -D

"scripts": {
 "build:client": "vue-cli-service build",
 "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build",
 "build": "npm run build:server && npm run build:client"
}

執行打包:npm run build

最後修改宿主檔案/public/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <!--vue-ssr-outlet-->
    </body>
</html>

是伺服器端渲染入口位置,注意不能為了好看而在前後加空格

安裝vuex

npm install -S vuex

建立vuex工廠函數

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore () {
    return new Vuex.Store({
        state: {
            count:108
        },
        mutations: {
            add(state){
                state.count += 1;
            }
        }
    })
}

main.js檔案中掛載store

import { createStore } from './store'
export function createApp (context) {
    // 建立範例
    const store = createStore()
    const app = new Vue({
        store, // 掛載
        render: h => h(App)
    })
    return { app, router, store }
}

伺服器端渲染的是應用程式的"快照",如果應用依賴於⼀些非同步資料,那麼在開始渲染之前,需要先預取和解析好這些資料

store進行一步資料獲取

export function createStore() {
    return new Vuex.Store({
        mutations: {
            // 加⼀個初始化
            init(state, count) {
                state.count = count;
            },
        },
        actions: {
            // 加⼀個非同步請求count的action
            getCount({ commit }) {
                return new Promise(resolve => {
                    setTimeout(() => {
                        commit("init", Math.random() * 100);
                        resolve();
                    }, 1000);
                });
            },
        },
    });
}

元件中的資料預取邏輯

export default {
    asyncData({ store, route }) { // 約定預取邏輯編寫在預取鉤⼦asyncData中
        // 觸發 action 後,返回 Promise 以便確定請求結果
        return store.dispatch("getCount");
    }
};

伺服器端資料預取,entry-server.js

import { createApp } from "./app";
export default context => {
    return new Promise((resolve, reject) => {
        // 拿出store和router範例
        const { app, router, store } = createApp(context);
        router.push(context.url);
        router.onReady(() => {
            // 獲取匹配的路由元件陣列
            const matchedComponents = router.getMatchedComponents();

            // 若⽆匹配則丟擲異常
            if (!matchedComponents.length) {
                return reject({ code: 404 });
            }

            // 對所有匹配的路由元件調⽤可能存在的`asyncData()`
            Promise.all(
                matchedComponents.map(Component => {
                    if (Component.asyncData) {
                        return Component.asyncData({
                            store,
                            route: router.currentRoute,
                        });
                    }
                }),
            )
                .then(() => {
                // 所有預取鉤⼦ resolve 後,
                // store 已經填充⼊渲染應⽤所需狀態
                // 將狀態附加到上下⽂,且 `template` 選項⽤於 renderer 時,
                // 狀態將⾃動序列化為 `window.__INITIAL_STATE__`,並注⼊ HTML
                context.state = store.state;

                resolve(app);
            })
                .catch(reject);
        }, reject);
    });
};

使用者端在掛載到應用程式之前,store 就應該獲取到狀態,entry-client.js

// 匯出store
const { app, router, store } = createApp();
// 當使⽤ template 時,context.state 將作為 window.__INITIAL_STATE__ 狀態⾃動嵌⼊到最終的 HTML 
// 在使用者端掛載到應⽤程式之前,store 就應該獲取到狀態:
if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__);
}

使用者端資料預取處理,main.js

Vue.mixin({
    beforeMount() {
        const { asyncData } = this.$options;
        if (asyncData) {
            // 將獲取資料操作分配給 promise
            // 以便在元件中,我們可以在資料準備就緒後
            // 通過運⾏ `this.dataPromise.then(...)` 來執⾏其他任務
            this.dataPromise = asyncData({
                store: this.$store,
                route: this.$route,
            });
        }
    },
});

修改伺服器啟動檔案

// 獲取⽂件路徑
const resolve = dir => require('path').resolve(__dirname, dir)
// 第 1 步:開放dist/client⽬錄,關閉預設下載index⻚的選項,不然到不了後⾯路由
app.use(express.static(resolve('../dist/client'), {index: false}))
// 第 2 步:獲得⼀個createBundleRenderer
const { createBundleRenderer } = require("vue-server-renderer");
// 第 3 步:伺服器端打包⽂件地址
const bundle = resolve("../dist/server/vue-ssr-server-bundle.json");
// 第 4 步:建立渲染器
const renderer = createBundleRenderer(bundle, {
    runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext
    template: require('fs').readFileSync(resolve("../public/index.html"), "utf8"), // 宿主⽂件
    clientManifest: require(resolve("../dist/client/vue-ssr-clientmanifest.json")) // 使用者端清單
});
app.get('*', async (req,res)=>{
    // 設定url和title兩個重要引數
    const context = {
        title:'ssr test',
        url:req.url
    }
    const html = await renderer.renderToString(context);
    res.send(html)
})

小結

  • 使用ssr不存在單例模式,每次使用者請求都會建立一個新的vue範例

  • 實現ssr需要實現伺服器端首屏渲染和使用者端啟用

  • 伺服器端非同步獲取資料asyncData可以分為首屏非同步獲取和切換元件獲取

    • 首屏非同步獲取資料,在伺服器端預渲染的時候就應該已經完成
    • 切換元件通過mixin混入,在beforeMount勾點完成資料獲取

(學習視訊分享:)

以上就是什麼是SSR?vue中怎麼實現SSR伺服器端渲染?的詳細內容,更多請關注TW511.COM其它相關文章!