ssr + service worker 實(shí)踐

客戶端渲染(首屏在1.6s時出現(xiàn))

image.png

服務(wù)端渲染(首屏在400ms時出現(xiàn))

image.png

當(dāng)頁面加載的 js 和 css 更多更大時厉萝,網(wǎng)路不夠流暢時,客戶端渲染的首屏出現(xiàn)時間會更晚蟹腾。

vue + ssr + service worker

項(xiàng)目結(jié)構(gòu)


image.png

項(xiàng)目搭建步驟

  1. vue create v3
  2. 配置相關(guān)文件坏挠,參見:https://v3.vuejs.org/guide/ssr/structure.html#introducing-a-build-step
    2.1 新建app.js
    該文件的目的是創(chuàng)建vue實(shí)例,配置router和vuex等屯阀,client和server共用這一個文件。
$ scr/app.js
import { createSSRApp } from 'vue'
import App from './App.vue'
import createRouter from './router'
// 導(dǎo)入echarts的目的是測試client和server渲染的性能轴术,在頁面要加載echart.js文件時蹲盘,服務(wù)端渲染的首屏?xí)r間會明顯縮短
import * as echarts from 'echarts';
echarts;
// export a factory function for creating a root component
export default function (args) {
    args;
    const app = createSSRApp(App)
    const router = createRouter()
    app.use(router)
    return {
        app,
        router
    }
}
$ scr/router/index.js
// router.js
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
import Main from '../views/Main.vue';
const isServer = typeof window === 'undefined'
const history = isServer ? createMemoryHistory() : createWebHistory()

const routes = [
    {
        path: '/',
        component: Main,
    },
    {
        path: '/main',
        component: Main,
    },
    {
        path: '/doc',
        component: () => import("../views/Doc.vue"),
    }
]

export default function () {
    return createRouter({ routes, history })
}

2.2 創(chuàng)建entry-client.js
該文件注冊了service worker并將vue實(shí)例掛載到#app節(jié)點(diǎn)。

$ src/entry-client.js
import createApp from './app'
if ('serviceWorker' in navigator) {
     window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js').then(reg => {
        console.log('SW registered: ', reg);
    }).catch(regError => {
        console.log('SW registration failed: ', regError);
    });
     });
}
const { app, router } = createApp({
    // here we can pass additional arguments to app factory
})
router.isReady().then(() => {
    app.mount('#app')
})

2.3 創(chuàng)建entry-server.js

$ src/entry-server.js
import createApp from './app'
export default function () {
    const { app, router } = createApp({
        /*...*/
    })
    return {
        app,
        router
    }
}

2.4 配置vue.config.js
如果是自己寫sw.js文件膳音,詳細(xì)步驟見下方service worker小節(jié)。sw.js文件放在public文件夾下铃诬。
如果使用workbox-webpack-plugin插件自動生成service-worker.js文件祭陷,需要額外的配置
以下配置的參考文章:https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.GenerateSW#GenerateSW

webpackConfig.plugin("workbox").use(
                new WorkboxPlugin.GenerateSW({
                    // 這些選項(xiàng)幫助快速啟用 ServiceWorkers
                    // 不允許遺留任何“舊的” ServiceWorkers
                    clientsClaim: true,
                    skipWaiting: true,
                    // 需要緩存的路由
                    additionalManifestEntries: [
                        { url: "/doc", revision: null },
                        { url: "/main", revision: null },
                    ],
                    runtimeCaching: [
                        {
                            urlPattern: /.*\.js|css|html.*/i,
                            handler: "CacheFirst",
                            options: {
                                // Configure which responses are considered cacheable.
                                cacheableResponse: {
                                    statuses: [200],
                                },
                            },
                        }
                    ],
                })
            );
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const nodeExternals = require("webpack-node-externals");
const webpack = require("webpack");

module.exports = {
    chainWebpack: (webpackConfig) => {
        // We need to disable cache loader, otherwise the client build
        // will used cached components from the server build
        webpackConfig.module.rule("vue").uses.delete("cache-loader");
        webpackConfig.module.rule("js").uses.delete("cache-loader");
        webpackConfig.module.rule("ts").uses.delete("cache-loader");
        webpackConfig.module.rule("tsx").uses.delete("cache-loader");
        if (!process.env.SSR) {
           // workbox-webpack-plugin 插件配置
            webpackConfig
                .entry("app")
                .clear()
                .add("./src/entry-client.js");
            return;
        }

        // Point entry to your app's server entry file
        webpackConfig
            .entry("app")
            .clear()
            .add("./src/entry-server.js");

        // This allows webpack to handle dynamic imports in a Node-appropriate
        // fashion, and also tells `vue-loader` to emit server-oriented code when
        // compiling Vue components.
        webpackConfig.target("node");
        // This tells the server bundle to use Node-style exports
        webpackConfig.output.libraryTarget("commonjs2");

        webpackConfig
            .plugin("manifest")
            .use(new WebpackManifestPlugin({ fileName: "ssr-manifest.json" }));

        // https://webpack.js.org/configuration/externals/#function
        // https://github.com/liady/webpack-node-externals
        // Externalize app dependencies. This makes the server build much faster
        // and generates a smaller bundle file.

        // Do not externalize dependencies that need to be processed by webpack.
        // You should also whitelist deps that modify `global` (e.g. polyfills)
        webpackConfig.externals(nodeExternals({ allowlist: /\.(css|vue)$/ }));

        webpackConfig.optimization.splitChunks(false).minimize(false);

        webpackConfig.plugins.delete("preload");
        webpackConfig.plugins.delete("prefetch");
        webpackConfig.plugins.delete("progress");
        webpackConfig.plugins.delete("friendly-errors");

        webpackConfig.plugin("limit").use(
            new webpack.optimize.LimitChunkCountPlugin({
                maxChunks: 1,
            })
        );
    },
};

2.5 配置package.json
build:server 命令使用cross-env庫設(shè)置了環(huán)境變量SSR苍凛。如此process.env.SSR為true

 "scripts": {
    "build": "vue-cli-service build",
    "serve": "vue-cli-service serve",
    "build:client": "vue-cli-service build --dest dist/client",
    "build:server": "cross-env SSR=true vue-cli-service build --dest dist/server",
    "build:both": "npm run build:client && npm run build:server"
  },
  1. 分別打包 client 和 server 端代碼
  2. 執(zhí)行命令:node server.js
    注意點(diǎn):
    1、需額外攔截service-worker.js請求兵志,返回對應(yīng)的js文件
    2醇蝴、目前攔截workbox-53dfa3d6.js處理有問題,因?yàn)槊看未虬膆ash值不同想罕,不能直接配固定悠栓。
    3、攔截請求并生成對應(yīng)dom結(jié)構(gòu)后按价,替換模板文件中的指定部分惭适,該項(xiàng)目中是<div id="app">
/* eslint-disable no-useless-escape */
const path = require('path')
const express = require('express')
const fs = require('fs')
const { renderToString } = require('@vue/server-renderer')
const manifest = require('./dist/server/ssr-manifest.json')
const server = express()
const appPath = path.join(__dirname, './dist', 'server', manifest['app.js'])
const createApp = require(appPath).default
server.use('/img', express.static(path.join(__dirname, './dist/client')))
server.use('/js', express.static(path.join(__dirname, './dist/client', 'js')))
server.use('/css', express.static(path.join(__dirname, './dist/client', 'css')))
// 注意點(diǎn)1
server.use(
    '/service-worker.js',
    express.static(path.join(__dirname, './dist/client', 'service-worker.js'))
)
server.use(
    '/workbox-53dfa3d6.js',
    express.static(path.join(__dirname, './dist/client', 'workbox-53dfa3d6.js'))
)
server.use(
    '/favicon.ico',
    express.static(path.join(__dirname, './dist/client', 'favicon.ico'))
)
server.get('*', async (req, res) => {
    const { app, router } = createApp()

    router.push(req.url)
    await router.isReady()

    const appContent = await renderToString(app)
    console.log('appContent', appContent);

    fs.readFile(path.join(__dirname, '/dist/client/index.html'), (err, html) => {
        if (err) {
            throw err
        }

        html = html
            .toString()
            .replace('<div id="app">', `<div id="app">${appContent}`)
        res.setHeader('Content-Type', 'text/html')
        res.send(html)
    })
})

console.log('You can navigate to http://localhost:8085')

server.listen(8085)
  1. 在瀏覽器訪問localhost:8085,勾選offline復(fù)選框測試離線緩存
    怎么控制precache和runtime各自緩存的文件,還沒搞清楚...
    workbox 的官方文檔好難讀啊楼镐。https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin
    以下效果是使用workbox 配置自動生成的service-worker.js緩存的文件癞志。
    參考文章:https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin
    在Application/Service Workers可以看到注冊的sw
    image.png

在Application/Cache Storage中查看sw緩存的資源


image.png

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市框产,隨后出現(xiàn)的幾起案子凄杯,更是在濱河造成了極大的恐慌,老刑警劉巖秉宿,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戒突,死亡現(xiàn)場離奇詭異,居然都是意外死亡描睦,警方通過查閱死者的電腦和手機(jī)膊存,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酌摇,“玉大人膝舅,你說我怎么就攤上這事∫ざ啵” “怎么了仍稀?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長埂息。 經(jīng)常有香客問我技潘,道長,這世上最難降的妖魔是什么千康? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任享幽,我火速辦了婚禮,結(jié)果婚禮上拾弃,老公的妹妹穿的比我還像新娘值桩。我一直安慰自己,他們只是感情好豪椿,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布奔坟。 她就那樣靜靜地躺著携栋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咳秉。 梳的紋絲不亂的頭發(fā)上婉支,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機(jī)與錄音澜建,去河邊找鬼向挖。 笑死,一個胖子當(dāng)著我的面吹牛炕舵,可吹牛的內(nèi)容都是我干的何之。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼幕侠,長吁一口氣:“原來是場噩夢啊……” “哼帝美!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起晤硕,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤悼潭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后舞箍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舰褪,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年疏橄,在試婚紗的時候發(fā)現(xiàn)自己被綠了占拍。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡捎迫,死狀恐怖晃酒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窄绒,我是刑警寧澤贝次,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站彰导,受9級特大地震影響蛔翅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜位谋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一山析、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掏父,春花似錦笋轨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸟款。三九已至,卻和暖如春茂卦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背组哩。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工等龙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伶贰。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓蛛砰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親黍衙。 傳聞我的和親對象是個殘疾皇子泥畅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容