Open CommanderXL opened 3 years ago
哈喽, 看了上面的关于你梳理的HRM流程, 相当详细呀.
我在实际应用中, 遇到一点问题, 像请教一下
我的使用场景是这样的, 我是 react 技术栈, 页面结构大致如下
顶层 App 组件 -> 中间的监听路由变化的 Router 组件 -> 页面组件 View
我的页面组件会有很多, 都是通过 import() 函数, 懒加载到 Router 组件中的, 每次路由变化, Router 组件会完成页面的切换
我现在想实现页面组件层的HRM, 我通过 webpack babel-loader 往每个页面组件中都插入了如下代码
`javascript
if (module.hot) {
module.hot.accept(['xxx'], () => {
// 更新操作
})
}
` 可是当我修改某个页面组件时, 会导致整个浏览器重新加载, 并不会触发热更新 我断点调试了 webpack 关于 hrm 运行时部分的代码, 发现修改的代码的影响会往其父组件传递, 直到根节点, 如果在这期间的每个组件中没有设置热更新代码, 最后就会触发浏览器完全刷新, 不知道我这样理解对不对呀
我现在的需求, 是希望我修改页面组件的时候, 只热更新页面组件, 不要把影响往上层扩散. 不知道你有没有好的办法, 谢谢
@yangfanzn 你好,react 技术生态我了解的不多。这块在 react 社区有相关解决的方案吗?
webpack hmr
webpack-dev-server
在使用 webpack-dev-server 的过程中,如果指定了 hot 配置的话(使用 inline mode 的前提下), wds 会在内部更新 webpack 的相关配置,即将 HotModuleReplacementPlugin 加入到 webpack 的 plugins 当中。
HotModuleReplacementPlugin
在 HotModuleReplacementPlugin 执行的过程中主要是完成了以下几个工作:
其中在 addParserPlugins 方法当中添加了具体有关 parser hook 的回调,有几个比较关键的 hook 单独拿出来说下:
这个 hook 主要是在 parser 编译代码过程中遇到
module.hot.accept
的调用的时候会触发,主要的工作就是处理当前模块部署依赖模块的依赖分析,在编译阶段处理好依赖的路径替换等内容。这个 hook 同样是在 parser 编译代码过程中遇到
module.hot.decline
的调用的时候触发,所做的工作和上面的 hook 类似。这个 hook 主要完成的工作是在生成 webpack bootstrap runtime 代码当中对加载 module 的
require function
进行替换,变为hotCreateRequire(${varModuleId})
的形式,这样做的目的其实就是对于 module 的加载做了一层代理,在加载 module 的过程当中建立起相关的依赖关系(需要注意的是这里的依赖关系并非是 webpack 在编译打包构建过程中的那个依赖关系,而是在 hmr 模式下代码执行阶段,一个 module 加载其他 module 时在 hotCreateRequire 内部会建立起相关的加载依赖关系,方便之后的修改代码之后进行的热更新操作),具体这块的分析可以参见下面的章节。在这个 hooks.bootstrap 当中所做的工作是在 mainTemplate 渲染 bootstrap runtime 的代码的过程中,对于
hotInitCode
代码进行字符串的匹配和替换工作。hotInitCode
这部分的代码其实就是下面章节所要讲的HotModuleReplacement.runtime
向 bootstrap runtime 代码里面注入的 hmr 运行时代码。在这个 hooks.moduleObj 当中所做的工作是对
__webpack_require__
这个函数体内部的 installedModules 缓存模块变量进行拓展。几个非常关键的点就是:hot: hotCreateModule(${varModuleId})
配置。这个 module.hot api 即对应这个 module 有关热更新的 api,可以看到这个部署 hot api 的工作是由 hotCreateModule 这个方法来完成的(这个方法是由 hmr runtime 代码提供的,下面的章节会讲)。最终和这个 module 所有有关热更新相关的接口都通过module.hot.*
去访问。HotModuleReplacement.runtime
Webpack 内部提供了 HotModuleReplacement.runtime 即热更新运行时部分的代码。这部分的代码并不是通过通过添加 webpack.entry 入口文件的方式来注入这部分的代码,而是通过 mainTemplate 在渲染 boostrap runtime 代码的阶段完成代码的注入工作的(对应上面的 mainTemplate.hooks.boostrap 所做的工作)。
在这部分热更新运行时的代码当中所做的工作主要包含了以下几个点:
hotCreateRequire
方法,用以对__webpack_require__
模块引入方法进行代理,当一个模块依赖其他模块,并将其引入的时候,会建立起宿主模块和依赖模块之间的相互依赖关系,这个依赖关系也是作为之后某个模块发生更新后,寻找与其有依赖关系的模块的凭证。hotCreateModule
方法,用以给每个 module 都部署热更新相关的 api:在 hotCreateModule 方法当中完成 module.hot.* 和热更新相关接口的定义。这些 api 也是暴露给用户部署热更新代码的接口。
其中
hot.accept
和hot.decline
方法主要是用户来定义发生热更新的模块及其依赖是否需要热更新的相关策略。例如hot.accept
方法用来决定当前模块所依赖的哪些模块发生更新的话,自身也需要完成一些更新相关的动作。而hot.decline
方法用来决定当前模块依赖的模块发生更新后,来决定自身是否需要进行更新。而
hot.check
和hot.apply
两个方法其实是 webpack 内部使用的2个方法,其中hot.check
方法:首先调用hotDownloadManifest
方法,通过发送一个 Get 请求去 server 获取本次发生变更的相关内容。// TODO: 相关内容的具体格式和字段?// TODO: 补一个 hot.check 执行的流程图 总结下
hot.check
方法执行的流程其实就是:接下来看下被下载的更新的 chunk 具体内容:
可以看到的是返回的 chunk 内容是可以立即执行的函数:
对应所做的工作就是将需要更新的模块缓存至
hotUpdate
上,同时判断需要更新的 chunk 是否已经下载完了,如果全部下载完成那么执行hotUpdateDownloaded
方法,其内部实际就是调用hotApply
进行接下来进行细粒度的模块更新和替换的工作。首先先讲下
hotApply
内部的执行流程:hotUpdate
需要更新的模块,找出和需要更新的模块有依赖关系的模块;所以当一个模块发生变化后,依赖这个模块的 parentModule 有如下几种热更新执行的策略:
当依赖的模块发生更新后,这个模块需要通过重新加载去完成本模块的全量更新。
当依赖的模块且为
xxx
模块发生更新后,这个模块会执行 callback 来完成相关的更新的动作。而不需要通过重新加载的方式去完成更新。这个模块不管其依赖的模块是否发生了变化。这个模块都不会发生更新。
当依赖的模块为
xxx
发生更新的情况下,这个模块不会发生更新。当依赖的其他模块(除了xxx
模块外)发生更新的话,那么最终还是会将本模块从缓存中删除。这些热更新的 api 也是需要用户自己在代码当中进行部署的。就拿平时我们使用的 vue 来说,在本地开发阶段, vue sfc 经过 vue-loader 的编译处理后,会自动帮我们在组件代码当中当中注入和热更新相关的代码。
vue-loader
通过 genHotReloadCode 方法在处理 vue sfc 代码的时候完成热更新 api 的部署功能。这里大致讲下 vue component 进行热更新的流程:$forceUpdate
);<template>
,<script>
中的内容外会进行热更新外,在我们修改<style>
样式内容的时候也有热更新的效果。这也是 vue component 在编译阶段在 vue style block 的代码当中部署了热更新代码的原因。具体更新策略可参见vue-style-loader相关资料: