解讀 rollup Plugin (二)--你最不想看的各類鉤子函數

這是我參與更文挑戰的第 18 天,活動詳情查看: 更文挑戰javascript

Lynne,一個能哭愛笑永遠少女心的前端開發工程師。身處互聯網浪潮之中,熱愛生活與技術。前端

rollup plugin 的實現

接上一篇解讀 rollup Plugin (一)java

固然,也不可能就這麼簡單啦~畢竟還要考慮實際複雜應用場景的~~git

插件驅動

PluginDriver --- 插件驅動器,調用插件和提供插件環境上下文等github

鉤子函數的調用時機

日常書寫rollup插件的時候,最關注的就是鉤子函數部分了,鉤子函數的調用時機有三類:web

  • const chunks = rollup.rollup執行期間的構建鉤子函數 - Build Hooks
  • chunks.generator(write)執行期間的輸出鉤子函數 - Output Generation Hooks
  • 監聽文件變化並從新執行構建的rollup.watch執行期間的 watchChange 鉤子函數

構建鉤子函數

爲了與構建過程交互,你的插件對象須要包含一些構建鉤子函數。構建鉤子是構建的各個階段調用的函數。構建鉤子函數能夠影響構建執行方式、提供構建的信息或者在構建完成後修改構建。rollup 中有不一樣的構建鉤子函數:數組

  • async: 處理promise的異步鉤子,也有同步版本
  • first: 若是多個插件實現了相同的鉤子函數,那麼會串式執行,從頭至尾,可是,若是其中某個的返回值不是null也不是undefined的話,會直接終止掉後續插件。
  • sequential: 若是多個插件實現了相同的鉤子函數,那麼會串式執行,按照使用插件的順序從頭至尾執行,若是是異步的,會等待以前處理完畢,在執行下一個插件。
  • parallel: 同上,不過若是某個插件是異步的,其後的插件不會等待,而是並行執行。

構建鉤子函數在構建階段執行,它們被 [rollup.rollup(inputOptions)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L34) 觸發。它們主要關注在 Rollup 處理輸入文件以前定位、提供和轉換輸入文件。構建階段的第一個鉤子是 options,最後一個鉤子老是 buildEnd,除非有一個構建錯誤,在這種狀況下 closeBundle 將在這以後被調用。promise

此外,在觀察模式下,watchChange 鉤子能夠在任什麼時候候被觸發,以通知新的運行將在當前運行產生其輸出後被觸發。另外,當 watcher 關閉時,closeWatcher 鉤子函數將被觸發。緩存

輸出鉤子函數

輸出生成鉤子函數能夠提供關於生成的包的信息並在構建完成後立馬執行。它們和構建鉤子函數擁有同樣的工做原理和相同的類型,可是不一樣的是它們分別被 ·[bundle.generate(output)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L44)[bundle.write(outputOptions)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L64) 調用。只使用輸出生成鉤子的插件也能夠經過輸出選項傳入,由於只對某些輸出運行。markdown

輸出生成階段的第一個鉤子函數是 outputOptions,若是輸出經過 bundle.generate(...) 成功生成則第一個鉤子函數是 generateBundle,若是輸出經過 [bundle.write(...)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/watch/watch.ts#L200) 生成則最後一個鉤子函數是 [writeBundle](https://github.com/rollup/rollup/blob/master/src/rollup/rollup.ts#L176),另外若是輸出生成階段發生了錯誤的話,最後一個鉤子函數則是 renderError

另外,closeBundle 能夠做爲最後一個鉤子被調用,但用戶有責任手動調用 bundle.close() 來觸發它。CLI 將始終確保這種狀況發生。

鉤子函數加載實現

[PluginDriver](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/utils/PluginDriver.ts#L124) 中有 9 個 hook 加載函數。主要是由於每種類別的 hook 都有同步和異步的版本。 ​

接下來從分類來看函數鉤子的應用場景: ​

1. hookFirst

加載 first 類型的鉤子函數,場景有 resolveIdresolveAssetUrl 等

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);
    });
  }
  // 返回 hook 過的 promise
  return promise;
}
複製代碼

2.hookFirstSync

hookFirst 的同步版本,使用場景有 resolveFileUrlresolveImportMeta 等

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;
}
複製代碼

3. hookParallel

並行執行 hook,不會等待當前 hook 完成。使用場景 buildEndbuildStartmoduleParsed 等。

hookParallel<H extends AsyncPluginHooks & ParallelPluginHooks>(
  hookName: H,
  args: Parameters<PluginHooks[H]>,
  replaceContext?: ReplaceContext
): Promise<void> {
  const promises: Promise<void>[] = [];
  for (const plugin of this.plugins) {
    const hookPromise = this.runHook(hookName, args, plugin, false, replaceContext);
    if (!hookPromise) continue;
    promises.push(hookPromise);
  }
  return Promise.all(promises).then(() => {});
}
複製代碼

4.hookReduceArg0

對 arg 第一項進行 reduce 操做。使用場景: optionsrenderChunk 等

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;
}
複製代碼

5.hookReduceArg0Sync

hookReduceArg0 同步版本,使用場景 transformgenerateBundle 等

6. hookReduceValue

將返回值減小到類型T,分別處理減小的值。容許鉤子做爲值。

hookReduceValue<H extends PluginValueHooks, T>(
		hookName: H,
		initialValue: T | Promise<T>,
		args: Parameters<AddonHookFunction>,
		reduce: ( reduction: T, result: ResolveValue<ReturnType<AddonHookFunction>>, plugin: Plugin ) => T,
		replaceContext?: ReplaceContext
	): Promise<T> {
		let promise = Promise.resolve(initialValue);
		for (const plugin of this.plugins) {
			promise = promise.then(value => {
				const hookPromise = this.runHook(hookName, args, plugin, true, replaceContext);
				if (!hookPromise) return value;
				return hookPromise.then(result =>
					reduce.call(this.pluginContexts.get(plugin), value, result, plugin)
				);
			});
		}
		return promise;
	}
複製代碼

7. hookReduceValueSync

hookReduceValue的同步版本

8. hookSeq

加載 sequential 類型的鉤子函數,和 hookFirst 的區別就是不能中斷,使用場景有 onwritegenerateBundle 等

async function hookSeq<H extends keyof PluginHooks>( hookName: H, args: Args<PluginHooks[H]>, replaceContext?: ReplaceContext, // hookFirst 經過 skip 參數決定是否跳過某個鉤子函數 ): 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;
}
複製代碼

9.hookSeqSync

hookSeq 同步版本,不須要構造 promise,而是直接使用 runHookSync 執行鉤子函數。使用場景有 closeWatcherwatchChange 等。

hookSeqSync<H extends SyncPluginHooks & SequentialPluginHooks>(
  hookName: H,
  args: Parameters<PluginHooks[H]>,
  replaceContext?: ReplaceContext
): void {
  for (const plugin of this.plugins) {
    this.runHookSync(hookName, args, plugin, replaceContext);
  }
}
複製代碼

經過觀察上面幾種鉤子函數的調用方式,咱們能夠發現,其內部有一個調用鉤子函數的方法: runHook(Sync),該函數執行插件中提供的鉤子函數。 ​

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(() => {
      // 許可值容許返回值,而不是一個函數鉤子,使用 hookReduceValue 或 hookReduceValueSync 加載。
      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 }));
}
複製代碼

插件上下文

rollup給鉤子函數注入了context,也就是上下文環境,用來方便對chunks和其餘構建信息進行增刪改查。

const context: PluginContext = {
    addWatchFile(id) {},
    cache: cacheInstance,
    emitAsset: getDeprecatedContextHandler(...),
    emitChunk: getDeprecatedContextHandler(...),
    emitFile: fileEmitter.emitFile,
    error(err)
    getAssetFileName: getDeprecatedContextHandler(...),
    getChunkFileName: getDeprecatedContextHandler(),
    getFileName: fileEmitter.getFileName,
    getModuleIds: () => graph.modulesById.keys(),
    getModuleInfo: graph.getModuleInfo,
    getWatchFiles: () => Object.keys(graph.watchFiles),
    isExternal: getDeprecatedContextHandler(...),
    meta: {
        rollupVersion,
        watchMode: graph.watchMode
    },
    get moduleIds() {
        const moduleIds = graph.modulesById.keys();
        return wrappedModuleIds();
    },
    parse: graph.contextParse,
    resolve(source, importer, { custom, skipSelf } = BLANK) {
        return graph.moduleLoader.resolveId(source, importer, custom, skipSelf ? pidx : null);
    },
    resolveId: getDeprecatedContextHandler(...),
    setAssetSource: fileEmitter.setAssetSource,
    warn(warning) {}
};
複製代碼

插件的緩存

插件還提供緩存的能力,實現的很是巧妙.

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,
    // ...
}
複製代碼

以後咱們就能夠在插件中就可使用cache進行插件環境下的緩存,進一步提高打包效率:

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();
});
複製代碼

總結

恭喜你,把 rollup 那麼幾種鉤子函數都熬着看過來了,可能實際的插件開發中咱們未必會用到這些知識,咱們也未必能一一掌握,但有些東西你必須得先知道,才能進行下一步~~

相關文章
相關標籤/搜索