客戶端渲染(首屏在1.6s時出現(xiàn))
服務(wù)端渲染(首屏在400ms時出現(xiàn))
當(dāng)頁面加載的 js 和 css 更多更大時厉萝,網(wǎng)路不夠流暢時,客戶端渲染的首屏出現(xiàn)時間會更晚蟹腾。
vue + ssr + service worker
項(xiàng)目結(jié)構(gòu)
項(xiàng)目搭建步驟
- vue create v3
- 配置相關(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"
},
- 分別打包 client 和 server 端代碼
- 執(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)
- 在瀏覽器訪問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
在Application/Cache Storage中查看sw緩存的資源