当前位置:网站首页>[design] 1359- how umi3 implements plug-in architecture
[design] 1359- how umi3 implements plug-in architecture
2022-06-23 23:14:00 【pingan8787】

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 .
边栏推荐
- 详解四元数
- How to set the search bar of website construction and what should be paid attention to when designing the search box
- Detailed explanation of MySQL database configuration information viewing and modification methods
- Problems and solutions of MacOS installation go SQLite3
- PHPMailer 发送邮件 PHP
- What server is used for website construction? What is the price of the server
- How PostgreSQL creates partition tables
- WebService客户端请求失败 can not create a secure xmlinputfactory
- Data interpretation! Ideal L9 sprints to "sell more than 10000 yuan a month" to grab share from BBA
- Micro build low code tutorial - Application creation
猜你喜欢
随机推荐
Aicon2021 | AI technology helps content security and promotes the healthy development of Internet Environment
Section 30 high availability (HA) configuration case of Tianrongxin topgate firewall
数据解读!理想L9冲刺「月销过万」,从BBA手中抢份额
Ant won the finqa competition champion and made a breakthrough in AI technology of long text numerical reasoning
Oracle关闭回收站
PHP的curl功能扩展基本用法
First talk about the necessary skills of Architecture
How to use data warehouse to create time series
The technical design and practice of decrypting the red envelopes of Tiktok Spring Festival
Payment industry tuyere project: smart digital operation 3.0
[JS reverse hundred examples] the first question of the anti crawling practice platform for netizens: JS confusion encryption and anti hook operation
Isolement des transactions MySQL
MySQL事务隔离
Detailed process of deploying redis cluster and micro service project in docker
Summary of cloud native pipeline tools
Face and lining of fresh food pre storage
How to connect the fortress machine to the new server? What can I do if there is a problem with the fortress machine?
生鲜前置仓的面子和里子
How can manufacturing enterprises go to the cloud?
Data interpretation! Ideal L9 sprints to "sell more than 10000 yuan a month" to grab share from BBA







