3

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

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有效的文本。

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

索引.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'),
        ],
      },
    ],
  },
}

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

自定义.d.ts

declare module '*.txt'

tsconfig.json

{}

包.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": {}
}

对于那些想在家尝试的人,克隆这个最小的复制项目

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

4

2 回答 2

3

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.txtaddDependency在这里调用
  • 之后,第一个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.ts&custom.d.ts事件不做任何事情,因为它看到没有更改。

  • 第二个ts-loader也将获得所有 3 个更改事件,如果您只是触摸它,它将忽略hello.txt更改,或者在您编辑它时重新编译它。之后,它看到了custom.d.ts变化,意识到它还没有编译那个,并会尝试编译它,同时调用它之后指定的所有加载器。更改也会发生同样的事情index.ts

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

    • For index.ts:您.tsconfig没有指定includeorexcludefiles,因此ts-loader将使用 for 的默认值["**"]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.ts&custom.d.ts现在都在两个ts-loaders 的缓存中,因此只有在这些文件中的任何一个发生实际更改时才会调用您的自定义加载程序。


2. 类似问题

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


3. 潜在的解决方案

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

3.1。只做.txt ts-loadertranspile

In transpileOnly: true-modets-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通过你的*.txtwebpack 规则传递文件。

所以下面的定义也可以工作:

/* ... */
    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-loader'sinstance选项

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 实例化的加载器会决定选项。您传递给ts-loader具有相同instance选项的所有其他选项的选项会被忽略

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)}`
}
于 2021-12-08T22:40:02.553 回答
1

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

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

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

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

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

...
{
  test: /\.txt$/,
  use: [
    { 
      loader: 'ts-loader',
      options: { appendTsSuffixTo: [/\.txt$/], onlyCompileBundledFiles: true },
    }
    path.resolve('txt-loader.js'),
  ],
},
...
于 2021-11-29T19:27:27.403 回答