写在前面
学习 webpack 建议先按照官网指南流程操作一遍,对于指南中不理解的问题建议再看完整个指南后再去研究。webpack 目前最新已经是 5.1.0,指南中有些方法已经过时,在内容最后我整理了操作时遇到的问题。
何为 webpack?
官网定义: 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
简单来说 webpack 就是一个前端资源打包工具,它根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应静态资源。下图完美阐释了这一点
为什么会需要使用它?
想象我们最开始学习 html 和 js 写过的 demo,在 html 中通过<script />
引入 js 文件,并且在各个 js 文件中互相引用,当时我们的目的是可以运行,这样做并没有什么问题。
当一个正式项目中,有几百上千个 js 文件时,想象一下,调用一个 js 方法,那么浏览器就要依次发送请求去请求这个 js 中所引用别的 js 文件中方法,其中一个因网络问题返回延迟会导致这个页面显示错误。这时候你会想,是不是我把所有 js 文件合成一个文件就好了呢?是的,webpack 就是帮我们做这件事的。webpack 依赖强大的 loader 和 plugins 为前端开发提供了更多的可能。
什么是 bundle?什么是 chunk?什么是 module?
bundle: 是由 webpack 打包出来的文件
chunk: 代码块,是指 webpack 在进行模块依赖分析的时候代码分割出来的代码块。主要体现在
CommonsChunkPlugin
插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunkmodule: 是开发中的单个模块,在 webpack 中,一个模块对应一个文件,webpack 会从配置的 entry 中递归开始找出所有依赖的模块
核心概念
- 入口(entry): 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。每个依赖项随即被处理,最后输出到 bundles 文件中。详细介绍
module.exports = {
entry: {
app: './src/index.js'
}
}
- 出口(output): 告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为
./dist
。详细介绍
module.exports = {
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
- 模式(mode): 通过选择
development
或production
之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化。详细介绍
module.exports = {
mode: "production",
}
- loader: loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。详细介绍
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' },
{ test: /\.ts$/, use: 'ts-loader' }
]
}
};
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, compilation => {
console.log("webpack 构建过程开始!");
});
}
}
常见 loader 和 plugins
loader
style-loader
: 通过注入<style>
标签将CSS
添加到DOM
css-loader
: 加载CSS
,解释@import
和url()
,建议将style-loader
与css-loader
结合使用file-loader
: 把文件输出到一个文件夹中,在代码中通过相对 url 去引用url-loader
: 和file-loader
类似,但是能在很小的情况下以base64的方式把文件内容注入到代码中slint-loader
: 使用 ESLint 清理代码
plugins
CleanwebpackPlugin
: 在每次构建前清理 /dist 文件夹HtmlwebpackPlugin
: 解决在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle而造成的引用名称没有变的问题CommonsChunkPlugin
: 通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存到缓存中供后续使用HotModuleReplacementPlugin
: 启用热替换模块DefinePlugin
: 允许创建一个在编译时可以配置的全局常量
loader 和 plugin 的不同?
作用
- loader: 加载器。让 webpack 具有加载和解析非 JavaScript 文件的能力。
- plugin: 插件。监听 webpack 的运行生命周期事件,在合适的时机通过 webpack 提供的 API 改变输出结果。
用法
- loader: 在 module.rules 中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
- plugin: 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。
webpack与 grunt、gulp 的不同?
grunt 和 gulp 是基于任务和流(Task、Stream)的。找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。
const { src, dest, parallel } = require('gulp');
const pug = require('gulp-pug');
const less = require('gulp-less');
const minifyCSS = require('gulp-csso');
const concat = require('gulp-concat');
function html() {
return src('client/templates/*.pug')
.pipe(pug())
.pipe(dest('build/html'))
}
function css() {
return src('client/templates/*.less')
.pipe(less())
.pipe(minifyCSS())
.pipe(dest('build/css'))
}
function js() {
return src('client/javascript/*.js', { sourcemaps: true })
.pipe(concat('app.min.js'))
.pipe(dest('build/js', { sourcemaps: true }))
}
exports.js = js;
exports.css = css;
exports.html = html;
exports.default = parallel(html, css, js);
webpack是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 loader 来处理不同的文件,用 plugin 来扩展webpack 功能。
const path = require('path');
const HtmlwebpackPlugin = require('html-webpack-plugin');
const { CleanwebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: {
app: './src/index.js' // 配置多个入口
},
output: {
filename: '[name].bundle.js', // 输出文件名
path: path.resolve(__dirname, 'dist') // 输出路径,绝对路径
},
mode: "production", // 模式
devtool: 'inline-source-map', // 追踪错误和警告在源代码中的原始位置
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
open: true,
port: 3000,
hot: true, // 开启热更新
hotOnly: true // 热更新失败时不刷新页面
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new HtmlwebpackPlugin({
title: 'Output Management'
}),
new CleanwebpackPlugin(), // 每次编译前清理历史
new webpack.HotModuleReplacementPlugin() // 热部署
],
};
webpack 的构建流程?
1 解析配置:合并 shell 传入和 webpack.config.js 文件配置参数,生产最终配置。
2 开始编译:初始化 compiler 对象,注册所有配置的插件,插件监听 webpack 构建生命周期的事件,执行对象的 run 方法开始执行编译。
3 确定入口:从 webpack 配置中的 entry 开始,解析文件构建 AST(抽象语法树)。
4 编译模块:根据文件类型和 loader 配置,递归对文件进行转换。再找出该模块依赖的模块,再处理,直到所有入口依赖文件全部经过处理。
5 编译完成输出:得到每个文件编译结果,包含每个模块以及他们之间的依赖关系,根据 entry 置生成 chunk
6 输出完成:输出所有的 chunk 到文件系统,生成浏览器可以运行的 bundle。
webpack 的热更新是如何做到的?
先理解图中这几个名称概念:
- webpack-dev-server :一个服务器插件,相当于 express 服务器,启动一个 Web 服务,只适用于开发环境。
- webpack-dev-middleware:一个 webpack-dev-server 的中间件,作用简单总结为:通过watch mode,监听资源的变更,然后自动打包。
- webpack-hot-middleware:结合 webpack-dev-middleware 使用的中间件,它可以实现浏览器的无刷新更新,也就是 HMR。
1 监控代码变化,重新编译打包
在 webpack 的 watch 模式下,若发现文件中代码发生修改,则根据配置文件对模块重新编译打包。
2 保存编译结果
webpack 与 webpack-dev-middleware 交互,webpack-dev-middleware 调用 webpack 的 API 对代码变化进行监控,并通知 webpack 将重新编译的代码通过 JavaScript 对象保存在内存中。
3 监控文件变化,刷新浏览器
webpack-dev-server 开始监控文件变化,与第 1 步不同的是,这里并不是监控代码变化重新编译打包。
当我们在配置文件中配置了 devServer.watchContentBase
为 true
,webpack-dev-server 会监听配置文件夹中静态文件的变化,发生变化时,通知浏览器端对应用进行浏览器刷新,这与 HMR 不一样。
4 建立 WS,同步编译阶段
这一步都是 webpack-dev-server 中处理,主要通过 sockjs(webpack-dev-server 的依赖),在 webpack-dev-server 的浏览器端(Client)和服务器端(webpack-dev-middleware)之间建立 WebSocket 长连接。
然后将 webpack 编译打包的各个阶段状态信息同步到浏览器端。其中有两个重要步骤:
- 发送状态
webpack-dev-server 通过 webpack API 监听 compile 的 done
事件,当 compile 完成后,webpack-dev-server 通过 _sendStats
方法将编译后新模块的 hash 值用 socket 发送给浏览器端。
- 保存状态
浏览器端将_sendStats
发送过来的 hash
保存下来,它将会用到后模块热更新
5 浏览器端发布消息
当 hash 消息发送完成后,socket
还会发送一条 ok
的消息告知 webpack-dev-server,由于客户端(Client)并不请求热更新代码,也不执行热更新模块操作,因此通过 emit
一个 webpackHotUpdate
消息,将工作转交回 webpack。
6 传递 hash 到 HMR
webpack/hot/dev-server 监听浏览器端 webpackHotUpdate
消息,将新模块 hash 值传到客户端 HMR 核心中枢的 HotModuleReplacement.runtime ,并调用 check
方法检测更新,判断是浏览器刷新还是模块热更新。
如果是浏览器刷新的话,则没有后续步骤。
7 检测是否存在更新
当 HotModuleReplacement.runtime 调用 check 方法时,会调用 JsonpMainTemplate.runtime 中的 hotDownloadUpdateChunk
(获取最新模块代码)和 hotDownloadManifest
(获取是否有更新文件)两个方法。其中 hotEnsureUpdateChunk
方法中会调用 hotDownloadUpdateChunk 。
8 请求更新最新文件列表
在调用 check
方法时,会先调用 JsonpMainTemplate.runtime 中的 hotDownloadManifest
方法, 通过向服务端发起 AJAX 请求获取是否有更新文件,如果有的话将 mainfest
返回给浏览器端。
9 请求更新最新模块代码
在 hotDownloadManifest
方法中,还会执行 hotDownloadUpdateChunk
方法,通过 JSONP 请求最新的模块代码,并将代码返回给 HMR runtime 。然后 HMR runtime 会将新代码进一步处理,判断是浏览器刷新还是模块热更新。
10 更新模块和依赖引用
这一步是整个模块热更新(HMR)的核心步骤,通过 HMR runtime 的 hotApply
方法,移除过期模块和代码,并添加新的模块和代码实现热更新。
从 hotApply
方法可以看出,模块热替换主要分三个阶段:
(1)找出过期模块 outdatedModules
和过期依赖 outdatedDependencies
;
(2)从缓存中删除过期模块、依赖和所有子元素的引用;
(3)将新模块代码添加到 modules 中,当下次调用 __webpack_require__
(webpack 重写的 require
方法)方法的时候,就是获取到了新的模块代码了。
hotApply
方法执行之后,新代码已经替换旧代码,但是我们业务代码并不知道这些变化,因此需要通过 accept
事件通知应用层使用新的模块进行“局部刷新”,我们在业务中是这么使用。
11 热更新错误处理
在热更新过程中,hotApply
过程中可能出现 abort
或者 fail
错误,则热更新退回到刷新浏览器(Browser Reload),整个模块热更新完成。
webpack 长缓存
通过使用 output.filename
进行文件名替换,可以确保浏览器获取到修改后的文件。[hash]
替换可以用于在文件名中包含一个构建相关(build-specific)的 hash,但是更好的方式是使用 [chunkhash]
替换,在文件名中包含一个 chunk 相关(chunk-specific)的哈希。
const path = require('path');
const CleanwebpackPlugin = require('clean-webpack-plugin');
const HtmlwebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new CleanwebpackPlugin(['dist']),
new HtmlwebpackPlugin({
- title: 'Output Management'
+ title: 'Caching'
})
],
output: {
- filename: 'bundle.js',
+ filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'dist')
}
};
如何利用 webpack 来优化前端性能
- 多入口情况下,使用
CommonsChunkPlugin
来提取公共代码 - 通过
externals
配置来提取常用库 - 使用
webpack-uglify-parallel
来提升uglifyPlugin
的压缩速度 - 使用
Tree-shaking
和Scope Hoisting
来剔除多余代码 - 利用
DllPlugin
和DllReferencePlugin
预编译资源模块 通过DllPlugin
来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过DllReferencePlugin
将预编译的模块加载进来。
如何提高 webpack 的构建速度
- 压缩代码。可以利用 webpack 的
UglifyJsPlugin
和ParallelUglifyPlugin
来压缩 JS 文件 - 利用CDN加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的
publicPath
参数来修改资源路径 - 删除死代码。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数
--optimize-minimize
来实现 - 提取公共代码。使用
CommonsChunkPlugin
来提取公共代码