背景
一个monorepo项目,里面有子项目a和子项目sdk,a项目依赖于sdk(有代码引用),我们发现,当使用alias的时候,不仅要在tsconfig里的compilerOption的path配置,还要在webpack的resolve里面的alias配置,显然是非常的麻烦,于是我们打算复现这个最小demo,并写一个webpack插件,让代码在运行的时候,自动识别tsconfig里的alias,并注入到webpackconfig里面,这样我们就可以只管理tsconfig,大大节约了开发精力,减少出错概率
复现过程
技术选型 pnpm:
monorepo,我们肯定是首选pnpm,在 monorepo 中,pnpm提供了更好的开发支持,如Turborepo 、Lerna 等工具原生支持 pnpm workspaces,无需额外适配;使用 workspace:
协议直接引用 Monorepo 中的其他包,开发过程中本地实时同步,无需发布到 npm。
步骤
首先我们构建一个如下图结构的项目,我们一步步来
先创建这样一个框架:
- a-project的tsconfig:
//这里重点使要配置依赖项
- sdk的tsconfig:
然后就是在sdk导出点小东西让a引入,创建好插件需要的背景:
sdk/src/index.ts:
project-a/src/index.ts:
配置好project-a的webpack.config.js:
曲折的路程
环境搭建好了接下来就是酸爽的写plugin环节了
第一版:
当时的思路就是,把tsconfig的alias里的key和value的/*去掉,然后拼接起来注入到webpack的alias里
相信有经验的小伙伴一看就知道问题在哪,就是beforRun实在是太晚了,webpack已经读取了选项并且进入构建流程了,这时候在读取tsconfig并注入webpack optopn无异于先冲水后拉*【手动狗头】。当然,这是我后面解决了问题的视角,当时我并没有怀疑到钩子头上,因为当时我在a-project项目下运行webpack并没有问题,只是webpack serve的时候会报错:
思考问题:
为什么webpack输出到dist就行,webpack serve就不行呢?我十分费解,其实一开始我就想到了,可能是钩子挂错了,但是webpack输出到dist又没问题,只是webpack serve报错,按理来说如果钩子挂错了,dist也不能正常输出才对,想了一晚上没得到答案,我决定启动最终大招:cosole.log(打这么多666是为了方便查找)
发现alias为空对象,好家伙,为什么alias为空webpack还能正常打包,他怎么识别得到@/sdk/index的?它长脑子了?简直是灵异事件。
解决问题
想了很久,最后实在没办法,启动了终极大招中的终极大招---GPT: 我把bundle放进GPT让他帮我看一下,alias为空,webpack是怎么识别成功@/sdk/index并且输出dist的,这种事情实在是太诡异了,没想到居然遇到了代码错了却能跑的事情。最后GPT给出了答案:
我直呼666,好你个eval,我都没允许,自己给自己加戏,搞得我想了一晚上,下面我们来解析eval的机制,看看它是怎么骗我的
eval()
模式的模块解析机制
(1) 记录模块的路径
Webpack 在构建时会解析每个模块的路径,记录在内部的模块表(module graph
)中。例如:
@sdk/index
: 如果resolve.alias
配置正确,会解析为../sdk/src/index.ts
。 如果resolve.alias
为空,Webpack 会直接尝试解析文件系统路径。
Webpack 将这些路径直接写入 eval()
调用中,例如:
bash
eval("\nvar sdk = __webpack_require__(/*! ../sdk/src/index.ts */ "../sdk/src/index.ts");\n");
(2) 如何解析路径
即使 alias
配置为空,Webpack 仍然能够通过以下规则解析到正确路径:
-
文件系统路径解析:
- 如果路径是
../sdk/src/index.ts
(文件系统路径),Webpack 直接查找目标文件。 eval()
模式将路径硬编码进模块中,所以即使没有alias
,也能解析成功。
- 如果路径是
-
模块路径回退 : 如果
@sdk/index
这种路径未找到,Webpack 会尝试回退到文件系统路径。例如:- 查找
../sdk/src/index.ts
。 - 如果找不到,再尝试
node_modules
或其他目录。
- 查找
(3) eval 模式下的动态加载
- 在
eval()
模式下,Webpack 打包文件中会包含模块的完整路径。 - 在运行时,
eval()
会使用这些路径动态加载模块。
eval解析步骤步骤
也就是说eval模式使得即使路径解释不成功,也会全局去查找文件 步骤是这样的:
Step 1:检查 resolve.alias
配置
-
解析路径:
@sdk/index
应被替换为:makefileC:\Users\30984\Desktop\monorepo-project\packages\sdk\src\index.ts
-
结果: 如果
alias
生效,Webpack 会直接找到C:\Users\30984\Desktop\monorepo-project\packages\sdk\src\index.ts
并解析成功。 -
为什么会失败? 如果 alias 配置未生效(例如插件在
webpack-dev-server
中未正确触发,在这里我是钩子挂错了),Webpack 会跳过 alias 解析。
Step 2:检查相对路径或绝对路径
如果 alias 不匹配,Webpack 会尝试解析文件系统路径。
-
解析为相对路径: Webpack 认为
@sdk/index
是一个模块名,而不是文件路径。-
它会从当前文件的目录
C:\Users\30984\Desktop\monorepo-project\packages\project-a\src
出发,尝试以下路径:perl./@sdk/index ../@sdk/index ../../@sdk/index
结果: 找不到模块,继续下一步。
-
-
解析为绝对路径: 如果 alias 不存在且相对路径解析失败,Webpack 可能尝试从文件系统根目录解析:
perlC:\@sdk\index
结果: 仍然找不到。
Step 3:检查 node_modules
如果文件系统路径解析失败,Webpack 会尝试在 node_modules
中查找。
-
模块搜索路径 : Webpack 会依次在以下目录中查找
@sdk
:javaC:\Users\30984\Desktop\monorepo-project\packages\project-a\node_modules\@sdk C:\Users\30984\Desktop\monorepo-project\node_modules\@sdk C:\Users\node_modules\@sdk
-
结果: 如果
node_modules
中不存在@sdk
,Webpack 报错。
Step 4:回退到文件系统路径解析
即使 alias 未生效,Webpack 的解析机制可能会直接找到文件系统中的模块。
-
动态路径硬编码(eval 模式特性) :
-
Webpack 在
eval()
模式下,会记录模块的实际路径(例如../sdk/src/index.ts
),并在构建时写入打包文件。 -
@sdk/index
被替换为:bash../sdk/src/index.ts
-
-
硬编码路径执行: 打包后的代码:
basheval("var sdk = __webpack_require__(/*! ../sdk/src/index.ts */ "../sdk/src/index.ts");");
这里
../sdk/src/index.ts
是文件系统中的相对路径,Webpack 已经将其解析为硬编码路径。 -
结果: Webpack 能通过文件系统找到
../sdk/src/index.ts
,即使 alias 不存在。
说人话环节
是不是很抽象?举个生活举例子,eval就像是个饥渴男大,工学院找不到女朋友就去管理学院找,再不行就去艺术学院,最后还真给他找到了。
总结说起来,就是eval模式下,webpack打包起来,除了万不得已都不会报错,你有点错误他就给你弥补了,但是运行webpack serve的时候是没有eval机制的,自然就报错了,机制就是我上面说的。
总结
最后发现是钩子问题,换成initialize就好了,过程十分的曲折,搞得我是十分烦躁,不过好在问题最后还是解决了,这是最终的插件:
在a-project 的webpack.config.js导入并使用就好了:
泪目了兄弟萌 项目地址: