自從vue、react或者angular這類框架流行後,單頁應用的數量也愈來愈多。可是限制於單頁應用的一些缺點,好比:seo、首屏時間等因素,不少應用的結構仍是保持了多頁面結構。此篇講述的是如何在多頁面應用結構的基礎上,利用webpack生成帶hashcode文件名的方式實現靜態資源的增量更新方案。javascript
多頁應用的結構在用戶訪問時每每會在當前頁面加載一些公共資源和當前頁面的js和css,可能有些應用還在用比較傳統的:css
https://url/[版本號]/xxx.[js|css]
html
或前端
https://url/xxx.js?r=xxx
vue
的方式來保證當應用更新時客戶端也能及時獲取到最新的資源文件。而當前流行的前端的架構中單頁應用在發佈時,每每能夠經過編譯時在生成的資源文件名中加入文件的hashcode值來保證每一個資源都有本身獨立的"版本號"。客戶端加載帶有hashcode文件名的資源文件,當某個資源文件更新時也不會影響其餘資源文件的名稱,能夠有效利用客戶端的強緩存策略,增長資源文件的緩存命中率。java
下面咱們將實如今多頁架構中如何實現靜態文件名加入hashcode,並在服務端引用文件的例子:node
webpack.conf.jsreact
{
entry: './app.js',
output: {
filename: 'js/[name].[chunkhash:7].js',
chunkFilename: 'js/[name].[chunkhash:7].js',
}
}
複製代碼
配置結束!webpack
就是這麼簡單,固然這樣配置只會在webpack打包出來的js文件名中加入文件的hashcode值,若是應用中的css也須要hashcode的話則須要在mini-css-extract-plugin
插件中配置:git
webpack.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
{
entry: './app.js',
output: {
filename: 'js/[name].[chunkhash:7].js',
chunkFilename: 'js/[name].[chunkhash:7].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:7].css'
})
]
}
複製代碼
在output中定義filename和chunkFilename的命名規則,filename是指配置項中entry入口文件的輸出命名規則,chunkFilename是指在代碼中chunk包輸出文件的命名規則,譬如:require.ensure或import導入的異步包名稱
小夥伴們能夠發現配置中既使用了chunkhash
又使用了contenthash
,那其中有什麼區別呢?
其中的chunkhash
是指webpack在打包chunk塊時,根據chunk塊內容生成的hashcode文件內容不改變hash值不變。而css是經過js模塊導入的,因此理論上css也屬於js的內容部分,因此css內容改變時js的hash也會變化,可是咱們能夠經過contenthash
讓js文件改變時css文件hash不變。
hash文件打包以後咱們須要一份原文件名和帶hash文件名的映射關係。
接下來咱們須要爲編譯後的N多帶hash的文件生成一份manifest清單,webpack-manifest-plugin
插件能夠作到這件事情 ,具體能夠參考:github.com/danethurber…
webpack.conf.js
const ManifestPlugin = require('webpack-manifest-plugin')
{
entry: './app.js',
output: {
filename: 'js/[name].[chunkhash:7].js',
chunkFilename: 'js/[name].[chunkhash:7].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:7].css'
}),
new ManifestPlugin({
fileName: 'manifest-x.x.x.json'
})
]
}
複製代碼
插件支持generate函數能夠自定義生成manifest.json文件的內容,fileName能夠自定義manifest文件名稱,建議文件名和業務版本號綁定。
打包編譯後:
通過自定義後的manifest.json:
{
"common": {
"vendors": {
"js": "//cdn.xxx.cn/js/vendors.fda30d2.js"
},
"main": {
"js": "//cdn.xxx.cn/js/main.eeb79b4.js",
"css": "//cdn.xxx.cn/css/main.58eaf53.css"
}
},
"pages": {
"product": {
"detail": {
"js": "//cdn.xxx.cn/js/page.product.detail.1bfd90d.js",
"css": "//cdn.xxx.cn/css/page.page.product.detail.19743f3.css"
}
}
}
}
複製代碼
清單文件生成後,服務端須要引用清單文件並對頁面js作映射加載實際的帶hashcode名的資源文件(因此清單文件須要和服務端應用一同發佈,不一樣構建環境有不一樣的實現方式)。
咱們的服務端應用是Nodejs的express框架,handlebars做爲模板渲染引擎。下面講述咱們實現服務端讀取的方式。
在每一個請求的業務邏輯處理完畢後咱們都須要調用一次res.render函數來選擇模板文件和傳入渲染模板所須要的數據。除了頁面須要的渲染數據,咱們也會把當前這個頁面因此須要引用的js和css文件名一同傳遞到頁面中(若是進入頁面邏輯以前就能夠肯定頁面所引用資源名稱那下面就不用這麼複雜了)。
res.render('search/goods-list', {
module: 'product',
page: 'search-list',
data: {
pageData: {}, // 頁面數據
pageName: 'product/search-list' // js和css名稱(頁面名稱)
}
});
複製代碼
但這裏有一個小問題頁面名稱是在每一個具體的頁面業務邏輯中定義的(只有在調用render時纔會傳入),咱們但願在業務邏輯以前添加一個讀取清單文件的中間件,可在業務邏輯以前尚未肯定頁面名稱。在業務邏輯以後的話,由於調用了res.render後續中間件也不會被執行,最後在具體業務邏輯中去調用讀取清單文件更不合適。因此重寫express的render方法,並在實際輸出渲染內容以前以事件的方式把頁面參數emit出來。
res.emit('beforeRender', {module, page, others});
複製代碼
這樣咱們能夠在實際業務邏輯以前的中間件就能夠註冊這個事件,獲取到頁面名稱後經過require的方式加載清單文件json,並找到頁面映射的資源文件實際地址,最後把實際資源地址merge到渲染數據中,最後在handlebars中加載,下面示例僅供參考實際實際場景會更復雜一些:
middleware.js
const _ = require('lodash');
const manifest = require('path/manifest.json');
function getStatic(path) {
return _.get(manifest, path);
}
module.exports = (req, res, next) => {
res.on('beforeRender', (params) => {
const {pageName} = params;
res.renderData.statics = {
name: `${pageName}`,
styles: [
getStatic(`common.main.css`),
getStatic(`pages.${pageName}.css`)
],
javascripts: [
getStatic('common.vendors.js'),
getStatic('common.main.js'),
getStatic(`pages.${pageName}.js`)
]
};
});
return next();
};
複製代碼
頁面渲染layout模板:
layout.hbs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
{{#statics.preloads}}
<link rel="preload" href="{{url}}" as="{{as}}">
{{/statics.preloads}}
{{#statics.styles}}
<link rel="stylesheet" media="all" href="{{.}}">
{{/statics.styles}}
</head>
<body>
{{{body}}}
{{#statics.javascripts}}
<script src="{{.}}" crossorigin="anonymous"></script>
{{/statics.javascripts}}
</body>
</html>
複製代碼
*能夠在文檔頭部經過preload預先加載,提升資源加載速度。
到此爲止咱們已經實現了靜態資源打包生成文件hashcode,node加載hashcode清單文件輸出頁面加載腳本了。
ps: 使用Service Worker技術的話極力推薦google的workbox框架:developers.google.com/web/tools/w… 能夠更方便、更簡單的解決Service Worker絕大多數問題。
預緩存代碼:
sw.js:
self.workbox.precaching.precacheAndRoute([
'/common.offline.js',
'/common.offline.css'
]);
複製代碼
按照以前的構建方式這麼寫沒問題,可是現有構建模式中文件名已經和文件hashcode綁定了,這裏的文件名應該是帶有hashcode的文件地址。咱們也能夠在sw.js中讀取manifest.json文件來加載實際的文件地址,但這樣顯然不合適。
幸虧workbox提供了webpack的workbox-webpack-plugin插件,能夠經過其中的InjectManifest插件聲明須要注入的chunks,生成一份precache-manifest清單,最後經過importScripts導入到現有的sw.js文件中:
webpack配置:
const {InjectManifest} = require('workbox-webpack-plugin');
const suffix = isDev ? 'dev' : 'prod';
new InjectManifest({
importWorkboxFrom: 'disabled',
swSrc: path.join(__dirname, 'path/sw.js'),
swDest: isDev ? 'sw.js' : path.join(__dirname, 'dist/sw.js'),
chunks: ['page.common.offline'],
importScripts: [
'https://cdn.xxx.cn/workbox/workbox-sw.js',
`https://cdn.xxx.cn/workbox/workbox-core.${suffix}.js`,
`https://cdn.xxx.cn/workbox/workbox-precaching.${suffix}.js`,
`https://cdn.xxx.cn/workbox/workbox-routing.${suffix}.js`,
`https://cdn.xxx.cn/workbox/workbox-cache-expiration.${suffix}.js`]
})
複製代碼
生成的precache-manifest.js文件:
self.__precacheManifest = [
{
"revision": "52d9fa25e9a080052ab2",
"url": "//cdn.xxx.cn/js/page.common.offline.52d9fa2.js"
},
{
"revision": "52d9fa25e9a080052ab2",
"url": "//cdn.xxx.cn/css/page.common.offline.241a79d.css"
}
];
複製代碼
sw.js文件中只須要一句:
self.workbox.precaching.precacheAndRoute(self.__precacheManifest);
複製代碼
最後編譯的結果:
importScripts("https://cdn.xxx.cn/workbox/workbox-sw.js", "https://cdn.xxx.cn/workbox/workbox-core.prod.js", "https://cdn.xxx.cn/workbox/workbox-precaching.prod.js", "https://cdn.xxx.cn/workbox/workbox-routing.prod.js", "https://cdn.xxx.cn/workbox/workbox-strategies.prod.js", "https://cdn.xxx.cn/workbox/workbox-cache-expiration.prod.js", "https://cdn.xxx.cn/workbox/workbox-cacheable-response.prod.js", "//cdn.xxx.cn/precache-manifest.6f42fce0d1707a193aaa90b5f613205f.js");
self.workbox.precaching.precacheAndRoute(self.__precacheManifest);
/* some codes ... */
複製代碼
站點資源的強緩存策略 All done!
下圖能夠大概說明目前的靜態資源架構: