# webpack
站在巨人的肩膀
# Webpack是什么?
Webpack是一个模块打包工具,在Webpack里一切文件皆模块。通过loader转换文件,通过plugin注入钩子,最后输出由多个模块组合的文件。Webpack专注构建模块化项目。 Webpack可以看作是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其他一些不能被浏览器直接运行的扩展语音(如:Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。
# Webpack与Grunt、Gulp的不同?
Grunt/Gulp是一种能够优化前端开发流程的工具,而Webpack是一种模块化的解决方案。
# 工作方式不同
Grunt/Gulp的工作方式是:在一个配置文件中,指明某些文件进行类似编译/组合/压缩等任务的具体步骤,之后工具可以自动帮你完成这些任务
Webpack的工作方式是:把项目当作是一个整体,通过指定的入口文件,Webpack会从这个入口文件开始找到项目所有的依赖文件,然后使用loader处理它们,最后打包成一个或多个浏览器能够识别的JavaScript文件
# 构建思路不同
Grunt/Gulp需要将整个前端构建过程拆分成多个task,合理控制所有task的调用关系
Webpack需要定义好入/出口,并需要清楚对于不同类型资源应该用什么loader解析编译
Grunt/Gulp是基于任务和流(task和stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列的链式操作,更新流上的数据,整条链式操作构成了一个任务,多个任务就构成了整个Web的构建流程。
Webpack是基于入口的。Webpack会自动的递归解析入口所需要加载的所有资源文件,然后用不同的loader来处理不同的文件,用pulgin扩展Webpack功能。
# 背景知识不同
Grunt/Gulp更像是后端开发者的思路,需要对整个流程了如指掌。Webpack更倾向于前端开发者的思路。
详细的可以看下边的源码分析
# 分别介绍bundle,chunk,module是什么?
- bundle:由Webpack打包出来的文件
- chunk:代码块,webpack在进行模块的依赖分析的时候,代码分割出来的代码块
- module:是开发中的单个模块,在Webpack中,一切皆模块,一个模块对应一个文件
# 什么是Loader?什么是Plugin?
- loader: 模块转换器,用于对模块的源代码进行转换
- plugin: 自定义webpack打包过程的方式,插件含有apply属性的JavaScript对象,apply属性会被webpack compiler调用,并且compiler对象可以在整个编译生命周期内访问
# loader和plugin有哪些不同?
# 不同的作用
- loader直译为“加载器",Webpack将一切文件视为模块,但是Webpack原生只能解析JavaScript和JSON类型文件。如果想加载解析其他类型文件,就会用到loader。所以loader是让Webpack拥有加载和解析非JavaScript文件的能力
- plugin直译为”插件“,plugin可以扩展Webpack的功能,让Webpack具有更多的灵活性。在Webpack运行的生命周期中会广播许多事件,plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果
# 不同的用法
loader在module rules中配置,也就说它作为模块解析规则存在。类型为Array,每一项都是一个Object,里面描述了什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
plugin单独在plugins中单独配置。类型为Array,每项都是一个plugin的实例,参数是通过构造函数传入
# 有哪些常见的Loader?
- file-loader: 将文件输出到一个文件夹中,在代码中通过相对路径(url)去引用输出的文件
- url-loader: 和file-loader类似,但是能在文件很小的情况下,以base64的方式将内容注入到代码中
- image-loader: 加载并压缩图片文件
- babel-lodader: 脚本js
- css-loader: 加载CSS,支持模块化/压缩/文件导入等特性
- style-loader:把CSS代码注入到JavaScript中,通过DOM操作去加载CSS
- eslint-loader: 通过ESlint检查JavaScript代码
# 有哪些常见的Plugin?
- define-plugin: 定义环境变量
- html-webpack-pulgin: 生成创建html入口文件,并引用对应的外部资源
- uglifyjs-plugin: 通过Uglifyjs压缩JavaScript代码
- mini-css-extract-plugin: 分离CSS文件
- clean-webpack-plugin: 删除打包文件
- happypack: 实现多线程加速编译(多核编译)
const HappyPack = require('happypack');
const os = require('os');
//开辟一个线程池
const happyThreadPoll = HappyPack.ThreadPool({ size: os.cpus().length }); module.exports.plugins = [
new HappyPack({
id: 'babel',
threadPool: happyThreadPoll,
loaders: [{
loader: 'babel-loader'
}] })
];
//use: 'happypack/loader?id=babel',
2
3
4
5
6
7
8
9
10
11
12
13
14
# Tree Shaking
为了使用tree shaking,需要满足以下条件:
- 使用ES2015语法(即import和export)
- 在项目package.json文件中,添加sideEffects入口
- 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如:UglifyJSPlugin)
# 将文件标记为无副作用(side-effect-free)
这种方式是通过package.json
的sideEffects
属性来实现的。
{
"sideEffects": false
}
2
3
「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个export
或多个export
。举例说明,例如polyfill
,它影响全局作用域,并且通常不提供export。
注意,任何导入的文件都会受到tree shaking
的影响。这意味着,如果在项目中使用类似css-loader
并导入CSS
文件,则需要将其添加到 side effect
列表中,以免在生产模式中无意中将它删除:
{
"sideEffects": ['*.css']
}
2
3
# 压缩输出
从 webpack 4
开始,也可以通过 "mode
" 配置选项轻松切换到压缩输出,只需设置为 "production
"。
也可以在命令行接口中使用--optimize-minimize
标记,来使用UglifyjsPlugin
。
# Code Splitting
code splitting
的必要性
- 不进行
code splitting
,打包后单文件提交较大,加载时长较长,影响用户体验 - 不进行
code splitting
,经常修改业务代码,重新打包后,浏览器不能进行缓存,导致性能较差,影响用户体验
# 同步代码
import _ from 'lodash';
webpack.config.js配置如下:
....
optimization: {
splitChunks: {
chunks: 'all'
}
}
....
2
3
4
5
6
7
配置后,会将公用类库进行打包,生成一个vendors~main.js文件。
# 异步代码
function getComponent() {
return import('lodash').then(({ default: _ }) => {
var element = document.createElement('div');
element.innerHTML = _.join(['Clear', 'love'], '');
return element;
})
}
getComponent().then(element => {
document.body.appendChild(element);
})
2
3
4
5
6
7
8
9
10
11
# 开发中的server---devServer配置
下边简单介绍一下
contentBase
该配置项指定了服务器资源的根目录,如果不配置contentBase
或使用false
的话,那么contentBase默认是当前执行的目录,一般是项目的根目录。
可能如上解析还不够清晰,没有关系,我们下面还是先看下我整个项目的目录结构,然后进行相关的配置,使用contentBase
配置项再来理解下:
### 目录结构如下:
demo1 # 工程名
| |--- dist # dist是打包后生成的目录文件
| |--- node_modules # 所有的依赖包
| |--- js # 存放所有js文件
| | |-- demo1.js
| | |-- main.js # js入口文件
| |
| |--- webpack.config.js # webpack配置文件
| |--- index.html # html文件
| |--- styles # 存放所有的css样式文件
| |--- .gitignore
| |--- README.md
| |--- package.json
2
3
4
5
6
7
8
9
10
11
12
13
14
在webpack配置加上如下配置,即配置项指定了服务器资源的根目录。比如我们打包后的文件放入 dist目录下。
module.exports = {
devServer: {
contentBase: path.join(__dirname, "dist")
},
}
2
3
4
5
如上配置完成后,我们再运行 npm run dev, 再在地址栏中 运行 http://localhost:8080/ 后看到如下信息:
也就是说 配置了 contentBase后,服务器就指向了资源的根目录,而不再指向项目的根目录。因此再访问 http://localhost:8080/index.html 是访问不到的。但是访问 http://localhost:8080/bundle.js 该js文件是可以访问的到的。
port
该配置属性指定了开启服务器的端口号
host
该配置属性指定了开启服务器的IP
headers 该配置项可以在HTTP响应中注入一些HTTP响应头。 比如如下:
module.exports = {
devServer: {
headers: {
'X-foo': '112233'
}
}
}
2
3
4
5
6
7
如上配置完成后,打包下,刷新下浏览器,可以看到请求头加了上面的信息,如下所示:
historyApiFallback
该配置项属性是用来应对返回404页面时定向跳转到特定页面的。一般是应用在 HTML5中History API
的单页应用,比如在访问路由时候,访问不到该路由的时候,会跳转到index.html页面。
module.exports = {
devServer: {
historyApiFallback: true
},
}
2
3
4
5
当然如上只是简单的配置下,当然我们也可以手动通过 正则来匹配路由,比如访问 /user 跳转到 user.html,访问 /home 跳转到 home.html, 如下配置:
当然我们需要在 dist 目录下 新建 home.html 和 user.html 了,如下基本配置:
historyApiFallback: {
// 使用正则来匹配路由
rewrites: [
{ from: /^\/user/, to: '/user.html' },
{ from: /^\/home/, to: '/home.html' }
]
}
2
3
4
5
6
7
hot
该配置项是指模块替换换功能,DevServer
默认行为是在发现源代码被更新后通过自动刷新整个页面来做到实时预览的,
但是开启模块热替换功能后,它是通过在不刷新整个页面的情况下通过使用新模块替换旧模块来做到实时预览的。
inline
webpack-dev-server 有两种模式可以实现自动刷新和模块热替换机制。
- 1.iframe
页面是被嵌入到一个iframe页面,并且在模块变化的时候重载页面。
可能如上解释,我们还不能完全能理解到底是什么意思,没有关系,我们继续来看下配置和实践效果。
module.exports = {
devServer: {
inline: false
},
}
2
3
4
5
如上代码配置 inline: false 就是使用iframe模式来重载页面了。
接着我们在浏览器下 输入 http://0.0.0.0:8081/webpack-dev-server/ 地址后 回车,即可看到页面,我们查看源代码的时候,会看到嵌入了一个iframe页面,如下图所示:
当我们重新修改main.js 或 它的依赖文件 demo1.js 的时候,保存后,它也会自动重新加载页面,这就是使用 iframe 模式来配置加载页面的。
iframe 模式的特点有:
- 在网页中嵌入了一个iframe,将我们自己的应用代码注入到 这个 iframe中去了。
- 在页面头部会有一个 App ready. 这个提示,用于显示构建过程的状态信息。
- 加载了 live.bundle.js文件,还同时包含了 socket.io的client代码,进行了 websocket通讯,从而完成了自动编译打包,页面自动刷新功能。
- 2.inline 模式
开启模式,只需要把上面的配置代码变为 inline: true即可,它在构建变化后的代码会通过代理客户端来控制网页刷新。
如上配置后,我们运行 webpack-dev-server 命令后,如下所示:
接着我们在地址栏中 http://0.0.0.0:8081/ 运行下 就可以访问到 项目中的根目录 index.html了,当我们修改入口文件的代码保存也一样 能实时刷新,其实效果是一样的。
inline模式的特点有:
- 构建的消息在控制台中直接显示出来。
- socket.io的client代码被打包进bundle.js当中,这样就能和websocket通讯,从而完成自动编译工作,页面就能实现自动刷新功能。
- 以后的每一个入口文件都会插入上面的socket的一段代码,这样会使的打包后的bundle.js文件变得臃肿。
open
该属性用于DevServer启动且第一次构建完成时,自动使用我们的系统默认浏览器去打开网页。
overlay
该属性是用来在编译出错的时候,在浏览器页面上显示错误。该属性值默认为false,需要的话,设置该参数为true。
stats(字符串)
该属性配置是用来在编译的时候再命令行中输出的内容,我们没有设置 stats的时候,输出是如下的样子:如下所示:
该属性值可以有如下值:
stats:'errors-only'
表示只有错误的才会被打印,没有错误就不打印,因此多余的信息就不会显示出来了。,我们添加下这个配置到devServer中;
该属性值还有 'minimal', 'normal', 'verbose' 等。
compress
该属性是一个布尔型的值,默认为false,当他为true的时候,它会对所有服务器资源采用gzip进行压缩。
proxy 实现跨域
有时候我们使用webpack在本地启动服务器的时候,由于我们使用的访问的域名是 http://localhost:8081 这样的,但是我们服务端的接口是其他的,
那么就存在域名或端口号跨域的情况下,但是很幸运的是 devServer有一个叫proxy配置项,可以通过该配置来解决跨域的问题,那是因为 dev-server 使用了 http-proxy-middleware 包(了解该包的更多用法 (opens new window) )。
假如现在我们本地访问的域名是 http://localhost:8081, 但是我现在调用的是百度页面中的一个接口,该接口地址是:http://news.baidu.com/widget?ajax=json&id=ad。现在我们只需要在devServer中的proxy的配置就可以了: 如下配置:
proxy: {
'/api': {
target: 'http://news.baidu.com', // 目标接口的域名
// secure: true, // https 的时候 使用该参数
changeOrigin: true, // 是否跨域
pathRewrite: {
'^/api' : '' // 重写路径
}
}
}
2
3
4
5
6
7
8
9
10
调用
axios.get('/api/widget?ajax=json&id=ad').then(res => {
console.log(res);
});
2
3
下面我们来理解下上面配置的含义:
- 首先是百度的接口地址是这样的:http://news.baidu.com/widget?ajax=json&id=ad;
- proxy 的配置项 '/api' 和 target: 'http://news.baidu.com' 的含义是,匹配请求中 /api 含有这样的域名 重定向 到 'http://news.baidu.com'来。因此我在接口地址上 添加了前缀 '/api', 如: axios.get('/api/widget?ajax=json&id=ad'); 因此会自动补充前缀,也就是说,url: '/api/widget?ajax=json&id=ad' 等价 于 url: 'http://news.baidu.com/api/widget?ajax=json&id=ad'.
- changeOrigin: true/false 还参数值是一个布尔值,含义是 是否需要跨域。
- secure: true, 如果是https请求就需要改参数配置,需要ssl证书吧。
- pathRewrite: {'^/api' : ''}的含义是重写url地址,把url的地址里面含有 '/api' 这样的 替换成 '', 因此接口地址就变成了 http://news.baidu.com/widget?ajax=json&id=ad; 因此就可以请求得到了,最后就返回 接口数据了。
# 什么 是模块热更新?
模块热更新是Webpack是的一个功能,它可以使得代码修改以后不需刷新浏览器就可以更新,是高级版的自动刷新浏览器。devServer通过hot属性可以控制模块热更替。
通过配置文件
const webpack = require('webpack');
const path = require('path');
let env = process.env.NODE_ENV == "development" ? "development" : "production";
const config = {
mode: env,
devServer: {
hot:true //模块热替换特性
},
plugins: [
new webpack.HotModuleReplacementPlugin(), //热加载插件
]
}
module.exports = config;
2
3
4
5
6
7
8
9
10
11
12
13
通过命令行
"script": {
"start": "NODE_EVN=development webpack-dev-server --config webpack-devlop-config.js --hot"
}
2
3
# Webpack的热更新是如何做到的?说明其原理?
Webpack的热更新有称为热替换(Hot Module Replacement),缩写为HMR。这个机制可以实现不刷新浏览器而将新变更的模块替换旧的模块。原来如下:
server端和client端都做了哪些具体工作:
- 1.第一步,在
Webpack
的watch
模式下,文件系统中某一个文件发生修改,Webpack
监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的JavaScript对象保存在内存中。 - 2.第二步是
Webpack-dev-server
和Webpack
之间的接口交互,而在这一步,主要是dev-server
的中间件Webpack-dev-middleware
和Webpack
之间的交互,Webpack-dev-middleware
调用Webpack
暴露的API对代码变化进行监控,并且告诉webpack,将代码打包到内存中。 - 3.第三步是
Webpack-dev-server
对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase
为true
的时候,Server
会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行live reload
。注意,这儿是浏览器刷新,和HMR
是两个概念。 - 4.第四步也是
webpack-dev-server
代码的工作,该步骤主要是通过sockjs
(webpack-dev-server
的依赖)在浏览器端和服务端之间建立一个websocket
长连接,将Webpack
编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中Server
监听静态文件变化的信息。浏览器端根据这些socket
消息进行不同的操作。当然服务端传递的最主要信息还是新模块的hash
值,后面的步骤根据这一hash
值来进行模块热替换。 - 5.
webpack-dev-server/client
端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了Webpack
,webpack/hot/dev-server
的工作就是根据webpack-dev-server/client
传给它的信息以及dev-server
的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。 - 6.
HotModuleReplacement.runtime
是客户端HMR
的中枢,它接收到上一步传递给他的新模块的hash
值,它通过JsonpMainTemplate.runtime
向server
端发送Ajax
请求,服务端返回一个json
,该json
包含了所有要更新的模块的hash
值,获取到更新列表后,该模块再次通过jsonp
请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。 - 7.而第 10 步是决定
HMR
成功与否的关键步骤,在该步骤中,HotModulePlugin
将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。 - 8.最后一步,当
HMR失
败后,回退到live reload
操作,也就是进行浏览器刷新来获取最新打包代码。
# webpack-dev-server和http服务器如nginx有什么区别?
webpack-dev-server使用内存来存储Webpack开发环境下打包的文件,并且可以使用模块热更新,它比传统的http服务对开发更加简单高效。
# 如何提高webpack的构建速度?
# 常规
保持版本最新 使用最新稳定版本的webpack、node、npm等,较新的版本更够建立更高效的模块树以及提高解析速度。 优化loaders配置
由于loader对文件的转换操作很耗时,所以需要让尽可能少的文件被loader处理。我们可以通过以下3方面优化loader配置:
- 优化正则匹配
- 通过cacheDirectory选项开启缓存
- 通过include、exclude来减少被处理的文件
// webpack.common.js
module: {
rules: [
{
test:/\.js$/,
//babel-loader支持缓存转换出的结果,通过cacheDirectory选项开启
loader:'babel-loader?cacheDirectory',
//只对项目根目录下的src 目录中的文件采用 babel-loader
include: [path.resolve('src')],
//排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules')
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
优化最小化css代码 它是专门对css做压缩、去重的专门为cssnano做的一款插件用来提取用的
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
}
]
},
plugins: [
new ExtractTextPlugin('styles.css'),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: ['default', { discardComments: { removeAll: true } }],
},
canPrint: true
})
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
optimization.splitChunks 提取公共代码
Webpack 4
移除了CommonsChunkPlugin
取而代之的是两个新的配置项optimization.splitChunks
和optimization.runtimeChunk
来简化代码分割的配置。
通过设置 optimization.splitChunks.chunks: "all"
来启动默认的代码分割配置项。
当满足如下条件时,webpack
会自动打包 chunks
:
- 当前模块是公共模块(多处引用)或者模块来自node_modules
- 当前模块大小大于30kb, 如果此模块是按需加载,并行请求的最大数量小于等于5
- 如果此模块在初始页面加载,并行请求的最大数量小于等于 3
runtimeChunk
的作用是将包含chunks
映射关系的 list
单独从 app.js
里提取出来,因为每一个 chunk
的 id
基本都是基于内容 hash
出来的,所以你每次改动都会影响它,如果不将它提取出来的话,等于app.js
每次都会改变。缓存就失效了。
optimization: {
runTimeChunk:{
name:'runtime'
},
splitChunks: {
chunks: 'async', 是否对异步代码进行的代码分割
//默认作用于异步chunk,值为all/initial/async/function(chunk),值为function时第一个参数为遍历所有入口chunk时的chunk模块,chunk._modules为gaichunk所有依赖的模块,通过chunk的名字和所有依赖模块的resource可以自由配置,会抽取所有满足条件chunk的公有模块,以及模块的所有依赖模块,包括css
minSize: 30000, // 引入模块大于30kb才进行代码分割 //默认值是30kb
maxSize: 0, // 引入模块大于Xkb时,尝试对引入模块二次拆分引入
minChunks: 2, // 引入模块至被使用2次后才进行代码分割 //被多少模块共享
maxAsyncRequests: 5, // //所有异步请求不得超过5个,最大请求数
maxInitialRequests: 3, //初始话并行请求不得超过3个
automaticNameDelimiter: '~', // 模块间的连接符,默认为"~"
name: true,//打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
cacheGroups: {//对缓存的文件生效//设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
common: {
name: 'common', //抽取的chunk的名字
chunks(chunk) { //同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取
},
test(module, chunks) { //可以为字符串,正则表达式,函数,以module为维度进行抽取,只要是满足条件的module都会被抽取到该common的chunk中,为函数时第一个参数是遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组。自己尝试过程中发现不能提取出css,待进一步验证。
},
priority: 10, //优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中
minChunks: 2, //最少被几个chunk引用
reuseExistingChunk: true,// 如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码
enforce: true // 如果cacheGroup中没有设置minSize,则据此判断是否使用上层的minSize,true:则使用0,false:使用上层minSize
}
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10 // 优先级,越小优先级越高
},
default: { // 默认设置,可被重写
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 如果本来已经把代码提取出来,则重用存在的而不是重新产生
}
}
}
}
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
为啥移除呢? CommonsChunkPlugin存在以下三个问题:
1:产出的chunk在引入的时候,会包含重复的代码;
2: 无法优化异步chunk;
3:高优的chunk产出需要的minchunks配置比较复杂。
为了解决这些问题,webpack4中用splitchunks替代了CommonsChunkPlugin。
Smaller = false
减少编译的整体大小,以提高构建性能。尽量保持chunks
小巧。
- 使用更小/更少的库
- 移除不需要的代码
- 只编译你在开发的代码
Worker Pool
thread-loader
可以将非常耗性能的loaders
转存到worker pool
中。
不要使用太多的workers
,因为Node.js
的runtime
和loader
有一定的启动开销。最小化workers
和主进程间的模块传输。进程间通讯(IPC
)是非常消耗资源的。
其实就是在多个线程上做计算 **注意:**这个一般是在大项目上使用,在小项目上使用反而会加长构建时间。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve("src"),
use: [
"thread-loader",
// your expensive loader (e.g babel-loader)
]
}
]
}
}
//with option
{
loader: "thread-loader",
// loaders with equal options will share worker pools
options: {
// the number of spawned workers, defaults to (number of cpus - 1) or
// fallback to 1 when require('os').cpus() is undefined
workers: 2,或者require('os').cpus - 1
// number of jobs a worker processes in parallel
// defaults to 20
workerParallelJobs: 50,
// additional node.js arguments
workerNodeArgs: ['--max-old-space-size=1024'],
// Allow to respawn a dead worker pool
// respawning slows down the entire compilation
// and should be set to false for development
poolRespawn: false,
// timeout for killing the worker processes when idle
// defaults to 500 (ms)
// can be set to Infinity for watching builds to keep workers alive
poolTimeout: 2000,
// number of jobs the poll distributes to the workers
// defaults to 200
// decrease of less efficient but more fair distribution
poolParallelJobs: 50,
// name of the pool
// can be used to create different pools with elsewise identical options
name: "my-pool"
}
},
// your expensive loader (e.g babel-loader)
babel-loader
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
持久化缓存
对于一些性能开销较大的loader
之前可以添加cache-loader
,启用持久化缓存。
使用package.json
中的postinstall
清楚缓存目录。
module.exports = {
module: {
rules: [
{
test: /\.ext$/,
use: ['cache-loader', ...loaders],
include: path.resolve('src'),
},
],
},
};
2
3
4
5
6
7
8
9
10
11
Dlls
使用DllPlugin
将更新不频繁的代码进行单独编译。这将改善引用程序的编译速度。即使它增加了构建过程的复杂度。
利用DllPlugin
和DllReferencePlugin
预编译资源模块, 通过DllPlugin
来对那些我们引用但是绝对不会修改的npm
包来进行预编译,再通过DllReferencePlugin
将预编译的模块加载进来。
解析(resolve)
以下几步可以提高解析速度:
- 通过externals踢出去一部分文件,防止打俩份,或者可以直接使用cdn
- 尽量减少
resolve.modules
、resolve.extensions
、resolve.mainFiles
、resolve.desciriptionsFiles
中类目的数量,因为它们会增加文件系统的调用次数。 - 如果你不使用
symlinks
,可以设置resolve.symlinks: false
- 如果你使用自定义解析
plugins
,并且没有指定context
信息,可以设置resolve.cacheWithContext: false
# 开发环境 Development
在内存中编译
以下几个实用的工具通过在内存中进行代码的编译和资源的提供,但并不写入磁盘来提高性能:
- webpack-dev-server
- webpack-hot-middleware
- webpack-dev-middleware
Devtool
需要注意在不同的devtool
的设置,会导致不同的性能差异。
source-map 对于开发调试,打包速度还是影响很大的
inline
不产生独立的 .map 文件,而把 source-map 的内容以 dataURI的方式追加到 bundle 件末尾cheap
忽略列信息,也就是出了问题只能定位到某一行,不能定位到这行的哪一列, cheap 主要是为了提升打包速度,很好理解嘛,只关注行,不关注列,生成map的速度肯定快啊~~module
module 的作用是 map 到 loader 处理前的文件,如果不加 module, 实际上是 map 到源文件经过对应 loader 处理后的样子。这个需要 loader 的支持hidden-source-map
:就是不在 bundle 文件结尾处追加 sourceURL 指定其 sourcemap文件的位置,但是仍然会生成 sourcemap 文件。这样,浏览器开发者工具就无法应用sourcemap, 目的是避免把sourcemap文件发布到生产环境,造成源码泄露。而在生产环境应该用错误报告工具结合 sourcemap 文件来查找问题nosources-source-map
:sourcemap 中不带有源码,这样,sourcemap
可以部署到生产环境而不会造成源码泄露,同时一旦出了问题,error stacks
中会显示准确的错误信息,比如发生在哪个源文件的哪一行,如图:如果你能接受稍微差一些的
mapping
质量,你可以使用cheap-source-map
选择来提高性能使用
eval-source-map
配置进行增量编译
在大多数情况下,cheap-module-eval-source-map
是最好的选择。
避免在生产环境在才会用到的工具
某些实用工具,plugins
和loaders
都只能在构建生产环境时才使用。例如,在开发时使用UglifyJsPlugin
来压缩和修改代码是没有意义的。以下这些工具在开发中通常被排除在外:
- UglifyJsPlugin
- ExtractTextPlugin
[hash]/[chunkhash]
- AggressiveSplittingPlugin
- AggressiveMergingPlugin
- ModuleConcatenationPlugin
最小化入口chunk
webpack只会在文件系统中生成已更新的chunk。应当在生成入口chunk时,尽量减少入口chunk的体积,以提高性能。
# 生产环境 Production
不要为了非常小的性能增益,牺牲了你应用程序的质量!!请注意,在大多数情况下优化代码质量,比构建性能更重要。
多个编译时 当进行多个编译时,以下工具可以帮助到你:
parallel-webpack
: 它允许编译工作在woker池中进行。cache-loader
: 缓存可以在多个编译之间共享。
# 工具相关问题
Babel 项目中的
preset/plugins
数量最小化 TypeScript
- 在单独的进程中使用
fork-ts-checker-webpack-plugin
进行类型检查 - 配置
loaders
时跳过类型检查 - 使用
ts-loader
时,设置happyPackMode: true
以及transpileOnly: true
Saas
node-sass
中有个来自Node.js
线程池的阻塞线程的bug
。当使用thread-loader
时,需要设置workParallelJobs: 2
# 如何利用Webpack来优化前端性能?(提高性能和体验)
用Webpack
优化前端性能是指优化Webpack
输出结果,让打包的结果在浏览器运行快速高效。
- 压缩代码。删除多余的代码/注释,简化代码的写法等等方式。可以利用
Webpack
的UglifyJsPlugin
和ParallelUglifyPlugin
来压缩JavaScript
代码。利用css-loader?minimize
来压缩CSS - 压缩图片。利用
imagemin-webpack-plugin
等图片资源压缩插件,对引用的图片资源进行压缩处理 - 合理的图片资源引用。使用
url-loader
加载解析图片资源时,可以通过配置options limit
参数,将较小的图片资源转换成base64
格式,减少http
请求 - 利用
CDN
加速。在构建过程中,将引用的静态资源路径修改为CDN
上对应的路径。可以利用Webpack
对于output
参数和各个loader
的publicPath
参数来修改资源路径 - 删除死代码(
Tree Shaking
)。将代码中没有引用的代码片段删除掉。可以通过在启动Webpack
时追加参数--optimize-minimize
来实现 - 提取公共代码
# npm打包时需要注意哪些?如何利用Webpack来更好的构建?
npm模块需要注意以下问题:
- 要支持CommonJS模块化规范,所以打包后的最后结果也要支持该规则
- npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传
- npm包大小应该是尽量小(有些仓库会限制包大小)
- 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况
- UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里
# 基于以上需要注意的问题,我们可以对于Webpack配置做以下扩展和优化:
CommonJS
模块化规范的解决方案: 设置output.libraryTarget='commonjs2'
使输出的代码符合CommonJS2
模块化规范,以供给其它模块导入使用- 输出ES5代码的解决方案:使用
babel-loader
把ES6代码转换成ES5
的代码。再通过开启devtool: 'cheap-module-eval-source-map'
输出SourceMap
以发布调试 npm
包大小尽量小的解决方案:Babel
在把ES6
代码转换成ES5
代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc
文件,为其加入transform-runtime
插件- 不能将依赖模块打包到
npm
模块中的解决方案:使用externals
配置项来告诉Webpack
哪些模块不需要打包 - 对于依赖的资源文件打包的解决方案:通过
css-loade
r和extract-text-webpack-plugin
来实现,配置如下:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({ use: ['css-loader'] }) // 提取出chunk中的css到单独的文件中
}
]
},
plugins: [
new ExtractTextPlugin({ filename: 'index.css' })
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 分析工具
speed-measure-webpack-plugin 为你的原始配置包一层 smp.wrap 就可以了,接下来执行构建,你就能在 console 面板看到如它 demo 所示的各类型的模块的执行时长。
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap(YourWebpackConfig);
2
3
4
webpack-bundle-awalyzer 会把所有打包后的文件生成一份文档 webpack-jarvis 这个比上一个 叫美观和详细
# 如何优雅的编写你的Entry
if (/.+\/([a-zA-Z]+-[a-zA-Z]+)(\.entry\.js$)/g.test(item) == true) { const entrykey = RegExp.$1
_entry[entrykey] = item;
const [dist, template] = entrykey.split(“-");
}
2
3
4
# 开发webpack loader
# loader源码分析
⚠️注意:
1、一个 Loader 的职责是单一的,只需要完成一种转换。如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。在调用多个 Loader 去转换一个文件时,每个 Loader 会链式的顺序执行, 第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果会传给下一个接着处理,最后的 Loader 将处理后的最终结果返回给 Webpack。
2、所以,在你开发一个 Loader 时,请保持其职责的单一性,你只需关心输入和输出。
3.use: ['bar-loader', 'foo-loader'] 时,loader 是以相反的顺序执行的
4.最后的 loader 最早调用,传入原始的资源内容(可能是代码,也可能是二进制文件,用 buffer 处理)第一个 loader 最后调用,期望返回是 JS 代码和 sourcemap 对象(可选)中间 的 loader 执行时,传入的是上一个 loader 执行的结果
5.多个 loader 时遵循这样的执行顺序,但对于大多数单个 loader 来说无须感知这一点,只负 责好处理接受的内容就好。
6.还有一个场景是 loader 中的异步处理。有一些 loader 在执行过程中可能依赖于外部 I/O 的结果,导致它必须使用异步的方式来处理,这个使用需要在 loader 执行时使用 this.async() 来标识该 loader 是异步处理的,然后使用 this.callback 来返回 loader 处理结果。
在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源 代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结 构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
Webpack 提供的一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些loader ,比如 url-loader 等等来让我们可以直接在源文件中引用各类资源。最后调用 acorn(Esprima) 解析经 loader 处理后的源文件生成抽象语法树 AST
- type:描述该语句的类型 --变量声明语句
- kind:变量声明的关键字 -- var
- declaration: 声明的内容数组,里面的每一项也是一个对象 type: 描述该语句的类型
- id: 描述变量名称的对象 type:定义
- name: 是变量的名字 init: 初始化变量值得对象
- type: 类型
- value: 值 "is tree" 不带引号 row: ""is tree"" 带引号
//入口文件
const a = 20;
-------------------------
loader/index.js
-------------------------
//loader执行之前
module.exports.pitch = function(r1,r2,data){
data.value = 'yd';
}
//同步loader
module.exports = function(content, map, meta){
console.log('得到的数据', content);//将const a = 20;转化成buffer <Buffer xxxxx ... >
console.log('loader预先得到的数据', this.data.value);//yd
return '{};'+content;
// this.callback(null, content, map, meta);
};
//异步loader
module.exports = function(content, map, meta){
var callback = this.async();
(funciton(){....}).then(function(){
if(err){
callback(err);
}else{
callback(null, ......)
}
})
};
//流的方式
//module.exports.raw = true;
-----------------------------------
webpack.config.js
-----------------------------------
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: path.resolve('./loader/index.js')
}
]
}
};
//最后生成代码
eval("{};const a = 20;\r\n\r\n//import bar from './bar.js';\r\n//bar.init();\n\n//# sourceURL=webpack:///./src/index.js?");
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
41
42
43
44
45
46
# 编写过webpack插件
webpack实现插件机制的⼤大体⽅方式是:
- 「创建」—— webpack在其内部对象上创建各种钩⼦;
- 「注册」—— 插件将⾃己的方法注册到对应钩⼦子上,交给webpack;
- 「调⽤」—— webpack编译过程中,会适时地触发相应钩⼦子,因此也 就触发了插件的⽅法。
打开 Webpack 4.0 的源码中一定会看到下面这些以 Sync、Async 开头,以 Hook 结尾的方法,这些都是 tapable 核心库的类,为我们提供不同的事件流执行机制,我们称为 “钩子”。
// 引入 tapable 如下
const {
SyncHook, //串行同步执行,不关心事件处理函数的返回值,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数。
SyncBailHook, //同样为串行同步执行,如果事件处理函数执行时有一个返回值不为空(即返回值为 undefined),则跳过剩下未执行的事件处理函数(如类的名字,意义在于保险)。
SyncWaterfallHook, // 为串行同步执行,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,依次类推,正因如此,只有第一个事件处理函数的参数可以通过 call 传递,而 call 的返回值为最后一个事件处理函数的返回值。
SyncLoopHook, // 为串行同步执行,事件处理函数返回 true 表示继续循环,即循环执行当前事件处理函数,返回 undefined 表示结束循环,SyncLoopHook 与 SyncBailHook 的循环不同,SyncBailHook 只决定是否继续向下执行后面的事件处理函数,而 SyncLoopHook 的循环是指循环执行每一个事件处理函数,直到返回 undefined 为止,才会继续向下执行其他事件处理函数,执行机制同理。
AsyncParallelHook, //异步并行执行,通过 tapAsync 注册的事件,通过 callAsync 触发,通过 tapPromise 注册的事件,通过 promise 触发(返回值可以调用 then 方法)。
AsyncParallelBailHook,异步并行执行,返回值不为 undefined,即有返回值,则立即停止向下执行其他事件处理函数,
AsyncSeriesHook, //异步串行同上
SyncBailHook。
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
2
3
4
5
6
7
8
9
10
11
12
13
详细Webpack 核心模块 tapable 解析 (opens new window)
上面的实现事件流机制的 “钩子” 大方向可以分为两个类别,“同步” 和 “异步”,“异步” 又分为两个类别,“并行” 和 “串行”,而 “同步” 的钩子都是串行的。
webpack 利用了 tapable 这个库( (opens new window))来协助实现对于整个 构建流程各个步骤的控制。 tapable 定义了主要构建流程后,使用 tapable 这个库添加了各种各样的 钩子方法来将 webpack 扩展至功能十分丰富,这就是plugin 的机制。也可以说webpack核心使用Tapable 来实现插件(plugins)的binding和applying.Tapable是一个用于事件发布订阅执行的插件架构。Tapable就是webpack用来创建钩子的库。
那么让我们打开webpack->package.json->main -> webpac.js 一起分析~
创建 Compiler ->
调用 compiler.run 开始构建 ->
创建 Compilation ->
基于配置开始创建 Chunk ->
使用 Parser 从 Chunk 开始解析依赖 ->
使用 Module 和 Dependency 管理代码模块相互关系 ->
使用 Template 基于 Compilation 的数据生成结果代码
2
3
4
5
6
7
⚠️注意:
- 1.事件钩子会有不同的类型 SyncBailHook,AsyncSeriesHook,SyncHook等。
- 2.如果是异步的事件钩子,那么可以使用 tapPromise 或者 tapAsync 来注册事件函数, tapPromise 要求方法返回 Promise 以便处理异 步,而 tapAsync 则是需要用 callback 来返回结 果。
compiler.hooks.done.tapPromise('PluginName', (stats) => {
return new Promise((resolve, reject) => {
// 处理promise的返回结果 reject(err) : resolve()})
2
3
4.compiler.hooks.done.tapAsync('PluginName', (stats, callback) => { callback(err)) })
5.除了同步和异步的,名称带有 parallel 的,注册的事件函数会并行调用,名称带有 bail 的,注册的事件函数会被顺序调用,直至一个处理方法有返回值名称带有 waterfall 的,每个 注册的事件函数,会将上一个方法的返回结果作为输入参数。有一些类型是可以结合到一起 的,如 AsyncParallelBailHook,这样它就具备了更加多样化的特性。
compiler.hooks.done.tapPromise('PluginName', (stats) => {
return new Promise((resolve, reject) => {
// 处理promise的返回结果 reject(err) : resolve()
});
compiler.hooks.done.tapAsync('PluginName', (stats, callback) => {
callback( err))
});
2
3
4
5
6
7
- 1、Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
- 2、Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。并且可以通过 compiler 对象去操作 Webpack。
- 3、在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。Compiler 和 Compilation 的含义如下:Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
- 4、Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
- 5、开发插件时需要注意:只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用、传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件、有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。
# webpack编译过程或构建流程是什么?
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程,
- 1、初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- 2、开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 3、确定入口:根据配置中的 entry 找出所有的入口文件;
- 4、编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 5、完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 6、输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 7、输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
详细的可以看下边的源码分析
# Webpack整体运行流程
=======
# Webpack整体运行流程
webpack 本质上就是一个 JS Module Bundler,用于将多个代码模块进行打包。bundler 从一个构 建入口出发,解析代码,分析出代码模块依赖关系,然后将依赖的代码模块组合在一起,在 JavaScript bundler 中,还需要提供一些胶水代码让多个代码模块可以协同工作,相互引用。下边会 举一些简单的例子来说明一下这几个关键的部分是怎么工作的。
// 分别将各个依赖模块的代码⽤ modules 的⽅式组织起来打包成⼀个⽂件
================================entry======================================
// entry.js
import { bar } from './bar.js'; // 依赖 ./bar.js 模块
// bar.js
const foo = require('./foo.js'); // 依赖 ./foo.js 模块
递归下去,直至没有更多的依赖模块,最终形成一颗模块依赖树。
分析出依赖关系后,webpack 会利用 JavaScript Function 的特性提供一些代码来将各个模块整 合到一起,即是将每一个模块包装成一个 JS Function,提供一个引用依赖模块的方法,如下面例子 中的 __webpack__require__,这样做,既可以避免变量相互干扰,又能够有效控制执行顺序
================================moudles======================================
// entry.js
modules['./entry.js'] = function() {
const { bar } = __webpack__require__('./bar.js')
}
// bar.js
modules['./bar.js'] = function() {
const foo = __webpack__require__('./foo.js')
};
// foo.js
modules['./foo.js'] = function() {
// ...
}
// 分别将各个依赖模块的代码用 modules 的方式组织起来打包成一个文件
================================output===========================
// 已经执⾏的代码模块结果会保存在这⾥
(function(modules){
const installedModules = {}
function __webpack__require__(id) {
// 如果 installedModules 中有就直接获取
// 没有的话从 modules 中获取 function 然后执⾏,
//将结果缓存在 installedModules 中然后返回结果
}
})({
"./entry.js": (function(__webpack_require__){
var bar = __webpack_require__(/*code内容*/)
}),
"./bar.js": (function(){}),
"./foo.js": (function(){}),
})
其实webpack就是把AST分析树 转化成 链表
// 如果 installedModules 中有就直接获取
// 没有的话从 modules 中获取 function 然后执行,
//将结果缓存在 installedModules 中然后返回结果
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
41
42
43
44
45
46
47
48
49
50
1.
Compiler
webpack 的运行入口,compiler
对象代表了完整的webpack
环境配置。这个对象 在启动webpack
时被一次性建立,并配置好所有可操作的设置,包括options
,loader
和plugin
。当在webpack
环境中应用一个插件时,插件将收到此compiler
对象的引用,可以使用 它来访问webpack
的主环境。2.
Compilation
对象代表了一次资源版本构建。当运行webpack
开发环境中间件时,每当检 测到一个文件变化,就会创建一个新的compilation
,从而生成一组新的编译资源。一个compilation
对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状 态信息。compilation
对象也提供了很多关键步骤的回调,以供插件做自定义处理时选择使 用。3.
Chunk
,即用于表示chunk
的类,对于构建时需要的chunk
对象由Compilation
创建后保存 管理(webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实 例)4.Module (opens new window),用于表示代码 模块的基础类,衍生出很多子类用于处理不同的情况(NormalModule (opens new window))关于代码模块的所有信息都会存在 Module 实例中,例如 dependencies 记录代码模块的依赖等,当一个
Module
实例被创建后,比较重要的一步是执行compilation.buildModule
这个方法,它 会调用Module
实例的build
方法来创建Module
实例需要的一些东西,然后调用自身的runLoaders
方法。runLoaders
:loader-runner (opens new window),执 行对应的loaders
,将代码源码内容一一交由配置中指定的loader
处理后,再把处理的结果保 存起来。5.
Parser
,其中相对复杂的一个部分,基于acorn
来分析AST
语法树,解析出代码模块的依 赖6.
Dependency
,解析时用于保存代码模块对应的依赖使用的对象。Module
实例的build
方法 在执行完对应的loader
时,处理完模块代码自身的转换后,继续调用Parser
的实例来解析自 身依赖的模块,解析后的结果存放在module.dependencies
中,首先保存的是依赖的路径,后续会经由compilation.processModuleDependencies
方法,再来处理各个依赖模块,递归地去建立 整个依赖。7.
Template
,生成最终代码要使用到的代码模板,像上述提到的胶水代码就是用对应的Template
来生成。Template
基础类:lib/Template.js (opens new window) 常用的主要Template
类:lib/MainTemplate.js (opens new window)
# 源码分析webpack执行流程
- 1.webpack.config.js,shell options参数解析
- 2.new webpack(options)
- 3.run() 编译的入口方法
- 4.compile() 触发make事件
- 5.addEntry() 找到js文件,进行下一步模块绑定
- 6._addModuleChain() 解析js入口文件,创建模块
- 7.buildModule() 编译模块,loader处理与acorn处理AST语法树
- 8.seal() 每一个chunk对应一个入口文件
- 9.createChunkAssets() 生成资源文件
- 10.MainTemplate.render() __webpack__require()引入
- 11.ModuleTemplate.render() 生成模版
- 12.module.source() 将生成好的js保存在compilation.assets中
- 13.Compiler.emitAssets()通过emitAssets将最终的js输出到output的path中
1.参数解析
(function(){
yargs.options({...})
yargs.parse(process.argv.slice(2), (err, argv, output) => {...})
})()
2
3
4
5
加载webpack.config.js
(function(){
...
yargs.parse(process.argv.slice(2), (err, argv, output) => {
...
//解析argv,拿到配置文件参数
let options = require("./convert-argv")(argv);
function processOptions(options){
...
}
processOptions(options);
})
})()
2
3
4
5
6
7
8
9
10
11
12
13
执行webpack()
(function(){
...
yargs.parse(process.argv.slice(2), (err, argv, output) => {
...
//解析argv,拿到配置文件参数
let options = require("./convert-argv")(argv);
function processOptions(options){
...
const webpack = require("webpack");
compiler = webpack(options);
}
processOptions(options);
})
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
webpack.js
const webpack = (options, callback) => {
//验证webpack.config.js合法性
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
/*
[
{ entry: './index1.js', output: { filename: 'bundle1.js' } },
{ entry: './index2.js', output: { filename: 'bundle2.js' } }
]
*/
if (Array.isArray(options)) {
compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if(typeof options === "object"){
...
//创建一个comiler对象
compiler = new Compiler(options.context);
//往comiler中注册插件
new NodeEnvironmentPlugin().apply(compiler);
//执行config中配置的插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
//执行插件environment生命周期钩子方法
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
//执行webpack内置插件
compiler.options = new
WebpackOptionsApply().process(options, compiler);
}else {
throw new Error("Invalid argument: options");
}
if (callback) {
...
//调用compiler.run开始编译
compiler.run(callback);
}
//将compiler对象返回
return compiler
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
//NodeEnvironmentPlugin.js
class NodeEnvironmentPlugin {
apply(compiler) {
...
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
});
}
}
module.exports = NodeEnvironmentPlugin;
2
3
4
5
6
7
8
9
10
class WebpackOptionsApply extends OptionsApply {
constructor() {
super();
}
process(options, compiler) {
//挂载配置,执行插件
let ExternalsPlugin;
compiler.outputPath = options.output.path;
compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
compiler.recordsOutputPath =
options.recordsOutputPath || options.recordsPath;
compiler.name = options.name;
new EntryOptionPlugin().apply(compiler);
new HarmonyModulesPlugin(options.module).apply(compiler);
new LoaderPlugin().apply(compiler);
...
}
}
module.exports = WebpackOptionsApply;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
compiler.run() 开始编译
class Compiler extends Tapable{
constructor(context){
...
}
watch(){...}
run(callback){
...
const onCompiled = (err, compilation){
...
}
//执行生命周期钩子
this.hooks.beforeRun.callAsync(this, err => {
...
this.hooks.run.callAsync(this, err =>{
this.readRecords(err =>{
...
//开始编译
this.compile(onCompiled);
})
}
}
}
compile(callback) {
//拿到参数
const params = this.newCompilationParams();
//执行编译前钩子
this.hooks.beforeCompile.callAsync(params, err => {
...
//创建compilation对象
const compilation = this.newCompilation(params);
//开始构建模块对象
this.hooks.make.callAsync(compilation, err =>{
})
}
}
createCompilation() {
//创建comilation对象
return new Compilation(this);
}
newCompilation(params) {
//调用创建compilation对象方法
const compilation = this.createCompilation();
}
}
module.exports = Compiler;
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
41
42
43
44
45
46
47
48
49
创建 Compilation()
class Compilation extends Tapable {
constructor(compiler) {
super();
...
//初始化配置
this.compiler = compiler;
this.resolverFactory = compiler.resolverFactory;
this.inputFileSystem = compiler.inputFileSystem;
this.requestShortener = compiler.requestShortener;
//初始化模版
this.mainTemplate = new MainTemplate(this.outputOptions);
this.chunkTemplate = new ChunkTemplate(this.outputOptions);
this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate(
this.outputOptions
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainTemplate extends Tapable {
this.hooks.requireExtensions.tap("MainTemplate", (source, chunk, hash) => {
const buf = [];
const chunkMaps = chunk.getChunkMaps();
// Check if there are non initial chunks which need to be imported using require-ensure
if (Object.keys(chunkMaps.hash).length) {
buf.push("// This file contains only the entry chunk.");
buf.push("// The chunk loading function for additional chunks");
buf.push(`${this.requireFn}.e = function requireEnsure(chunkId) {`);
buf.push(Template.indent("var promises = [];"));
buf.push(
Template.indent(
this.hooks.requireEnsure.call("", chunk, hash, "chunkId")
)
);
buf.push(Template.indent("return Promise.all(promises);"));
buf.push("};");
} else if (
chunk.hasModuleInGraph(m =>
m.blocks.some(b => b.chunkGroup && b.chunkGroup.chunks.length > 0)
)
) {
// There async blocks in the graph, so we need to add an empty requireEnsure
// function anyway. This can happen with multiple entrypoints.
buf.push("// The chunk loading function for additional chunks");
buf.push("// Since all referenced chunks are already included");
buf.push("// in this file, this function is empty here.");
buf.push(`${this.requireFn}.e = function requireEnsure() {`);
buf.push(Template.indent("return Promise.resolve();"));
buf.push("};");
}
buf.push("");
buf.push("// expose the modules object (__webpack_modules__)");
buf.push(`${this.requireFn}.m = modules;`);
buf.push("");
buf.push("// expose the module cache");
buf.push(`${this.requireFn}.c = installedModules;`);
buf.push("");
buf.push("// define getter function for harmony exports");
buf.push(`${this.requireFn}.d = function(exports, name, getter) {`);
buf.push(
Template.indent([
`if(!${this.requireFn}.o(exports, name)) {`,
Template.indent([
"Object.defineProperty(exports, name, { enumerable: true, get: getter });"
]),
"}"
])
);
buf.push("};");
buf.push("");
buf.push("// define __esModule on exports");
buf.push(`${this.requireFn}.r = function(exports) {`);
buf.push(
Template.indent([
"if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {",
Template.indent([
"Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });"
]),
"}",
"Object.defineProperty(exports, '__esModule', { value: true });"
])
);
buf.push("};");
buf.push("");
buf.push("// create a fake namespace object");
buf.push("// mode & 1: value is a module id, require it");
buf.push("// mode & 2: merge all properties of value into the ns");
buf.push("// mode & 4: return value when already ns object");
buf.push("// mode & 8|1: behave like require");
buf.push(`${this.requireFn}.t = function(value, mode) {`);
buf.push(
Template.indent([
`if(mode & 1) value = ${this.requireFn}(value);`,
`if(mode & 8) return value;`,
"if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;",
"var ns = Object.create(null);",
`${this.requireFn}.r(ns);`,
"Object.defineProperty(ns, 'default', { enumerable: true, value: value });",
"if(mode & 2 && typeof value != 'string') for(var key in value) " +
`${this.requireFn}.d(ns, key, function(key) { ` +
"return value[key]; " +
"}.bind(null, key));",
"return ns;"
])
);
buf.push("};");
buf.push("");
buf.push(
"// getDefaultExport function for compatibility with non-harmony modules"
);
buf.push(this.requireFn + ".n = function(module) {");
buf.push(
Template.indent([
"var getter = module && module.__esModule ?",
Template.indent([
"function getDefault() { return module['default']; } :",
"function getModuleExports() { return module; };"
]),
`${this.requireFn}.d(getter, 'a', getter);`,
"return getter;"
])
);
buf.push("};");
buf.push("");
buf.push("// Object.prototype.hasOwnProperty.call");
buf.push(
`${
this.requireFn
}.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };`
);
const publicPath = this.getPublicPath({
hash: hash
});
buf.push("");
buf.push("// __webpack_public_path__");
buf.push(`${this.requireFn}.p = ${JSON.stringify(publicPath)};`);
return Template.asString(buf);
});
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
make开始构建
//开始构建模块对象
this.hooks.make.callAsync(compilation, err =>{
})
2
3
4
//SingleEntryPlugin 监听make
class SingleEntryPlugin {
apply(compiler) {
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
//创建依赖
const dep = SingleEntryPlugin.createDependency(entry, name);
//添加入口文件
compilation.addEntry(context, dep, name, callback);
}
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Compilation.js
class Compilation extends Tapable {
addEntry(context, entry, name, callback) {
...
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
...
}
);
}
_addModuleChain(context, dependency, onModule, callback) {
...
//获取模块工厂
const moduleFactory = this.dependencyFactories.get(Dep);
this.semaphore.acquire(() => {
...
//创建模块
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},...)
}
}
}
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
class NormalModuleFactory extends Tapable {
...
create(data, callback) {
...
this.buildModule(module, false, null, null, err => {
}
}
buildModule(module, optional, origin, dependencies, thisCallback) {
...
//开始编译
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,...)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//NodmalModule
doBuild(options, compilation, resolver, fs, callback) {
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
...
//开始运行loader
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
);
) }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
总结
初始化阶段
事件名 | 解释 | 代码位置 |
---|---|---|
读取命令行参数 | 从命令行中读取用户输入的参数 | require('./convert-argv')(argv) |
实例化Compiler | 1.用上一步得到的参数初始化Compiler实例 2.Compiler负责文件监听和启动编译3.Compiler实例中包含了完整的Webpack配置(所有的),全局只有一个Compiler实例 | compiler = webpack(options) |
加载插件 | 1.依次调用插件的apply方法,让插件可以监听后续的所有事件节点,同时给插件传入Compiler实例的引用,以方便插件通过compiler调用webpack提供的API | plugin.apply(compiler) |
处理入口 | 读取配置的Entry,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备 | new EntryOptionsPlugin() new SingleEntryPlugin(context,item,name) compiler.hooks.make.tapAsync |
编译阶段
事件名 | 解释 | 代码位置 |
---|---|---|
run | 启动一次新的编译 | this.hooks.run.callAsync |
compile | 该事件是为了告诉插件一次新的编译将要启动,同时会给插件传入compiler对象 | compiler(callback) |
compilation | 当webpack以开发模式运行时,每当监测到文件变化,一次新的,Compilation将被创建一个Compilation对象包含了当前的模块资源,编译生成资源,变化的文件,Compilation对象也提供了很多事件回调供插件扩展 | newCompilation(params) |
make | 一个新的Compilation创建完毕开始编译 | this.hooks.make.callAsync |
addEntry | 即将从Entry开始读取文件 | compilation.addEntry this._addModuleChain |
moduleFactory | 创建模块工厂 | const moduleFactory = this.dependencyFactories.get(Dep) |
create | 开始创建模块 | factory(result,(err,module) this.hooks.resolver.tap("NormalModule") |
resolveRequestArray | 解析loader路径 | resolveRequestArray |
resolve | 解析资源路径 | resolve |
userRequest | 得到包括loader在内的资源文件的绝对路径用!拼起来的字符串 | userRequest |
ruleSet.exec | 它可以根据模块路径名,匹配出模块所需的loader | this.ruleSet.exec |
_run | 它可以根据模块路径名,匹配出模块所需的loader | _run |
loaders | 得到所有的loader数组 | results[0].concat(loaders,results[1],results[2]) |
getParser | 获取AST解析器 | this.getParser(type,setting.parser) |
buildModule | 开始编译模块 | thislbuildModule(module) buildModule(module,optional,origin,dependencies,thisCallback) |
build->doBuild | 开始编译 | build->doBuild |
loader | 使用loader进行转换 | runLoaders |
iteratePitchingLoaders | 开始递归执行pitchloader | iteratePitchingLoaders |
loadLoader | 加载loader | loadLoader |
runSyncOrAsync | 执行loader | runSyncOrAsync |
processResource | 开始处理资源 | processResource options.readResource iterateNormalLoaders |
createSource | 创建源码对象 | this.createSource |
parse | 使用parser转换抽象语法树以及抽象语法树 | this.parser.parse/parse(source,initialState) |
acorn.parse | 继续抽象ast语法树 | acorn.parse(code,parserOptions) |
ImportDependency | 遍历添加依赖 | parser.state.module.addDependency |
succeedModule | 生成语法树后就表示一个模块编译完成 | this.hooks.successdModule.call(module) |
processModuleDependencies | 递归编译依赖模块 | his.processModuleDependencies |
make后 | 结束make,也就是一个compilation编译结束 | this.hooks.make.callAsync |
finish | 编译完成 | compilation.finishi() |
结束阶段 |事件名|解释|代码位置| |:--😐:--😐:--😐 |seal|封装|compilation.seal| |addChunk|生成资源| addChunk(name)| |createChunkAssets|创建资源|this.createChunkAssets()| |getRenderManifest|获取要渲染的描述文件|getRenderManifest(options)| |render|渲染源码|source = fileManifest.render()| |afterCompile|编译结束|this.hooks.afterCompile| |shouldemit|所有属性输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要|this.hooks.shouldEmit| |emit|确定后要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容|this.emitAssets(compilation,this.hooks.emit.callAsync) const emitFiles = err this.outputFileSystem.writeFile| |this.emitRecords|写入记录|this.emitRecords| |done|全部完成|this.hooks.done.callAsync|
最后就是尝试给babel写个插件(https://github.com/jamiebuilds/babel-handbook/)
webpack的1版本和2版本都以及过时了,为了遇到一些老得项目时可用 后期补webpack1 2 3 4 的不同
← Vite2插件开发指南 yeoman →