前言#
我发现被流放到处理bug是真的舒服,有大把时间搞自己的东西,并且每次处理的缺陷奇形怪状,可以在这里面学到不错的知识。
bug#
问题背景#
由于公司的管理后台是一个基于webpack打包的多页面应用,并且做了自己的封装,所以每个文件夹就相当于是一个独立的项目。
由于历史原因,只有部分文件夹进行了上述的处理,还有一些文件夹并没有参与上述打包,而是采用的简单处理(也是基于webpack打包),基本上就是用babel转换成es5而已,比较原始。
然后某个需求中,某开发顺便帮忙迁移过去了。
问题代码#

对,就是在函数中没有声明,直接使用。
报错#
报错是一个很常见的:**params is not defined**。
原因#
那么到这里相信大家都知道是为什么有问题了:use strict: Strict mode - JavaScript | MDN (mozilla.org)
严格模式这里就不展开来说了,相信大家都清楚。
这个问题的原因就是因为严格模式下 未声明就直接使用会使得编译错误。
处理方案#
处理方案简单,给它声明下即可。
探讨#
这个缺陷处理自然是简单,但是它背后的问题我们并不清楚:这个严格模式是从哪里来的?
我们的代码中并没有注入严格模式,所以自然是来自于外部,那么第一个怀疑的对象自然就是webpack。
但是前面背景中有说到过,迁移前后都是基于webpack打包的,那么是公司封装的那一层?有可能。
排查公司相关的省略一万个字。。。
最终是确定非来自于公司封装的那一层。
Ok,那么怀疑的对象又来到webpack。
打包产物#
我们来看下打包产物:
迁移前:

迁移后:

可以看到迁移后确实多了use strict在执行runtime之前,这货就是罪魁祸首。
关键字眼#
迁移前后开头有一段差异:

那么会不会是这货相关的问题?
这里有个import字眼,这一点提醒了我,由于迁移前代码比较原始,是没有使用过import这种导入的(require)也没有。这个harmony import也可以作为关键词。
那我们搞个环境来测试下
测试环境#
webpack5: webpack
{
"name": "webpack-vue",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"serve": "webpack-dev-server",
"help": "webpack --help",
"build": "webpack",
"debug-1": "node --inspect-brk=3100 ./node_modules/webpack/bin/webpack.js",
"debug": "node --inspect-brk=3100 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js"
},
"dependencies": {
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"babel": "^6.23.0",
"babel-plugin-dynamic-import": "^1.0.0",
"vue": "^3.2.37"
},
"devDependencies": {
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"sass-loader": "^13.0.2",
"style-loader": "^3.3.1",
"vue-loader": "^17.0.0",
"vue-template-compiler": "^2.7.8",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.10.0"
}
}配置:
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: './main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[fullhash].js'
},
mode: 'development',
module: {
rules: [
{
test: /\.vue$/,
use: ['vue-loader']
},
// {
// test: /\.js$/,
// use: ['babel-loader']
// },
{
test: /\.scss$/,
use: ['sass-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new CleanWebpackPlugin(),
],
devServer: {
port: 8333
}
}测试对比#
main.js文件
改动前:


携带了use strict。
改动后:


果然没有了,那么问题就和是否使用过import有关了。
那么接下来就是进入调试阶段,看下源码中是哪里给套上use strict的。
问题原因#
现在我们知道了use strict是因为用了import,那么接下来的问题关键点就是:为什么import会导致use strict。
这个问题相信大家都知道是为啥了。
其实这个并不是要解决什么bug,实际上这是一种规范:ES6 module本身就是严格模式 :Strict mode - JavaScript | MDN

但是,webpack并不会让直接让你使用原生的import,而是转换成浏览器兼容的。
比如如下代码

会被webpack转换成
__vite_rsc_require__.r(__webpack_exports__);\n/* harmony import */ var _test_js__WEBPACK_IMPORTED_MODULE_0__ = __vite_rsc_require__(/*! ./test.js */ \"./test.js\");\n// import { createApp } from 'vue'\r\n// import App from './index/index.vue';\r\n\r\n// console.log(App);\r\n\r\n// createApp({\r\n// ...App\r\n// }).mount('#app')\r\n\r\nconsole.log((0,_test_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])())\r\n\n\n//# sourceURL=webpack://webpack-vue/./main.js? 实际上它是先把你引用的依赖也参与打包,放到头部,当执行到这块代码的时候它给你call进来调用。
问题原因我们是解决了,接下来我们来找到这块代码执行的位置。
寻源#
如何调试#
由于是编译阶段调试,不能基于浏览器的devTool去做,需要我们自行搭建调试环境。
我之前写过一篇文章就是关于如何调试webpack的,所以这里就不多说了,大家可以去看下如何调试: 坏蛋Dan:基于vscode实现调试webpack的源码
PS: 我很早以前其实调试分析过webpack源码,包括热更新的,感兴趣的可以去看下。
跟踪过程#
开始跟踪之前,需要吐槽下webpack代码可读性,不过可以理解,自由度越高,可读性自然就越差,或者说抽象程度就越高。
它的核心是基于tapable:https://github.com/webpack/tapable 的,简单的说就是你可以在任意地方通过注册不同生命周期hooks的方式来实现参与构建流程,而且是可以调整顺序的。
这就很自由,相对也比较独立,但是可读性自然就下去了。
所以这次我们不从入口顺序或者出口回溯找(其实是懒),直接从关键词中去找
现在我们的关键词中比较有用的是use strict,所以我们通过它来找。
然后发现这里有个plugin: node_modules\webpack\lib\UseStrictPlugin.js里面有关于use strict相关的内容

缺陷链接: https://github.com/webpack/webpack/issues/1970
根据注释和缺陷我们可以知道为什么要这么做, 由于webpack会改动我们的代码(不会影响代码逻辑),有些场景会往头部加上加上webpack自己的东西,比如前面harmony import这种,这个时候use strict位置就有问题了,所以这里先给它移除,然后后面render阶段再给他加上 。
这里我们发现了一个表达式buildInfo.strict = true,这个可能就是我们头顶多了一层绿帽......多了一层use strict的原因。
现在我们有三个关键词use strict, harmony import以及buildInfo.strict。
那么关键词优先级顺序调整为buildInfo.strict > use strict > harmony import。
然后我们又找到node_modules\webpack\lib\javascript\JavascriptModulesPlugin.js文件中有相关的内容
在renderModule函数中存在这么一段逻辑,关键词一下命中两个。

这里应该就是前面useStrictPlugin中提及的render处。
那么这里就是我们跟踪的代码范围的出口,我们还得找到入口,这样我们跟踪的范围才能确定下来。
别忘了这里打个断点,后面我们调试这货应该有用。
这里有一点我们是可以确定的,就是harmony import处理是在use strict之前,因为只有这样才能确定是用了import。
所以我们再去根据harmony import去找相关代码
node_modules\webpack\lib\dependencies\HarmonyImportDependency.js这个文件中存在harmony import这个字眼。

所以这个地方我们也别放过,打个断点上去。
然后我又发现了一处使用buildInfo.strict = true的地方:node_modules\webpack\lib\dependencies\HarmonyExports.js。

跟着它去找它被引用的文件: node_modules\webpack\lib\dependencies\HarmonyDetectionParserPlugin.js
在这里我们中了大奖:

没想到这个import也会调用这个HarmonyExports.enable。这就使得buildInfo.strict变为true。
然后我们在这打下断点,看下什么情况。
调试#
接下来我们就要进入调试状态了,直接按下F5 (别忘了把代码改回改动前)
- 最早调用的是
HarmonyDetectionParserPlugin,在这里确实将buildInfo.strict置为true。

\2. 接着来到HarmonyImportDependency,在这里套上的harmony import, 符合我们的预期,在use strict之前


并且在这里转换成webpack的"import"
\3. 最后来到JavscriptModulesPlugin

给头部加上use strict。
至此,我们的源码分析流程也就结束了。
总结#
问题很简单,就是在使用了import的情况下被定义为es module,然后按照规范:es module天生即是strict mode(不推荐在es module中使用use strict,是多此一举的行为)。
由于webpack会转换import,所以只能使用use strict来实现es module。 所以得给它加上use strict。
编辑于 2023-12-12 21:26・IP 属地广东
