webpack开发环境调优(二)|小册免费学

302 阅读7分钟

Webpack 作为打包工具的重要使命之一就是提升效率。下面我们记录一些对日常开发有一定帮助的 Webpack 插件以及调试方法,本系列文章将包含以下内容:

1. 模块热替换

在早期开发工具还比较简单和匮乏的年代,调试代码的方式基本都是改代码—刷新网页查看结果—在改代码,这样反复的修改和调试。后来,一些 Web 开发框架和工具提供了更便捷的方式—只要检测到代码改动就会自动重新构建,然后出发网页刷新。这种一般被称为 live reload。Webpack 则在 live reload 的基础上又进了一步,可以让代码在网页不刷新的前提下得到最新的改动,我们甚至不需要重新发起请求就能看到更新后的效果。这就是模块热替换功能(Hot Module Replacement,HMR)。

HMR 对于大型应用尤其适用。试想一个复杂的系统每改动一个地方都要经历资源重构建、网络请求、浏览器渲染等过程,怎么也要几秒甚至几十秒的时间才能完成;况且我们调试的页面可能位于很深的层级,每次还要通过一些认为操作才能验证结果,其效率是非常低下的。而 HMR 则可以在保留页面当前状态的前提下呈现出最新的改动,可以节省开发者大量的时间成本。

1.1 开启 HMR

HMR 是需要手动开启的,并且有一些必要条件。

首先我们要确保项目是基于 webpack-dev-server 或者 webpack-dev-middle 进行开发的,Webpack 本身的命令行并不支持 HMR。下面是一个使用 webpack-dev-server 开启 HMR 的例子。

const webpack = require('webpack');
module.exports = {
  // ...
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true
  }
}

上面配置产生的结果是 Webpack 会为每个模块绑定一个 module.hot 对象,这个对象包含了 HMR 的 API。借助这些 API 我们不仅可以实现对特定模块开启或关闭 HMR,也可以添加热替换之外的逻辑。比如,当得知应用中某个模块更新了,为了保证更新后的代码能够正常工作,我们可能还要添加一些额外的处理。

调用 HMR API 有两种方式:一种是手动地添加这部分代码;另一种是借助一些现成的工具,比如 react-hot-loader、vue-loader 等。

如果应用的逻辑比较简单,我们可以直接手动添加代码来开启 HMR。比如下面例子:

// main.js
import { add } from 'util.js';
add(2, 3);

if(module.hot) {
  module.hot.accept();
}

假设 main.js 是应用的入口,那么我们就可以把调用 HMR API 的代码放在该入口中,这样 HMR 对于 index.js 和其依赖的所有模块都会生效。当发现有模块发生变动时,HMR 会使应用在当前浏览器环境下重新执行一遍 index.js(包括其依赖)的内容,但是页面本身不会刷新。

大多数时候,还是建议应用的开发者使用第三方提供的 HMR 解决方案,因为 HMR 触发过程中可能会有很多预想不到的问题,导致模块更新后应用的表现和正常加载的表现不一致。为了解决这类问题,Webpack 社区中已经有许多相应的工具提供了解决方案。比如 react 组件的热更新由 react-hot-loader 来处理,我们直接拿来用就行。

1.2 HMR 原理

在开启 HMR 的状态下进行开发,你会发现资源的体积会比原本的大很多,这是因为 Webpack 为了实现 HMR 而注入了很多相关代码。在它的实现过程里也包含了很多有意思的问题,下面我们来详细了解一下 HMR 的工作原理。

在本地开发环境下,浏览器是客户端,webpack-dev-server(WDS)相当于是我们的服务端。HMR 的核心就是客户端从服务端拉取更新后的资源(准确的说,HMR 拉取的不是整个资源文件,而是 chunk diff,即 chunk 需要更新的部分)。

第一步就是浏览器什么时候去拉取这些更新。这需要 WDS 对本地源文件进行监听。实际上 WDS 与浏览器之间维护了一个 websocket,当本地资源发生变化时 WDS 会向浏览器推送更新事件,并带上这次构建的 hash,让客户端与上一次资源进行比对。通过 hash 的比对可以防止冗余更新的出现。因为很多时候源文件的更改并不一定代码构建结果的更改(比如添加了一个末尾空行等)。websocket 发送的事件列表如图所示:

websocket.png

这同时也解释了为什么当我们开启多个本地页面时,代码一改所有页面都会更新。当然 websocket 并不是只有开启了 HMR 才会有,live reload 其实也是依赖这个而实现的。

有了恰当的拉取资源的时机,下一步就是要知道拉取什么。这部分信息并没有包含在刚刚的 websocket 中,因为刚刚我们只是想知道这次构建的结果是不是和上次一样。现在客户端已经知道新的构建结果和当前的有了差别,就会向 WDS 发起一个请求来获取更改文件的列表,即哪些模块有了改动。通常这个请求的名字为[hash].hot-update.json。如下图分别展示了该接口的请求地址和返回值。

hash.hot-update.png

hot-update-response.png

该返回结果告诉客户端,需要更新的 chunk 为main,版本为(构建 hash)8925ce38ba852a466c3c。这样客户段就可以在借助这些信息继续向 WDS 获取该 chunk 的增量更新。下图展示了一个获取增量更新接口的示例:

增量update.png

增量update-response.png

现在客户端已经获取到了 chunk 的更新,到这里又遇到了一个非常重要的问题,即客户端获取到这些增量更新之后如何处理?哪些状态需要保留,哪些又需要更新?这个就不属于 webpack 的工作了,但是它提供了相关的 API (如前面提到的 module.hot.accept),开发者可以使用这些 API 针对自身场景进行处理。像 react-hot-loader 和 vue-loader 也都是借助这些 API 来实现的 HMR。

1.3 HMR API 示例

我们来看一个实际使用 HMR API 的例子。

// index.js
import { logToScreen } from './util.js';
let counter = 0;
console.log('setInterval starts');
setInterval(() => {
  counter += 1;
  logToScreen(counter);
}, 1000);

// util.js
export function logToScreen(content) {
  document.body.innerHTML = `content: ${content}`;
}

这个例子实现的是在屏幕上输出一个整数并每秒加1。现在我们要对它启用 HMR 应该怎么做呢?如果以最简单的方式来说的话就是添加如下代码:

if(module.hot) {
  module.hot.accept();
}

前面提到,这段代码的意思是让 index.js 及其依赖只要发生改变就在当前环境下全部重新执行一遍。但是发现它会带来一个问题:在当前的运行时我们已经有了一个 setInterval,而每次 HMR 过后又会添加新的 setInterval,并没有对之前的进行清除,所以最后看到屏幕上有不同的数字在累加。

为了避免这个问题,我们可以让 HMR 不对 index.js 生效。也就是说,当 index.js 发生改变时,就直接让整个页面刷新,以防止逻辑出现问题,但对于其他模块来说还要让 HMR 继续生效。那么可以将上面的代码修改如下:

if (module.hot) {
  module.hot.decline();
  module.hot.accept(['./util.js']);
}

module.hot.decline 是将当前 index.js 的 HMR 关掉,当 index.js 自身发生改变时禁止使用 HMR 进行更新,只能刷新整个页面。而后面一句 module.hot.accept(['./util.js'])的意思是当 util.js 改变时依然可以启用 HMR 更新。

上面只是一个简单的例子,展示了如何针对不同模块进行 HMR 的处理。更多相关的 API 还是要参考 Webpack 文档。

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情