关于 webpack
其实项目做了挺多的,但是我接触下来,多数都是用固有脚手架(像vue-cli | create-react-app
)去构建的,或者部分是二次开发的,业务代码写的越多,构建方面的东西就越来越少了,下决心好好了解下webpack
。
本文会通过从 0 配置一个使用 Vue 的完整项目,来说明 webpack 各个属性的功能作用及使用方式;
webpack
首先可以看做是个模块打包机;不过它还具备一些分析项目结构,代码编译(像TS | CSS 预编译语言
),代码优化的工具;webpack
最终输出的结果是HTML | CSS | JS
这些浏览器可执行的代码包;概括一下webpack
的功能内容:
- 代码编译:
TS
编译成JS
,less
编译成CSS
; - 文件优化:代码压缩,代码优化,静态资源处理等;
- 代码分割:提取公共代码,代码按模块进行划分,提高首屏加载效率等等;
- 模块合并:模块的合并和分割;
- 热更新:
dev
环境时,项目的热更新; - 代码校验:利用
git husky
等工具,可以添加提交钩子,在提交代码时校验格式风格,单元测试等等; - 自动发布:devops 的一环,提交后可配置自动打包构建生产包并传输到发布系统上;
webpack 核心概念
entry
: 入口,执行开发环境或者生产环境构建时,将从入口文件开始,递归查找依赖包进行构建;output
: 输出,经过一系列打包处理后,得到的最终代码包;loader
: 模块转换器,用于把模块内容按照需求转换成新内容,常见的有css-loader
;plugin
: 插件,在 webpack 构建流程中的特定时机,注入扩展逻辑来改变构建结果;stats
: stats 可以控制打包的时候,terminal
的日志输出项包括哪些;
webpack 中所有的文件都是模块,最后都会转换成按照 JS 的模块来加载;
2022-03-22 更新
感觉这篇文章写偏了,我想的是记录学习 webpack 的核心原理或者概念的过程,重启一下。
loader
我个人理解loader
是一种将各类资源转换成 JS 能识别的文件的工具。比如css-loader, ts-loader
等等。
loader 的使用
在配置文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.config.js
const webpackConfig = {
module: {
rules: [
{
test: /\.js/,
loader: 'testLoader'
},
{
test: /\.jsx/,
use: ['loader1', 'loader2']
}
]
},
resolveLoader: {
// 配置loader的路径
modules: [
'node_modules'
// ...
]
}
};
注意:多个 loader 执行顺序是自下而上,自右向左执行
实践中发现,如果test
属性是一样的,那么写在后面的rules
会覆盖前面的。
写一个 loader
loader
本质是一个函数;且函数体不应该用箭头函数声明,因为经常需要在函数内部只用this
访问一些变量
参数:content
文件内容通常是string | buffer
,map
文件 sourcemap 的内容, meta
文件的元信息。
返回值:将处理后的content
返回。
loader
又分为同步和异步,看两个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 同步loader
const syncLoader = function (content, map, meta) {
console.log(content);
return content;
// 或者也可以调用callback方法
// this.callback(null, content,...)
// return undefined
};
// 异步loader
const asyncLoader = function (content, map, meta) {
const callback = this.async();
asynFunc(content, map, meta)
.then((content1, map1, meta1) => {
callback(null, content, map, meta);
})
.catch(err => {
callback(err);
});
};
在 loader 函数内部,还有很多属性是webpack
在构建时注入到上下文this
上的,详见https://webpack.docschina.org/api/loaders/,很多都能在开发时用到;
记录一个自己写的 loader
在一个项目中,需要进行项目的迁移,从 h5 端迁移到小程序端,用了微信团队开源的一个kbone
框架,在处理css
文件时,我把项目中用到rem
的地方都转成了用rpx
,这里使用了一个自己写的loader
,基本思路和postcss-pxtoviewport
差不多。由于设计图都是按750px
物理像素出的,所以其实1rem = 100px = 200rpx
,按这个思路去转换的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const getUnitRegexp = function (unit) {
return new RegExp('"[^"]+"|\'[^\']+\'|url\\([^\\)]+\\)|(\\d*\\.?\\d+)' + unit, 'g');
};
const modulesReg = /node\_modules/;
module.exports = function (content, map, meta) {
// console.warn(content);
// 不处理node_modules的css文件
if (modulesReg.test(this.resourcePath)) {
this.callback(null, content, map, meta);
return void 0;
}
const reg = getUnitRegexp('rem');
const callback = this.async();
callback(
null,
content.replace(reg, (m, $1) => {
if (!$1) return m;
// console.warn($1);
return `${$1 * 200}rpx`;
})
);
return void 0;
};
从 0 开始配置 webpack
初始化
初始化空项目,安装 webpack,命令行工具,开发使用的服务器插件;
1
2
npm init
npm i webpack webpack-cli webpack-dev-server -D
webpack 基础配置文件,webpack.config.js
;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const path = require('path');
module.exports = {
// 区别生产/开发
mode: 'development',
// 入口
entry: path.resolve(__dirname, './src/index.js'),
output: {
// 输出目录
path: path.resolve(__dirname, 'dist'),
// 输出文件名
filename: 'bundle.js'
}
};
npm 配置文件package.json
中,修改scripts
属性:
1
2
3
4
5
{
"scripts": {
"dev": "webpack --config ./webpack.config.js"
}
}
这样我们在终端里运行npm run dev | yarn run dev
都能运行webpack
的打包代码;
添加 HTML 解析功能
由于我们的项目通常是 SPA 项目,所以会有入口的 HTML 文件,但 webpack 无法打包 HTML 文件,所以要引入一些插件来帮助处理,选择常用的html-webpack-plugin
,添加配置到webpack.config.js
中(npm 安装插件的步骤就不重复了):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 区别生产/开发
mode: 'development',
// 入口
entry: path.resolve(__dirname, './src/index.js'),
output: {
// 输出目录
path: path.resolve(__dirname, 'dist'),
// 输出文件路径和文件名
filename: 'js/[name].js'
},
plugins: [
new HtmlWebpackPlugin({
// 使用的 html 模板路径
template: path.resolve(__dirname, './index.html'),
// 打包后输出的文件名
filename: 'index.html',
// index.html 模板内,通过 <%= htmlWebpackPlugin.options.title %> 拿到的变量
title: '手搭 Vue 开发环境'
})
]
};
添加对 Vue 文件的解析功能
和上面的 HTML 一样,webpack 无法对.vue
文件进行解析和打包,所以需要添加几个loader
来识别;
- 首先安装
vue-loader
:
它是一个基于 webpack 的loader
插件,用于解析和编译.vue
文件;我们都知道单文件组件由template | script | style
三部分组成,vue-loader
会把这三部分提取出来,分别交给对应的其他loader
去处理,所以其实vue-loader
的最主要作用是提取.vue
文件;
- 然后是
vue/compiler-sfc
:
在 vue2.x 版本里插件名是vue-template-compiler
,其作用是将template
编译为 ast 语法树,再由语法树生成 render 函数(Vue 所有组件都可以直接用 render 函数的形式写出来)
在webpack.config.js
中添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 最新的 vue-loader 中,VueLoaderPlugin 插件的位置有所改变
const { VueLoaderPlugin } = require('vue-loader/dist/index');
module.exports = {
// 区别生产/开发
mode: 'development',
// 入口
entry: path.resolve(__dirname, './src/index.js'),
output: {
// 输出目录
path: path.resolve(__dirname, 'dist'),
// 输出文件路径和文件名
filename: 'js/[name].js'
},
module: {
rules: [
{
test: /\.vue$/,
use: ['vue-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
// 使用的 html 模板路径
template: path.resolve(__dirname, './index.html'),
// 打包后输出的文件名
filename: 'index.html',
// index.html 模板内,通过 <%= htmlWebpackPlugin.options.title %> 拿到的变量
title: '手搭 Vue 开发环境'
}),
// 添加 VueLoaderPlugin 插件
new VueLoaderPlugin()
]
};
添加VueLoaderPlugin
的作用是将定义过的其他规则比如css/js
的规则,应用到.vue
文件里;
现在我们有了处理HTML | Vue
的工具,还差css
的;添加:
style-loader
:将css
样式插入到页面的style
标签中;css-loader
:处理样式中的url
,比如图片引入经常用到的url('@/xxx.png')
,此时浏览器识别不了@
符号;
对于处理样式的工具,还有预编译语言
less | scss
之类的,处理方法都是一样的,它们也有对应的loader
工具;
在 rule 里面添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 最新的 vue-loader 中,VueLoaderPlugin 插件的位置有所改变
const { VueLoaderPlugin } = require('vue-loader/dist/index');
module.exports = {
// 区别生产/开发
mode: 'development',
// 入口
entry: path.resolve(__dirname, './src/index.js'),
output: {
// 输出目录
path: path.resolve(__dirname, 'dist'),
// 输出文件路径和文件名
filename: 'js/[name].js'
},
module: {
rules: [
{
test: /\.vue$/,
use: ['vue-loader']
},
{
test: '/.css$/',
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
// 使用的 html 模板路径
template: path.resolve(__dirname, './index.html'),
// 打包后输出的文件名
filename: 'index.html',
// index.html 模板内,通过 <%= htmlWebpackPlugin.options.title %> 拿到的变量
title: '手搭 Vue 开发环境'
}),
// 添加 VueLoaderPlugin 插件
new VueLoaderPlugin()
]
};
- 其他常用插件
clean-webpack-plugin
:作用是每次打包时都会清除原有的打包目录,防止文件重新打包后还有残留文件;
1
2
3
4
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
{
plugins: [new CleanWebpackPlugin()];
}
添加 babel
由于前端的浏览器兼容性是一个比较难处理的问题,所以我们在编译打包的时候,会考虑把所有的 JS 代码重新编译一遍,编译为设定的版本(比如 ES5…);
babel-loader
:这个 loader 有几个核心的插件:
@babel/core
:核心库;@babel/preset-env
:通过配置浏览器版本,来决定编译后的 JS 的版本;babel-loader
:webpack
的 loader;
在 rules 添加代码:
1
2
3
4
5
6
7
8
9
10
11
{
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 不编译node_modules下的文件
loader: 'babel-loader'
}
];
}
}
读取的 babel 配置文件,.babelrc
:
1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["last 2 versions"] // 最近 2 个版本的浏览器
}
}
]
]
}
添加 devServer
webpack 提供的供开发环境下使用的本地 server 插件;需要安装依赖:webpack-dev-server
,需要的配置都在这里了:https://webpack.docschina.org/configuration/dev-server/#root
webpack的基本工作流程
运行方式
·webpack·运行方式有两种,分别是命令行式的,以及直接在代码中运行的方式;
1
2
3
4
5
6
7
// cmd
webpack --config webpack.config.js
// js
const webpack = require('webpack')
const config = require('./webpack.config')
webpack(config, (err, status) => {})
流程
以上两种运行webpack
的方式,本质上都是执行webpack.js
中的webpack函数,实际该函数就是处理了一下传入的配置,调用各类webpack内部插件,然后调用compiler.run(callback)
;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack()
/**
* @param {WebpackOptions} options 配置对象
* @param {funtion(Error=, stats=): void=} callback 回调函数
* @return {compiler | multiCompiler} compiler对象
*/
const webpack = (options, callback) => {
// 默认的配置和传入的配置merge后生成的新配置对象
options = new WebpackOptionsDefaulter().process(options)
// 生成一个compiler
let compiler = new Compiler(options.context)
// 加载内部插件
compiler.options = new WebpackOptionsApply().process(options, compiler)
if(callback) {
compiler.run(callback)
}
return compiler
}
大致过程如下:
- 创建编译器
compiler
的实例; - 根据
webpack
参数加载参数中的插件,以及内置的默认插件; - 执行编译过程,创建编译过程的compilation实例,从入口递归添加并构建模块,模块构建完成后,冻结模块并进行优化;
- 构建与优化过程结束后提交产物,将产物内容写到输出文件中。
webpack生命周期与插件
webpack
核心模块compiler | compilation
都extends Tapable
,用于实现webpack工作流中的生命周期划分,生命周期通常被称为hook
;webpack引擎给予插件系统搭建,不同插件在工作流中的一个或几个时间段上对构建流程进行处理,最终将源码变成最终的产物代码;
一个webpack插件是包含apply
方法的JS对象,该方法执行逻辑通常是注册某个hook
,并添加对应hook中的处理函数;
一个例子:
1
2
3
4
5
6
7
8
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.run.tap("HelloWorldPlugin", compilation => {
console.log('hello world');
})
}
}
module.exports = HelloWorldPlugin;
以上代码在webpack4
及以上的版本是可以运行的,因为hooks从4.x版本开始支持,而在以前版本,通常使用Tapable.plugin
方法来调用对应的hooks
,例如上面的例子要写成:
1
2
3
4
5
6
7
class HelloWorldPlugin {
apply(compiler) {
compiler.plugin('run', (compilation, callback) => {
console.log('hello world');
})
}
}
compiler hooks
构建器的生命周期可以分成三个阶段:初始化阶段、构建阶段、产物生成阶段;
初始化阶段
environment, afterEnvironment
: 在创建了compiler实例且执行了配置内定义的插件的apply方法后触发;entryOption, afterPlugins, afterResolvers
:在webpackOptionsApply.js
中,这三个hooks
分别在执行entryOptions
和其他webpack内置插件,以及解析了resolver
配置后触发;
构建阶段
-
normalModuleFactory、contextModuleFactory
:在两类模块工厂创建后触发。 -
beforeRun、run、watchRun、beforeCompile、compile、thisCompilation、compilation、make、afterCompile
:在运行构建过程中触发。
产物生成阶段
shouldEmit、emit、assetEmitted、afterEmit
:在构建完成后,处理产物的过程中触发。
failed、done
:在达到最终结果状态时触发。