Since frameworks like Vue, React, or Angular became popular, the number of single-page apps has grown. However, limited to the disadvantages of single-page apps, such as SEO and first screen time, many apps still maintain a multi-page structure. This article describes how to use WebPack to generate hashcode file name based on the multi-page application structure to implement the incremental update scheme of static resources.
The structure of multi-page applications usually loads some common resources and JS and CSS of the current page when users visit. Some applications may still use traditional ones:
https://url/ / version number/XXX. [js | CSS]
or
https://url/xxx.js?r=xxx
To ensure that the client can get the latest resource files in time when applying updates. However, in the current popular front-end architecture, when publishing single-page applications, hashcode value of files can be added into the generated resource file name during compilation to ensure that each resource has its own independent “version number”. The client loads resource files with hashcode file names. When a resource file is updated, the names of other resource files are not affected. In this way, the strong cache policy of the client can be effectively used to increase the cache hit ratio of resource files.
Here is an example of how static file names can be added to hashCode and referenced on the server in a multi-page architecture:
1.Webpack compiles the build file to append hashCode
webpack.conf.js
{
entry: './app.js'.output: {
filename: 'js/[name].[chunkhash:7].js'.chunkFilename: 'js/[name].[chunkhash:7].js',}}Copy the code
Configuration complete!
If the CSS in your application also needs hashCode, you will need to configure it in the mini-CSs-extract-plugin:
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'}})]Copy the code
In output, define the naming rules for filename and chunkFilename. Filename refers to the output naming rule of the entry entry file in the configuration item, and chunkFilename refers to the naming rule of the chunk output file in the code, for example: Require. ensure or import Specifies the name of the imported asynchronous package
You can see that both chunkhash and contenthash are used in the configuration. What is the difference?
“Chunkhash” means that webpack generates hashcode files based on chunk contents without changing the hash value when packaging chunk contents. CSS is imported through THE JS module, so CSS theoretically belongs to the content of THE JS, so the JS hash will change when the CONTENT of the CSS changes, but we can use contenthash to keep the CSS hash unchanged when the JS file changes.
2. Generate the Manifest static resource file list
After the hash file is packaged, we need a mapping between the original file name and the hash file name.
Next we need to generate a manifest for the compiled N multi-hash file. The webpack-manifest-plugin can do this, see 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'}})]Copy the code
Json file. FileName can define the name of the manifest file. You are advised to bind the fileName to the service version number.
Packaged and compiled:
Manifest.json with customization:
{
"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"}}}}Copy the code
3. The server references the Manifest file
After the manifest file is generated, the server needs to reference the manifest file and map the page JS to load the actual resource file with the hashCode name (so the manifest file needs to be distributed with the server application, different build environments have different implementations).
Our server-side application is the Nodejs Express framework with Handlebars as the template rendering engine. Here’s how we implement server-side reading.
The res.render function is called once after the business logic of each request is processed to select the template file and pass in the data needed to render the template. In addition to the rendering data required by the page, we will also pass the js and CSS file names that the current page needs to reference to the page (if you can determine the resource name of the page before entering the page logic, this will not be so complicated).
res.render('search/goods-list', {
module: 'product'.page: 'search-list'.data: {
pageData: {}, // Page data
pageName: 'product/search-list' // js and CSS names (page names)}});Copy the code
But there is a small problem that the page name is defined in each specific page business logic (it is only passed in when render is called), we want to add a middleware that reads the manifest file before the business logic, but the page name is not determined before the business logic. After the business logic, because res.render is called the subsequent middleware will not be executed, and finally it is not appropriate to call the read manifest file in the specific business logic. So rewrite the Render method of Express and emit the page parameters as an event before actually output the render content.
res.emit('beforeRender', {module, page, others});
Copy the code
In this way, the middleware before the actual business logic can register this event, get the page name and load the manifest file JSON through require, find the actual resource file address of the page mapping, finally merge the actual resource address into the rendering data, and finally load it in Handlebars. The following example is for reference only. The actual scenario is more complicated:
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();
};
Copy the code
Page Render Layout template:
layout.hbs
<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>
Copy the code
* You can use preload in the header of the document to increase the speed of resource loading.
So far we have implemented the static resource packaging generating file HashCode, and Node loads the hashCode manifest file output page to load the script.
4. Hashcode problem in ServiceWorker’s Precache resource file
Ps: Google workbox is highly recommended to use Service Worker technology:Developers.google.com/web/tools/w…Most Service Worker problems can be solved more conveniently and simply.
Precached code:
Sw. Js:
self.workbox.precaching.precacheAndRoute([
'/common.offline.js'.'/common.offline.css'
]);
Copy the code
This is fine with the previous build, but the file name is already bound to the file hashcode, where the file name should be the address of the file with hashCode. We could also read the manifest.json file in sw.js to load the actual file address, but that would obviously not be appropriate.
Workbox provides the Workbox-webpack-plugin for Webpack. InjectManifest allows you to declare chunks that need to be injected and generate a Precache manifest. Finally import to an existing sw.js file via importScripts:
Webpack configuration:
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`]})Copy the code
Precach-manifest.js file generated:
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"}];Copy the code
The sw.js file only needs one sentence:
self.workbox.precaching.precacheAndRoute(self.__precacheManifest);
Copy the code
The result of the final compilation:
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 ... * /
Copy the code
All done!
The following diagram provides an overview of the current static resource architecture: