[Расширенный веб-пакет] Вы действительно освоили загрузчик? - загрузчик десять вопросов

внешний интерфейс исходный код JavaScript Webpack
[Расширенный веб-пакет] Вы действительно освоили загрузчик? - загрузчик десять вопросов

Как друзья, добро пожаловать, чтобы следовать за мнойблогилиНовостная лента. Прошлые статьи:

1. загрузчик десять вопросов

В процессе изучения загрузчика webpack я также прочитал много связанных статей в Интернете и многому научился. Однако большинство из них представляют только метод настройки загрузчика или метод написания загрузчика, а введение параметров, API и других деталей неясно.

Вот «Десять вопросов о загрузчиках», некоторые из вопросов, которые у меня возникли перед чтением исходного кода загрузчика:

  1. Где обрабатывается конфигурация веб-пакета по умолчанию?Есть ли у загрузчика какая-либо конфигурация по умолчанию?
  2. В веб-пакете есть понятие распознавателя, которое используется для анализа реального абсолютного пути к файлу модуля, поэтому загрузчик и распознаватель обычных модулей используют один и тот же?
  3. Мы знаем, что помимо загрузчика в конфиге можно написать еще и встроенный загрузчик, так каков же порядок выполнения встроенного загрузчика и обычного загрузчика конфига?
  4. в конфигурацииmodule.rulesКак это работает и реализуется в веб-пакете?
  5. Как и когда загрузчики играют роль в процессе компиляции веб-пакета?
  6. Почему загрузчик выполняется справа налево?
  7. Что именно произойдет, если значение будет возвращено в поле?
  8. Если вы написали загрузчик, его можно использовать в функции загрузчика.this,здесьthisЧто это, это экземпляр веб-пакета?
  9. в функции загрузчикаthis.dataКак это достигается?
  10. Как написать асинхронный загрузчик и как Webpack реализует асинхронный загрузчик?

Возможно, у вас возникнут подобные вопросы. Ниже я объединим часть исходного кода, относящегося к загрузчику, чтобы восстановить принципы проектирования и реализации загрузчика и ответить на эти сомнения.

2. Общий процесс работы погрузчика

Процесс компиляции веб-пакета очень сложен, но части, связанные с загрузчиками, в основном включают:

  • Конфигурация загрузчика по умолчанию (веб-пакет)
  • Используйте loaderResolver для разрешения путей модулей загрузчика.
  • согласно сrule.modulesСоздайте набор правил RulesSet
  • Запустите загрузчик с помощью loader-runner

Соответствующий общий процесс выглядит следующим образом:

Прежде всего,Compiler.jsЧтобы объединить пользовательскую конфигурацию с конфигурацией по умолчанию, включена часть загрузчика.

Затем webpack создаст два ключевых объекта на основе конфигурации:NormalModuleFactoryа такжеContextModuleFactory. Они эквивалентны двум фабрикам классов, через которыеNormalModuleа такжеContextModule. вNormalModuleОсновное внимание в этой статье уделяется классу, webpack сгенерирует соответствующий файл модуля в исходном коде.NormalModuleпример.

Создан на заводеNormalModuleЕсть несколько необходимых шагов перед экземпляром, наиболее важным для загрузчика является разрешение пути загрузчика через преобразователь загрузчика.

существуетNormalModuleПосле того, как экземпляр будет создан, он пройдет.build()способ сборки модуля. Первым шагом в создании модуля является использование загрузчика для загрузки и обработки содержимого модуля. Библиотека loader-runner является исполнителем загрузчика в webpack.

Наконец, выведите содержимое модуля, обработанного загрузчиком, и приступайте к последующему процессу компиляции.

Выше приведен общий процесс, связанный с загрузчиком в webpack. Далее будет подробный анализ исходного кода, и в процессе чтения и анализа исходного кода вы найдете ответ на «Десять вопросов о загрузчиках».

3. Конкретный разбор ходовой части погрузчика

3.1 конфигурация веб-пакета по умолчанию

В: 1. Где обрабатывается дефолтная конфигурация вебпака, есть ли у загрузчика дефолтная конфигурация?

Как и другие инструменты, webpack работает через настройку. С непрерывным развитием веб-пакета его конфигурация по умолчанию также постоянно меняется; и некоторые передовые методы из предыдущих версий также вошли в конфигурацию веб-пакета по умолчанию с обновлением версии.

Входной файл веб-пакетаlib/webpack.js, установит параметры конфигурации во время компиляции в соответствии с файлом конфигурации(source code)(Предыдущий«Визуальный дисплей WebPack Internal Plug и Clooth Review 📈»Здесь же срабатывает упомянутый плагин)

options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;

Видно, что конфигурация по умолчанию находится вWebpackOptionsDefaulterвнутренний. Поэтому, если вы хотите просмотреть конкретное содержимое текущих элементов конфигурации веб-пакета по умолчанию, вы можете перейти кмодульПосмотреть в.

Например, вmodule.rulesЗначение по умолчанию для этой части[]; но вдобавок естьmodule.defaultRulesЭлемент конфигурации хоть и не открыт для разработчиков, но содержит конфигурацию загрузчика по умолчанию(source code):

this.set("module.rules", []);
this.set("module.defaultRules", "make", options => [
    {
        type: "javascript/auto",
        resolve: {}
    },
    {
        test: /\.mjs$/i,
        type: "javascript/esm",
        resolve: {
            mainFields:
                options.target === "web" ||
                options.target === "webworker" ||
                options.target === "electron-renderer"
                    ? ["browser", "main"]
                    : ["main"]
        }
    },
    {
        test: /\.json$/i,
        type: "json"
    },
    {
        test: /\.wasm$/i,
        type: "webassembly/experimental"
    }
]);

Также стоит упомянуть, что,WebpackOptionsDefaulterунаследовано отOptionsDefaulter,а такжеOptionsDefaulterЭто инкапсулированный метод доступа к элементам конфигурации, который инкапсулирует некоторые специальные методы для управления объектами конфигурации.

3.2. СоздатьNormalModuleFactory

NormalModuleЭто функция класса, которая должна быть упомянута в webpack. Модули в исходном коде будут генерировать соответствующие модули в процессе компиляции.NormalModuleпример.

NormalModuleFactoryдаNormalModuleзаводской класс. Он был создан вCompiler.jsосуществляется в г.Compiler.jsЭто управляющий класс для основного процесса компиляции webpack.compiler.run()Поток тела (хука) в методе выглядит следующим образом:

.run()запуск рядаbeforeRun,runДождавшись хука, он вызовет.compile()метод, первым шагом которого является вызовthis.newCompilationParams()СоздайтеNormalModuleFactoryпример.

newCompilationParams() {
    const params = {
        normalModuleFactory: this.createNormalModuleFactory(),
        contextModuleFactory: this.createContextModuleFactory(),
        compilationDependencies: new Set()
    };
    return params;
}

3.3. Определите реальный абсолютный путь загрузчика

В: 2. В веб-пакете есть понятие резолвера, который используется для анализа реального абсолютного пути к файлу модуля, поэтому один и тот же резолвер, используемый модулем загрузчика и обычным модулем (модулем исходного кода)?

существуетNormalModuleFactory, СоздайтеNormalModuleПеред экземпляром задействованы четыре хука:

  • beforeResolve
  • resolve
  • factory
  • afterResolve

Двумя наиболее важными являются:

  • Часть разрешения отвечает за разбор пути модуля загрузчика (например, какой путь к модулю загрузчика css-loader);
  • Фабрика отвечает за создание на основе возвращаемого значения хука разрешения.NormalModuleпример.

resolveМетод, зарегистрированный на хуке, длиннее, что также включает в себя разрешение пути самого ресурса модуля.resolverСуществует два типа: loaderResolver и normalResolver.

const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);

Помимо загрузчика, который можно настроить в конфиге, есть еще метод записи встроенного загрузчика, поэтому анализ пути файла загрузчика тоже делится на два типа: встроенный загрузчик и загрузчик в конфиге . Хук распознавателя сначала обработает встроенный загрузчик.

3.3.1. inline loader

import Styles from 'style-loader!css-loader?modules!./styles.css';

Выше приведен пример встроенного загрузчика. где запросstyle-loader!css-loader?modules!./styles.css.

Во-первых, webpack будет парсить нужный загрузчик из запроса(source code):

let elements = requestWithoutMatchResource
    .replace(/^-?!+/, "")
    .replace(/!!+/g, "!")
    .split("!");

Следовательно, изstyle-loader!css-loader?modules!./styles.cssЕсть два погрузчика, которые можно вывезти:style-loaderа такжеcss-loader.

Тогда "массив загрузчика модуля парсера" будет выполняться параллельно с "самим модулем парсера", который используется здесьneo-asyncэта библиотека.

neo-asyncбиблиотека иasyncБиблиотеки похожи тем, что все они предоставляют некоторые инструменты и методы для асинхронного программирования, но они болееasyncБиблиотеки работают быстрее.

Формат результата, возвращаемого синтаксическим анализом, примерно следующий:

[ 
    // 第一个元素是一个loader数组
    [ { 
        loader:
            '/workspace/basic-demo/home/node_modules/html-webpack-plugin/lib/loader.js',
        options: undefined
    } ],
    // 第二个元素是模块本身的一些信息
    {
        resourceResolveData: {
            context: [Object],
            path: '/workspace/basic-demo/home/public/index.html',
            request: undefined,
            query: '',
            module: false,
            file: false,
            descriptionFilePath: '/workspace/basic-demo/home/package.json',
            descriptionFileData: [Object],
            descriptionFileRoot: '/workspace/basic-demo/home',
            relativePath: './public/index.html',
            __innerRequest_request: undefined,
            __innerRequest_relativePath: './public/index.html',
            __innerRequest: './public/index.html'
        },
	resource: '/workspace/basic-demo/home/public/index.html'
    }
]

Первый элемент — это все встроенные загрузчики, задействованные при обращении к модулю, включая абсолютный путь и элементы конфигурации файла загрузчика.

3.3.2. config loader

В: 3. Мы знаем, что помимо загрузчика в конфиге можно написать еще и встроенный загрузчик, так каков порядок выполнения встроенного загрузчика и обычного загрузчика конфига?

В приведенном выше разделе веб-пакет сначала анализирует абсолютный путь и конфигурацию встроенного загрузчика. Следующим шагом будет разбор загрузчика в конфигурационном файле(source code),Прямо сейчасmodule.rulesЧасть конфигурации:

const result = this.ruleSet.exec({
    resource: resourcePath,
    realResource:
        matchResource !== undefined
            ? resource.replace(/\?.*/, "")
            : resourcePath,
    resourceQuery,
    issuer: contextInfo.issuer,
    compiler: contextInfo.compiler
});

NormalModuleFactoryесть одинruleSetВы можете просто понять это так: он может соответствовать загрузчику, требуемому модулем, в соответствии с именем пути к модулю.RuleSetДетали не перечислены здесь первыми, а конкретное содержание будет представлено в следующем разделе.

здесь, чтобыthis.ruleSet.exec()Путь исходного модуля передается, а возвращаемыйresultЭто загрузчик в конфиге, соответствующий текущему модулю. Если вы знакомы с конфигурацией веб-пакета, вы будете знатьmodule.rulesесть одинenforceполе. На основании этого поля webpack делит загрузчики на три типа: preLoader, postLoader и loader.(source code):

for (const r of result) {
    if (r.type === "use") {
        // post类型
        if (r.enforce === "post" && !noPrePostAutoLoaders) {
            useLoadersPost.push(r.value);
        // pre类型
        } else if (
            r.enforce === "pre" &&
            !noPreAutoLoaders &&
            !noPrePostAutoLoaders
        ) {
            useLoadersPre.push(r.value);
        } else if (
            !r.enforce &&
            !noAutoLoaders &&
            !noPrePostAutoLoaders
        ) {
            useLoaders.push(r.value);
        }
    }
    // ……
}

Наконец, используйте neo-aysnc для параллельного анализа трех типов массивов загрузчика.(source code):

asyncLib.parallel(
    [
        this.resolveRequestArray.bind(
            this,
            contextInfo,
            this.context,
            useLoadersPost, // postLoader
            loaderResolver
        ),
        this.resolveRequestArray.bind(
            this,
            contextInfo,
            this.context,
            useLoaders, // loader
            loaderResolver
        ),
        this.resolveRequestArray.bind(
            this,
            contextInfo,
            this.context,
            useLoadersPre, // preLoader
            loaderResolver
        )
    ]
    // ……
}

Итак, каков порядок финальных загрузчиков? Следующая строка кода объясняет это:

loaders = results[0].concat(loaders, results[1], results[2]);

вresults[0],results[1],results[2],loaderЭто postLoader, loader (обычный загрузчик конфигурации), preLoader и inlineLoader. Таким образом, порядок объединенного загрузчика следующий: post, inline, normal и pre.

Однако загрузчик выполняется справа налево, а реальный порядок выполнения загрузчика обратный, поэтому inlineLoader выполняется целиком после обычного загрузчика в конфиге.

3.3.3. RuleSet

В: 4. В конфигурацииmodule.rulesКак это работает и реализуется в веб-пакете?

использование веб-пакетаRuleSetобъект для соответствия загрузчикам, требуемым модулем.RuleSetЭквивалентно фильтру правил, применяющему resourcePath ко всемmodule.rulesправила для фильтрации нужного загрузчика. Два наиболее важных метода:

  • статический метод класса.normalizeRule()
  • метод экземпляра.exec()

Компиляция Webpack создаст экземпляр конфигурации пользователя и конфигурации по умолчанию.RuleSet. Во-первых, через статический метод на нем.normalizeRule()Преобразуйте значение конфигурации в нормализованный тестовый объект; он также хранитthis.referencesСвойство — тип карты хранилища, а ключ — тип и расположение загрузчика в конфигурации, например,ref-2Представляет третий в массиве конфигурации загрузчика.

p.s. Если вы распечатаете некоторые поля, связанные с запросом, в NormalModule на хуке в .compilation, те модули, которые используют загрузчик, будут выглядеть аналогичноref-ценность . Отсюда вы можете увидеть, использует ли модуль загрузчик и какое правило конфигурации срабатывает.

созданныйRuleSetЕго можно использовать для получения соответствующего загрузчика для каждого модуля. это созданоRuleSetэто то, что мы упоминали вышеNormalModuleFactoryна экземпляреthis.ruleSetАтрибуты. Фабрика каждый раз создает новыйNormalModuleбудет вызван, когдаRuleSetпример.exec()метод, загрузчик будет помещен в массив результатов только при выполнении различных тестовых условий.

3.4 Запуск загрузчика

3.4.1 Время работы загрузчика

В: 5. Как и когда работает загрузчик в процессе компиляции веб-пакета?

После разрешения абсолютного пути загрузчика вNormalModuleFactoryизfactoryХук создаст текущий модульNormalModuleобъект. На данный момент работа по предзаказу загрузчика практически завершена, и далее предстоит собственно запуск каждого загрузчика.

Все мы знаем, что запуск загрузчика для чтения и обработки модулей — это первый шаг в обработке модуля webpack. Но если дело доходит до подробного времени выполнения, это включает в себя компиляцию веб-пакета.compilationэтот очень важный объект.

webpack компилируется в измерении входа,compilationЕсть важный метод -.addEntry(), модуль будет построен на основе записи..addEntry()метод называется._addModuleChain()выполнит ряд методов модуля(source code)

this.semaphore.acquire(() => {
    moduleFactory.create(
        {
            // ……
        },
        (err, module) => {
            if (err) {
                this.semaphore.release();
                return errorAndCallback(new EntryModuleNotFoundError(err));
            }
            // ……
            if (addModuleResult.build) {
                // 模块构建
                this.buildModule(module, false, null, null, err => {
                    if (err) {
                        this.semaphore.release();
                        return errorAndCallback(err);
                    }

                    if (currentProfile) {
                        const afterBuilding = Date.now();
                        currentProfile.building = afterBuilding - afterFactory;
                    }

                    this.semaphore.release();
                    afterBuild();
                });
            }
        }
    )
}

Среди них, для модулей, которые не были построены, он в конечном итоге будет вызван дляNormalModuleобъект.doBuild()метод. в то время как строительный модуль (.doBuild()), первый шаг –запустить все загрузчики.

В это время на сцену выходит грузчик-раннер.

3.4.2 loader-runner — библиотека выполнения загрузчика

Q:.6 загрузчик Почему выполняются справа налево?

Webpack удаляет рабочий инструмент загрузчика и становится независимымбиблиотека загрузчика-раннера. Следовательно, вы можете написать загрузчик и использовать автономный загрузчик-раннер для проверки эффекта загрузчика.

Загрузчик-раннер разделен на две части: loadLoader.js и LoaderRunner.js.

loadLoader.js — это совместимый загрузчик модулей, который может загружать определения модулей, такие как cjs, esm или SystemJS. LoaderRunner.js — это основная часть работы модуля загрузчика. подвергается в.runLoaders()Метод — это метод запуска для запуска загрузчика.

Если вы писали или знаете, как написать загрузчик, вы точно знаете, что каждый модуль загрузчика поддерживает.pitchвышеприведенный метод будет иметь приоритет над фактическим выполнением метода загрузчика. На самом деле официальный веб-пакет также дает диаграмму последовательности выполнения методов питча и загрузчика:

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

Эти две стадии (шаговая и нормальная) соответствуют друг другу в загрузчике-раннере.iteratePitchingLoaders()а такжеiterateNormalLoaders()два метода.

iteratePitchingLoaders()будет выполняться рекурсивно и записывать загрузчикаpitchстатус и выполняется в настоящее времяloaderIndex(loaderIndex++). Когда будет достигнут максимальный порядковый номер загрузчика, фактический модуль будет обработан:

if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

когдаloaderContext.loaderIndexКогда значение достигает длины всего массива загрузчиков, это указывает на то, что все питчи выполнены (выполняется последний загрузчик), после чего будет выполнен вызов.processResource()для управления ресурсами модуля. В основном включают: добавление модуля в качестве зависимости и чтение содержимого модуля. Затем он будет выполняться рекурсивноiterateNormalLoaders()и выполнятьloaderIndex--операция, поэтому загрузчик выполняется «в обратном порядке».

Далее мы обсуждаем несколько деталей погрузчика-бегуна:

В: 7. Что произойдет, если значение будет возвращено в поле?

Официальный сайт говорит:

if a loader delivers a result in the pitch method the process turns around and skips the remaining loaders

В этом описании сказано, что возврат значения в поле пропускает остальные загрузчики. Это утверждение является относительно грубым, и есть несколько деталей, которые необходимо объяснить:

Во-первых, только когдаloaderIndexДостигнута максимальная длина массива, то есть он будет выполнен только после раскачки всех загрузчиков.processResource().

if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

Следовательно, помимо пропуска остальной части загрузчика, возвращаемое значение в поле не только сделает.addDependency()Не запускать (без добавления ресурса модуля) и не может прочитать содержимое файла модуля. Загрузчик обработает значение, возвращенное PITCH в «Содержимое файла», и вернет его в WebPack.


В: 8. Если вы написали загрузчик, его можно использовать в функции загрузчикаthis,здесьthisЧто это, это экземпляр веб-пакета?

На самом деле здесьthisНи экземпляры webpack, ни экземпляры компилятора, компиляции, normalModule и т.д. ноВызовloaderContextконкретный объект загрузчика-бегуна.

каждый звонокrunLoaders()метод, если контекст не передан явно, новый будет создан по умолчаниюloaderContext. Таким образом, различные API-интерфейсы загрузчика (обратный вызов, данные, loaderIndex, addContextDependency и т. д.), упомянутые на официальном веб-сайте, являются свойствами объекта.


В: 9. В функции загрузчикаthis.dataКак это достигается?

знать в загрузчикеthisНа самом делеloaderContextобъект, тоthis.dataРеализация на самом делеloaderContext.dataреализация(source code):

Object.defineProperty(loaderContext, "data", {
    enumerable: true,
    get: function() {
        return loaderContext.loaders[loaderContext.loaderIndex].data;
    }
});

здесь определяет.data(магазин) аксессуар. Видно, что вызовthis.data, другой нормальный загрузчик из-заloaderIndexРазные, вы получите разные значения, а формальные параметры метода основного тонаdataЭто и есть данные под разными загрузчиками(source code).

runSyncOrAsync(
    fn,
    loaderContext,
    [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
    function(err) {
        // ……
    }
);

runSyncOrAsync()массив в[loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}]- входной параметр метода основного тона, аcurrentLoaderObjectтекущийloaderIndexОбъект загрузчика для ссылки.

Поэтому, если вы хотите сохранить «полные» данные, рассмотрите возможность сохранения вthisВ других свойствах или путем изменения loaderIndex можно получить данные о других загрузчиках (сравните хак).


Q: 10. Как написать асинхронный загрузчик и как Webpack реализует асинхронный загрузчик?

Фактическое выполнение шага и нормального загрузчика находится вrunSyncOrAsync()в этом методе.

Согласно документации веб-пакета, когда мы вызываемthis.async(), он превратит загрузчик в асинхронный погрузчик и вернет асинхронный обратный вызов.

В конкретной реализации,runSyncOrAsync()есть один внутриisSyncпеременная, по умолчаниюtrue; когда мы звонимthis.async(), будет установлено значениеfalseи возвращаетinnerCallbackВ качестве уведомления обратного вызова после асинхронного выполнения:

context.async = function async() {
    if(isDone) {
        if(reportedError) return; // ignore
        throw new Error("async(): The callback was already called.");
    }
    isSync = false;
    return innerCallback;
};

Обычно мы используемthis.async()Обратный вызов вернулся, чтобы уведомить об асинхронном завершении, но на самом деле выполнениеthis.callback()Тот же эффект:

var innerCallback = context.callback = function() {
    // ……
}

В то же время вrunSyncOrAsync(), ТолькоisSyncопределены какtrueКогда функция загрузчика выполняется (синхронно), обратный вызов будет называться обратно для продолжения погрузчика-бегуна.

if(isSync) {
    isDone = true;
    if(result === undefined)
        return callback();
    if(result && typeof result === "object" && typeof result.then === "function") {
        return result.catch(callback).then(function(r) {
            callback(null, r);
        });
    }
    return callback(null, result);
}

Увидев это, вы обнаружите, что в коде есть место, которое будет определять, является ли возвращаемое значение обещанием (typeof result.then === "function"), если это обещание, обратный вызов будет вызываться асинхронно. Поэтому, чтобы получить асинхронный загрузчик, кроме упомянутых в документации вебпакаthis.async()метод, вы также можете напрямую вернуть Promise.

4. Эпилог

Выше приведен анализ исходного кода соответствующей части загрузчика webapck. Я полагаю, что к этому моменту вы уже ответили на первые «десять вопросов загрузчика». Я надеюсь, что эта статья поможет вам узнать больше о реализации загрузчика в дополнение к обучению настройке загрузчика и написанию простого загрузчика.

В процессе чтения исходного кода могут быть некоторые ошибки.Приглашаем к общению друг с другом.

Попрощайтесь с «инженером по настройке веб-пакета»

webpack — это мощный и сложный интерфейсный инструмент автоматизации. Одной из особенностей является то, что конфигурация сложная, что также делает популярным шутливое название «инженер по настройке веб-пакета»🤷 Однако вас действительно устраивает только игра с конфигурацией веб-пакета?

Очевидно нет. В дополнение к изучению того, как использовать веб-пакет, нам нужно углубиться в веб-пакет и изучить дизайн и реализацию каждой части. Даже если однажды веб-пакет «устарел», некоторые из его проектов и реализаций по-прежнему будут иметь ценность для изучения и справочное значение. Поэтому в процессе изучения webpack я подытожу серию статей [webpack advanced] и поделюсь ими с вами.

Заинтересованные студенты приглашаются к общению и вниманию!

Прошлые статьи: