Webpack
构建的基于zepto
的多页应用脚手架,本文聊聊本次项目中Webpack
构建多页应用的一些心得体会。
1.前言
由于公司旧版的脚手架是基于Gulp
构建的zepto
多页应用(有兴趣可以看看web-mobile-cli),有着不少的痛点。例如:
- 需要兼容低版本浏览器,只能采用
promise
,不能使用await
、generator
等。(因为babel-runtime
需要模块化); - 浏览器缓存不友好(只能全缓存而不是使用资源文件的后缀哈希值来达到局部缓存的效果);
- 项目的结构不友好(可以更好的结构化);
- 开发环境下的构建速度(内存);
Gulp
插件相对Webpack
少且久远,维护成本高等等。
这次升级有几个地方需要注意和改进:
- 项目旧代码尽量做到无缝转移;
- 资源文件的缓存;
- 组件式的组织目录结构。
Github仓库:
2.多页
Webpack
的多页应用通过多入口entry
和多实例html-webpack-plugin
配合来构建,html-webpack-plugin
的chunk
属性传入对应entry
的key
就可以做到关联,例如:
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
},
plugins: [
new HtmlWebpackPlugin({
filename: `pageOne.html`,
template: `./src/pageOne.html`,
chunks: ['pageOne']
}),
new HtmlWebpackPlugin({
filename: `pageTwo.html`,
template: `./src/pageTwo.html`,
chunks: ['pageTwo']
}),
new HtmlWebpackPlugin({
filename: `pageTwo.html`,
template: `./src/pageTwo.html`,
chunks: ['pageTwo']
})
]
}
那么问题来了,开发新的页面每次都得添加岂不是很麻烦。这里推荐神器glob根据正则规则匹配。
const glob = require('glob')
module.exports = {
entry: glob.sync('./src/js/*.js').reduce((pre, filepath) => {
const tempList = filepath.split('src/')[1].split(/js\//)
const filename = `${tempList[0]}${tempList[1].replace(/\.js/g, '')}`
return Object.assign(pre, {[filename]: filepath})
}, {}),
plugins: [
...glob.sync('./src/html/*.ejs').map((filepath, i) => {
const tempList = filepath.split('src/')[1].split(/html\//)
const fileName = tempList[1].split('.')[0].split(/[\/|\/\/|\\|\\\\]/g).pop()
const fileChunk = `${tempList[0]}${fileName}`
return new HtmlWebpackPlugin({
filename: `${fileChunk}.html`,
template: filepath,
chunks: [fileChunk]
})
})
]
}
3.模板
项目没有直接使用html
,而是使用了ejs
作为模板,这里有至少两个好处:
- 把公共的代码抽离出来;
- 传入公共的变量。
// header.ejs
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= title %></title>
</head>
// index.ejs
<!DOCTYPE html>
<html lang="en">
<% include ./header.ejs %>
<body>
<!-- page -->
</body>
<script src="<%= publicPath %>lib/zepto.js"></script>
</html>
<% include ./header.ejs %>
就是引用了header.ejs
文件,<%= title %>
和<%= publicPath %>
是我在配置文件定义的两个变量,publicPath
是为了统一cdn
缓存服务器的域名,非常有用。
4.垫片
项目中使用了zepto
,所以需要垫片,所谓垫片就是shim 预置依赖,即全局依赖。
webpack compiler 能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些 third party(第三方库) 可能会引用一些全局依赖(例如 jQuery 中的 $)。因此这些 library 也可能会创建一些需要导出的全局变量。这些 "broken modules(不符合规范的模块)" 就是 shim(预置依赖) 发挥作用的地方。
垫片有两种方式:
- 传统方式的垫片就是在
html
文件中,所有引用的js
文件的最前面引用的文件(例如zepto
); Webpack
配置shim预置依赖
。
最终我选择了Webpack
配置shim预置依赖
这种方式,因为:
- 传统的方式需要每个页面都手动引入(虽说搭配
ejs
可以抽离出来成为公共模块,但还是需要每个页面手动引入公共模块); - 传统的方式需要多发一次请求去请求垫片;
Webpack
可以把所有第三方插件的代码都拆分打包成为一个独立的chunk
,只需一个请求。
module.exports = {
entry: {...},
module: {
rules: [
{
test: require.resolve('zepto'),
use: 'imports-loader?this=>window'
}
]
},
plugins: [
new webpack.ProvidePlugin({$: 'zepto'})
]
}
5.拆分
一般来讲Webpack
的配置entry
中每个key
就对应输出一个chunk
,那么该项目中会提取这几类chunk
:
- 页面入口(
entry
)对应的chunk
; common
:多次引用的公共文件;vender
:第三方依赖;manifest
:Webpack
运行时(runtime
)代码,它存储着Webpack
对module
和chunk
的信息。
module.exports = {
entry: {...},
module: {...},
plugins: [],
optimization: {
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
name: 'vendors',
filename: 'js/vendors.[contenthash:8].js',
priority: 2,
reuseExistingChunk: true
},
common: {
test: /\.m?js$/,
chunks: 'all',
name: 'common',
filename: 'js/common.[contenthash:8].js',
minSize: 0,
minChunks: 2,
priority: 1,
reuseExistingChunk: true
}
}
}
}
}
这里注意的有两点:
- 优先顺序:第三方插件的
priority
比common
代码的priority
大; - 提取
common
代码:minChunks
为引用次数,我设置为引用2次即提取为公共代码。minSize
为最小字节,设置为0。
6.缓存
缓存的目的是为了提高加载速度,Webpack
在缓存方面已经是老生常谈的了,每个文件赋予唯一的hash值,只有更新过的文件,hash值才改变,以达到整体项目最少文件改动。
6.1 hash值
Webpack
中有三种hash
值:
hash
:全部文件同一hash
,一旦某个文件改变,全部文件的hash都将改变(同一hash
不满足需求);chunkhash
:根据不同的入口文件(Entry
)进行依赖文件解析、构建对应的chunk,生成对应的哈希值(问题是css
作为模块import
到JavaScript
文件中的,它们的chunkhash
是一致的,一旦改变js
文件,即使import
的css
文件内容没有改变,其chunkhash
值也会一同改变,不满足需求);contexthash
:只有模块的内容变了,那么hash值才改变(采用)。
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
},
output: {
path: 'src',
chunkFilename: 'j[name].[contenthash:8].js',
filename: '[name].[contenthash:8].js'
},
plugins: [
new HtmlWebpackPlugin({
filename: `pageOne.html`,
template: `./src/pageOne.html`,
chunks: ['pageOne']
}),
new HtmlWebpackPlugin({
filename: `pageTwo.html`,
template: `./src/pageTwo.html`,
chunks: ['pageTwo']
}),
new HtmlWebpackPlugin({
filename: `pageTwo.html`,
template: `./src/pageTwo.html`,
chunks: ['pageTwo']
})
]
}
6.2 module id
仅仅使用contexthash
还不足够,每当import
的资源文件顺序改变时,chunk
依然会改变,目的没有达成。要解决这个问题首先要理解module
和chunk
分别是什么,简单理解:
module
:一个import
对应一个module
(例如:import zepto from 'zepto'
中的zepto
就是一个module
);chunk
:根据配置文件打包出来的包,就是chunk
。(例如多页应用中每个entry
的key
值对应的文件)。
因为Webpack
内部维护了一个自增的id
,依照顺序赋予给每个module
,每当新增或者删减导致module
的顺序改变时,受影响的chunk
的hash
值也会改变。解决办法就是使用唯一的hash
值替代自增的id
。
module.exports = {
entry: {...},
module: {...},
plugins: [],
optimization: {
moduleIds: 'hashed'
}
}
7.优化
优化的目的是提高执行和打包的速度。
7.1 查找路径
告诉Webpack
解析模块时应该搜索的目录,缩小编译范围,减少不必要的编译工作。
const {resolve} = require('path')
module.exports = {
entry: {...},
module: {...},
plugins: [],
optimization: {...},
resolve: {
alias: {
'@': resolve(__dirname, '../src'),
},
modules: [
resolve('src'),
resolve('node_modules'),
]
}
}
7.2 指定目录
指定loader
的include
目录,作用是缩小编译范围。
const {resolve} = require('path')
module.exports = {
entry: {...},
module: {
rules: [
{
test: /\.css$/,
include: [
resolve("src"),
],
use: ['style-loader', 'css-loader']
}
]
},
plugins: [],
optimization: {...},
resolve: {...}
}
7.3 babel
缓存目录
babel-loader
开始缓存目录cacheDirectory
。
const {resolve} = require('path')
module.exports = {
entry: {...},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
include: [
resolve("src"),
],
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime']
}
}
}
]
},
plugins: [],
optimization: {...},
resolve: {...}
}
7.4 插件TerserJSPlugin
TerserJSPlugin
插件的作用是压缩JavaScript
,优化的地方是开启缓存目录和开启多线程。
const {resolve} = require('path')
module.exports = {
entry: {...},
module: {...},
plugins: [],
optimization: {
minimizer: [
new TerserJSPlugin({
parallel: true,
cache: true,
})
]
},
resolve: {...}
}
8.总结
通过这次学习Webpack
到升级脚手架,对前端工程化有了进一步的了解,也感受到了Webpack4
带来的开箱即用,挺方便的。
参考文章:
Webpack官方文档
【实战】webpack4 + ejs + express 带你撸一个多页应用项目架构
基于 webpack 的持久化缓存方案