查找打包入口文件

  1. 通过 npm scripts 运行 webpack

    • 开发环境: npm run dev
    • 生产环境:npm run build
  2. 通过 webpack 直接运行

    • webpack entry.js bundle.js

在命令行运行以上命令后,npm会让命令行工具进入node_modules.bin 目录查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在,就执行,不存在,就抛出错误。实际的入口文件是:node_modules\webpack\bin\webpack.js

webpack.js

process.exitCode = 0; //1\. 正常执行返回
const runCommand = (command, args) =>{...}; //2\. 运行某个命令
const isInstalled = packageName =>{...}; //3\. 判断某个包是否安装
const CLIs =[...]; //4\. webpack 可用的 CLI: webpack-cli 和
webpack-command
const installedClis = CLIs.filter(cli => cli.installed); //5\. 判断是否两个 ClI 是否安装了
if (installedClis.length === 0){...}else if //6\. 根据安装数量进行处理
(installedClis.length === 1){...}else{...}.

最终找到 webpack-cli (webpack-command) 这个 npm 包,并且执行 CLI

webpack-cli做了什么

  • 引入 yargs,对命令行进行定制
  • 分析命令行参数,对各个参数进行转换,组成编译配置项
  • 引用webpack,根据配置项进行编译和构建

主要看引用webpack做的事情

Webpack 的本质

Webpack可以将其理解是一种基于事件流的编程范例,一系列的插件运行。

class Compiler extends Tapable{
 //...
}
class Compilation extends Tapable {
 // ... 
}

Tabable是什么

Tapable 是一个类似于 Node.js 的 EventEmitter 的库, 主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。

Tapable库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子

const {
    SyncHook, //同步钩子
    SyncBailHook, //同步熔断钩子
    SyncWaterfallHook, //同步流水钩子
    SyncLoopHook, //同步循环钩子
    AsyncParallelHook, //异步并发钩子
    AsyncParallelBailHook, //异步并发熔断钩子
    AsyncSeriesHook, //异步串行钩子
    AsyncSeriesBailHook, //异步串行熔断钩子
    AsyncSeriesWaterfallHook //异步串行流水钩子
} = require("tapable");

Tabable钩子类型

type function

Hook 所有钩子的后缀

Waterfall 同步方法,但是它会传值给下一个函数

Bail 熔断:当函数有任何返回值,就会在当前执行函数停止

Loop 监听函数返回true表示继续循环,返回undefined表示结束循环

Sync 同步方法

AsyncSeries 异步串行钩子

AsyncParallel 异步并行执行钩子

Tapable的使用 -new Hook 新建钩子

Tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子

class接受数组参数options,非必传。

const hook1 = new SyncHook(['arg1','arg2','arg3'])

Tapable的使用 -钩子的绑定与执行

Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。

Async* Sync*

绑定:tapAsync/tapPromise/tap 绑定:tap

执行:callAsync/promise 执行:call

Tapable的使用 -hook基本用法示例

const hook1 = new SyncHook(['arg1','arg2','arg3'])
//绑定事件到webpack事件流
hook1.tap('hook1',(arg1,arg2,arg3)=>{
    console.log(arg1,arg2,arg3)
})
//执行绑定的事件
hook1.call(1,2,3)

Tapable是如何和webpack联系起来的

if (Array.isArray(options)) {
    compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if (typeof options === "object") {
    options = new WebpackOptionsDefaulter().process(options);
    compiler = new Compiler(options.context);
    compiler.options = options;
    new NodeEnvironmentPlugin().apply(compiler);
    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);
            }
        }
    }
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    compiler.options = new WebpackOptionsApply().process(options, compiler);
}
```![](https://uploadfiles.nowcoder.com/images/20190919/357373100_1568889674884_BDC23150C11D4AEE6F7FB8FAE85AE2C3)
# Chunk生成
1\. webpack 先将 entry 中对应的 module 都生成一个新的 chunk
2\. 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
3\. 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个 新的 chunk,继续遍历依赖
4\. 重复上面的过程,直至得到所有的 chunks

HMR

webpack 对文件系统进行 watch 打包到内存中

webpack-dev-middleware 调用 webpack 的 api 对文件系统 watch,当文件发生改变后,webpack 重新对文件进行编译打包,然后保存到内存中。

devServer 通知浏览器端文件发生改变

在启动 devServer 的时候,sockjs 在服务端和浏览器端建立了一个 webSocket 长连接,以便将 webpack 编译和打包的各个阶段状态告知浏览器,最关键的步骤还是 webpack-dev-server 调用 webpack api 监听 compile的 done 事件,当compile 完成后,webpack-dev-server通过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端。

webpack-dev-server/client 接收到服务端消息做出响应

webpack-dev-server 修改了webpack 配置中的 entry 属性,在里面添加了 webpack-dev-client 的代码,这样在最后的 bundle.js 文件中就会接收 websocket 消息的代码了。

webpack-dev-server/client 当接收到 type 为 hash 消息后会将 hash 值暂存起来,当接收到 type 为 ok 的消息后对应用执行 reload 操作。

在 reload 操作中,webpack-dev-server/client 会根据 hot 配置决定是刷新浏览器还是对代码进行热更新(HMR)。代码如下:

webpack 接收到最新 hash 值验证并请求模块代码

首先 webpack/hot/dev-server(以下简称 dev-server) 监听第三步 webpack-dev-server/client 发送的 webpackHotUpdate 消息,调用 webpack/lib/HotModuleReplacement.runtime(简称 HMR runtime)中的 check 方法,检测是否有新的更新。
在 check 过程中会利用 webpack/lib/JsonpMainTemplate.runtime(简称 jsonp runtime)中的两个方法 hotDownloadManifest 和 hotDownloadUpdateChunk。
hotDownloadManifest 是调用 AJAX 向服务端请求是否有更新的文件,如果有将发更新的文件列表返回浏览器端。该方法返回的是最新的 hash 值。
hotDownloadUpdateChunk 是通过 jsonp 请求最新的模块代码,然后将代码返回给 HMR runtime,HMR runtime 会根据返回的新模块代码做进一步处理,可能是刷新页面,也可能是对模块进行热更新。该 方法返回的就是最新 hash 值对应的代码块。
最后将新的代码块返回给 HMR runtime,进行模块热更新。

HotModuleReplacement.runtime 对模块进行热更新

业务代码需要做些什么?

当用新的模块代码替换老的模块后,但是我们的业务代码并不能知道代码已经发生变化,也就是说,当 hello.js 文件修改后,我们需要在 index.js 文件中调用 HMR 的 accept 方法,添加模块更新后的处理函数,及时将 hello 方法的返回值插入到页面中。代码如下

  // index.js
  if(module.hot) {
      module.hot.accept('./hello.js', function() {
          div.innerHTML = hello()
      })
  }