Webpack 导入顺序在文件中的代码和文件夹中的代码之间创建阴影 [英] Webpack import order creating shadowing between code in a file and code in a folder

查看:30
本文介绍了Webpack 导入顺序在文件中的代码和文件夹中的代码之间创建阴影的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们有时会遇到以下情况:

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 = {应用(解析器){const beforeResolved = resolver.getHook('before-resolved');//`cb`- 指链中的下一个 `tap` 函数beforeResolved.tapAsync('DetectShadowingPlugin', (req, ctx, cb) => {//要检查钩子的链直到这一刻,请参阅`ctx.stack`(从上到下)//console.log(ctx);//`path` 将为我们提供我们正在寻找的文件的完整路径const { path: filePath } = req;const ext = path.extname(filePath);如果(分机!== '.js'){//继续处理返回 cb();}const fileName = path.basename(filePath, path.extname(filePath));//https://stackoverflow.com/a/19811573/9632621const possibleDirectoryPath = path.resolve(filePath, '..', fileName);fs.access(possibleDirectoryPath, err => {如果(!错误){const message = `除了文件${filePath},还有一个目录${possibleDirectoryPath}`;cb(新错误(消息));返回;}cb();});});},};/*** @type {import("webpack/types").配置}*/常量配置 = {/* ... */解决: {插件:[DetectShadowingPlugin]},};module.exports = config;

结果:

文件结构如下:

├── deps│ ├── foo│ │ └── index.js│ └── foo.js├──区│ └── main.js├── index.js└── webpack.config.js

foo 是这样导入的:

import defaultFooFn from './deps/foo';

如果你想尝试上面的例子,你可以查看

如您所见,parsed-resolve 在第一个位置和最后一个位置都作为参数出现.你也可以看到它使用了各种插件,但它们有一个共同点:一般来说,第一个字符串是,最后一个字符串是目标.我之前提到过,这个过程可以看作是分支的分支.好吧,这些分支由节点组成(直观地说),其中一个节点在技术上称为钩子.

起点是 resolve 钩子(来自 for 循环).它之后的下一个节点是parsed-resolve(它是resolve 钩子的目标).parsed-resolve 钩子的目标是 scribed-resolve 钩子.等等.

现在,有一件重要的事情要提.您可能已经注意到,describe-resolve 钩子被多次用作.每次发生这种情况时,都会添加一个新步骤(技术上称为 tap).当从一个节点移动到另一个节点时,将使用这些步骤.如果该插件(一个步骤由插件添加)决定这样做(这可能是插件中满足某些条件的结果),您可以从一个步骤走另一条路线.

所以,如果你有这样的事情:

plugins.push(new Plugin1(描述解析",another-target-1"));plugins.push(new Plugin2(描述-解析",另一个目标-1"));plugins.push(new Plugin3(描述-解析",另一个目标-2"));

described-resolve,您可以从2 个步骤转到another-target-1(因此有2 种方法可以到达).如果插件中不满足一个条件,它会转到下一个条件,直到满足插件的条件.如果根本没有选择another-target-1,那么Plugin3的条件可能会导致another-target-2.

所以,就我的观点而言,这就是这个过程背后的逻辑.在这个过程的某个地方,有一个钩子(或者一个节点,如果我们坚持最初的类比),它会在文件被成功找到之后被调用.这是resolved钩子,也代表了流程的最后一部分.
如果我们到达了这一点,我们肯定知道一个文件存在.我们现在可以做的是检查是否存在同名文件夹.这就是这个自定义插件正在做的事情:

const DetectShadowingPlugin = {应用(解析器){const beforeResolved = resolver.getHook('before-resolved');beforeResolved.tapAsync('DetectShadowingPlugin', (req, ctx, cb) => {const { path: filePath } = req;const ext = path.extname(filePath);如果(分机!== '.js'){返回 cb();}const possibleDirectoryPath = path.resolve(filePath, '..', fileName);fs.access(possibleDirectoryPath, err => {如果(!错误){const message = `除了文件${filePath},还有一个目录${possibleDirectoryPath}`;cb(新错误(消息));返回;}cb();});});},};

这里有一个有趣的实现细节,它是before-resolved.请记住,每个钩子,为了确定它的新目标,它必须经历一些由使用相同源的插件定义的条件.我们在这里做了类似的事情,除了我们告诉 webpack 首先运行我们的自定义条件.我们可以说它增加了一些优先级.如果我们想在最后一个条件中运行它,我们会将 before 替换为 after.


为什么它首先选择 requestName.js 路径而不是requestName/index.js

这是由于添加内置插件的顺序造成的.如果您在 ResolverFactory 中向下滚动一点,您应该会看到以下几行:

//将首先选择`requestName.js`!插件.push(新的条件插件(描述相关",{目录:假},空值,真的,原始文件"));//如果找到一条成功的路径,就没有回头路了.//所以如果上面的方式没问题,这个插件的条件不会被调用.插件.push(新的条件插件(描述相关",{完全指定:假},作为目录",真的,目录"));

您可以通过注释掉上面的 raw-file 插件来测试它:

然后,根据 repo,您​​应该会看到类似的内容,表明已被选中:

您还可以在该工作树中的任何位置放置断点,然后按 F5 以检查程序的执行情况.一切都在 launch.json 文件中.

We have on occasion the situation where we'll have:

import { foo, bar } from '../../services/blaService';

where we have both the file blaService.ts and the folder blaService/index.ts.

Webpack loads the file first and discards the code in the folder which is expected behaviour.

Could we have a way to guard against this by for instance throwing an error when such a code shadowing scenario occurs?

解决方案

TLDR;

Here would be a way to solve it:

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;

Result:

The file structure is as follows:

├── deps
│   ├── foo
│   │   └── index.js
│   └── foo.js
├── dist
│   └── main.js
├── index.js
└── webpack.config.js

and foo is imported like this:

import defaultFooFn from './deps/foo';

If you want to try out the above example, you can check out this Github repo. I will add the set-up details in the repo's readme later(surely.. later :D), but until then, here are the steps:

  • git clone --recurse-submodules
  • cd webpack
    • yarn
    • yarn link
  • cd ..
  • yarn link webpack
  • yarn understand - also check package.json's scripts for more information about it

Explanation

webpack uses a resolver for finding the location of the files. I see this discovery process as a collection of branch ramifications. Sort of like git branches. It has a starting point and based on some conditions, it chooses the paths to take until it reaches and endpoint.

If you copied the repo I linked in the previous section, you should see the webpack repo, in the webpack folder. If you want to better visualize these ramifications of choices, you can open the webpack/node_modules/enhanced-resolve/lib/ResolverFactory.js file. You don't have to understand what's going on, but just notice the connections between the steps:

as you can see, parsed-resolve appears as an argument both on the first position and on the last position. You can also see that it's using all kinds of plugins, but they have a thing in common: generally, the first string is the source and the last string is the target. I mentioned earlier that this process can be seen as a ramification of branches. Well, these branches are composed of nodes(intuitively speaking), where a node is technically called a hook.

The starting point is the resolve hook(from the for loop). The next node after it is parsed-resolve(it is the resolve hook's target). The parsed-resolve hook's target is described-resolve hook. And so forth.

Now, there is an important thing to mention. As you might have noticed, the described-resolve hook is used multiple times as a source. Every time this happens, a new step(technically called tap) is added. When moving from one node to another, these steps are used. From one step you can go another route if that plugin(a step is added by a plugin) decides so(this can be the result of certain conditions being fulfilled in a plugin).

So, if you have something like this:

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"));

From described-resolve you can go to another-target-1 from 2 steps(so there are 2 ways to arrive to it). If one condition is not met in a plugin, it goes to the next condition until the plugin's condition is met. If another-target-1 has not been chosen at all, then maybe Plugin3's condition will lead the way to another-target-2.

So, this this the logic behind this process, as far as my perspective is concerned. Somewhere in this process, there is a hook(or a node, if we were to stick to the initial analogy) which is invoked after the file has been successfully found. This is the resolved hook, which is also represents the last part of the process.
If we reached the point, we know for sure that a file exists. What we could do now is to check whether a folder with the same name exists. And this is what this custom plugin is doing:

const DetectShadowingPlugin = {
  apply (resolver) {
    const beforeResolved = resolver.getHook('before-resolved');

    beforeResolved.tapAsync('DetectShadowingPlugin', (req, ctx, cb) => {
      const { path: filePath } = req;
      
      const ext = path.extname(filePath);
      if (ext !== '.js') {
        return cb();
      }

      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();
      });
    });
  },
};

There is an interesting implementation detail here, which is before-resolved. Remember that each hook, in order to determine its new target, it has to go through some conditions which are defined by plugins which use the same source. We're doing a similar thing here, with the exception that we're telling webpack to run our custom condition first. We could say that it adds some priority to it. If we wanted to run this among the last conditions, we'd replace before with after.


Why it chooses the requestName.js path first instead of requestName/index.js

This is due to the order in which the built-in plugins are added. If you scroll down in ResolverFactory a bit, you should arrive at these lines:

// 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",
        true,
        "directory"
    )
);

You can test it out by commenting out the raw-file plugin from above:

then, according to the repo, you should see something like that, indicating that had been chosen:

You can also place breakpoints wherever in that working tree and then press F5 to inspect the execution of the program. Everything is in the launch.json file.

这篇关于Webpack 导入顺序在文件中的代码和文件夹中的代码之间创建阴影的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆