ssr + service worker 实践
2021-09-29 本文已影响0人
努力学习的小丸子
客户端渲染(首屏在1.6s时出现)
image.png服务端渲染(首屏在400ms时出现)
image.png当页面加载的 js 和 css 更多更大时,网路不够流畅时,客户端渲染的首屏出现时间会更晚。
vue + ssr + service worker
项目结构
image.png
项目搭建步骤
- vue create v3
- 配置相关文件,参见: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"
},
- 分别打包 client 和 server 端代码
- 执行命令: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)
- 在浏览器访问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