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结构后,替换模板文件中的指定部分,该项目中是
/* 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('
', `
${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

你可能感兴趣的:(ssr + service worker 实践)