当前位置:网站首页>[design] 1359- how umi3 implements plug-in architecture

[design] 1359- how umi3 implements plug-in architecture

2022-06-23 23:14:00 pingan8787

69cb25d66ef5bdce08e59aa899b0e343.png

Plug in Architecture

Plug in Architecture (Plug-in Architecture), Also known as microkernel architecture (Microkernel Architecture), It's a scalable architecture for function oriented splitting , It can be seen in many mainstream front-end frameworks today . Today we are going to umi Frame based , Let's take a look at the implementation idea of plug-in architecture , At the same time, compare the similarities and differences of plug-in implementation ideas in different frameworks .

The similarities and differences of plug-in of various mainstream frameworks

Let's draw a conclusion without saying anything .


Trigger mode plug-in unit API Plug in features
umi be based on tapable The publish and subscribe model of 10 There are three core methods ,50 Methods of extension ,9 Three core attributes On route 、 Generate the file 、 Build packaging 、HTML operation 、 Command, etc
babel be based on visitor The visitor pattern of be based on @babel/types about AST Operation, etc
rollup be based on hook Callback mode for Build hook 、 Output hook 、 Monitoring hook The ability to customize the build and packaging phases
webpack be based on tapable The publish and subscribe model of Mainly for compolier and compilation Provide a range of hooks loader What cannot be achieved depends on it
vue-cli be based on hook Callback mode for The generation phase is Generator API, The operation phase is chainWebpack Etc webpack Configuration based api Building projects 、 Project operation and vue ui Stage providing capability

A complete plug-in system should consist of three parts :

Plug in kernel (plugiCore): For managing plug-ins ;

Plug-in interface (pluginApi): For providing api For plug-ins ;

plug-in unit (plugin): Function module , Different plug-ins implement different functions .

Therefore, we also analyze from these three parts umi Plug in of .

umi plug-in unit (plugin)

Let's start with the simplest , Get to know one umi plug-in unit What does it look like . We use the plug-in set preset(@umijs/preset-built-in) A built-in plug-in in umiInfo(packages/preset-built-in/src/plugins/features/umiInfo.ts) For example , Come and meet umi plug-in unit .

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

export default (api: IApi) => {
  //  Call extension method addHTMLHeadScripts stay  HTML  Add script to the header 
  api.addHTMLHeadScripts(() => [
    {
      content: `//! umi version: ${process.env.UMI_VERSION}`,
    },
  ]);
  //  Call extension method addEntryCode Add code at the end of the entry file 
  api.addEntryCode(
    () => `
    window.g_umi = {
      version: '${process.env.UMI_VERSION}',
    };
  `,
  );
};

You can see umi The plug-in exports a function , The function passes parameters for the call api Two method properties on , It mainly realizes two functions , One is in html Add script to file header , The other is to add code at the end of the entry file . among ,preset Is a collection of plug-ins . The code is very simple , Namely require A series of plugin. Plug in set preset(packages/preset-built-in/src/index.ts) as follows :

export default function () {
  return {
    plugins: [
      //  Register method plug-ins 
      require.resolve('./plugins/registerMethods'),

      //  Routing plug-in 
      require.resolve('./plugins/routes'),

      //  Generate file related plug-ins 
      require.resolve('./plugins/generateFiles/core/history'),
      ……
      //  Package and configure related plug-ins 
      require.resolve('./plugins/features/404'),
      ……
      // html Operate related plug-ins 
      require.resolve('./plugins/features/html/favicon'),
      ……
      //  Command related plug-ins 
      require.resolve('./plugins/commands/build/build'),
      ……

}

these plugin It mainly includes a Register method plug-ins (packages/preset-built-in/src/plugins/registerMethods.ts), One Routing plug-in (packages/preset-built-in/src/plugins/routes.ts), some Generate file related plug-ins (packages/preset-built-in/src/plugins/generateFiles/*), some Package and configure related plug-ins (packages/preset-built-in/src/plugins/features/*), some html Operate related plug-ins (packages/preset-built-in/src/plugins/features/html/*) As well as some Command related plug-ins (packages/preset-built-in/src/plugins/commands/*).

Register the method plug-in registerMethods(packages/preset-built-in/src/plugins/registerMethods.ts) in ,umi Dozens of methods have been registered , These methods are umi Plug in in the document api Of Extension method .

export default function (api: IApi) {
  //  Centralized registration of extension methods 
  [
    'onGenerateFiles',
    'onBuildComplete',
    'onExit',
    ……
  ].forEach((name) => {
    api.registerMethod({ name });
  });

  //  Separate registration writeTmpFile Method , And the reference fn, Convenient for other extension methods 
  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');
      }
    },
  });
}

When we're on the console umi Type the command under the path npx umi dev after , And it started umi command , Incidental dev Parameters , Instantiate after a series of operations Service object ( route :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: [
        //  Configure the built-in default plug-in set 
        require.resolve('@umijs/preset-built-in'),
        ...(opts.presets || []),
      ],
      plugins: [require.resolve('./plugins/umiAlias'), ...(opts.plugins || [])],
    });
  }
}

export { Service };

stay Service The default plug-in set mentioned above is passed into the constructor of preset(@umijs/preset-built-in), for umi Use . So far, we have introduced the default plug-in set preset As a representative of the umi plug-in unit .

Plug-in interface (pluginApi)

Service object (packages/core/src/Service/Service.ts) Medium getPluginAPI Method provides for plug-ins Plug-in interface .getPluginAPI The interface is the bridge of the whole plug-in system . It uses proxy mode to umi plug-in unit The core approach 、 Initialization process hook node api、Service Object method properties And through the @umijs/preset-built-in Sign up to service On the object Extension method Organized together , For plug-ins to call .

getPluginAPI(opts: any) {
  // Instantiation PluginAPI object ,PluginAPI Object contains describe,register,registerCommand,registerPresets,registerPlugins,registerMethod,skipPlugins Seven core plug-in methods 
    const pluginAPI = new PluginAPI(opts);

    //  register umi During service initialization hook node 
    [
      'onPluginReady', //  The plug-in is initialized 
      'modifyPaths', //  Modify the path 
      'onStart', //  start-up umi
      'modifyDefaultConfig', //  Modify default configuration 
      'modifyConfig', //  Modify the configuration 
    ].forEach((name) => {
      pluginAPI.registerMethod({ name, exitsError: false });
    });

    return new Proxy(pluginAPI, {
      get: (target, prop: string) => {
        //  because  pluginMethods  Need to be in  register  Stage available 
        //  Must pass  proxy  Dynamic access to the latest , To achieve the effect of using while registering 
        if (this.pluginMethods[prop]) return this.pluginMethods[prop];
        //  register umi service Properties and core methods on objects 
        if (
          [
            'applyPlugins',
            'ApplyPluginsType',
            'EnableBy',
            'ConfigChangeType',
            'babelRegister',
            'stage',
            ……
          ].includes(prop)
        ) {
          return typeof this[prop] === 'function'
            ? this[prop].bind(this)
            : this[prop];
        }
        return target[prop];
      },
    });
  }
Plug in kernel (pluginore)
1. Initialize configuration

It's about starting umi It will be instantiated later Service object ( route :packages/umi/src/ServiceWithBuiltIn.ts), And pass in preset Plug in set (@umijs/preset-built-in). The object is inherited from CoreServeice(packages/core/src/Service/Service.ts).CoreServeice During instantiation, the plug-in set and plug-ins will be initialized in the constructor :

//  initialization  Presets  and  plugins,  From everywhere 
    // 1.  structure  Service  The ginseng 
    // 2. process.env  It is specified in 
    // 3. package.json  in  devDependencies  Appoint 
    // 4.  The user is in  .umirc.ts  Configuration in file 
    this.initialPresets = resolvePresets({
      ...baseOpts,
      presets: opts.presets || [],
      userConfigPresets: this.userConfig.presets || [],
    });
    this.initialPlugins = resolvePlugins({
      ...baseOpts,
      plugins: opts.plugins || [],
      userConfigPlugins: this.userConfig.plugins || [],
    });

After conversion , A plug-in in umi An object in the system will eventually be represented in the following format :

{
    id, // @umijs/plugin-xxx, The plug-in name 
    key, // xxx, The plug-in is unique key
    path: winPath(path), //  route 
    apply() {
      //  Delay loading plug-ins 
      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, //  The default configuration 
  };
2. Initializing plug-ins

umi Instantiation Service Object Service Object's run Method . The initialization of the plug-in is in run Method . initialization preset and plugin The process is very similar , Let's focus on initialization plugin The process of .

//  Initializing plug-ins 
  async initPlugin(plugin: IPlugin) {
    //  After initializing the plug-in configuration in the first step , Plug in umi The system becomes one object after another , Here we export id, key And delay loading functions apply
    const { id, key, apply } = plugin;
    //  Get the bridge plug-in interface of the plug-in system PluginApi
    const api = this.getPluginAPI({ id, key, service: this });

    //  Register plug-ins 
    this.registerPlugin(plugin);
    //  Execute plug-in code 
    await this.applyAPI({ api, apply });
  }

Here we want to focus on the beginning preset When registering extension methods in the first registered method plug-in registerMethod Method .

registerMethod({
    name,
    fn,
    exitsError = true,
  }: {
    name: string;
    fn?: Function;
    exitsError?: boolean;
  }) {
    //  Handling of cases where the registered method already exists 
    if (this.service.pluginMethods[name]) {
      if (exitsError) {
        throw new Error(
          `api.registerMethod() failed, method ${name} is already exist.`,
        );
      } else {
        return;
      }
    }
    //  There are two situations : When the first method is registered, it passes fn Parameters , Then the registration method is fn Method ; The second case is not transmitted fn, Returns a function , The function will pass in fn Parameter to hook Hook and register , Mount to service Of hooksByPluginId Attribute 
    this.service.pluginMethods[name] =
      fn || function (fn: Function | Object) {
        const hook = {
          key: name,
          ...(utils.lodash.isPlainObject(fn) ? fn : { fn }),
        };
        // @ts-ignore
        this.register(hook);
      };
  }

So when executing plug-in code , If it is the core method, execute directly , If it is an extension method, the exception is writeTmpFile, The rest are in hooksByPluginId Registered under hook. Come here Service Completed the initialization of the plug-in , Execute the core method and extension method called by the plug-in .

3. initialization hooks

Through the following code ,Service The dimension will be configured with the plug-in name hook, Convert to hook Name the callback set configured for the dimension .

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);
      });
    });

With addHTMLHeadScripts Extension method as an example Before conversion :

'./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)] } ],

After the transformation :

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'
    }
  ],

At this point, the plug-in system is ready pluginReady state .

4. Trigger hook

When the program reaches pluginReady Post state ,Service A trigger is executed immediately hook operation .

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

So how is it triggered ? Let's take a closer look at applyPlugins Code implementation of :

async applyPlugins(opts: {
    key: string;
    type: ApplyPluginsType;
    initialValue?: any;
    args?: any;
  }) {
    //  Find the corresponding trigger hook Can mobilize , there hooks It is configured with the plug-in name as the dimension hook Convert to hook Name the callback set configured for the dimension 
    const hooks = this.hooks[opts.key] || [];
    //  Judge event type ,umi Divide callback events into add、modify and event Three 
    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.`,
          );
        }
        //  Event management is based on webpack Of Tapable library , It's only used. AsyncSeriesWaterfallHook An event control mode , Asynchronous serial waterfall flow callback mode : asynchronous , All hooks are handled asynchronously ; Serial , Execute sequentially ; The waterfall flow , The result of the previous hook is the parameter of the next hook .
        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,
            },
            // Unlike the other two event types ,add Type will return the results of all hooks 
            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,
            },
            //  Different from the other two hooks ,modify Type will return the final hook result 
            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 type , Only execute hook , No results returned 
            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}.`,
        );
    }
  }

thus ,umi The overall plug-in workflow has been introduced , The following code is umi According to the needs of the process, all kinds of hook To complete the whole umi All the functions of . except umi, Other frameworks have also been applied Plug in mode , Here is a brief introduction and comparison .

babel Plug-in mechanism

babel The main function is Grammar conversion ,babel The whole process is divided into three parts : analysis , Convert code to an abstract syntax tree (AST); transformation , Traverse AST Syntax conversion operations are performed on nodes in ; Generate , According to the latest AST Generate target code . In the process of transformation, it is the basis babel Configured plug-ins to complete .

babel plug-in unit
const createPlugin = (name) => {
  return {
    name,
    visitor: {
      FunctionDeclaration(path, state) {},
      ReturnStatement(path, state) {},
    }
  };
};

You can see babel Our plug-in also returns a function , and umi It's very similar . however babel The operation of plug-ins is not based on publish and subscribe Event driven mode , instead Visitor mode .babel Through a visitor visitor Uniformly traverse nodes , Provide methods and maintain node relationships , The plug-in only needs to be in visitor Register the node types you care about in , When visitor When traversing the relevant nodes, the plug-in will be called to visitor And execute .

webpack Plug-in mechanism

webpack The whole is based on two pillar functions : One is loader, Used to convert the source code of the module , be based on Pipeline mode ; The other is plugin, For resolution loader Unsolvable problems , seeing the name of a thing one thinks of its function ,plugin Is based on Plug-in mechanism Of . Take a look at a typical webpack plug-in unit :

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log('webpack  Build is starting !');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

webpack The plug-in will be executed uniformly during initialization apply Method . Plug ins are registered Compiler and compilation The hook function of , It can be accessed throughout the compilation life cycle compiler object , Complete the plug-in function . At the same time, the whole event driven function is based on webpack Core tools of Tapable.Tapable also umi Event driven tools for . You can see umi and webpack The overall idea is very similar .

rollup Plug-in mechanism

rollup It is also a module packaging tool , And webpack comparison rollup It's more suitable for packing pure js Class library of . Again rollup It also has a plug-in mechanism . A typical rollup plug-in unit :

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

rollup The plug-in maintains a set of synchronization / asynchronous 、 Serial / parallel 、 Fuse / Event callback mechanism for parameter transmission , However, there is no separate class library in this part , But in rollup Maintained in the project . adopt Plug in controller (src/utils/PluginDriver.ts)、 Plug in context (src/utils/PluginContext.ts)、 Plug in cache (src/utils/PluginCache.ts), Finished providing plug-ins api And plug-in kernel capabilities .

vue-cli Plug-in mechanism

vue-cli Compared with other plug-ins, our plug-ins have some characteristics , The plug-in is divided into several cases , A project generation phase , The plug-in is not installed. You need to install the plug-in ; The other is the project operation stage , Start the plug-in ; The other is UI plug-in unit , Running vue ui You will use .

vue-cli The package directory structure of the plug-in

├── generator.js  # generator( Optional )
├── index.js      # service  plug-in unit 
├── package.json
└── prompts.js    # prompt  file ( Optional )
└── ui.js    # ui  file ( Optional )
Generation phase

among generator.js and prompts.js Execute... With the plug-in installed ,index Execute in the operation phase .generator Example :

module.exports = (api, options) => {
  //  Expand package.json Field 
  api.extendPackage({
    dependencies: {
      'vue-router-layout': '^0.1.2'
    }
  })
  // afterAnyInvoke hook   The function will be executed repeatedly 
  api.afterAnyInvoke(() => {
  //  File operations 
  })
  // afterInvoke hook , This hook will be called after the file is written to the hard disk 
  api.afterInvoke(() => {})

}

prompts Will interact with users during installation , Get the option configuration of the plug-in and click generator.js When called, it is stored as a parameter .

Pass during the project generation phase packages/@vue/cli/lib/GeneratorAPI.js Provide plug-in unit api; stay packages/@vue/cli/lib/Generator.js Initialization plug-in , Execute plug-in registration api, stay packages/@vue/cli/lib/Creator.js Run the hook function registered by the plug-in in , Finally, the function of the plug-in is called .

Operation phase

vue-cli Run time plug-ins :

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
        }
      ])
  })
}

The plug-ins in the project running phase are mainly used to modify webpack Configuration of , Create or modify commands . from packages/@vue/cli-service/lib/PluginAPI.js Provide pluginapi,packages/@vue/cli-service/lib/Service.js Complete the initialization and operation of the plug-in . and vue-cli The operation of plug-ins is mainly managed based on the mode of callback function .

Through the above introduction , It can be found that the plug-in mechanism is an indispensable part of the modern front-end project engineering framework , There are many forms of plug-ins , But the overall structure is roughly the same , Since plug-in unit (plugin) plug-in unit api(pluginApi) Plug in core (pluginCore) Three parts . The plug-in core is used to register and manage plug-ins , Complete the initialization and operation of the plug-in , plug-in unit api It is a bridge between plug-ins and systems , Make the plug-in complete specific functions , Through the combination of different plug-ins, a set of front-end framework system with complete functions is formed .

原网站

版权声明
本文为[pingan8787]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/174/202206231930049791.html