Оригинальный накопитель — это такой простой плагин

rollup.js
Оригинальный накопитель — это такой простой плагин

Привет всем, я Xiaoyu Xiaoyu, посвященный обмену интересными и практическими техническими статьями.

Контент разделен на перевод и оригинал.Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь комментировать или отправить личное сообщение, и надеемся добиться прогресса вместе со всеми.

Ваша поддержка является движущей силой моего творчества.

строить планы

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

Это последняя статья в серии сводок, вот ссылки на все статьи.

TL;DR

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

Но я не хочу, чтобы webpack различал загрузчик и плагин, плагин Rollup может играть роль загрузчика, а может играть роль традиционного плагина. Функция ловушки, предоставляемая rollup, является ядром, например, загрузка и преобразование для анализа и изменения фрагмента, resolveFileUrl может легально анализировать загруженный модуль, параметры для динамического обновления конфигурации и т. д. ~

будь осторожен

Все аннотации находятся вздесь, вы можете прочитать это сами

!!!Советы => отмеченные TODO - это конкретные детали реализации, которые будут проанализированы в зависимости от ситуации.

!!!Примечание => Каждый подзаголовок реализован внутри родительского заголовка (функции)

!!!Акцент => id модуля (файла) в роллапе - это адрес файла, поэтому что-то наподобие resolveID означает разрешить адрес файла, мы можем вернуть тот id файла, который хотим вернуть (то есть адрес, относительный путь, путь решения), чтобы позволить загрузке свертки

rollup — это ядро, которое выполняет только самые основные функции, такие как предоставлениеМеханизм загрузки модуля (файла) по умолчанию, такие как упаковка в различные стили контента, наш плагин обеспечивает такие операции, как загрузка путей к файлам, анализ содержимого файлов (обработка ts, sass и т. д.), который представляет собой подключаемый дизайн, аналогичный веб-пакету. Pluggable — это очень гибкий дизайн, который можно многократно обновлять в течение длительного времени, а также ядро ​​среднего и крупного фреймворка.

Основные общие модули и их значения

  1. График: глобально уникальный график, включающий записи и взаимосвязи различных зависимостей, методов работы, кешей и т. д. является ядром накопительного пакета
  2. PathTracker: отслеживание ссылок (вызовов)
  3. PluginDriver: драйвер подключаемого модуля, вызов подключаемых модулей и предоставление контекста среды подключаемого модуля и т. д.
  4. FileMitter: Манипулятор ресурсов
  5. GlobalScope: глобальная область действия, а не локальная.
  6. ModuleLoader: загрузчик модулей
  7. NodeBase: базовый класс построения синтаксиса ast (ArrayExpression, AwaitExpression и т. д.)

Анализ механизма плагина

Плагин rollup на самом деле является обычной функцией, функция возвращает объект, объект содержит некоторые основные свойства (такие как имя) и перехватывает функции на разных этапах, например:

function plugin(options = {}) {
  return {
    name: 'rollup-plugin',
    transform() {
      return {
        code: 'code',
        map: { mappings: '' }
      };
    }
  };
}

Вот официальное предложениесоглашение.

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

  1. const chunks = во время выполнения rollup.rollupBuild Hooks
  2. Во время выполнения chunks.generator(запись)Output Generation Hooks
  3. Прислушивайтесь к изменениям файлов и повторно выполняйте функцию ловушки watchChange во время выполнения сборки rollup.watch.

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

  1. async: асинхронные хуки для обработки промисов, также есть синхронные версии.
  2. first: если несколько подключаемых модулей реализуют одну и ту же функцию ловушки, они будут выполняться последовательно, от начала до конца, но если возвращаемое значение одного из них не является ни нулевым, ни неопределенным, последующие подключаемые модули будут завершены напрямую.
  3. последовательный: если несколько плагинов реализуют одну и ту же функцию ловушки, они будут выполняться последовательно, от начала до конца в том порядке, в котором используются плагины.Если он асинхронный, он будет ждать завершения предыдущей обработки перед выполнением следующего плагин.
  4. parallel: То же, что и выше, но если плагин асинхронный, последующие плагины не будут ждать, а будут выполняться параллельно.

Текстовое выражение относительно бледное, давайте посмотрим на несколько реализаций:

  • Функция крючка: hookFirst Сценарии использования: resolveId, resolveAssetUrl и т. д.
function hookFirst<H extends keyof PluginHooks, R = ReturnType<PluginHooks[H]>>(
    hookName: H,
    args: Args<PluginHooks[H]>,
    replaceContext?: ReplaceContext | null,
    skip?: number | null
): EnsurePromise<R> {
    // 初始化promise
    let promise: Promise<any> = Promise.resolve();
    // this.plugins在初始化Graph的时候,进行了初始化
    for (let i = 0; i < this.plugins.length; i++) {
        if (skip === i) continue;
        // 覆盖之前的promise,换言之就是串行执行钩子函数
        promise = promise.then((result: any) => {
            // 返回非null或undefined的时候,停止运行,返回结果
            if (result != null) return result;
            // 执行钩子函数
            return this.runHook(hookName, args as any[], i, false, replaceContext);
        });
    }
    // 最后一个promise执行的结果
    return promise;
}
  • Функция ловушки: hookFirstSync Сценарии использования: resolveFileUrl, resolveImportMeta и т. д.
// hookFirst的同步版本,也就是并行执行
function hookFirstSync<H extends keyof PluginHooks, R = ReturnType<PluginHooks[H]>>(
    hookName: H,
    args: Args<PluginHooks[H]>,
    replaceContext?: ReplaceContext
): R {
    for (let i = 0; i < this.plugins.length; i++) {
        // runHook的同步版本
        const result = this.runHookSync(hookName, args, i, replaceContext);
        // 返回非null或undefined的时候,停止运行,返回结果
        if (result != null) return result as any;
    }
    // 否则返回null
    return null as any;
}
  • Функция ловушки: hookSeq Сценарии использования: onwrite, generateBundle и др.
// 和hookFirst的区别就是不能中断
async function hookSeq<H extends keyof PluginHooks>(
    hookName: H,
    args: Args<PluginHooks[H]>,
    replaceContext?: ReplaceContext
): Promise<void> {
    let promise: Promise<void> = Promise.resolve();
    for (let i = 0; i < this.plugins.length; i++)
        promise = promise.then(() =>
            this.runHook<void>(hookName, args as any[], i, false, replaceContext)
        );
    return promise;
}
  • Функция хука: hookParallel Сценарии использования: buildStart, buildEnd, renderStart и др.
// 同步进行,利用的Promise.all
function hookParallel<H extends keyof PluginHooks>(
    hookName: H,
    args: Args<PluginHooks[H]>,
    replaceContext?: ReplaceContext
): Promise<void> {
    // 创建promise.all容器
    const promises: Promise<void>[] = [];
    // 遍历每一个plugin
    for (let i = 0; i < this.plugins.length; i++) {
        // 执行hook返回promise
        const hookPromise = this.runHook<void>(hookName, args as any[], i, false, replaceContext);
        // 如果没有那么不push
        if (!hookPromise) continue;
        promises.push(hookPromise);
    }
    // 返回promise
    return Promise.all(promises).then(() => {});
}
  • Функция ловушки: hookReduceArg0 Сценарии использования: outputOptions, renderChunk и др.
// 对arg第一项进行reduce操作
function hookReduceArg0<H extends keyof PluginHooks, V, R = ReturnType<PluginHooks[H]>>(
    hookName: H,
    [arg0, ...args]: any[], // 取出传入的数组的第一个参数,将剩余的置于一个数组中
    reduce: Reduce<V, R>,
    replaceContext?: ReplaceContext //  替换当前plugin调用时候的上下文环境
) {
    let promise = Promise.resolve(arg0); // 默认返回source.code
    for (let i = 0; i < this.plugins.length; i++) {
        // 第一个promise的时候只会接收到上面传递的arg0
        // 之后每一次promise接受的都是上一个插件处理过后的source.code值
        promise = promise.then(arg0 => {
            const hookPromise = this.runHook(hookName, [arg0, ...args], i, false, replaceContext);
            // 如果没有返回promise,那么直接返回arg0
            if (!hookPromise) return arg0;
            // result代表插件执行完成的返回值
            return hookPromise.then((result: any) =>
                reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i])
            );
        });
    }
    return promise;
}

Наблюдая за методами вызова вышеуказанных функций-ловушек, мы можем обнаружить, что существует метод для вызова функции-ловушки: runHook(Sync), который выполняет функцию-ловушку, предоставленную в подключаемом модуле.

Реализация проста:

function runHook<T>(
    hookName: string,
    args: any[],
    pluginIndex: number,
    permitValues: boolean,
    hookContext?: ReplaceContext | null
): Promise<T> {
    this.previousHooks.add(hookName);
    // 找到当前plugin
    const plugin = this.plugins[pluginIndex];
    // 找到当前执行的在plugin中定义的hooks钩子函数
    const hook = (plugin as any)[hookName];
    if (!hook) return undefined as any;

    // pluginContexts在初始化plugin驱动器类的时候定义,是个数组,数组保存对应着每个插件的上下文环境
    let context = this.pluginContexts[pluginIndex];
    // 用于区分对待不同钩子函数的插件上下文
    if (hookContext) {
        context = hookContext(context, plugin);
    }
    return Promise.resolve()
        .then(() => {
            // permit values allows values to be returned instead of a functional hook
            if (typeof hook !== 'function') {
                if (permitValues) return hook;
                return error({
                    code: 'INVALID_PLUGIN_HOOK',
                    message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.`
                });
            }
            // 传入插件上下文和参数,返回插件执行结果
            return hook.apply(context, args);
        })
        .catch(err => throwPluginError(err, plugin.name, { hook: hookName }));
}

Конечно, не все будут использовать плагины в начале, поэтому сам сверток также предоставляет несколько необходимых нам функций-ловушек, которые мы можем использовать, и выполнять операции concat с пользовательскими плагинами при создании экземпляра Graph:

import { getRollupDefaultPlugin } from './defaultPlugin';

this.plugins = userPlugins.concat(
    // 采用内置默认插件或者graph的插件驱动器的插件,不管怎么样,内置默认插件是肯定有的
    // basePluginDriver是上一个PluginDriver初始化的插件
    // preserveSymlinks: 软连标志
    basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)]
);

Какие необходимые хуки-функции предоставляет rollup:

export function getRollupDefaultPlugin(preserveSymlinks: boolean): Plugin {
	return {
        // 插件名
		name: 'Rollup Core',
		// 默认的模块(文件)加载机制,内部主要使用path.resolve
		resolveId: createResolveId(preserveSymlinks) as ResolveIdHook,
        // this.pluginDriver.hookFirst('load', [id])为异步调用,readFile内部用promise包装了fs.readFile,并返回该promise
		load(id) {
			return readFile(id);
		},
        // 用来处理通过emitFile添加的urls或文件
		resolveFileUrl({ relativePath, format }) {
			// 不同format会返回不同的文件解析地址
			return relativeUrlMechanisms[format](relativePath);
		},
        // 处理import.meta.url,参考地址:https://nodejs.org/api/esm.html#esm_import_meta)
		resolveImportMeta(prop, { chunkId, format }) {
			// 改变 获取import.meta的信息 的行为
			const mechanism = importMetaMechanisms[format] && importMetaMechanisms[format](prop, chunkId);
			if (mechanism) {
				return mechanism;
			}
		}
	};
}

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

Мало того, свертка вводит контекст в функцию ловушки, то есть среду контекста, которая используется для облегчения добавления, удаления, модификации и проверки фрагментов и другой информации о сборке.

ДокументацияТам тоже четко написано, например:

  • Используйте this.parse для вызова экземпляра acron внутри накопительного пакета для анализа ast
  • Используйте this.emitFile для увеличения выходного файла, см. этопример.

Давайте просто рассмотрим операцию преобразования.Когда ast был преобразован ранее, вызывался хук преобразования:


graph.pluginDriver
    .hookReduceArg0<any, string>(
        'transform',
        [curSource, id], // source.code 和 模块id
        transformReducer,
    	// 第四个参数是一个函数,用来声明某些钩子上下文中需要的方法
        (pluginContext, plugin) => {
            // 这一大堆是插件利用的,通过this.xxx调用
            curPlugin = plugin;
            if (curPlugin.cacheKey) customTransformCache = true;
            else trackedPluginCache = getTrackedPluginCache(pluginContext.cache);
            return {
                ...pluginContext,
                cache: trackedPluginCache ? trackedPluginCache.cache : pluginContext.cache,
                warn(warning: RollupWarning | string, pos?: number | { column: number; line: number }) {
                    if (typeof warning === 'string') warning = { message: warning } as RollupWarning;
                    if (pos) augmentCodeLocation(warning, pos, curSource, id);
                    warning.id = id;
                    warning.hook = 'transform';
                    pluginContext.warn(warning);
                },
                error(err: RollupError | string, pos?: number | { column: number; line: number }): never {
                    if (typeof err === 'string') err = { message: err };
                    if (pos) augmentCodeLocation(err, pos, curSource, id);
                    err.id = id;
                    err.hook = 'transform';
                    return pluginContext.error(err);
                },
                emitAsset(name: string, source?: string | Buffer) {
                    const emittedFile = { type: 'asset' as const, name, source };
                    emittedFiles.push({ ...emittedFile });
                    return graph.pluginDriver.emitFile(emittedFile);
                },
                emitChunk(id, options) {
                    const emittedFile = { type: 'chunk' as const, id, name: options && options.name };
                    emittedFiles.push({ ...emittedFile });
                    return graph.pluginDriver.emitFile(emittedFile);
                },
                emitFile(emittedFile: EmittedFile) {
                    emittedFiles.push(emittedFile);
                    return graph.pluginDriver.emitFile(emittedFile);
                },
                addWatchFile(id: string) {
                    transformDependencies.push(id);
                    pluginContext.addWatchFile(id);
                },
                setAssetSource(assetReferenceId, source) {
                    pluginContext.setAssetSource(assetReferenceId, source);
                    if (!customTransformCache && !setAssetSourceErr) {
                        try {
                            return this.error({
                                code: 'INVALID_SETASSETSOURCE',
                                message: `setAssetSource cannot be called in transform for caching reasons. Use emitFile with a source, or call setAssetSource in another hook.`
                            });
                        } catch (err) {
                            setAssetSourceErr = err;
                        }
                    }
                },
                getCombinedSourcemap() {
                    const combinedMap = collapseSourcemap(
                        graph,
                        id,
                        originalCode,
                        originalSourcemap,
                        sourcemapChain
                    );
                    if (!combinedMap) {
                        const magicString = new MagicString(originalCode);
                        return magicString.generateMap({ includeContent: true, hires: true, source: id });
                    }
                    if (originalSourcemap !== combinedMap) {
                        originalSourcemap = combinedMap;
                        sourcemapChain.length = 0;
                    }
                    return new SourceMap({
                        ...combinedMap,
                        file: null as any,
                        sourcesContent: combinedMap.sourcesContent!
                    });
                }
            };
        }
    )

В runHook есть суждение, которое заключается в использовании контекстной среды:

function runHook<T>(
		hookName: string,
		args: any[],
		pluginIndex: number,
		permitValues: boolean,
		hookContext?: ReplaceContext | null
) {
    // ...
    const plugin = this.plugins[pluginIndex];
    // 获取默认的上下文环境
    let context = this.pluginContexts[pluginIndex];
    // 如果提供了,就替换
    if (hookContext) {
        context = hookContext(context, plugin);
    }
    // ...
}

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

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

кеш плагина

Также в плагине предусмотрена возможность кэширования, что очень хитро реализовано:

export function createPluginCache(cache: SerializablePluginCache): PluginCache {
	// 利用闭包将cache缓存
	return {
		has(id: string) {
			const item = cache[id];
			if (!item) return false;
			item[0] = 0; // 如果访问了,那么重置访问过期次数,猜测:就是说明用户有意向主动去使用
			return true;
		},
		get(id: string) {
			const item = cache[id];
			if (!item) return undefined;
			item[0] = 0; // 如果访问了,那么重置访问过期次数
			return item[1];
		},
		set(id: string, value: any) {
            // 存储单位是数组,第一项用来标记访问次数
			cache[id] = [0, value];
		},
		delete(id: string) {
			return delete cache[id];
		}
	};
}

Затем, после создания кеша, он будет добавлен в контекст плагина:

import createPluginCache from 'createPluginCache';

const cacheInstance = createPluginCache(pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)));

const context = {
	// ...
    cache: cacheInstance,
    // ...
}

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

function testPlugin() {
  return {
    name: "test-plugin",
    buildStart() {
      if (!this.cache.has("prev")) {
        this.cache.set("prev", "上一次插件执行的结果");
      } else {
        // 第二次执行rollup的时候会执行
        console.log(this.cache.get("prev"));
      }
    },
  };
}
let cache;
async function build() {
  const chunks = await rollup.rollup({
    input: "src/main.js",
    plugins: [testPlugin()],
    // 需要传递上次的打包结果
    cache,
  });
  cache = chunks.cache;
}

build().then(() => {
  build();
});

Но стоит отметить, чтоoptionsФункция хука не внедряется в контекст, и метод ее вызова отличается от других хуков:

function applyOptionHook(inputOptions: InputOptions, plugin: Plugin) {
	if (plugin.options){
        // 指定this和经过处理的input配置,并未传入context
    	return plugin.options.call({ meta: { rollupVersion } }, inputOptions) || inputOptions;
    }

	return inputOptions;
}

Суммировать

На этом серия роллапов подошла к концу.От растерянного взгляда в начале чтения, до десятиликого взгляда, опирающегося на коллекцию и различные инструменты, и теперь это незабываемые впечатления~

Изучение операций больших парней и взятие эссенции, удаление шлака похоже на борьбу с монстрами и апгрейд, пробуешь, пробуешь внимательно. Ха-ха

В этот период также вводят в заблуждение некоторые вещи.Прочитав больше, вы обнаружите, что на самом деле рутины одинаковы, и вы можете найти их путь.核心框架, а затем исправлять функции, продолжать обновлять и повторять, может быть, мы тоже станем авторами шедевров с открытым исходным кодом.

Если охарактеризовать свертывание в нескольких словах:

Чтение и слияние конфигурации -> создание графа зависимостей -> чтение содержимого модуля ввода -> заимствование синтаксического анализатора спецификации estree с открытым исходным кодом для анализа исходного кода, получение зависимостей, повторное выполнение этой операции -> создание модуля, монтирование модуля, связанного с соответствующей информацией о файле -> анализ ast, построить каждый экземпляр узла -> сгенерировать фрагменты -> вызвать рендеринг, переписанный каждым узлом -> использовать магическую строку для выполнения операций сращивания строк и переноса -> запись

В двух словах:

строка -> АСТ -> строка

Если изменение серии может вам немного помочь, пожалуйста, пошевелите пальцами и поддержите это~

пока пока~