问:webpack 究竟解决了什么问题

  • 模块化解决方案

在早前web前端只需要一个简单的 html 页面,插入几条script标签 去引用 js 文件就可以满足需求,随着项目越来越复杂,要实现的功能越来越多,文件也越来越多,全部都这么引入已经不再现实,这时候前端模块化就出现了,从AMD、CMD 到现在的 ES6 模块化写法,我们可以把代码拆成一个个 JS 文件,通过 import 去关联依赖文件,最后再通过某个打包工具把这么多 js 文件按照依赖关系最终打包成一个或多个 js 文件在html 页面去引入。
所以 webpack首要要解决的问题是将多个模块化的 js文件 按照依赖关系打包为一个或多个文件,所以我们通常都会说他是一个模块化解决方案

  • 处理资源转换

随着 ES6,ES7,ES8 的出现,还有 vue、react 等前端框架的出现,我们发现这些文件浏览器是不能直接执行的,需要我们中间编译转换一下为浏览器可执行的文件,所以这时候 webpack 要做的事情又多了一项,按照依赖打包的同时,还要对源文件进行编译转换处理,也就是我们日常配置的 loader 处理。

  • tree-shaking以及代码压缩

现在webpack已经支持了对文件编译转换后再进行打包,满足了我们的基本需求。这时候我们又开始对性能提出了要求,希望打包出的体积越小越好。比如有些文件虽然整个引用了,但其实真正只用了其中部分代码,没用到的部分希望可以被剔除掉。这种是通过剔除无效代码来减小总的打包体积,另外一种方式是通过代码压缩,比如空格、较长的函数名都可以被压缩。因此webpack支持了 tree-shaking和代码压缩。

  • 代码拆分(异步加载 + 抽出第三方公用库)

现在 webpack 打包结果是不是做到了极致了呢?不行,我们还是嫌弃最终打包出的文件体积太大了。这时候懒加载(异步加载)出现了,你只需要把进入首页时所需要的所有资源打包为一个文件输出就行,这样进入首页我只需要加载该文件就行,其他资源文件等我真正执行的时候再去加载就可以。就这样,webpack又支持了异步加载文件的拆包功能,这时候我们最终打包出的主文件只是当前首页需要的资源。

  • 开发辅助工具的提供

我们对于打包的基本需求以及性能需求终于得到了满足,又开始追求开发时的体验了,开发越便捷越好,webpack 就提供了一系列的开发辅助功能,比如 devserver,HMR 等等什么的帮助我们高效的开发。

现在我们回过头总结下看,webpack帮我们做了好多事啊。

  • 作为一个模块化解决方案,帮助我们将繁多的 JS 模块化文件按照依赖关系打包 为一个或多个文件输出
  • 支持针对文件指定文件进行编译转换后再打包
  • 支持针对打包后的内容优化、压缩处理
    来减小总的文件体积
  • 支持异步加载以及其他拆包方式
  • 提供一系列开发辅助工具

概述

webpack的构建从处理入口文件开始着手,首先解析入口文件,需要 经过 loader转换编译这时候就转换编译,转换完了开始分析是否有依赖文件,有依赖文件就接着处理依赖文件,流程和刚刚一致,需要编译转换就转换,然后接着解析依赖文件是否还有依赖文件,有再接着处理。就这样通过入口文件以及依赖关系,webpack 可以获取并处理所有的依赖文件。然后再基于这些文件做进一步的优化处理,比如 treeshaking 或者 代码压缩,最后生成为我们需要的一个或多个js 文件。

1. 准备工作

webpack 首先会将我们的配置文件和它自己的默认配置做一个 merge,生成最终的一个配置文件,其次会将这个最终配置文件里的所有插件plugin在这个时候都注册好,在这里要提一下 webpack 的事件机制,他是基于一个 tapable库做的事件流控制,在整个的编译过程中暴露出各种hook,而我们写的 plugin 也就是去注册监听了某个 hook,在这个 hook 触发时,去执行我们的 plugin。

2. 处理入口文件

在 webpack 的处理中多种入口最后都会转化为同一方法去处理,单入口不用说,多入口我可以先遍历,再去执行该方法,动态入口,我先执行函数再去处理,最终都会进入到 生成入口文件 module 实例阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 入口文件
module.exports = {
// 单入口
entry: {
main: './src/index.js'
},

// 多入口
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
},

// 动态入口
entry: () => new Promise((resolve) => resolve(['./demo', './demo2']))
};

大家都说 webpack 中一切文件都是 module,那 module 是什么呢,其实他就是一个存了当前文件所有信息的一个对象而已,这个文件包含了以下信息。

1
2
3
4
5
6
7
8
9
10
11
12
module = {
type,
request,
userRequest,
rawRequest,
loaders,
resource,
matchResource,
parser,
generator,
resolveOptions
}

3.生成文件 module实例

1. resolve 阶段

通过我们的 resolve 配置和 rules 配置去获取到当前文件的绝对路径和需要经过哪些loader 进行处理,然后将这些信息存到我们当前这个文件对应的 module 实例里面

2.执行 loader 阶段

loader 的执行是倒序

loader的执行分为 2 个阶段: pitching,执行 loader 上的 pitch 方法;normal,执行 loader 常规方法

执行完 loader 后,也就是对文件做了编译转换,使其变成了最终可以被浏览器执行的代码。

3. parse 阶段

webpack 是采用将loader 执行过后的源文件source转换为AST 去分析依赖。这里使用了acorn 库,将source生成对应的 AST。生成的 AST 划分为 3部分,ImportDeclaration、FunctionDeclaration和VariablesDeclaration,接下来遍历 AST 去收集依赖。找到 import 等关键字去达到依赖收集的目的。

4.递归处理依赖

基于我们解析到的依赖文件,我们要开始递归处理依赖了,又回到了我们处理入口文件的整个流程,去生成依赖文件的 module 实例,再执行 对应loader。就这样 webpack 递归处理了所有的依赖文件并完成了所有文件的转换

4. 生成 chunk

根据用户配置的 optimization.slitChunks 或者默认的配置,用上一步生成的module,来生成 chunk

  1. 生成 Module-graph
  2. 生成 Basic-chunk-graph
  3. 生成最终的 chunk-graph

5. 优化

  1. 首先生成对应的 moduleId ,不做任何配置的话,默认采用以自增 id 的方式,推荐 hash 的方式,有利于缓存
  2. 基于生成的 moduleId进行排序
  3. 接着类似于 module 的操作,对应生成 chunkId ,并根据 chunkId进行排序
  4. 分别为 module 和chunk 生成hash

6. 生成文件

webpack 把这些文件按照内置的 template 渲染生成最终的打包文件。

总结

总结一下 webpack 的整个构建打包过程,首先通过依赖关系和 loader 配置获取经过编译转换后的所有module 实例,然后再根据配置进行拆分为一个或多个chunk,最后按照内置的template 渲染出最终的文件输出。