ssr + service worker 实践

客户端渲染(首屏在1.6s时出现)

image.png

服务端渲染(首屏在400ms时出现)

image.png

当页面加载的 js 和 css 更多更大时,网路不够流畅时,客户端渲染的首屏出现时间会更晚。

vue + ssr + service worker

项目结构


image.png

项目搭建步骤

  1. vue create v3
  2. 配置相关文件,参见:https://v3.vuejs.org/guide/ssr/structure.html#introducing-a-build-step
    2.1 新建app.js
    该文件的目的是创建vue实例,配置router和vuex等,client和server共用这一个文件。
$ scr/app.js
import { createSSRApp } from 'vue'
import App from './App.vue'
import createRouter from './router'
// 导入echarts的目的是测试client和server渲染的性能,在页面要加载echart.js文件时,服务端渲染的首屏时间会明显缩短
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 创建entry-client.js
该文件注册了service worker并将vue实例挂载到#app节点。

$ 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 创建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文件,详细步骤见下方service worker小节。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({
                    // 这些选项帮助快速启用 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库设置了环境变量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. 执行命令:node server.js
    注意点:
    1、需额外拦截service-worker.js请求,返回对应的js文件
    2、目前拦截workbox-53dfa3d6.js处理有问题,因为每次打包的hash值不同,不能直接配固定。
    3、拦截请求并生成对应dom结构后,替换模板文件中的指定部分,该项目中是<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')))
// 注意点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复选框测试离线缓存
    怎么控制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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容