问题描述
我们有时会遇到以下情况:
import { foo,bar } from '../../services/blaService';
我们同时拥有文件 blaService.ts
和文件夹 blaService/index.ts
。
Webpack 首先加载文件并丢弃文件夹中的代码,这是预期行为。
我们是否可以通过例如在发生这种代码阴影场景时抛出错误来防止这种情况发生?
解决方法
TLDR;
这是一种解决方法:
webpack.config.js
const path = require('path');
const fs = require('fs');
const DetectShadowingPlugin = {
apply (resolver) {
const beforeResolved = resolver.getHook('before-resolved');
// `cb`- refers to the next `tap` function in the chain
beforeResolved.tapAsync('DetectShadowingPlugin',(req,ctx,cb) => {
// To inspect the hook's chain until this moment,see `ctx.stack`(from top to bottom)
// console.log(ctx);
// The `path` will give us the full path for the file we're looking for
const { path: filePath } = req;
const ext = path.extname(filePath);
if (ext !== '.js') {
// Continuing the process
return cb();
}
const fileName = path.basename(filePath,path.extname(filePath)); // https://stackoverflow.com/a/19811573/9632621
const possibleDirectoryPath = path.resolve(filePath,'..',fileName);
fs.access(possibleDirectoryPath,err => {
if (!err) {
const message = `Apart from the file ${filePath},there is also a directory ${possibleDirectoryPath}`;
cb(new Error(message));
return;
}
cb();
});
});
},};
/**
* @type {import("webpack/types").Configuration}
*/
const config = {
/* ... */
resolve: {
plugins: [DetectShadowingPlugin]
},};
module.exports = config;
结果:
文件结构如下:
├── deps
│ ├── foo
│ │ └── index.js
│ └── foo.js
├── dist
│ └── main.js
├── index.js
└── webpack.config.js
和 foo
是这样导入的:
import defaultFooFn from './deps/foo';
如果您想尝试上面的示例,可以查看this Github repo。稍后我将在 repo 的自述文件中添加设置详细信息(当然……稍后:D),但在那之前,这里是步骤:
- git clone --recurse-submodules
- cd webpack
- 纱线
- 纱线链接
- cd ..
- yarn 链接 webpack
- 理解纱线 - 还可以查看
package.json
的脚本以获取更多信息
说明
webpack 使用 resolver
来查找文件的位置。我将这个发现过程视为分支分支的集合。有点像 git 分支。它有一个起点,并根据某些条件选择到达终点的路径。
如果您复制了我在上一节中链接的存储库,您应该会在 webpack
文件夹中看到 webpack 存储库。如果您想更好地可视化这些选择的后果,您可以打开 webpack/node_modules/enhanced-resolve/lib/ResolverFactory.js
文件。您不必了解发生了什么,只需注意步骤之间的联系即可:
如您所见,parsed-resolve
在第一个位置和最后一个位置都作为参数出现。你也可以看到它使用了各种插件,但它们有一个共同点:一般来说,第一个字符串是源,最后一个字符串是目标。我之前提到过,这个过程可以看作是分支的分支。好吧,这些分支由节点组成(直观地说),其中一个节点在技术上称为钩子。
起点是 resolve
钩子(来自 for 循环)。它之后的下一个节点是 parsed-resolve
(它是 resolve
钩子的目标)。 parsed-resolve
钩子的目标是 described-resolve
钩子。等等。
现在,有一件重要的事情要提。您可能已经注意到,described-resolve
钩子被多次用作源。每次发生这种情况时,都会添加一个新步骤(技术上称为 tap
)。当从一个节点移动到另一个节点时,将使用这些步骤。如果该插件(一个步骤由插件添加)决定这样做(这可能是插件中满足某些条件的结果),您可以从一个步骤走另一条路线。
所以,如果你有这样的事情:
plugins.push(new Plugin1("described-resolve","another-target-1"));
plugins.push(new Plugin2("described-resolve","another-target-1"));
plugins.push(new Plugin3("described-resolve","another-target-2"));
从 described-resolve
您可以从 2 个步骤前往 another-target-1
(因此有 2 种到达方式)。如果插件中不满足一个条件,它会转到下一个条件,直到满足插件的条件。如果根本没有选择 another-target-1
,那么可能 Plugin3
的条件会导致 another-target-2
。
所以,就我的观点而言,这就是这个过程背后的逻辑。在这个过程的某个地方,有一个钩子(或者一个节点,如果我们坚持最初的类比),它会在文件被成功找到之后被调用。这是 resolved
钩子,也代表了流程的最后一部分。
如果我们到达了这一点,我们肯定知道一个文件存在。我们现在可以做的是检查是否存在同名文件夹。这就是这个自定义插件正在做的事情:
const DetectShadowingPlugin = {
apply (resolver) {
const beforeResolved = resolver.getHook('before-resolved');
beforeResolved.tapAsync('DetectShadowingPlugin',cb) => {
const { path: filePath } = req;
const ext = path.extname(filePath);
if (ext !== '.js') {
return cb();
}
const possibleDirectoryPath = path.resolve(filePath,};
这里有一个有趣的实现细节,它是 before-resolved
。请记住,每个钩子,为了确定它的新目标,它必须经历一些由使用相同源的插件定义的条件。我们在这里做了类似的事情,除了我们告诉 webpack 首先运行我们的自定义条件。我们可以说它增加了一些优先级。如果我们想在最后一个条件中运行它,我们会将 before
替换为 after
。
为什么它首先选择 requestName.js
路径而不是 requestName/index.js
这是由于添加内置插件的顺序造成的。如果您稍微向下滚动 ResolverFactory
,您应该会看到以下几行:
// The `requestName.js` will be chosen first!
plugins.push(
new ConditionalPlugin(
"described-relative",{ directory: false },null,true,"raw-file"
)
);
// If a successful path was found,there is no way of turning back.
// So if the above way is alright,this plugin's condition won't be invoked.
plugins.push(
new ConditionalPlugin(
"described-relative",{ fullySpecified: false },"as directory","directory"
)
);
您可以通过注释掉上面的 raw-file
插件来测试它:
然后,根据 repo,您应该会看到类似的内容,表明已被选中:
您还可以在该工作树中的任何位置放置断点,然后按 F5
检查程序的执行情况。一切都在 launch.json
文件中。