三石的webpack(HMR篇)

756 阅读5分钟

devServer

我们都知道开发工作是一个连续的过程,特别是前端开发,我们需要不断的调整代码来实现UI效果,需要改动一个样式后,马上就能看到其在浏览器中的效果。这时候,通过webpack打包构建的方式不仅非常麻烦,而且耗时。webpack提供了一个解决方案,完美解决了这个问题,那就是DevServer

webpack-dev-server 提供了一个基本的 web 服务器,并且具有实时重新加载功能,也就是热更新

安装:

npm install --save-dev webpack-dev-server

配置:

// dev.config.js文件
const webpack = require('webpack');
const path = require('path');
const outputPath = path.resolve(__dirname, './output/public');

module.exports = {
  mode: 'development',
  entry: {
    ...
  },
  output: {
    ...
  },
  devServer: {
        // 仅在需要提供静态文件时才进行配置,项目构建后的路径
        contentBase: outputPath,
        // 启动gzip压缩
        compress: true,
        host: 'localhost',
        port: 9000,
        watchContentBase: true,
        // 在服务器启动后打开浏览器
        open: true,
        // 指定打开浏览器时要浏览的页面
        openPage: ['pages/preview.html'],
        // 将产生的文件写入硬盘。 写入位置为 output.path 配置的目录
        writeToDisk: true,
    }
}

启动:

// webpack5
npx webpack serve
// webpack4
npx webpack-dev-server

HMR 是什么

全称 Hot Module Replacement,即模块热替换/热更新

这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

注意!HMR 绝对不能被用在生产环境

为什么需要 HMR?

在 webpack HMR 功能之前,已经有很多 live reload 的工具或库,这些库监控文件的变化,然后通知浏览器端刷新页面,那么我们为什么还需要 HMR 呢? 其实从上一节就已经大致知道原因了:

  • live reload 工具并不能够保存应用的状态,当刷新页面后,应用之前状态丢失,要恢复到之前状态,还需重新操作。而HMR 则不会刷新浏览器,而是运行时对模块进行热替换,保证了应用状态不会丢失,提升了开发效率。
  • 在古老的开发流程中,我们可能需要手动运行命令对代码进行打包,并且打包后再手动刷新浏览器页面,而这一系列重复的工作都可以通过 HMR 工作流来自动化完成,避免把时间浪费在重复的工作上。
  • HMR 兼容市面上大多前端框架或库,比如 React Hot LoaderVue-loader,能够监听 React 或者 Vue 组件的变化,实时将最新的组件更新到浏览器端。

HMR vs WebpackDevServer

他俩都是自动打包并刷新页面内容,但他俩又不是同一层面的东西

  • WebpackDevServer 每次都是整体刷新页面,即使只修改了小部分也会刷新整个界面,无法保持页面操作状态。它解决的是以前代码改动后,webpack需要重新打包构建的问题
  • HMR的核心是模块更新,不刷新界面,只刷新变更的模块,其实,也就是解决了WebpackDevServer 遗留的问题

如何使用

通过 webpack.HotModuleReplacementPlugin 插件实现该功能。

有两种启动方式,均是在安装 devServer 的前提下,毕竟 HMR 是给 devServer 补漏

方法1:

  1. 引入插件 webpack.HotModuleReplacementPlugin
  2. 设置 devServer.hot: true
    现在HMR是默认开启的,可以直接在 config 中控制
// dev.config.js文件
const webpack = require('webpack');
const path = require('path');
const outputPath = path.resolve(__dirname, './output/public');

module.exports = {
  mode: 'development',
  entry: {
    ...
  },
  output: {
    ...
  },
  plugins: [
    // 大多数情况下不需要任何配置
    new webpack.HotModuleReplacementPlugin(),
  ],
  devServer: {
        // 仅在需要提供静态文件时才进行配置
        contentBase: outputPath,
        // publicPath: '', // 值默认为'/'
        compress: true,
        host: 'localhost',
        port: 9000,
        watchContentBase: true,
        hot: true,
        // 在服务器启动后打开浏览器
        open: true,
        // 指定打开浏览器时要浏览的页面
        openPage: ['pages/preview.html'],
        // 将产生的文件写入硬盘。 写入位置为 output.path 配置的目录
        writeToDisk: true,
    }
}
  1. 在入口文件中新增:
if(module && module.hot) {
    module.hot.accept()
}

方法2:

命令行加 --hot 参数
注意: 启动 devServer 可能有所不同

// webpack5启动
{
  "scripts": {
    "start": "NODE_ENV=development webpack serve --progress --mode=development --config=scripts/dev.config.js --hot"
  }
}

HMR原理

核心流程:

  1. 使用 webpack-dev-server (后面简称 WDS)托管静态资源,同时以 Runtime 方式注入 HMR 客户端代码
  2. 浏览器加载页面后,与 WDS 建立 WebSocket 连接
  3. Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送 hash 事件
  4. 浏览器接收到 hash 事件后,与上一次资源hash进行对比,对比出差异后会向 WDS 发起 Ajax 请求来获取资源列表(manifest)
  5. 借助资源列表向 WDS 发起 jsonp 请求获取该chunk的增量更新(chunk diff)
  6. 浏览器加载发生变更的增量模块
  7. Webpack 运行时触发变更模块的 module.hot.accept 回调,执行代码变更逻辑

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loadervue-loader 都是借助这些 API 实现 HMR。

详细讲解请参考下列文章,图文并茂,你值得拥有

参考

Webpack HMR 原理解析

讲的很清楚的一篇原理-阅

Webpack 原理系列十:HMR 原理全解析