更多文章请点击 Jade
webpack 热更新问题解决及分析(一)
在前一篇文章最后, 我们已经锁定问题 -- 热更新过程中导致入口文件 module.exports 输出的对象无法被正确挂载到 window 上. 而至于真实情况是就是没有挂载还是挂载后被冲掉 (复写) 了, 这个就是这篇文章中需要探索的.
探索前务必要先看一下 Webpack 热更新的源码知道其原理. 有关 Webpack 热更新原理在此将不再赘述, 推荐两篇相关文章:
Webpack HMR 原理解析
Webpack 热更新实现原理分析
在此, 我再用通俗的语言来阐述一下 Webpack 热更新的原理.
不得不再提一下我们的项目架构: 本项目以 express 搭建服务端, 本地开发配合 webpack-dev-middleware,wepack-hot-middleware,webpack.HotModuleReplacementPlugin 实现热加载更新. 我们看到有 webpack-dev-middlerware,webpack-hot-middleware,webpack.HotModuleReplacementPlugin 各种工具, 但是我们要明白一点, 打包这个活儿本质上还是 webpack 干的, 并不是加了热更新相关的插件或者中间件后, 打包这项活就是插件或者中间件干了. 插件和中间件的作用仅仅是在 webpack 编译打包核心的外围做了一些事儿(手脚). 所以在看待这几个插件的时候, 我们只要抓住热更新这一个关键功能就可以了. 以下这个图来自文章 - Webpack HMR 原理解析
这样理解起来就简化不少, 热更新的机制本质也是 client-server 的机制, client 就在我们的浏览器端, 它需要知道文件变化了并且做出一系列的动作然后把最终结果通过浏览器让我们看到, 那 server 端的职责就很明确了, 它需要监控模块文件的变化, 如果变化了就要让 webpack(webpack 对外暴露的 API)重新打包, 期间还要和 client 保持通信, 把自己这边的状况告诉 client,client 根据信息采取动作.
以上就是进入问题分析前的准备工作.
深入实际问题分析
为了便于看清问题, 实验的例子越简单越好. 所以我们仍旧采用 上一篇文章 中使用的例子. 入口文件 - examples/test/index.js
const Test = {
text: 'hello world!'
}
module.exports = Test
html - example/test/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>test</title>
</head>
<body>
Test
<!-- <script src="../../dist/components/test.js"></script> -->
<script src="/examples/test.js"></script>
<script>
console.log(test)
console.log(test.text)
</script>
</body>
</html>
然后用生产的 webpack 打包配置和本地开发的打包配置分别打包出 test_s.js(这个是直接打到物理磁盘的, 即为发布包)和 test_f.js(开发包是打在内存的, 但是浏览器能访问到, 我们直接右键另存为即可),test_s.js 能够按照预期运行, test_f.js 无法拿到预期入口.
我们看下 test_s.js 和 test_f.js 文件内容, 我们可以在注释中找到这么一句话『Load entry module and return exports', 没错, 这就是最终打包后暴露出来的方法. 看截图:
不管是 test_s.js 还是 test_f.js, 入口都是一个叫做 341 的模块, 这没毛病, 毕竟是同样的一份代码嘛. 继续看.
test_s.js 是生产包, 摒除了热加载, 本身 test.js 也没啥代码, 所以打包后的代码也很少, 一眼就能看到 341 是入口, 341 里头_webpack_require_(342), 而 342 就是我们 test.js 的代码内容, 成功找到了 Test. 对于_webpack_require_有疑问的同学可以查看 (这篇文章)[https://github.com/ShowJoy-com/showjoy-blog/issues/39], 在这里只要了解 在入口模块中调用了 webpack_require(342), 就会得到 342 这个模块返回的 module.exports, 所以就能将 Test 找到, 并且赋给 module.exports, 所以 341 作为入口文件, 最终暴露出来的就是 Test.
同样在 test_f.js 中, 我们也能找到入口引用, 看下图:
341 作为入口模块, 341 中_webpack_require_(342),342 模块就是我们的 test.js, 出乎意料的是 341 中同时引入了_webpack_require_(32), 而且 module.exports 上挂载的不是 342 是 32!!!. 至此问题彻底明了, 热加载过程中导致入口文件 module.exports 输出的对象无法被正确挂载到 window 上. 事实情况是只引入了我们的入口文件并没有对输出进行挂载 (不是冲掉) 挂载的是 32 模块.
那这是不是 bug 呢? 是 bug 我就去提 issue 啦, 看来还是太天真:).
继续看 32 模块.
如果你认真看过最前面推荐的文章 -- Webpack 热更新实现原理分析 , 并且浏览过 HRM 源码, 就会有深刻印象. 这是 HRM 的 client 模块代码, 核心功能是通过 EventSource, 将与 HRM server 建立连接, 进行模块更新通信. 看一下代码注释 /* WEBPACK VAR INFECTION */, 没错, 就是这段注入代码, 成了真正的入口函数.
再回头看一下 webpack-develop.config.js:
entry: {
test: [path.resolve(__dirname, 'examples/test/index.js'), 'webpack-hot-middleware/client?path=/examples/__what&reload=true'],
},
entry test 的数组依次是 index.js, 然后是 webpack-hot-middleware. 所以 index.js 的包引到了, 但是入口确是后者 webpack-hot-middleware.
问题的最终方法
webpack - develop.config.js: entry: {
// test: [path.resolve(__dirname, 'examples/test/index.js'), 'webpack-hot-middleware/client?path=/examples/__what&reload=true'],
// 更换顺序, 正确书写 entry
test: ['webpack-hot-middleware/client?path=/examples/__what&reload=true', path.resolve(__dirname, 'examples/test/index.js')],
},
output: {
filename: '[name].js',
publicPath: '/examples/',
library: '[name]',
libraryTarget: 'umd',
},
至此, 该问题完美解决.
后记: 依样画葫芦的坑有时候更大呀, 而且极难理出排查思路.「别的组件入口都没问题, 偏偏这个组件有问题」. 有种情况叫负负得正了:).
来源: https://juejin.im/post/5a6d961cf265da3e355b4f1f