webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。
webpack作为现代前端打包最重要的工具之一,在前端开发应用中是必不可少的。虽然说工程化的项目中,比如 react 的 cra、 VUE 的 cli 以及其他的一些脚手架,都会将相关的配置给我们配置好,但是在大型的应用项目中,如果想要对自己的工程项目进行充分优化,那么对 webpack 的学习就是必不可少的了。
正如 lucasHC 的观点: webpack 工程师 是大于 前端开发工程师的。
同时,前端项目的工程化实现,也离不开 webpack 的参与。
本文将会结合 webpack 的官方文档,从如下几个角度依次带大家学习 webpack。
- webpack 的主要配置
- 学习 create-react-app 的 webpack 配置
- webpack的主要API (loader & plugin)
- 如果去实现一个 loader
- 如果去实现一个 plugin
- webpack 的源码解析
- 如何实现一个最小的 webpack 打包工具
-- 值得说明的是,本项目使用的webpack版本为 webpack ^5.x.x。
WebPack 的主要配置
这一部分的内容,其实在 webpack 的官方中文文档中阐述的比较清楚,下面我们就一起看一个普通项目的 webpack 配置。
1. chunk | module | bundle?
在说配置之前,需要先讲清楚这几个概念。
先来说说chunk
chunk: 此 webpack 特定术语在内部用于管理捆绑过程。输出束(bundle)由块组成,其中有几种类型(例如 entry 和 child )。通常,块 直接与 输出束 (bundle)相对应,但是,有些配置不会产生一对一的关系。
以上是官方给出的解释,我自认为写得太抽象了,有些人说,每有一个entry,就对应一个chunk,其实这种说法也不完全准确。
准确的说,chunk是webpack执行的中间文件。
而对应到module和bundle,可以说是 webpack的输入和输出。
比如:
我们写了一些es6 module的模块、一些css文件,以及一些静态资源 --module
通过打包工具,从某个entry进行打包,在webpack内部就形成了一 个chunk
最后我们通过例如 MiniCssExtractPlugin等插件,生成了一些 xx.bundle.js、xx.bundle.css文件。
2. 输入(entry) | 输出(output) | loader | 插件(plugins)
2.1 entry
entry 是一个 webpack 配置的入口文件,
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js',
},
};
这里值得说明的有几点:
-
当我们有多个应用入口,但是使用了相同的一些第三方库,比如
echarts、react等,我们有两种方法去对共享的代码进行单独打包。a. 使用
optimization.splitChunks为页面间共享的应用程序代码创建bundle。b. 在
entry中使用dependOn来实现。entry: { shared: ['echarts','react','react-dom'], catalog: { import: './src/react/catalog.js', filename: './catalog.js', dependOn: 'shared', }, personal: { import: './src/react/personal.js', filename: './personal.js', dependOn: 'shared', }, }, -
必要的时候,我们亦可以在
entry中指明输出文件的名称。 -
常见的代码分割有三种方式:
- 在
webpack配置的入口点entry中去进行分割; - 使用
SplitChunkPlugin进行分割; - 在工程中使用
import().then(), 或者react的lazy,VUE的dynamicImport进行分割。
- 在
2.2 output
output
默认情况下,我们导出的是支持浏览器解析的版本。
output的 chunkFormat 、chunkLoading 等参数,是和 target 深度绑定的。(详细内容可以参考 webpack/lib/defaults.js中的设置。)
当然,这里也可以通过 chunkFormat 、chunkLoading 等设置,导出 node 等相关的版本。
关于 output 的配置还有很多,一般情况下我们在使用框架开发时,不太能用得到,这里只给出来一些比较简单的。
这里值得说明的有几点:
-
hash | contenthash | chunkhash 这几者的区别
- hash: 针对每一次单独打包时进行生成,所以不同的打包环境,生成的不同,不利于缓存。同时,对于多入口的文件,每一次打包的 hash 值是相同的。
- contenthash: 根据文件内容生成不同的 hash 值,改变文件内容时, hash 值会发生变化。
- chunkhash: 顾名思义,不同的
chunk, 生成的 hash 值不同。
2.3 loader
loader 的配置在 module 选项中,这里关于 module 的其他选项我们暂不介绍,直接说说loader,即 module.rules
一个典型的 rule 的配置包括了 test 和 use 的配置。
{
module:{
rules:[{
test: /\.js/i,
use:[
{loader :"babel-loader"}
]
}]
}
}
loader 在使用时,我需要注意的是:
- 必要时请声明 include 和 exclude,这样会减少 loader 的编译内容,从而提高打包的性能;
- 老生长谈的是,loader 是自右向左进行加载的,如对less文件的解析,可能的配置如下:
use: ["style-loader", "css-loader", "less-loader"],
2.4 plugins
整个的webpack是基于插件化实现的,如果我们尝试读过源码,可以发现,很多配置内的参数,在内部是通过一个个插件,在打包的不同阶段进行处理的。
关于插件的解释,我们在后面会详细给出。
3. resolve | library | optimization
3.1 resolve
resolve 作为解析模块,更多的是在我们进行工程项目开发时,可能会遇到的一些问题的处理。
如 alias 的使用:
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/'),
},
},
// 在代码里,我们可以直接使用
// import Utilities/index.js 取代:
// import ../../src/utiluties/index.js
如 extensions 的使用:
可以指定默认扩展名称的加载类型;
如 modules 的使用: 告诉 webpack 解析模块时应该搜索的目录,一般为 node_modules
值得说明的是:webpack 5 不再自动 polyfill Node.js 的核心模块,这意味着如果你在浏览器或类似的环境中运行的代码中使用它们,你必须从 NPM 中安装兼容的模块,并自己包含它们。
3.2 library
library的配置应用于,当你想要打包一个库的时候。可以选择在 output 中去使用它。
这里指的说明的是umd规范。
当我们用这种规范去打包一个库的时候,可以通过多种方式去引用它。
比如:
-
CommonJS module require:
const webpackNumbers = require('webpack-numbers'); // ... webpackNumbers.wordToNum('Two'); -
AMD module require:
require(['webpackNumbers'], function (webpackNumbers) { // ... webpackNumbers.wordToNum('Two'); }); -
script tag:
<!DOCTYPE html> <html> ... <script src="https://example.org/webpack-numbers.js"></script> <script> // ... // Global variable webpackNumbers.wordToNum('Five'); // Property in the window object window.webpackNumbers.wordToNum('Five'); // ... </script> </html>
3.3 optimization
从名称上我们也能看得出来,这一部分的配置主要用于优化的工作。
我们最常用的几个地方如下:
minimize / minimizer
这里主要是用于设置一些对代码压缩的工具和插件。
splitChunk 和 runtimeChunk
这两个都是对一些代码做chunk上的拆分,笔者在学习的时候也常常搞混,这里我们举个例子,来详细地说明一下。
我们创建一个项目,在 ./src下增加一个react的文件夹,并建立如下几个文件:
// ./src/react/app.js
import React, { Component } from 'react';
export default class app extends Component {
render() {
return (
<div>
this is a React app
</div>
)
}
}
// ./src/react/index.js
import ReactDOM from 'react-dom';
import App from './app';
import React from 'react';
ReactDOM.render(<App />, document.getElementById('root'))
我们使用如下的配置进行打包,因为我的测试程序的webpack.config.js是自行编辑的,所以声明了执行目录:
module.exports = {
mode:"production",
context: path.resolve(process.cwd()),
entry: { app: './src/react/home.js' },
output: {
clean:true,
path:path.resolve(process.cwd(),'./dist04'),
filename:'[name].[chunkhash].js'
},
module:{
rules: [{
test: /\.(js|jsx)$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
},
include: path.resolve(process.cwd(), './src/react'),
exclude: path.resolve(process.cwd(), './node_modules/')
}]
},
}
我们生成的文件如下:
app.bdeb74ac3557b6d6c47a.js ( 129KB )
很显然,这个129KB 的文件里,包含了 react 的相关的源码,那么我们先加一个 runtimecHUNK 的配置:
optimization:{ runtimeChunk:'single'}
我们发现,打包完成了的内容中,多了一个:
runtime.ecfe325a56eb578bdcaf.js (1KB)
这里其实是把 app 中的运行时函数给切分了出来。这里由于文件太单一,看不出来,我们再在 src/react下增加一个文件:
// ./src/react/home.js
import ReactDOM from 'react-dom';
import App from './app';
import React from 'react';
ReactDOM.render(<App />, document.getElementById('root'))
然后再在配置中增加:
entry: {
home:'./src/react/index.js',
app: './src/react/home.js'
},
optimization:{
splitChunks :{
chunks:'all'
},
runtimeChunk:'multiple'
}
我们再看看打包后的结果:
我们发现,react、react-dom等相关的第三方库,被打到了单独的文件中。
我们来总结一下:
-
runtimeChunk会根据配置内容是multiple还是single,对runtime相关的文件,打包单独的或共享的chunk; -
splitChunks中,会对多入口上共用的文件,打成单独的包。当然,在单一入口下,也会生成相应的内容。当然,
splitChunks作为一个老版本的独立插件,还是有很多强大的功能,如cacheGroups中的各种配置,对缓存的处理,都是值得我们去学习的。
4. watch | devServer | middleware
最后,我们来说说自动编译。
我们知道,在每次编译代码时,手动运行 npm run build 会显得很麻烦。
webpack 提供几种可选方式,帮助你在代码发生变化后自动编译代码:
多数场景中,你可能需要使用 webpack-dev-server,但是不妨探讨一下以上的所有选项。
4.1 watch 模式
我们可以通过在配置中增加{ watch: true }, 或者直接在脚本的运行中添加 webpack -- watch的方法启动 watch 模式。
值得注意的是,在这种模式下如果我们想要提高 watch 的效率,最好的办法是添加一些配置,让 watch 的内容缩减到最小:
{
watchOptions: {
ignored: ['**/node_modules', "**/files/**/*.js"]
}
}
4.2 webpack-dev-server
第二种方式是使用 webpack-dev-server,也是目前 React 的脚手架中,使用的方法。
使用 webpack-dev-server 至少要满足两个条件:
- 第一:添加一个
html-webpack-plugin - 第二:在
webpack的命令中增加:serve --open
同时,你可以修改相关的配置,增加 devServer: { contentBase: './dist' , port:'8080'}等配置,但这不是必须的。
值得说明的是,我们也可以使用 node.js的方式进行执行:
var compiler = webpack(config);
const devServer = new WebpackDevServer(compiler, serverConfig);
devServer.listen(port, () => {
})
这种方式在 react 的 cra 中有详细的时候,后面我们可以进行讨论。
4.3 webpack-dev-middleware
关于 webpack-dev-middleware也很简单,我们只需要启动一个node-server,即可以实现,这里不作详细讨论。
总结
到了这里,关于 webpack 常用的一些配置就说完了。
当然还有很多更复杂的内容我们没有讲解,比如一些关于微前端、优化、缓存、lib库的内容,其实都是很重要的。
在后续的文章中,我们还会针对某一块的内容,做更详细的探讨。