<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【設計】1359- Umi3 如何實現(xiàn)插件化架構

          共 29108字,需瀏覽 59分鐘

           ·

          2022-06-28 15:23

          插件化架構

          插件化架構(Plug-in Architecture),也被稱為微內(nèi)核架構(Microkernel Architecture),是一種面向功能進行拆分的可擴展性架構,在如今的許多前端主流框架中都能看到它的身影。今天我們以 umi 框架為主,來看看插件化架構的實現(xiàn)思路,同時對比一下不同框架中插件化實現(xiàn)思路的異同。

          各個主流框架插件化異同

          二話不說先上結論。


          觸發(fā)方式插件 API插件功能
          umi基于 tapable 的發(fā)布訂閱模式10 種核心方法,50 種擴展方法,9 個核心屬性在路由、生成文件、構建打包、HTML 操作、命令等方面提供能力
          babel基于 visitor 的訪問者模式基于@babel/types對于 AST 的操作等
          rollup基于 hook 的回調模式構建鉤子、輸出鉤子、監(jiān)聽鉤子定制構建和打包階段的能力
          webpack基于 tapable 的發(fā)布訂閱模式主要為 compolier 和 compilation 提供一系列的鉤子loader 不能實現(xiàn)的都靠它
          vue-cli基于 hook 的回調模式生成階段為 Generator API,運行階段為 chainWebpack 等更改 webpack 配置為主的 api在生成項目、項目運行和 vue ui 階段提供能力

          一個完整的插件系統(tǒng)應該包括三個部分:

          插件內(nèi)核(plugiCore):用于管理插件;

          插件接口(pluginApi):用于提供 api 給插件使用;

          插件(plugin):功能模塊,不同的插件實現(xiàn)不同的功能。

          因此我們也從這三部分入手去分析 umi 的插件化。

          umi 插件(plugin)

          我們先從最簡單的開始,認識一個umi 插件長什么樣。我們以插件集preset(@umijs/preset-built-in)中的一個內(nèi)置插件umiInfo(packages/preset-built-in/src/plugins/features/umiInfo.ts)為例,來認識一下 umi 插件。

          import { IApi } from '@umijs/types';

          export default (api: IApi) => {
            // 調用擴展方法addHTMLHeadScripts在 HTML 頭部添加腳本
            api.addHTMLHeadScripts(() => [
              {
                content: `//! umi version: ${process.env.UMI_VERSION}`,
              },
            ]);
            // 調用擴展方法addEntryCode在入口文件最后添加代碼
            api.addEntryCode(
              () => `
              window.g_umi = {
                version: '${process.env.UMI_VERSION}',
              };
            `,
            );
          };

          可以看到 umi 插件導出了一個函數(shù),函數(shù)內(nèi)部為調用傳參 api 上的兩個方法屬性,主要實現(xiàn)了兩個功能,一個是在 html 文件頭部添加腳本,另一個是在入口文件最后添加代碼。其中,preset是一系列插件的合集。代碼非常簡單,就是 require 了一系列的plugin。插件集preset(packages/preset-built-in/src/index.ts)如下:

          export default function () {
            return {
              plugins: [
                // 注冊方法插件
                require.resolve('./plugins/registerMethods'),

                // 路由插件
                require.resolve('./plugins/routes'),

                // 生成文件相關插件
                require.resolve('./plugins/generateFiles/core/history'),
                ……
                // 打包配置相關插件
                require.resolve('./plugins/features/404'),
                ……
                // html操作相關插件
                require.resolve('./plugins/features/html/favicon'),
                ……
                // 命令相關插件
                require.resolve('./plugins/commands/build/build'),
                ……

          }

          這些plugin主要包括一個注冊方法插件(packages/preset-built-in/src/plugins/registerMethods.ts),一個路由插件(packages/preset-built-in/src/plugins/routes.ts),一些生成文件相關插件(packages/preset-built-in/src/plugins/generateFiles/*),一些打包配置相關插件(packages/preset-built-in/src/plugins/features/*),一些html 操作相關插件(packages/preset-built-in/src/plugins/features/html/*)以及一些命令相關插件(packages/preset-built-in/src/plugins/commands/*)。

          在注冊方法插件registerMethods(packages/preset-built-in/src/plugins/registerMethods.ts)中,umi集中注冊了幾十個方法,這些方法就是umi文檔中插件 api 的擴展方法

          export default function (api: IApi) {
            // 集中注冊擴展方法
            [
              'onGenerateFiles',
              'onBuildComplete',
              'onExit',
              ……
            ].forEach((name) => {
              api.registerMethod({ name });
            });

            // 單獨注冊writeTmpFile方法,并傳參fn,方便其他擴展方法使用
            api.registerMethod({
              name: 'writeTmpFile',
              fn({
                path,
                content,
                skipTSCheck = true,
              }: {
                path: string;
                content: string;
                skipTSCheck?: boolean;
              }) {
                assert(
                  api.stage >= api.ServiceStage.pluginReady,
                  `api.writeTmpFile() should not execute in register stage.`,
                );
                const absPath = join(api.paths.absTmpPath!, path);
                api.utils.mkdirp.sync(dirname(absPath));
                if (isTSFile(path) && skipTSCheck) {
                  // write @ts-nocheck into first line
                  content = `// @ts-nocheck${EOL}${content}`;
                }
                if (!existsSync(absPath) || readFileSync(absPath, 'utf-8') !== content) {
                  writeFileSync(absPath, content, 'utf-8');
                }
              },
            });
          }

          當我們在控制臺umi路徑下鍵入命令npx umi dev后,就啟動了 umi 命令,附帶 dev 參數(shù),經(jīng)過一系列的操作后實例化Service對象(路徑:packages/umi/src/ServiceWithBuiltIn.ts),

          import { IServiceOpts, Service as CoreService } from '@umijs/core';
          import { dirname } from 'path';

          class Service extends CoreService {
            constructor(opts: IServiceOpts) {
              process.env.UMI_VERSION = require('../package').version;
              process.env.UMI_DIR = dirname(require.resolve('../package'));

              super({
                ...opts,
                presets: [
                  // 配置內(nèi)置默認插件集
                  require.resolve('@umijs/preset-built-in'),
                  ...(opts.presets || []),
                ],
                plugins: [require.resolve('./plugins/umiAlias'), ...(opts.plugins || [])],
              });
            }
          }

          export { Service };

          Service的構造函數(shù)中就傳入了上面提到的默認插件集preset(@umijs/preset-built-in),供umi使用。至此我們介紹了以默認插件集preset為代表的umi插件。

          插件接口(pluginApi)

          Service對象(packages/core/src/Service/Service.ts)中的getPluginAPI方法為插件提供了插件接口getPluginAPI接口就是整個插件系統(tǒng)的橋梁。它使用代理模式將umi插件核心方法、初始化過程hook 節(jié)點api、Service 對象方法屬性和通過@umijs/preset-built-in 注冊到 service 對象上的擴展方法組織在了一起,供插件調用。

            getPluginAPI(opts: any) {
            //實例化PluginAPI對象,PluginAPI對象包含describe,register,registerCommand,registerPresets,registerPlugins,registerMethod,skipPlugins七個核心插件方法
              const pluginAPI = new PluginAPI(opts);

              // 注冊umi服務初始化過程中的hook節(jié)點
              [
                'onPluginReady', // 插件初始化完畢
                'modifyPaths', // 修改路徑
                'onStart', // 啟動umi
                'modifyDefaultConfig', // 修改默認配置
                'modifyConfig', // 修改配置
              ].forEach((name) => {
                pluginAPI.registerMethod({ name, exitsError: false });
              });

              return new Proxy(pluginAPI, {
                get: (target, prop: string) => {
                  // 由于 pluginMethods 需要在 register 階段可用
                  // 必須通過 proxy 的方式動態(tài)獲取最新,以實現(xiàn)邊注冊邊使用的效果
                  if (this.pluginMethods[prop]) return this.pluginMethods[prop];
                  // 注冊umi service對象上的屬性和核心方法
                  if (
                    [
                      'applyPlugins',
                      'ApplyPluginsType',
                      'EnableBy',
                      'ConfigChangeType',
                      'babelRegister',
                      'stage',
                      ……
                    ].includes(prop)
                  ) {
                    return typeof this[prop] === 'function'
                      ? this[prop].bind(this)
                      : this[prop];
                  }
                  return target[prop];
                },
              });
            }
          插件內(nèi)核(pluginore)
          1.初始化配置

          上面講到啟動umi后會實例化Service對象(路徑:packages/umi/src/ServiceWithBuiltIn.ts),并傳入preset插件集(@umijs/preset-built-in)。該對象繼承自CoreServeice(packages/core/src/Service/Service.ts)。CoreServeice在實例化的過程中會在構造函數(shù)中初始化插件集和插件:

              // 初始化 Presets 和 plugins, 來源于四處
              // 1. 構造 Service 傳參
              // 2. process.env 中指定
              // 3. package.json 中 devDependencies 指定
              // 4. 用戶在 .umirc.ts 文件中配置
              this.initialPresets = resolvePresets({
                ...baseOpts,
                presets: opts.presets || [],
                userConfigPresets: this.userConfig.presets || [],
              });
              this.initialPlugins = resolvePlugins({
                ...baseOpts,
                plugins: opts.plugins || [],
                userConfigPlugins: this.userConfig.plugins || [],
              });

          經(jīng)過轉換處理,一個插件在umi系統(tǒng)中最終會表示為如下格式的一個對象:

          {
              id, // @umijs/plugin-xxx,插件名稱
              key, // xxx,插件唯一的key
              path: winPath(path), // 路徑
              apply() {
                // 延遲加載插件
                try {
                  const ret = require(path);
                  // use the default member for es modules
                  return compatESModuleRequire(ret);
                } catch (e) {
                  throw new Error(`Register ${type} ${path} failed, since ${e.message}`);
                }
              },
              defaultConfig: null, // 默認配置
            };
          2.初始化插件

          umi實例化Service對象后會調用Service對象的run方法。插件的初始化就是在run方法中完成的。初始化presetplugin的過程大同小異,我們重點看初始化plugin的過程。

            // 初始化插件
            async initPlugin(plugin: IPlugin) {
              // 在第一步初始化插件配置后,插件在umi系統(tǒng)中就變成了一個個的對象,這里導出了id, key和延遲加載函數(shù)apply
              const { id, key, apply } = plugin;
              // 獲取插件系統(tǒng)的橋梁插件接口PluginApi
              const api = this.getPluginAPI({ id, key, service: this });

              // 注冊插件
              this.registerPlugin(plugin);
              // 執(zhí)行插件代碼
              await this.applyAPI({ api, apply });
            }

          這里我們要重點看一下在最開始preset集中第一個注冊方法插件中注冊擴展方法時曾提到的registerMethod方法。

            registerMethod({
              name,
              fn,
              exitsError = true,
            }: {
              name: string;
              fn?: Function;
              exitsError?: boolean;
            }) {
              // 注冊的方法已經(jīng)存在的情況的處理
              if (this.service.pluginMethods[name]) {
                if (exitsError) {
                  throw new Error(
                    `api.registerMethod() failed, method ${name} is already exist.`,
                  );
                } else {
                  return;
                }
              }
              // 這里分為兩種情況:第一種注冊方法時傳入了fn參數(shù),則注冊的方法就是fn方法;第二種情況未傳入fn,則返回一個函數(shù),函數(shù)會將傳入的fn參數(shù)轉換為hook鉤子并注冊,掛載到service的hooksByPluginId屬性下
              this.service.pluginMethods[name] =
                fn || function (fn: Function | Object) {
                  const hook = {
                    key: name,
                    ...(utils.lodash.isPlainObject(fn) ? fn : { fn }),
                  };
                  // @ts-ignore
                  this.register(hook);
                };
            }

          因此當執(zhí)行插件代碼時,如果是核心方法則直接執(zhí)行,如果是擴展方法則除了writeTmpFile,其余都是在hooksByPluginId下注冊了hook。到這里Service完成了插件的初始化,執(zhí)行了插件調用的核心方法和擴展方法。

          3.初始化 hooks

          通過下述代碼,Service將以插件名稱為維度配置的hook,轉換為以hook名稱為維度配置的回調集。

              Object.keys(this.hooksByPluginId).forEach((id) => {
                const hooks = this.hooksByPluginId[id];
                hooks.forEach((hook) => {
                  const { key } = hook;
                  hook.pluginId = id;
                  this.hooks[key] = (this.hooks[key] || []).concat(hook);
                });
              });

          addHTMLHeadScripts擴展方法為例 轉換前:

            './node_modules/@@/features/devScripts': [
              { key: 'addBeforeMiddlewares', fn: [Function (anonymous)] },
              { key: 'addHTMLHeadScripts', fn: [Function (anonymous)] },
          ……
            ],
            './node_modules/@@/features/umiInfo': [
              { key: 'addHTMLHeadScripts', fn: [Function (anonymous)] },
              { key: 'addEntryCode', fn: [Function (anonymous)] }
            ],
            './node_modules/@@/features/html/headScripts': [ { key: 'addHTMLHeadScripts', fn: [Function (anonymous)] } ],

          轉換之后:

            addHTMLHeadScripts: [
              {
                key: 'addHTMLHeadScripts',
                fn: [Function (anonymous)],
                pluginId: './node_modules/@@/features/devScripts'
              },
              {
                key: 'addHTMLHeadScripts',
                fn: [Function (anonymous)],
                pluginId: './node_modules/@@/features/umiInfo'
              },
              {
                key: 'addHTMLHeadScripts',
                fn: [Function (anonymous)],
                pluginId: './node_modules/@@/features/html/headScripts'
              }
            ],

          至此插件系統(tǒng)就緒達到pluginReady狀態(tài)。

          4.觸發(fā) hook

          在程序達到 pluginReady 狀態(tài)后,Service 立即執(zhí)行了一次觸發(fā) hook 操作。

              await this.applyPlugins({
                key: 'onPluginReady',
                type: ApplyPluginsType.event,
              });

          那么是如何觸發(fā)的呢?我們來詳細看一下applyPlugins的代碼實現(xiàn):

            async applyPlugins(opts: {
              key: string;
              type: ApplyPluginsType;
              initialValue?: any;
              args?: any;
            }) {
              // 找到對應需要觸發(fā)的hook會調集,這里的hooks就是上面以插件名稱為維度配置的hook轉換為以hook名稱為維度配置的回調集
              const hooks = this.hooks[opts.key] || [];
              // 判斷事件類型,umi將回調事件分為add、modify和event三種
              switch (opts.type) {
                case ApplyPluginsType.add:
                  if ('initialValue' in opts) {
                    assert(
                      Array.isArray(opts.initialValue),
                      `applyPlugins failed, opts.initialValue must be Array if opts.type is add.`,
                    );
                  }
                  // 事件管理基于webpack的Tapable庫,只用到了AsyncSeriesWaterfallHook一種事件控制方式,既異步串行瀑布流回調方式:異步,所有的鉤子都是異步處理;串行,依次執(zhí)行;瀑布流,上一個鉤子的結果是下一個鉤子的參數(shù)。
                  const tAdd = new AsyncSeriesWaterfallHook(['memo']);
                  for (const hook of hooks) {
                    if (!this.isPluginEnable(hook.pluginId!)) {
                      continue;
                    }
                    tAdd.tapPromise(
                      {
                        name: hook.pluginId!,
                        stage: hook.stage || 0,
                        // @ts-ignore
                        before: hook.before,
                      },
                      //與其他兩種事件類型不同,add類型會返回所有鉤子的結果
                      async (memo: any[]) => {
                        const items = await hook.fn(opts.args);
                        return memo.concat(items);
                      },
                    );
                  }
                  return await tAdd.promise(opts.initialValue || []);
                case ApplyPluginsType.modify:
                  const tModify = new AsyncSeriesWaterfallHook(['memo']);
                  for (const hook of hooks) {
                    if (!this.isPluginEnable(hook.pluginId!)) {
                      continue;
                    }
                    tModify.tapPromise(
                      {
                        name: hook.pluginId!,
                        stage: hook.stage || 0,
                        // @ts-ignore
                        before: hook.before,
                      },
                      // 與其他兩種鉤子不同,modify類型會返回最終的鉤子結果
                      async (memo: any) => {
                        return await hook.fn(memo, opts.args);
                      },
                    );
                  }
                  return await tModify.promise(opts.initialValue);
                case ApplyPluginsType.event:
                  const tEvent = new AsyncSeriesWaterfallHook(['_']);
                  for (const hook of hooks) {
                    if (!this.isPluginEnable(hook.pluginId!)) {
                      continue;
                    }
                    tEvent.tapPromise(
                      {
                        name: hook.pluginId!,
                        stage: hook.stage || 0,
                        // @ts-ignore
                        before: hook.before,
                      },
                      // event類型,只執(zhí)行鉤子,不返回結果
                      async () => {
                        await hook.fn(opts.args);
                      },
                    );
                  }
                  return await tEvent.promise();
                default:
                  throw new Error(
                    `applyPlugin failed, type is not defined or is not matched, got ${opts.type}.`,
                  );
              }
            }

          至此,umi的整體插件工作流程介紹完畢,后續(xù)代碼就是umi根據(jù)流程需要不斷觸發(fā)各類的hook從而完成整個umi的各項功能。除了umi,其他的一些框架也都應用了插件模式,下面做簡單介紹對比。

          babel 插件機制

          babel主要的作用就是語法轉換babel的整個過程分為三個部分:解析,將代碼轉換為抽象語法樹(AST);轉換,遍歷 AST 中的節(jié)點進行語法轉換操作;生成,根據(jù)最新的 AST 生成目標代碼。其中在轉換的過程中就是依據(jù)babel配置的各個插件去完成的。

          babel 插件
          const createPlugin = (name) => {
            return {
              name,
              visitor: {
                FunctionDeclaration(path, state) {},
                ReturnStatement(path, state) {},
              }
            };
          };

          可以看到babel的插件也是返回一個函數(shù),和umi的很相似。但是babel插件的運行卻并不是基于發(fā)布訂閱的事件驅動模式,而是采用訪問者模式babel會通過一個訪問者visitor統(tǒng)一遍歷節(jié)點,提供方法及維護節(jié)點關系,插件只需要在visitor中注冊自己關心的節(jié)點類型,當visitor遍歷到相關節(jié)點時就會調用插件在visitor上注冊的方法并執(zhí)行。

          webpack 插件機制

          webpack整體基于兩大支柱功能:一個是loader,用于對模塊的源碼進行轉換,基于管道模式;另一個就是plugin,用于解決 loader 無法解決的問題,顧名思義,plugin 就是基于插件機制的。來看一個典型的webpack插件:

          const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

          class ConsoleLogOnBuildWebpackPlugin {
            apply(compiler) {
              compiler.hooks.run.tap(pluginName, (compilation) => {
                console.log('webpack 構建正在啟動!');
              });
            }
          }

          module.exports = ConsoleLogOnBuildWebpackPlugin;

          webpack在初始化時會統(tǒng)一執(zhí)行插件的apply方法。插件通過注冊Compilercompilation的鉤子函數(shù),在整個編譯生命周期都可以訪問compiler對象,完成插件功能。同時整個事件驅動的功能都是基于 webpack 的核心工具TapableTapable同樣也是umi的事件驅動工具。可以看到umiwebpack的整體思路是很相似的。

          rollup 插件機制

          rollup也是模塊打包工具,與 webpack 相比rollup更適合打包純 js 的類庫。同樣rollup也具有插件機制。一個典型的rollup插件:

          export default function myExample() {
            return {
              name: 'my-example',
              resolveId(source) {},
              load(id) {},
            };
          }

          rollup 插件維護了一套同步/異步、串行/并行、熔斷/傳參的事件回調機制,不過這部分并沒有單獨抽出類庫,而是在 rollup 項目中維護的。通過插件控制器(src/utils/PluginDriver.ts)、插件上下文(src/utils/PluginContext.ts)、插件緩存(src/utils/PluginCache.ts),完成了提供插件 api 和插件內(nèi)核的能力。

          vue-cli 插件機制

          vue-cli的插件與其他相比稍有特點,就是將插件分為幾種情況,一種項目生成階段,插件未安裝需要安裝插件;另一種是項目運行階段,啟動插件;還有一種是UI插件,在運行vue ui時會用到。

          vue-cli插件的包目錄結構

          ├── generator.js  # generator(可選)
          ├── index.js      # service 插件
          ├── package.json
          └── prompts.js    # prompt 文件(可選)
          └── ui.js    # ui 文件(可選)
          生成階段

          其中generator.jsprompts.js在安裝插件的情況下執(zhí)行,index 則在運行階段執(zhí)行。generator 示例:

          module.exports = (api, options) => {
            // 擴展package.json字段
            api.extendPackage({
              dependencies: {
                'vue-router-layout''^0.1.2'
              }
            })
            // afterAnyInvoke鉤子 函數(shù)會被反復執(zhí)行
            api.afterAnyInvoke(() => {
            // 文件操作
            })
            // afterInvoke鉤子,這個鉤子將在文件被寫入硬盤之后被調用
            api.afterInvoke(() => {})

          }

          prompts 會在安裝期間與用戶交互,獲取插件的選項配置并在 generator.js 調用時作為參數(shù)存入。

          在項目生成階段通過 packages/@vue/cli/lib/GeneratorAPI.js 提供插件 api;在 packages/@vue/cli/lib/Generator.js 中初始化插件,執(zhí)行插件注冊的 api,在 packages/@vue/cli/lib/Creator.js 中運行插件注冊的鉤子函數(shù),最終完成插件功能的調用。

          運行階段

          vue-cli運行階段插件:

          const VueAutoRoutingPlugin = require('vue-auto-routing/lib/webpack-plugin')

          module.exports = (api, options) => {
            api.chainWebpack(webpackConfig => {
              webpackConfig
              .plugin('vue-auto-routing')
                .use(VueAutoRoutingPlugin, [
                  {
                    pages: 'src/pages',
                    nested: true
                  }
                ])
            })
          }

          在項目運行階段的插件主要用來修改webpack的配置,創(chuàng)建或者修改命令。由 packages/@vue/cli-service/lib/PluginAPI.js 提供pluginapi,packages/@vue/cli-service/lib/Service.js 完成插件的初始化和運行。而vue-cli插件的運行主要是基于回調函數(shù)的模式來管理的。

          通過以上介紹,可以發(fā)現(xiàn)插件機制是現(xiàn)代前端項目工程化框架中必不可少的一部分,插件的實現(xiàn)形式多種多樣,但總的結構是大體一致的,既由插件(plugin)插件 api(pluginApi)插件核心(pluginCore)三部分組成。其中通過插件核心去注冊和管理插件,完成插件的初始化和運行工作,插件 api 是插件和系統(tǒng)之間的橋梁,使插件完成特定功能,再通過不同插件的組合形成了一套功能完整的前端框架系統(tǒng)。


          瀏覽 19
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产乱伦导航 | 黄色毛片网址 | 欧美成人在线免费观看视频 | 国产黄在线看 | 国产成人麻豆作品 |