Почему webpack --watch вызывает мой пользовательский загрузчик для несвязанных файлов?
У меня есть простой пользовательский загрузчик Webpack, который генерирует код TypeScript из файла:
txt-loader.js
module.exports = function TxtLoader(txt) {
console.log(`TxtLoader invoked with: ${txt}`)
return `export const TEXT = ${JSON.stringify(txt)}`
}
Это позволяет мне импортировать текстовый файл следующим образом:
index.ts
import { TEXT } from './hello.txt'
console.log(TEXT)
Загрузчик настроен так:
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'),
],
},
],
},
}
Наконец, несколько битов TypeScript, чтобы собрать все это воедино:
custom.d.ts
declare module '*.txt'
tsconfig.json
{}
Это установка. Все работает нормально, кроме одного:
webpack watch
(и его двоюродный брат
webpack serve
). Первая компиляция прошла успешно:
$ yarn webpack watch
yarn run v1.22.17
$ /tmp/hex/node_modules/.bin/webpack watch
TxtLoader invoked with: Hello world!
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 3932 ms
Но потом я меняю
hello.txt
файл:
$ touch hello.txt
И вдруг происходят странные вещи:
TxtLoader invoked with: import { TEXT } from './hello.txt'
console.log(TEXT)
TxtLoader invoked with: declare module '*.txt'
TxtLoader invoked with: Hello world!
asset main.js 250 bytes [emitted] [minimized] (name: main)
./index.ts 114 bytes [built] [code generated]
./hello.txt 97 bytes [built] [code generated]
webpack 5.64.3 compiled successfully in 499 ms
Похоже, что Webpack решил бросить в мой загрузчик больше материала, чем он готов обработать. Конечный результат точно такой же, как будто эти дополнительные операции полностью избыточны.
Для меня это проблема, потому что в моей реальной ситуации я не копирую вслепую
.txt
файл, а анализируя
.csv
file, который вызывает исключение, когда вы бросаете в него контент, отличный от CSV.
Это ошибка в Webpack? В ц-загрузчике? В моей комплектации?
Для тех, кто хочет попробовать это дома, клонируйте этот минимальный репро-проект .
2 ответа
1. Проблема
Основная проблема в том, что будут загружаться дополнительные файлы и вручную вызывать на них ваш загрузчик.
В вашей текущей конфигурации веб-пакета вы получите 2 независимых экземпляра:
- Один для
.ts
файлы - И один для файлов
1.1. первая компиляция
Во время первоначальной компиляции произойдет следующее:
- будет обрабатываться первым экземпляром, который попытается его скомпилировать.
- Первый не знает, как загрузить файл, поэтому ищет объявления некоторых модулей, находит и загружает его.
- Теперь, когда первый умеет обращаться с файлами, он будет регистрироваться и как зависимый от (
addDependency
Звоните сюда ) - После этого первый экземпляр попросит webpack скомпилировать .
- будет загружен вторым экземпляром через ваш собственный загрузчик (как и следовало ожидать)
2.1. вторая компиляция
Как только вы нажмете (или измените) , webpack уведомит всех наблюдателей об изменениях. Но поскольку & зависят от , все наблюдатели также будут уведомлены об изменениях этих двух.
Первый получит все 3 события изменения, проигнорирует одно, поскольку оно не скомпилировано, и ничего не сделает для событий &, поскольку видит, что изменений нет.
Второй также получит все 3 события изменения, он будет игнорировать
hello.txt
измените, если вы только что коснулись его, или перекомпилируйте его, если вы его редактировали. После этого он видит изменение, понимает, что он еще не скомпилировал его и попытается скомпилировать и его, вызывая при этом все указанные после него загрузчики . То же самое происходит и с изменением.Причина, по которой второй даже пытается загрузить эти файлы, заключается в следующем:
- Для тебя
.tsconfig
не указывает илиexclude
или жеfiles
, поэтому будет использоваться значение по умолчанию["**"]
заinclude
, то есть все, что он может найти. Поэтому, как только он получит уведомление об изменении, он попытается загрузить его.- Это также объясняет, почему вы не получаете его, потому что в этом случае он понимает, что должен игнорировать этот файл.
- Поскольку это в основном то же самое, но они все равно будут включены даже с
onlyCompileBundledFiles: true
:Поведение ts-loader по умолчанию состоит в том, чтобы действовать как вставная замена для команды tsc, поэтому он учитывает параметры включения, файлов и исключения в вашем tsconfig.json, загружая любые файлы, указанные этими параметрами. Параметр onlyCompileBundledFiles изменяет это поведение, загружая только те файлы, которые фактически включены в пакет webpack, а также любые файлы .d.ts, включенные в настройки tsconfig.json. Файлы .d.ts по-прежнему включены, потому что они могут понадобиться для компиляции без явного импорта и, следовательно, не подхватываются webpack.
- Для тебя
1.3. любая компиляция после этого
Если вы измените свой код, чтобы он не выбрасывал, а возвращал содержимое без изменений, то есть:
if (txt.indexOf('Hello') < 0) {
return txt;
}
Мы можем видеть, что происходит на третьей, четвертой и т. д. компиляции.
Поскольку оба
index.ts
&
custom.d.ts
теперь находятся в кешах обоих s, ваш пользовательский загрузчик будет вызываться только в том случае, если в любом из этих файлов есть фактическое изменение.
2. Похожие проблемы
Вы не единственный, кто столкнулся с этой «функцией», для нее даже есть открытая проблема на github:
- пользовательский загрузчик в цепочке с ts-loader потребляет файлы ts
- ts-loader прерывает обслуживание веб-пакета из-за повторного использования loaderContext
- Предложение сделать ts-loader лучшим гражданином веб-пакета
3. Возможные решения
Есть несколько способов избежать этой проблемы:
3.1. сделать транспайл-только
В
transpileOnly: true
-mode будет игнорировать все остальные файлы и обрабатывать только те, которые 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. убедитесь, что есть только один экземпляр
Пока вы указываете одинаковые параметры для каждого загрузчика, экземпляр загрузчика будет использоваться повторно.
Таким образом, у вас есть общий кеш для файлов и файлов, поэтому он не пытается передавать файлы через ваш
*.txt
правило вебпака.
Таким образом, следующее определение также будет работать:
/* ... */
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 использование опции 's
имеет (скорее скрытый)вариант.
Обычно это используется для разделения двух экземпляров с одинаковыми параметрами, но его также можно использовать для принудительного объединения двух экземпляров.
Так что это тоже сработает:
/* ... */
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
файлы на случай, если он будет вызван с одним из них. Это не чистое решение, но тем не менее оно работает: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)}`
}
В вашем минимальном воспроизведении я обнаружил, что комментирование этих строк устранило проблему:
...
{
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-loader
) всякий раз, когда что-то меняется. Обычно это хорошо, потому что ваш проект будет перекомпилирован, если, например,
.d.ts
изменения файла, которые включаются только неявно через
tsconfig.json
, а не через явный оператор импорта, обработанный веб-пакетом.
По крайней мере, в предоставленном вами простом репродукции пакет, похоже, генерируется и работает без в
/\.txt$/
конвейер вообще, что может быть достаточно, чтобы решить вашу проблему.
Но на случай, если по какой-то причине в вашем реальном случае необходимо включить в этот конвейер, вы должны быть в состоянии сказать
ts-loader
только просматривать/просматривать явно связанные файлы с помощью
onlyCompileBundledFiles
вариант (см. документы):
...
{
test: /\.txt$/,
use: [
{
loader: 'ts-loader',
options: { appendTsSuffixTo: [/\.txt$/], onlyCompileBundledFiles: true },
}
path.resolve('txt-loader.js'),
],
},
...