Привет всем, я Xiaoyu Xiaoyu, посвященный обмену интересными и практическими техническими статьями.
Контент разделен на перевод и оригинал.Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь комментировать или отправить личное сообщение, и надеемся добиться прогресса вместе со всеми.
Ваша поддержка является движущей силой моего творчества.
строить планы
Планируется, что накопительная серия будет выпускаться глава за главой, содержание будет более кратким, конкретным и понятным.
Это последняя статья в серии сводок, вот ссылки на все статьи.
TL;DR
Плагины rollup аналогичны другим крупномасштабным фреймворкам.Все они предоставляют единый стандартный интерфейс, определяют общедоступную конфигурацию с помощью соглашения большего, чем конфигурация, и вводят свойства и методы, связанные с текущим результатом сборки, чтобы разработчики могли добавлять, удалять, изменить и проверить. Это обеспечивает прочную основу для стабильного и устойчивого роста!
Но я не хочу, чтобы webpack различал загрузчик и плагин, плагин Rollup может играть роль загрузчика, а может играть роль традиционного плагина. Функция ловушки, предоставляемая rollup, является ядром, например, загрузка и преобразование для анализа и изменения фрагмента, resolveFileUrl может легально анализировать загруженный модуль, параметры для динамического обновления конфигурации и т. д. ~
будь осторожен
Все аннотации находятся вздесь, вы можете прочитать это сами
!!!Советы => отмеченные TODO - это конкретные детали реализации, которые будут проанализированы в зависимости от ситуации.
!!!Примечание => Каждый подзаголовок реализован внутри родительского заголовка (функции)
!!!Акцент => id модуля (файла) в роллапе - это адрес файла, поэтому что-то наподобие resolveID означает разрешить адрес файла, мы можем вернуть тот id файла, который хотим вернуть (то есть адрес, относительный путь, путь решения), чтобы позволить загрузке свертки
rollup — это ядро, которое выполняет только самые основные функции, такие как предоставлениеМеханизм загрузки модуля (файла) по умолчанию, такие как упаковка в различные стили контента, наш плагин обеспечивает такие операции, как загрузка путей к файлам, анализ содержимого файлов (обработка ts, sass и т. д.), который представляет собой подключаемый дизайн, аналогичный веб-пакету. Pluggable — это очень гибкий дизайн, который можно многократно обновлять в течение длительного времени, а также ядро среднего и крупного фреймворка.
Основные общие модули и их значения
- График: глобально уникальный график, включающий записи и взаимосвязи различных зависимостей, методов работы, кешей и т. д. является ядром накопительного пакета
- PathTracker: отслеживание ссылок (вызовов)
- PluginDriver: драйвер подключаемого модуля, вызов подключаемых модулей и предоставление контекста среды подключаемого модуля и т. д.
- FileMitter: Манипулятор ресурсов
- GlobalScope: глобальная область действия, а не локальная.
- ModuleLoader: загрузчик модулей
- NodeBase: базовый класс построения синтаксиса ast (ArrayExpression, AwaitExpression и т. д.)
Анализ механизма плагина
Плагин rollup на самом деле является обычной функцией, функция возвращает объект, объект содержит некоторые основные свойства (такие как имя) и перехватывает функции на разных этапах, например:
function plugin(options = {}) {
return {
name: 'rollup-plugin',
transform() {
return {
code: 'code',
map: { mappings: '' }
};
}
};
}
Вот официальное предложениесоглашение.
Когда мы обычно пишем подключаемый модуль свертки, наиболее важной частью является функция ловушки.Существует три типа синхронизации вызова функции ловушки:
- const chunks = во время выполнения rollup.rollupBuild Hooks
- Во время выполнения chunks.generator(запись)Output Generation Hooks
- Прислушивайтесь к изменениям файлов и повторно выполняйте функцию ловушки watchChange во время выполнения сборки rollup.watch.
В дополнение к различным категориям, накопительный пакет также предоставляет несколькофункция ловушкиМетод выполнения каждого метода делится на синхронный или асинхронный, что удобно для внутреннего использования:
- async: асинхронные хуки для обработки промисов, также есть синхронные версии.
- first: если несколько подключаемых модулей реализуют одну и ту же функцию ловушки, они будут выполняться последовательно, от начала до конца, но если возвращаемое значение одного из них не является ни нулевым, ни неопределенным, последующие подключаемые модули будут завершены напрямую.
- последовательный: если несколько плагинов реализуют одну и ту же функцию ловушки, они будут выполняться последовательно, от начала до конца в том порядке, в котором используются плагины.Если он асинхронный, он будет ждать завершения предыдущей обработки перед выполнением следующего плагин.
- 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, построить каждый экземпляр узла -> сгенерировать фрагменты -> вызвать рендеринг, переписанный каждым узлом -> использовать магическую строку для выполнения операций сращивания строк и переноса -> запись
В двух словах:
строка -> АСТ -> строка
Если изменение серии может вам немного помочь, пожалуйста, пошевелите пальцами и поддержите это~
пока пока~