对某个模块做了修改,页面只做局部更新而不需要刷新整个页面来
在 JavaScript 运行时更新各种模块,而无需完全刷新
HMR (Hot Module Replacement)
当我们修改代码并保存后,Webpack 将对代码重新打包,HMR 会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面
难点:如何把代码的编译过程与运行过程联系起来
加快开发速度
- 保留应用(组件)状态
- 只动态更新变更的内容
- 快速调整样式
HotModuleReplacementPlugin
webpack自带的HMR插件,其他第三方热更新插件都要依赖该插件提供的API,自身接口将在编译时注入代码,通过统一的 Module ID 将编译时的文件与运行时的模块对应起来暴露在远端 module.hot 属性下面
流程
- 在 Webpack-dev-server 的浏览器端(Client)和服务器端(Webpack-dev-middleware)之间建立 WebSocket 长连接
- 当 Webpack-dev-server 监听到项目中的文件/模块代码发生变化并重新编译打包后,将新模块的 hash 值用 socket 发送给远端的 Webpack-dev-server
- 远端 Webpack-dev-server 保存 hash值,通过ws发送ok消息,并将hash值发送给HMR Runtime
- HMR Runtime调用
check方法检测更新,判断是浏览器刷新还是模块热更新- 向服务端发起 AJAX 请求获取是否有更新文件,如果有的话将
mainfest返回给浏览器端 - 根据
mainfest文件并通过 JSONP 请求最新的模块代码
- 向服务端发起 AJAX 请求获取是否有更新文件,如果有的话将
- 通过 HMR Runtime 的
hotApply方法,移除过期模块和代码,并添加新的模块和代码 - 通过 HMR Runtime
accept事件通知应用层使用新的模块进行“局部刷新”
应用层示例
// webpack.config.jsmodule.exports = {entry: {app: './src/index.js',},devtool: 'inline-source-map',devServer: {contentBase: './dist',+ hot: true,},plugins: [...// src/index.jsif (module.hot) {// 接受给定依赖项的更新// 启动回调以响应这些更新module.hot.accept('./rootContainer.js', function() {// 获取模块;重新渲染模块// ReactDOM.render 多次调用是没有副作用的// 除了在第一次会将 mount node 的子节点全部清空// 剩下的时候都会调用 React 的 diff 算法,高效更新const NextRootContainer = require('./rootContainer.js').default;ReactDOM.render(<NextRootContainer />, document.getElementById('react-root'));})}
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)、
}
