演示仓库地址(可以翻 commit 记录):https://github.com/wangpeng1994/webpack-demo
- 自动清理构建目录产物
- PostCSS插件autoprefixer自动补齐CSS3前缀
- 移动端CSS px自动转换成rem
- 静态资源内联
- 多页面应用打包通用方案
- 使用sourcemap
- 提取页面公共资源
1. 自动清理构建目录产物
每次构建前不清理输出目录,会造成文件越来越多。
package.json 里的构建脚本中这样写:
rm -rf ./dist && webpack
更优雅的方式是使用 clean-webpack-plugin 插件,默认会删除 output 指定的输出目录:
// ...
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
//...
plugins: [
new CleanWebpackPlugin()
]
// ...
2. PostCSS插件autoprefixer自动补齐CSS3前缀
由于每个浏览器的标准化程度不同,使用新的 CSS 属性时为了兼容可能需要添加前缀。
image.png.box {
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
-o-border-radius: 10px;
border-radius: 10px;
}
loader 中增加 postcss-loader 并配置 autoprefixer(我的版本是"autoprefixer": "^9.7.3"),至少要在css-loader 将 css 转变为 commonjs 模块之前使用 postcss-loader,推荐放在紧挨着 css-loader 的位置(注意 use 数组中的 loader 是从尾向首的顺序执行):
// ...
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
},
'less-loader',
]
},
// ...
按照现版本的 autoprefixer 的建议,在 package.json 中增加 browserslist
字段:
"browserslist": [
"last 2 version",
">1%",
"ios 7"
],
编译前:
.search-text{
display:flex;
}
编译后可以看到已经成功加了前缀:
.search-text{display:-webkit-box;display:flex;}
3. 移动端CSS px自动转换成rem
使用 px2rem-loader 纯粹的做单位转换,使用 lib-flexible 库在页面加载时自动计算合理的根元素 font-size 大小(除了 rem 现在还有 vw 方案)。
// ...
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
},
{
loader: 'px2rem-loader',
options: {
remUnit: 75, // 1rem = 75px
remPrecesion: 8 // 转为rem后,小数位个数
}
},
'less-loader'
]
},
// ...
.search-text{display:-webkit-box;display:flex;font-size:.266667rem;color:#00f}
4. 静态资源内联
资源内联的意义例举:
代码层面:保证重要的东西能随着 html 的加载一同完成,如页面框架的初始化脚本、数据埋点
工程维护:内联多页面时公共的 html 片段,小图片小字体内联
页面加载性能: 小图片小字体内联,减小 HTTP 网络请求数
页面加载体验: css 内联避免页面闪烁
css-loader 的作用是将 css 转换成 commonjs 对象,也就是样式代码会被放到 js 里面去了。style-loader 是代码运行时才动态的创建 style 标签并插入样式,而这里所说的静态资源内联是指在构建阶段完成的。为了方便维护,在开发时还是把文件独立出来编写,然后由 webpack 自动完成内联任务。
4.1 内联 HTML 和 JS
使用 raw-loader 读取文件字符串然后插入到 html 模板中(html-webpack-plugin 默认就是 ejs 模板引擎)。
斗鱼那里抄了些 meta.html:
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta name="renderer" content="webkit">
<meta name="keywords"
content="游戏直播,电竞直播,手游直播,lol直播,lpl直播,kpl直播,英雄联盟直播,dota2直播,dnf直播,炉石传说直播,cf直播,绝地求生直播,王者荣耀直播,游戏直播,赛事直播,美女直播,户外直播,二次元直播,视频直播,单机游戏直播">
<meta name="description"
content="斗鱼 - 每个人的直播平台提供高清、快捷、流畅的视频直播和游戏赛事直播服务,包含英雄联盟lol直播、穿越火线cf直播、dota2直播、美女直播等各类热门游戏赛事直播和各种名家大神游戏直播,内容丰富,推送及时,带给你不一样的视听体验,一切尽在斗鱼 - 每个人的直播平台。">
<link rel="canonical" href="https://www.douyu.com">
<link rel="dns-prefetch" href="//shark.douyucdn.cn">
<link rel="dns-prefetch" href="//apic.douyucdn.cn">
准备将 meta.html 内联到下面的 index.html 模板挖好的完形填空中,该 index.html 模板会作为 html-webpack-plugin 的 template:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 内联html -->
${require('raw-loader!./meta.html').default}
<!-- 内联js -->
<script>${require('raw-loader!../node_modules/lib-flexible/flexible.js').default}</script>
<title>Document</title>
</head>
<body>
</body>
</html>
webpack.prod.js:
// ...
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(__dirname, 'src/index.html'),
chunks: ['index'], // HTML 中只引入感兴趣的 chunk
minify: {
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true, // 压缩源 html 文件中的内联 css
minifyJS: true // 压缩源 html 文件中的内联 js
}
})
// ...
结果就是在 html-webpack-plugin 处理 html 模板的过程中,借助 raw-loader 把相关文件的字符串复制到了目标模板中,一起配合把坑填上了:
image.png
4.2 内联 CSS
CSS 内联的思路:
- 首先使用 mini-css-extract-plugin(而非 style-loader)将 css 提取打包成一个独立的 css chunk 文件
- 然后使用 html-webpack-plugin 生成 html 页面
- 最后使用 html-inline-css-webpack-plugin 读取打包好的 css chunk 内容注入到页面,原本 html-webpack-plugin 只会引入 css 资源地址,现在实现了 css 内联。
直接摘抄该插件的官方演示吧(用到的三个插件缺一不可):
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
}),
new HtmlWebpackPlugin(),
new HTMLInlineCSSWebpackPlugin(),
],
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
}
]
}
}
4.3 内联图片、字体
第一章中介绍过了,最简单的做法就是使用 url-loader,让小于指定大小的图片或者字体文件在构建阶段自动 base64:
// ...
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240, // 小于 10KB 会转为 base64 URIs,否则内部会自动调用 file-loader
name: '[name]_[hash:8].[ext]'
}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
name: '[name]_[hash:8].[ext]'
}
}
]
}
// ...
再重复一遍,这里所说的静态资源内联是在构建阶段就完成了的。
静态资源内联参考目录:
5. 多页面应用打包通用方案
每一次页面跳转,后端都会返回新的 html 文档,此为多页网站,亦为多页应用(MPA)。
优势:页面之间解耦,对 seo 友好。
思路:每个页面对应一个 entry,一个 html-webpack-plugin。
为了避免每次新增或删除页面需要修改 webpack 配置。因此需要动态获取 entry 和设置对应的 html-webpack-plugin。
做法很简单,使用 glob 库,通过 shell 风格的通配符去自动匹配文件路径。
主要是动态生成 entry 入口和 html-webpack-plugin 配置即可,这里约定每个页面都在 src 下面对应的文件夹中,每个文件夹的 html 模板都叫做 index.html,对应的入口 js 都叫做 index.js。
以下提供关键代码:
const glob = require('glob');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const setMPA = () => {
const entry = {};
const HtmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js')); // 返回路径字符串数组
entryFiles.forEach((pagePath, index) => {
const pageName = pagePath.match(/\/([a-z\_\$]+)\/index.js$/)[1];
entry[pageName] = pagePath;
HtmlWebpackPlugins.push(
new HtmlWebpackPlugin({
filename: `${pageName}.html`,
template: path.join(__dirname, `src/${pageName}/index.html`),
chunks: [pageName],
minify: {
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true
}
})
);
});
return {
entry,
HtmlWebpackPlugins
};
}
const { entry, HtmlWebpackPlugins } = setMPA();
module.exports = {
// entry: {
// index: './src/index/index.js',
// search: './src/search/index.js'
// }
entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js',
},
// ...
plugins: [
// ... other plugins
...HtmlWebpackPlugins,
// ... other plugins
]
};
image.png
6. 使用sourcemap
作用:通过 sourcemap 定位到源代码用于排查 bug
开发环境开启,线上环境关闭,线上排查问题的时候可以将 sourcemap 上传到错误监控系统(如 Sentry)。
module.exports = {
// ...
devtool: 'source-map'
};
选项还挺多的,简单起见,开发时使用 source-map 这个即可,等遇到问题、瓶颈或知道哪里想要优化了,再回头研究其它选项也不迟。
命名规则:
- eval:使用 eval 包裹模块代码
- source map:产生 .map 文件
- cheap:不包含列信息
- inline:将 .map 作为 DataURl 嵌入,不单独生成 .map 文件
- module:包含 loader 的 sourcemap
eval
打包出的 js 文件末尾,用 sourceURL
告知了对应的源文件路径:
// ...
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _helloworld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);\n\ndocument.write(Object(_helloworld__WEBPACK_IMPORTED_MODULE_0__[\"helloworld\"])());\nvar arr = [1, 2, 3];\nvar one = arr[0],\n two = arr[1];\n\nvar foo = function foo() {\n return console.log(one, two);\n};\n\nfoo();\nconsole.log(555);\n\n//# sourceURL=webpack:///./src/index/index.js?");
source-map
会有独立的 map 文件,在打包出的 js 文件最后一行可以看到对应的 map 文件路径:
// ...
//# sourceMappingURL=index_a5364e21.js.map
而在 map 文件中也能看到对应的 源 js:
{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/index/index.js","webpack:///./src/index/helloworld.js"], /*后面略*/}
inline-source-map
刚才的 sourcemap 内容内联到打包出的 js 文件中的最后一行,并且转为了 DataURL(base64):
// ...
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi
7. 提取页面公共资源
这个配置项有些复杂,结合具体需求时再深究吧。继续前进,不能耽误整体学习进度。
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
vendor: {
test: /(react|react-dom)/, // 建议仅包括核心框架和实用程序
name: 'vendors', // 打包文件名
chunks: 'all' // 所有引入的库进行分离(推荐)
},
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2 // 只要引用两次就打包为一个文件
}
}
}
image.png
网友评论