对某个模块做了修改,页面只做局部更新而不需要刷新整个页面来
在 JavaScript 运行时更新各种模块,而无需完全刷新

HMR (Hot Module Replacement)

当我们修改代码并保存后,Webpack 将对代码重新打包,HMR 会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面
难点:如何把代码的编译过程与运行过程联系起来

加快开发速度

  1. 保留应用(组件)状态
  2. 只动态更新变更的内容
  3. 快速调整样式

HotModuleReplacementPlugin

webpack自带的HMR插件,其他第三方热更新插件都要依赖该插件提供的API,自身接口将在编译时注入代码,通过统一的 Module ID 将编译时的文件与运行时的模块对应起来暴露在远端 module.hot 属性下面

流程

  1. 在 Webpack-dev-server 的浏览器端(Client)和服务器端(Webpack-dev-middleware)之间建立 WebSocket 长连接
    1. 当 Webpack-dev-server 监听到项目中的文件/模块代码发生变化并重新编译打包后,将新模块的 hash 值用 socket 发送给远端的 Webpack-dev-server
  2. 远端 Webpack-dev-server 保存 hash值,通过ws发送ok消息,并将hash值发送给HMR Runtime
  3. HMR Runtime调用 check 方法检测更新,判断是浏览器刷新还是模块热更新
    1. 向服务端发起 AJAX 请求获取是否有更新文件,如果有的话将 mainfest 返回给浏览器端
    2. 根据 mainfest 文件并通过 JSONP 请求最新的模块代码
  4. 通过 HMR Runtime 的 hotApply 方法,移除过期模块和代码,并添加新的模块和代码
  5. 通过 HMR Runtime accept事件通知应用层使用新的模块进行“局部刷新”

image.png

应用层示例

  1. // webpack.config.js
  2. module.exports = {
  3. entry: {
  4. app: './src/index.js',
  5. },
  6. devtool: 'inline-source-map',
  7. devServer: {
  8. contentBase: './dist',
  9. + hot: true,
  10. },
  11. plugins: [...
  12. // src/index.js
  13. if (module.hot) {
  14. // 接受给定依赖项的更新
  15. // 启动回调以响应这些更新
  16. module.hot.accept('./rootContainer.js', function() {
  17. // 获取模块;重新渲染模块
  18. // ReactDOM.render 多次调用是没有副作用的
  19. // 除了在第一次会将 mount node 的子节点全部清空
  20. // 剩下的时候都会调用 React 的 diff 算法,高效更新
  21. const NextRootContainer = require('./rootContainer.js').default;
  22. ReactDOM.render(<NextRootContainer />, document.getElementById('react-root'));
  23. })
  24. }

webpack HMR 冒泡(bubble)机制

  • 当模块发生变化,但是模块没有用HMR代码捕获变化,则模块的变化消息将冒泡到依赖该模块的其他模块中
  • 其他模块由于使用了HMR代码进行捕获变化,那么应用的变化就按照代码进行了更新;并且停止冒泡
  • 若入口文件仍然没有使用HMR代码捕获变化,则会刷新整个页面更新变化 或者控制台提示

babel-plugin-dva-hmr

babel-plugin-dva-hmr 插件自动替我们在入口模块添加HMR代码

(function() {
  console.log('[HMR] inited with babel-plugin-dva-hmr');
  const router = require('${routerPath}');
  ${appName}.router(router.default || router);
  ${appName}.use({
    onHmr(render) {
      if (module.hot) {
        const renderNormally = render;
        const renderException = (error) => {
          const RedBox = require('redbox-react');
          ReactDOM.render(React.createElement(RedBox, { error: error }), document.querySelector('${container}'));
        };
        const newRender = (router) => {
          try {
            renderNormally(router);
          } catch (error) {
            console.error('error', error);
            renderException(error);
          }
        };
        module.hot.accept('${routerPath}', () => {
          const router = require('${routerPath}');
          newRender(router.default || router);
        });
      }
    },
  });
  // 为每个model注册model更新回调
  modelPaths.map(modelPath => {
    if (module.hot) {
      const modelNamespaceMap = {};
      let model = require('${modelPath}');
      if (model.default) model = model.default;
      modelNamespaceMap['${modelPath}'] = model.namespace;
      module.hot.accept('${modelPath}', () => {
        try {
          // 卸载model
          app.unmodel(modelNamespaceMap['${modelPath}']);
          let model = require('${modelPath}');
          if (model.default) model = model.default;
          // 重新引用model
          app.model(model);
        } catch(e) { console.error(e); }
      });
    }
  })
})()

react-hot-loader

不仅达到模块的热更新,还要保持各个模块的状态不会丢失
用法

loaders: [{
  test: /\.js$/,
  loaders: ['react-hot', 'babel'],
  include: path.join(__dirname, 'src')
}]

难点:

  • 如何找到每一个componen去代理? 方案:webpack-loader(为每个启用该loader的js文件都注入HMR代码)、Babel Plugin(在 babel 编译过程中侵入 React Comeponent 的编译结果)
  • 如何判断组件,组件的表现形式太多,用静态方法包裹组件相当复杂,3.0之前React Transform HMR 为每一个传入的 component 创建了一个代理,并且在全局对象里面保持了一个代理的清单,当同一个组件再次经历 transform,它去更新这些 component

解决方案:
在源码中找到并且包裹React components是非常难做到的,可以通过 babel-plugin 检查一个文件,针对顶层 class、function 以及 被 export 出来的模块在文件末尾做标记
register() 至少会判断传进来的值是不是一个函数,如果是,创建一个 React Proxy 包裹它。它不会替换你的 class 或者 function,这个proxy将会待在全局的map里面,等待着,直到你使用React.createElement()。

class Counter extends Component {
  render() {
    return ()
  }
}

const __exports_default = useSheet(styles)(Counter)
export default __exports_default

// 我们 generate 的标记代码:
// 在 *远端* 标记任何看上去像 React Component 的东西
register('Counter.js#Counter', Counter)
// every export too
register('Counter.js#exports#default', __exports_default)
import createProxy from 'react-proxy'
// 全局map
let proxies = {}
const UNIQUE_ID_KEY = '__uniqueId'

export function register(uniqueId, type) {
  Object.defineProperty(type, UNIQUE_ID_KEY, {
    value: uniqueId,
    enumerable: false,
    configurable: false
  })
  let proxy = proxies[uniqueId]
  if (proxy) {
    // 每次热更新,type 对应的是新的 component
    // update 接口会根据更新前后 Component 的差异
    // 对 ProxyComponent 原型链上的方法做更新
    proxy.update(type)
  } else {
    // 代理 component 的 type,组件更新的时候让 React 认为 type 没有变
    // 代理使得组件更新的时候不会销毁 state 和 DOM
    proxy = proxies[id] = createProxy(type)
  }
}

// Resolve 发生在 element 被创建的时候,而不是声明的时候
// hack 掉基础的 createElement
const realCreateElement = React.createElement
React.createElement = function createElement(type, ...args)  {
  if (type[UNIQUE_ID_KEY]) {
    type = proxies[type[UNIQUE_ID_KEY]].get()
  }
  return realCreateElement(type, ...args)、
}