【问题标题】:Why does webpack --watch invoke my custom loader on unrelated files?为什么 webpack --watch 会在不相关的文件上调用我的自定义加载器?
【发布时间】:2022-01-02 20:51:00
【问题描述】:

我有一个简单的自定义 Webpack 加载器,它从 .txt 文件生成 TypeScript 代码:

txt-loader.js

module.exports = function TxtLoader(txt) {
  console.log(`TxtLoader invoked on ${this.resourcePath} with content ${JSON.stringify(txt)}`)
  if (txt.indexOf('Hello') < 0) {
    throw new Error(`No "Hello" found`)
  }
  return `export const TEXT: string = ${JSON.stringify(txt)}`
}

在现实生活中,我正在对输入进行一些解析;在此示例中,我们假设文件必须包含文本 Hello 才有效。

这个加载器让我可以像这样导入文本文件:

index.ts

import { TEXT } from './hello.txt'

console.log(TEXT)

一切正常,除了一件事:webpack watch(及其表亲webpack serve)。第一次编译没问题:

$ /tmp/webpack-loader-repro/node_modules/.bin/webpack watch
TxtLoader invoked on /tmp/webpack-loader-repro/hello.txt with content "Hello world!\n"
asset main.js 250 bytes [compared for emit] [minimized] (name: main)
./index.ts 114 bytes [built] [code generated]
./hello.txt 97 bytes [built] [code generated]
webpack 5.64.3 compiled successfully in 3952 ms

但后来我更改了hello.txt 文件:

$ touch hello.txt

突然奇怪的事情发生了:

TxtLoader invoked on /tmp/webpack-loader-repro/index.ts with content "import { TEXT } from './hello.txt'\n\nconsole.log(TEXT)\n"
TxtLoader invoked on /tmp/webpack-loader-repro/custom.d.ts with content "declare module '*.txt'\n"
[webpack-cli] Error: The loaded module contains errors
    at /tmp/webpack-loader-repro/node_modules/webpack/lib/dependencies/LoaderPlugin.js:108:11
    at /tmp/webpack-loader-repro/node_modules/webpack/lib/Compilation.js:1930:5
    at /tmp/webpack-loader-repro/node_modules/webpack/lib/util/AsyncQueue.js:352:5
    at Hook.eval [as callAsync] (eval at create (/tmp/webpack-loader-repro/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:6:1)
    at AsyncQueue._handleResult (/tmp/webpack-loader-repro/node_modules/webpack/lib/util/AsyncQueue.js:322:21)
    at /tmp/webpack-loader-repro/node_modules/webpack/lib/util/AsyncQueue.js:305:11
    at /tmp/webpack-loader-repro/node_modules/webpack/lib/Compilation.js:1392:15
    at /tmp/webpack-loader-repro/node_modules/webpack/lib/HookWebpackError.js:68:3
    at Hook.eval [as callAsync] (eval at create (/tmp/webpack-loader-repro/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:6:1)
    at Cache.store (/tmp/webpack-loader-repro/node_modules/webpack/lib/Cache.js:107:20)
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

似乎 Webpack 决定在我的加载器中抛出比配置中指定的更多的文件。

如果我删除加载器中抛出的异常并返回一些任意有效的 TypeScript 代码,生成的 main.js 看起来完全一样。所以看起来这些额外的操作是完全多余的。但我不认为正确的解决方案是让我的装载机吞下这些异常。

加载器是这样配置的:

webpack.config.js

const path = require('path')

module.exports = {
  mode: 'production',
  entry: './index.ts',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
      },
      {
        test: /\.txt$/,
        use: [
          {
            loader: 'ts-loader',
            // Tell TypeScript that the input should be parsed as TypeScript,
            // not JavaScript: <https://stackoverflow.com/a/47343106/14637>
            options: { appendTsSuffixTo: [/\.txt$/] },
          },
          path.resolve('txt-loader.js'),
        ],
      },
    ],
  },
}

最后,这些是将它们放在一起的必要部分:

custom.d.ts

declare module '*.txt'

tsconfig.json

{}

package.json

{
  "name": "webpack-loader-repro",
  "license": "MIT",
  "private": true,
  "devDependencies": {
    "ts-loader": "9.2.6",
    "typescript": "4.5.2",
    "webpack": "5.64.3",
    "webpack-cli": "4.9.1"
  },
  "dependencies": {}
}

对于那些想在家尝试的人,clone this minimal repro project

这是 Webpack 中的错误吗?在 ts-loader 中?在我的配置中?

【问题讨论】:

    标签: typescript webpack ts-loader webpack-loader webpack-watch


    【解决方案1】:

    在您的最小复制中,我发现注释掉这些行可以解决问题:

    ...
    {
      test: /\.txt$/,
      use: [
        // remove ts-loader from this pipeline, and you don't get the unexpected watch behavior
        path.resolve('txt-loader.js'),
      ],
    },
    ...
    

    我认为正在发生的事情是,当您在 use 数组中为 /\.txt$/ 管道链接 ts-loader 时,它会监视它认为是整个 typescript 项目,然后重新调用管道(包括您的自定义 txt-loader)只要有任何变化。通常这是一件好事,因为它会重新编译您的项目,例如,如果 .d.ts 文件更改仅通过 tsconfig.json 隐式包含,而不是通过显式 webpack 处理的导入语句。

    至少在您提供的简单复制中,捆绑包似乎在/\.txt$/ 管道中生成并运行 ts-loader,这可能足以解决您的问题。 p>

    但如果在您的实际案例中出于某种原因有必要在此管道中包含ts-loader,您应该能够告诉ts-loader 仅通过使用查看/观看显式捆绑的文件onlyCompileBundledFiles 选项(参见docs):

    ...
    {
      test: /\.txt$/,
      use: [
        { 
          loader: 'ts-loader',
          options: { appendTsSuffixTo: [/\.txt$/], onlyCompileBundledFiles: true },
        }
        path.resolve('txt-loader.js'),
      ],
    },
    ...
    

    【讨论】:

    • .txt 省略ts-loader 是可行的,因为在这个简单的示例中生成的代码恰好也是有效的JavaScript(无类型)。我已经更新了这个例子。通过onlyCompileBundledFiles: true似乎并不能解决问题;我仍然看到TxtLoader invoked with: declare module '*.txt'
    • 还添加了在加载器中抛出异常,让问题更加明显。
    • 很奇怪。在我看来它是不确定的 - 我会仔细看看。
    • 我认为 Webpack 增加了一些并行性,因此处理文件的顺序不同,但在我的情况下,在重新构建时总是有额外的文件通过加载器传递。
    【解决方案2】:

    1。问题

    主要问题是ts-loader 会加载额外的文件并手动调用你的加载器。

    在您当前的 webpack 配置中,您最终会得到 2 个独立的 ts-loader 实例:

    • 一个用于.ts 文件
    • 还有一个用于.txt 文件
    1.1。第一次编译

    在初始编译期间将发生以下情况:

    • index.ts 将由第一个 ts-loader 实例处理,它会尝试编译它。
    • 第一个ts-loader 不知道如何加载.txt 文件,所以它四处寻找一些模块声明并找到custom.d.ts 并加载它。
    • 既然第一个ts-loader 知道如何处理.txt 文件,它会将index.tscustom.d.ts 注册为依赖于hello.txt (addDependency call here)
    • 之后第一个ts-loader 实例会要求webpack 编译hello.txt
    • hello.txt 将由第二个 ts-loader 实例通过您的自定义加载器加载(如预期的那样)
    2.1。第二次编译

    一旦你触摸(或修改)hello.txt,webpack 将尽职尽责地通知所有观察者hello.txt 已更改。但是因为index.ts & custom.d.ts 依赖于hello.txt,所以所有观察者都会被通知这两个有变化。

    • 第一个 ts-loader 将获得所有 3 个更改事件,忽略 hello.txt 一个,因为它没有编译那个,并且对于 index.tscustom.d.ts 事件什么也不做,因为它看到没有更改.

    • 第二个ts-loader 也将获得所有 3 个更改事件,如果您刚刚触摸它,它将忽略 hello.txt 更改,或者在您编辑它时重新编译它。之后它看到custom.d.ts 的变化,意识到它还没有编译那个并且将尝试编译它,while invoking all loaders specified after itindex.ts 更改也会发生同样的事情。

    • 第二个ts-loader 甚至尝试加载这些文件的原因如下:

      • 对于index.ts:您的.tsconfig 没有指定includeexcludefiles,因此ts-loader 将为include 使用默认的["**"],即它可以找到的所有内容.因此,一旦它收到index.ts 的更改通知,它就会尝试加载它。
        • 这也解释了为什么您无法使用 onlyCompileBundledFiles: true 获取它 - 因为在这种情况下 ts-loader 意识到它应该忽略该文件。
      • 对于custom.d.ts,基本相同,但即使使用onlyCompileBundledFiles: true,它们仍会包含在内:

        ts-loader 的默认行为是作为 tsc 命令的替代品,因此它尊重 tsconfig.json 中的包含、文件和排除选项,加载由这些选项指定的任何文件。 onlyCompileBundledFiles 选项修改了这种行为,只加载那些实际由 webpack 捆绑的文件,以及 tsconfig.json 设置包含的任何 .d.ts 文件。 .d.ts 文件仍然包含在内,因为编译时可能需要它们而没有显式导入,因此不会被 webpack 拾取。

    1.3。之后的任何编译

    如果您修改您的txt-loader.js 以不抛出而是返回未更改的内容,即:

    if (txt.indexOf('Hello') < 0) {
        return txt;
    }
    

    我们可以看到第三次、第四次等编译时会发生什么。

    由于index.tscustom.d.ts 现在都在ts-loaders 的缓存中,因此只有在其中任何一个文件发生实际更改时才会调用您的自定义加载程序。


    2。类似问题

    你不是唯一遇到这个“功能”的人,甚至还有一个开放的 github 问题:


    3。潜在的解决方案

    有几种方法可以避免这个问题:

    3.1。使 .txt ts-loader 仅转译

    transpileOnly: true-mode ts-loader 将忽略所有其他文件,只处理那些 webpack 明确要求编译的文件。

    这样就可以了:

    /* ... */
        rules: [
          {
            test: /\.ts$/,
            use: 'ts-loader',
          },
          {
            test: /\.txt$/,
            use: [
              {
                loader: 'ts-loader',
                options: { appendTsSuffixTo: [/\.txt$/], transpileOnly: true },
              },
              path.resolve('txt-loader.js'),
            ],
          },
        ],
    /* ... */
    

    虽然使用这种方法,但您将放松对 .txt 文件的类型检查。

    3.2.确保只有一个 ts-loader 实例

    只要您为每个加载器指定完全相同的选项,ts-loader 就会重用加载器实例。

    这样你就拥有了*.ts 文件和*.txt 文件的共享缓存,所以ts-loader 不会尝试通过你的*.ts webpack 规则传递*.ts 文件。

    所以下面的定义也可以:

    /* ... */
        rules: [
          {
            test: /\.ts$/,
            use: [
              {
                loader: 'ts-loader',
                options: { appendTsSuffixTo: [/\.txt$/] },
              }
            ],
          },
          {
            test: /\.txt$/,
            use: [
              {
                loader: 'ts-loader',
                options: { appendTsSuffixTo: [/\.txt$/] },
              },
              path.resolve('txt-loader.js'),
            ],
          },
        ],
    /* ... */
    
    3.2.1 使用ts-loaderinstance 选项

    ts-loader 有一个(相当隐藏的)instance 选项。

    通常这将用于隔离两个具有相同选项的 ts-loader 实例 - 但它也可用于强制合并两个 ts-loader 实例。

    所以这也可以:

    /* ... */
        rules: [
          {
            test: /\.ts$/,
            use: [
              {
                loader: 'ts-loader',
                options: { appendTsSuffixTo: [/\.txt$/], instance: "foobar" },
              }
            ],
          },
          {
            test: /\.txt$/,
            use: [
              {
                loader: 'ts-loader',
                options: { instance: "foobar", /* OTHER OPTIONS SILENTLY IGNORED */ },
              },
              path.resolve('txt-loader.js'),
            ],
          },
        ],
    /* ... */
    

    你需要小心这个,因为第一个被 webpack 实例化的加载器会决定选项。您传递给具有相同 instance 选项的所有其他 ts-loader 的选项会被忽略

    3.3 让你的加载器忽略*.ts文件

    最简单的选择是只更改您的txt-loader.js 以不修改*.ts 文件,以防它被调用。这不是一个干净的解决方案,但它仍然有效:D

    txt-loader.js:

    module.exports = function TxtLoader(txt) {
      // ignore .ts files
      if(this.resourcePath.endsWith('.ts'))
        return txt;
    
      // handle .txt files:
      return `export const TEXT: string = ${JSON.stringify(txt)}`
    }
    

    【讨论】:

    • 感谢您非常全面的回答!解决方案 3.2 似乎是要走的路。我要过几天才能尝试,但我会告诉你的。
    • @Thomas 哇,感谢 500 代表的赏金!
    • 是的,3.2 完美解决了这个问题,没有放弃类型检查!我只是将 ts-loader 配置放在一个变量中,以确保每次调用都相同。比我已经想出的 3.3 更好,但感觉不对。我之前已经设置了赏金,但在你回答之前它就过期了。看起来不公平:)
    猜你喜欢
    • 1970-01-01
    • 2019-04-13
    • 1970-01-01
    • 1970-01-01
    • 2014-08-09
    • 2013-09-07
    • 2022-01-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多