Table of Contents
- 构建工具
- 初识 webpack
- webpack 使用
- webpack 进阶
- webpack 性能分析和优化
- 深入 webpack
- 实战
构建工具
为什么需要构建工具?
- 转换 ES6/ES7 、TypeScript 语法
- 转换 JSX (Angular、React、Vue 模板语法)
- CSS 前缀补全/预处理器
- 压缩混淆
- 图片压缩
前端构建工具的演变历史
主流的有:
ant+YUI Tool——>grunt——> fis3/gulp——>rollup/webpack/parcel
为什么选择 webpack ?
- 社区生态丰富
- 配置灵活和插件化扩展
- 官方更新迭代速度快
初识 webpack
配置文件
基本的包括 entry、output、mode、module、plugins 几种
- 当 entry 为多入口时,output 可以通过占位符的方式支持多文件名称的区分
const path = require('path');
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js',
},
mode: 'production',
};
Loaders
Loaders 是 webpack 的核心概念。webpack 开箱即用只支持 JS 和 JSON 两种文件类型,通过 Loaders 去支持其它文件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。
Loaders 本身是一个函数,接受源文件作为参数,返回转换的结果。
常见的 Loaders
列举部分,更多见 loaders
babel-loader
转换 ES6\ES7 等 JS 新特性语法css-loader
支持.css 文件的加载和解析less-loader
将 less 文件转换成 cssts-loader
将 TS 转换成 JSfile-loader
进行图片、文字等的打包raw-loader
将文件以字符串的形式导入thread-loader
多进程打包 JS 和 CSS
Loaders 的用法
module: {
rules: [
// test 指定匹配规则,use 指定使用的 loader 名称
{ test: /\.txt$/, use: 'raw-loader' },
];
}
Plugins
Plugins
用于 bundle
文件的优化,资源管理和环境变量注入,作用域整个构建过程。
常见的 Plugins
列举部分,更多见 plugins
CommonsChunkPlugin
将 chunks 相同的模块代码提取成公共 jsCleanWebpackPlugin
清理构建目录ExtractTextWebpackPlugin
将 CSS 从 bundle 文件中提取成一个独立的 CSS 文件CopyWebpackPlugin
将文件或者文件夹拷贝到构建的输出目录HTMLWebpackPlugin
创建 html 文件去承输出的 bundleUglifyjsWebpackPlugin
压缩 JSZipWebpackPlugin
将打包出的资源生成一个 zip 包
Plugins 的使用
在配置文件中的 plugins 数组里将定义好的插件放入即可。
Mode
Mode 用来指定当前的构建环境是: production
、development
还是 none
, 对应 node.js 中的 process.env.NODE_ENV
。
设置 mode 可以使用 webpack 内置的函数,默认设置为 production
详细见:https://webpack.js.org/configuration/mode/
webpack 使用
练习使用
- 解析 ES6 和 React JSX
- 解析 CSS、Less 和 Sass
- 解析图片和字体、小图片直接 base64 打包
webpack 中文件监听
1、 --watch
监听文件改动,构建命令改为 webpack --watch
缺点:构建后需要手动刷新浏览器页面才能看到效果
2、文件监听的原理分析
轮询判断文件的最后编辑时间是否变化,某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout
module.exports = {
// 默认 false, 也就是不开启
watch: true,
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后等300ms再去执行,默认 300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停的询问系统指定文件有没有变化实现的,默认每秒问1000次
poll: 1000,
},
};
热更新 webpack-dev-server
- WDS 不刷新浏览器
- WDS 不输出文件(非 IO),而是放在内存中,速度有优势
- 使用
HotModuleReplacementPlugin
插件配合
// webpack.config.js 主要内容
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
contentBase: './dist',
hot: true,
}
// package.json script 脚本的配置
"dev": "webpack-dev-server --open"
热更新 webpack-dev-middleware
- WDM 将 webpack 输出的文件传给服务器
- 适用于灵活的定制场景
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
app.listen(3000, function() {
console.log('Example app listening on port 3000!\n');
});
热更新的原理分析
- Webpack Compile:将 JS 编译成 Bundle
- HMR Server:将热更新文件输出给 HMR Rumtime
- Bundle server: 提供文件在浏览器的访问
- HMR Runtime:会被注入到浏览器,更新文件的变化 (websocket 建立连接)
- bundle.js: 构建输出的文件
概述过程:
-
(启动阶段 1、2、A、B)源代码文件经过 Webpack Compiler 编译后,生成 bundle,然后 Bundle Server 提供 bundle 文件在浏览器中访问;
-
(热更新阶段 1、2、3、4)文件变化后,还是经过 Webpack Complier 进行编译,然后将变化的代码传给 HMR Server,HMR Server(服务端) 和 HMR Runtime(客户端) 一直通信,当有代码更新(通知过了 HMR Runtime 后),HMR Server 会将更新的代码模块以 JSON 的形式发送给 HMR Runtime,然后 HMR Runtime 进行一个代码的更新,而不需要刷新浏览器页面。
文件指纹策略
文件指纹的生成方式
- Hash : 和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
- Chunkhash :和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值
- Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变
文件指纹 webpack 配置
css 文件一般用 contenthash 来实现,js 则使用 chunkhash, 图片资源用 hash。相关配置和输出结果如下:
'use strict';
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 提取css 为文件(和 style-loader互斥)
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js',
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js',
},
mode: 'production',
module: {
rules: [
{
test: /.js$/,
use: 'babel-loader',
},
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /.less$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
},
{
test: /.png|.jpg|.jpeg|.gif$/, // /.(png|jpg|gif|jpeg)$/
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash:8][ext]',
},
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css',
}),
],
};
Built at: 03/25/2020 6:30:13 PM
Asset Size Chunks Chunk Names
index_443df551.js 1.01 KiB 0 [emitted] [immutable] index
search_650a0e60.js 129 KiB 1 [emitted] [immutable] search
search_6dd0097b.css 57 bytes 1 [emitted] [immutable] search
webpack_365b2ee8png 253 KiB [emitted] [big]
代码压缩
JS 文件压缩
内置插件 uglifyjs-webpack-plugin
CSS 文件压缩
使用 optimize-css-assets-webpack-plugin
,同时使用 cssnano
预处理器处理 CSS
plugins: [
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
}),
];
html 文件压缩
修改 htm-webpack-plugin
,设置压缩参数
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/search.html'),
filename: 'search.html',
chunks: ['search'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false,
},
}),
];
webpack 进阶
PostCSS 插件 autoprefixer 自动补齐 CSS3 前缀
浏览器内核和对应 CSS3 属性前缀:
- Trident(-ms)
- Geko(-moz)
- Webkit(-webkit)
- Presto(-o)
使用 autoprefixer
插件(配合 prostcss-loader 使用),根据 Can I Use 规则 (https://caniuse.com)
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{ // +++ 新增内容
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')({
// 浏览器支持的最新的两个版本、使用人数>1%、IOS 版本兼容到8
overrideBrowserslist: ['last 2 version', '>1%', 'ios 8'],
})
]
},
},
],
},
移动端 CSS px 自动转成 rem
-
使用
px2rem-loader
-
页面渲染时计算根元素的
font-size
值- 可以使用手淘的
lib-flexible
库 - https://github.com/amfe/lib-flexible
- 可以使用手淘的
-
viewpoint 得到更多的浏览器支持后,可以使用 viewpoint 代替 lib-flexible , px 到 vw 的转换插件
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
{
// +++
loader: 'px2rem-loader',
options: {
remUnit: 75, // 1rem 代表多少 px
remPrecision: 8, // 小数点保留位数
},
},
],
},
],
},
};
还需要将 lib-flexible
内联到 html head 头部,为了页面渲染的时候提早计算到根元素的 font-size 的大小。
静态资源的内联
资源内联的意义
代码层面:
- 页面框架的初始化脚本
- 上报相关打点
- css 内联避免页面闪动
请求层面:减少 HTTP 网络请求数
- 小图片或者字体内联 (url-loader)
HTML 和 JS 内联
raw-loader
内联 html
${require('raw-loader!babel-loader!./meta.html')}
raw-loader
内联 JS
<script>
${require('raw-loader!babel-loader!./node_modules/lib-flexible/flexible.js')}
</script>
CSS 内联
- 方案一:借助 style-loader
- 方案二:html-inline-css-webpack-plugin
多页面应用(MPA)打包通用方案
基本实现思路:每个页面对应一个 entry,一个 html-webpack-plugin。缺点:每次新增或删除页面都需要改 webpack 配置。
解决方案:动态获取 entry 和设置 html-webpack-plugin 数量,利用 glob.sync
entry: glob.sync(path.join(__dirname,'./src/*/index.js')),
source map
source map 类型关键词:
- eval: 使用 eval 包裹模块代码
- source map: 产生 .map 文件
- cheap: 不包含列信息
- inline: 将 .map 作为 DataURI 嵌入,不单独生产 .map 文件
- module: 包含 loader 的 sourcemap
Tree-shaking
DCE (Elimination)
tree-shaking 利用了 DCE 的特点做优化:
- 代码不会被执行,不可到达,比如
if(false){// 这里边的代码}
- 代码执行的结果不会被用到
- 代码只会影响死变量(只写不读)想
- 方法不能有副作用
Tree-shaking 的原理
- 利用 ES6 模块的特点:
- 只能作为模块顶层的语句出现
- import 的模块名只能是字符串常量
- import binding 是 immutable 的
- 代码擦除:uglify 阶段删除无用代码
ScopeHoisting 的使用和原理分析
https://segmentfault.com/a/1190000012600832
1、现象:webpack 构建后的带存在大量的闭包代码
eg:
假如现在有两个文件分别是 util.js:
export default 'Hello,Webpack';
和入口文件 main.js:
import str from './util.js';
console.log(str);
以上源码用 Webpack 打包后输出中的部分代码如下:
// 模块初始化函数,为了兼容各种浏览器
[
function(module, __webpack_exports__, __webpack_require__) {
var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1);
console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__['a']);
},
function(module, __webpack_exports__, __webpack_require__) {
__webpack_exports__['a'] = 'Hello,Webpack';
},
];
2、会导致什么问题?
- 大量函数闭包包裹代码,导致体积增大(模块越多越明显)
- 运行代码时创建的函数作用域变多,内存开销变大
3、结论
- 被 webpack 转换后的模块会加上一层包裹
import
会被转换成__webpack_require
4、进一步分析 webpack 的模块机制
分析:
- 打包出来的是一个 IIFE (匿名闭包)
- modules 是一个数组,每一项是一个模块初始化函数
- __webpack_require 用来加载模块,返回 module.exports
- 通过 WEBPACK_REQUIRE_METHOD(0) 启动程序
scope hoisting 原理
原理:将所有的模块的代码按照引用顺序放到一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突 对比:通过 scope hoisting 可以减少函数声明代码和内存开销
// 在开启 Scope Hoisting 后,同样的源码输出的部分代码如下
[
function(module, __webpack_exports__, __webpack_require__) {
var util = 'Hello,Webpack';
console.log(util);
},
];
使用
webpack v4 在将 mode 设置为 production
会默认开启 scope hoisting ,且必须是 ES6 语法,CJS 不支持(这点和 tree-shaking 一样)。
体积优化策略
- Tree-shaking
- ScopeHoisting
- 公共资源分离
- 图片压缩
- 动态 Polyfill
下边介绍后边三者
公共资源分离
图片压缩
要求:基于 Node 库的 imagemin 或者 tinypng API
使用:配置 image-webpack-loader
imagemin 的优点
- 有很多定制选项
- 可以引入更多第三方优化插件,列如 pngquant
- 支持处理多种图片格式
imagemin 的压缩原理
- pngquant:是一款 PNG 压缩器,通过图像转换为具有 alpha 通道(通常比 24/32 位 PNG 文件小 60%-80%)的更高效的 8 位 PNG 格式,可显著减小文件大小。
- pngcrush: 其主要目的是通过尝试不同的压缩级别和 PNG 过滤方法来降低 PNG IDAT 数据流的大小。
- optipng:其设计灵感来源于 pngcrush. optipng 可将图像文件重新压缩为更小的尺寸,而不会丢失任何信息。
- tinypng: 也是将 24 位 png 文件转化为更小有索引的 8 位图片,同时所有非必要的 metadata 也会被剥离掉。
动态 Polyfill
背景:babel-polyfill 打包后体积 88.49k。
构建体积优化:动态 Polyfill。如 polyfill.io
polyfill service 原理:识别 User Agent,下发不同的 Polyfill,不加载多余的已支持的特性
分割代码和动态 import
webpack 性能分析和优化
初级分析:使用 webpack 内置的 stats
速度分析:使用 speed-measure-webpack-plugin
体积分析:使用 webpack-bundle-analyszer
多进程/多实例构建
- thread-loader (官方)
- HappyPack
- parallet-webpack
多进程/多实例并行压缩
推荐使用 terser-webpack-plugin
,设置参数 parallel
进一步分包:预编译资源模块
分包:设置 Externals
思路:将 react、react-dom 基础包通过 cdn 引入,不大于 bundle 中
方法:使用 html-webpack-externals-plugin
问题:业务包多的时候,这种方式会在页面中引入很多 script 标签
进一步分包:预编译资源模块
思路:将 react、react-dom、redux、react-redux 基础包和业务基础包打包成一个文件
方法:使用 DLLPlugin
进行分包,DllReferencePlugin
对 manifest.json
引用
// webpack.dll.js
const webpack = require('webpack');
module.exports = {
context: process.cwd(),
// resolve: {
// extensions: ['.js', ''],
// },
entry: {
libaray: ['react', 'react-dom'],
},
output: {
filename: '[name]_[chunkhash].dll.js',
path: path.join(__dirname, 'build/library'),
library: '[name]',
},
plugins: [
new webpack.DllPlugin({
name: '[name]_[chunkhash]',
path: path.join(__dirname, 'build/library/[name].json'),
}),
],
};
然后 webpack.prod.js
中添加 DllReferencePlugin 插件,指定 manifest json
new webpack.DllReferencePlugin({
manifest: require('./build/library/library.json'),
});
充分利用缓存提升二次构建速度
缓存思路:
- babel-loader 开启缓存(后边加
?cacheDirectory=true
) - terser-webpack-plugin 开启缓存 (属性
cache:true
) - 使用 cache-loader 或者 hard-source-webpack-plugin
缩小构建目标
目的:尽可能的少构建模块,比如 babel-loader
不解析 node_module
。
减少文件搜索范围
- 优化 resolve.modules 配置(减少模块搜索层级)
- 优化 resolve.mainFields 配置
- 优化 resolve.extensions 配置
- 合理使用 alias
深入 webpack
webpack 启动过程分析
webpack-cli 源码阅读
实战
商城技术栈选型和整体架构
商城界面 UI 设计与模块拆分
demo代码:https://github.com/giscafer/webpack-study
内容为极客时间《玩转Webpack》学习笔记