preface

What is a plug-in? Plug-ins, also known as plug-ins, addin, addon, etc., are programs that follow a standard application programming interface (API) to extend features of a system that do not otherwise exist. At the same time, if a system supports the plug-in architecture, it also has the ability to achieve user customization.

The main reasons why an application supports plug-ins are as follows:

  • Third party developers can be supported to extend the application’s capabilities.
  • Support for extended new features can be made easier.
  • Reduce the code volume of the (core) application through plug-in design.

Of course, there are many other advantages.

The use of plug-ins in computers has a long history. For example, graphics software supports the processing of different formats of picture files through plug-ins; The mailbox client encrypts and decrypts emails through plug-ins. Editors or integrated development environments support different programming languages through plug-ins, and so on.

In the front end, there are many examples of the application of various frameworks and libraries to plug-ins, such as WebPack, VUex, DVA, Babel, PostCSS, etc., all have a set of their plug-in system. As a Web front-end, I would like to discuss a little about the posture of using plug-ins in the front-end, the thought principle of plug-ins, and how to achieve a custom plug-in system.

Know the plugin

Before analyzing plug-in design, we can use the following examples to understand how plug-ins are used in different front-end popular frameworks and libraries.

webpack

In the common webpack plug-in configuration, there are many plug-ins we are familiar with, such as HtmlWebpackPlugin, DllPlugin, ExtractTextWebpackPlugin and so on, see the official documentation: The plugins | webpack doc.

The configuration of the WebPack plug-in is very simple, as follows:

import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";

const config: webpack.Configuration = {
	// ...
  plugins: [
    new webpack.BannerPlugin({
      banner: '" ತ, _ ತ (c)'
    }),
    new HtmlWebpackPlugin()
  ]
};

export default config;
Copy the code

In Webpack, the loader’s primary responsibility is to convert other non-javascript and JSON types into valid modules for application parsing. In addition, plugin performs most of the Webpack tasks, such as packaging optimization, resource management, and so on. As the core pillar of Webpack, Plugin can basically solve the tasks that loader cannot complete, which shows the power of plugin. The Compiler and Compilation in WebPack are built primarily on top of the Tapable library, and plug-ins are built on top of the life cycles of the first two.

For example, Compiler has the following lifecycle hooks: Initialize, emit, make, compile, compilation, etc. There are about 28. Reference documents: the compiler – hooks | webpack. Each hook has its own type, asynchronous, synchronous, serial, parallel, insurance, waterfall flow, and so on.

Compliation also has lifecycle hooks: buildModule, rebuildModule, failedModule, succeedModule, SEAL, and so on. Triggering logic, like complier hooks, has different types. Reference: compliation – hooks | webpack.

How to write a webpack plugin, also have the official document: write a plugin | webpack. I won’t repeat it here. The basic form of the plug-in is as follows:

class FirstWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync("FirstWebpackPlugin", (compilation, callback) => {
      // ...callback(); }); }}Copy the code

Plug-ins are registered by using tap, tapAsync, tapPromise, etc., on the specified hooks, and tapable executes the functions registered by the plug-in by making call calls to specific hooks within webpack in different life cycles.

About 80% of the code in WebPack is plug-in, so WebPack is essentially an event-driven, plug-in based compiler.

babel

Babel is a general-purpose JavaScript compiler. In our projects, we often have a file called.babelrc. Because Babel is just a compiler and does nothing by default, we need to write.babelrc to tell Babel what to do, mainly by specifying presets and plugins.

Plugins, as the name suggests, are plugins for Babel, and presets are preset sets of plugins.

For example, with @babel/preset-env installed as a preset, we can write the latest JavaScript syntax code, which can be compiled and converted to the appropriate final code according to our target environment.

We can then install the @babel/ plugin-transform-Runtime plug-in, which can inject Babel’s utility assistant code into our compiled files, thus saving the volume of compiled code. The following. Babelrc:

{
  "presets": ["@babel/preset-env"]."plugins": ["@babel/plugin-transform-runtime"]}Copy the code

We can then parse through the CLI tools provided by Babel:

npx babel --config-file=./.babelrc source.js --out-file compiled.js
Copy the code

Of course, Babel has a huge number of plugins: Babel plugins, from ES2015 to ES2018, React, TypeScript, and so on. Modern development is largely dependent on Babel and its rich collection of plugins.

Babel itself parses the input code into an abstract syntax tree (AST) and then compiles the output object code. It does nothing, of course, like this:

const babel = code= > code;
Copy the code

But if a plugin is injected, the plugin modifies and adjusts the AST in the middle of the Transform process using Babel tools such as Babylon, Babel-traversal, babel-types, and so on. Babel then compiles the modified AST into the final code. Thus achieving the final requirements of the plug-in. The illustration is as follows:

The basic format of the Babel plug-in is as follows:

export default function FirstBabelPlugin({ types: t }) {
  return {
    name: "plugin-demo",
    pre(state) {},
    visitor: {
      Identifier(path, state) {},
      BinaryExpression(path, state) {},
      // ...
    },
    post(state) {}
  };
};
Copy the code

As a plug-in to Babel, you can adjust and modify AST nodes as long as you declare the types of AST nodes to be accessed, such as Identifier and BinaryExpression, as functions. Pre and POST hook functions are also provided, which can be executed before and after the plug-in is run.

Of course, if the students are interested, you can refer to the official handbook: Babel plugin faced | making, at the same time to see this site: The AST Explorer, with its familiarity with AST node types, makes it possible to write your own Babel plug-in to fulfill specific requirements.

dva

Like Webpack and Babel, DVA, as the front-end data flow management scheme, also relies on plug-in mode. Install custom plugins in the DVA application as follows:

import { create } from "dva-core";
import createImmerPlugin from "dva-immer";

const app = create();

app.use(createImmerPlugin());
Copy the code

Dva base layer depends on DVA-core. Plug-in design among them, can the reference files: Plugin. Js | dva – core.

So DVA-core itself also designed some hooks, such as onError, onReducer, onEffect, onHmr and so on. Compared to WebPack, Babel’s plug-in mechanism for DVA is much less complex.

The DVA plug-in is written as follows:

export default function FirstDvaPlugin() {
  return {
    onEffect() {},
    onReducer() {},
    // ...
  };
}
Copy the code

Once a plugin is registered with the DVA via app.use(plugin), dvA-core manages all the hook functions. At some point, plugin.apply triggers specific hooks to iterate through the corresponding array of functions.

Design ideas for plug-ins

Review the plugin mechanisms for WebPack, Babel, and DVA. As you can see, the design of a plug-in can be basically divided into three parts: hook declaration, hook registration, and hook invocation. As shown in figure:

Hook statement

A framework needs to identify its internal key events, lifecycle nodes. This life cycle does not have to be linear; it can be cyclic, point-like, and so on. Each lifecycle hook basically corresponds to a different scenario of the framework. For example, the Initialize hook of the Compiler in WebPack is meant to fire after the compiler object has been initialized.

If the framework has designed all the hooks, it can basically see the business scenarios and requirements of the framework itself from the design of the hooks.

Hooks call

Of course, hooks are always called, and uncalled hooks are theoretically worthless. The timing and location of the hook call determine the specific requirements of the hook.

For example, if you have a hook, onAppInit, that means apply the initialization hook. It should be called when the application is initialized, probably from the main entry of the application code.

For different scenarios, you can design whether the hook is asynchronous or synchronous, parallel or serial. The onAppInit hook should be designed as an asynchronous parallel hook and called in the application initialization entry, assuming that the application can be blocked by the receiver until A, B, or C business-specific requests are completed.

Hook the registered

Specific hooks (greater than or equal to one hook) are registered for specific plug-ins in different scenarios. Plug-in combinations use hooks of different features to insert specific business code into them. Once the framework loads the plug-in, the hooks are automatically registered. When the specified hook is run, the business code that the specific plug-in hooks into is executed accordingly.

summary

Hook design, registration, and invocation are code design separations between stable and unstable hooks. The framework kernel is relatively stable because it is weak or irrelevant to the business. Plug-ins, on the other hand, implement concrete business code that is inherently subject to change as business needs change, and are therefore relatively unstable. At the same time, plug-ins are not related to each other, which is equivalent to module decoupling in code.

Therefore, the design of a plug-in is essentially not limited to any specific form of implementation as long as the above relationships are satisfied.

Implement plug-in architecture

With that said, let’s get hands-on with how to write code that implements hook declarations, calls, and registrations to build a plug-in architecture. I will walk you through plug-in design in two main ways: native implementation and implementation using webPack’s core dependency tapable library.

A freestanding implementation

Here the code mainly refers to dVA-core source code implementation:

import invariant from 'invariant';

type Hook = (. args:any) = > void;
type IKernelPlugin<T extends string | symbol> = Record<T, Hook[]>;
type IPlugin<T extends string | symbol> = Partial<Record<T, Hook | Hook[]>>;

class PluginSystem<K extends string> {
  private hooks: IKernelPlugin<K>;

  constructor(hooks: K[] = []) {
    invariant(hooks.length, `plugin.hooks cannot be empty`);

    this.hooks = hooks.reduce((memo, key) = > {
      memo[key] = [];
      return memo;
    }, {} as IKernelPlugin<K>);
  }

  use(plugin: IPlugin<K>) {
    const { hooks } = this;
    for (let key in plugin) {
      if (Object.prototype.hasOwnProperty.call(plugin, key)) {
        invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
        hooks[key] = hooks[key].concat(plugin[key]);
      }
    }
  }

  apply(key: K, defaultHandler: Hook = (a)= >{{})const { hooks } = this;
    const fns = hooks[key];

    return (. args:any) = > {
      if (fns.length) {
        for (const fn of fns) {
          fn(...args);
        }
      } else {
        defaultHandler(...args);
      }
    };
  }

  get(key: K) {
    const { hooks } = this;
    invariant(key in hooks, `plugin.get: hook ${key} cannot be got`);

    returnhooks[key]; }}export default PluginSystem;
Copy the code

The above code, in essence, works very simply by having different hooks correspond to an array of functions. The framework can register plug-ins through the use method in an instance of PluginSystem (that is, the array of functions that update the corresponding hooks) and run hooks through the apply method.

The Apply method only supports running synchronous functions, but it can be modified to support running asynchronous functions. Of course, with the implementation of the plug-in system, we can write two plug-ins related to business requirements according to the interface design of the corresponding plug-in system:

function createLoggerPlugin(appId: string) {
  return {
    onAction(action: { type: string; params: any }) {
      log(`Log from appId: ${appId}`, action.type, action.params); }}; }function createAppInitPlugin() {
  return {
    onInit() {
      log(`App init, do something`); }}; }Copy the code

As you can see, this plugin is very simple and actually just returns a hook object through a function.

With the plug-in system, and plug-in, you can register the corresponding plug-in in the application, as shown in the following example:

import { log } from './util';
import Plugin from './Plugin';

type Hook = 'onInit' | 'onAction';

/** * A sample App built on native plug-ins */
(async function App() {
  /** * Initialize the plugin mechanism - hook declaration */
  const system = new PluginSystem<Hook>(['onInit'.'onAction']);
  const APP_ID = 'a57e41';

  const appInitPlugin = createAppInitPlugin();
  const loggerPlugin = createLoggerPlugin(APP_ID);
  /** * plug-in declaration, registration */
  system.use(appInitPlugin);
  system.use(loggerPlugin);

  /** * Plug-in hooks call */ at any time
  system.apply('onInit') (); system.apply('onAction') ({type: 'SET_AUTHOR_INFO',
    params: { name: 'sulirc', email: '[email protected]' }
  });
})();
Copy the code

So far, framework hooks and business plug-ins have reached a harmonious and unified ecosystem.

Use tapable

Of course, the above native implementation may be relatively simple, not suitable for more complex scenarios, such as asynchronous, parallel, waterfall flow, insurance hook types mentioned above, if the implementation of native, indeed, the cost and risk will be higher.

Fortunately, we have the core dependency library for WebPack: Tapable. It provides a variety of hook types, such as SyncHook for synchronous hook, AsyncParallelHook for asynchronous parallel hook, AsyncSeriesHook for asynchronous serial hook, and so on. To see what hook types mean, look at Tapable’s README.

Since Tapable is already extremely powerful, it would be unnecessary to do additional wrapping based on this, so assume that the framework declares three hooks onError, onAction, and onInit as shown in the code example:

import invariant from 'invariant';
import {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
  HookMap
} from 'tapable';

export interface IPluginHooks {
  onError: SyncHook;
  onAction: AsyncParallelHook;
  onInit: AsyncSeriesHook;
}

interface IPlugin {
  apply: (hooks: IPluginHooks) = > void;
}

class TapablePluginSystem {
  hooks: IPluginHooks;

  constructor(plugins: IPlugin[] = []) {
    /** * hook declaration, register */
    this.hooks = {
      onError: new SyncHook(['errMsg']),
      onAction: new AsyncParallelHook(['action']),
      onInit: new AsyncSeriesHook()
    };

    if (~plugins.length) {
      plugins.forEach(plugin= > this.use(plugin));
    }
  }

  use(plugin: IPlugin) {
    invariant(plugin.apply, 'plugin.apply cannot be undefined');

    plugin.apply(this.hooks); }}export default TapablePluginSystem;
Copy the code

Once the framework is implemented, again, two plug-ins are declared as follows:

class LoggerPlugin {
  private appId: string;

  constructor(appId: string) {
    this.appId = appId;
  }

  apply(hooks: IPluginHooks) {
    const PluginType = 'LoggerPlugin';
    hooks.onInit.tapPromise(PluginType, (a)= > {
      return fetch('LOGGER_INIT');
    });
    hooks.onAction.tapAsync(PluginType, (action, callback) = > {
      report(`Log action from appId: ${this.appId}`, action.type, action.params);
      fetch('APP_INFO')
        .then((a)= > callback())
        .catch(err= >callback(err)); }); }}class ReportPlugin {
  private appId: string;

  constructor(appId: string) {
    this.appId = appId;
  }

  apply(hooks: IPluginHooks) {
    const PluginType = 'ReportPlugin';
    hooks.onError.tap(PluginType, errMsg= > {
      report(`Report error from appId: ${this.appId}: `, errMsg);
    });
    hooks.onAction.tapAsync(PluginType, (action, callback) = > {
      report(`Report action from appId: ${this.appId}: `, action.type, action.params);
      fetch('APP_INFO')
        .then((a)= > callback())
        .catch(err= >callback(err)); }); }}Copy the code

The plugin style is very similar to that of the WebPack plugin. In fact, the principle is very simple. The framework calls the plugin instance apply and causes the plugin to register the hook successfully.

Finally, the application needs to call the hook at the right time and place, using the example:

/** * A sample App built based on the Tapable plugin */
(async function TapableApp() {
  const APP_ID = 'a57e41';

  /** * plug-in declaration, registration */
  const plugins = [new LoggerPlugin(APP_ID), new ReportPlugin(APP_ID)];
  const system = new TapablePluginSystem(plugins);

  /** * Plug-in hooks call */ at any time
  system.hooks.onInit.promise().then((a)= > {
    log('onInit hooks complete');
  });
  system.hooks.onAction.callAsync(
    {
      type: 'SET_AUTHOR_INFO',
      params: { name: 'sulirc', email: '[email protected]'}},(err: any) = > {
      if (err) {
        console.error(err);
        return;
      }
      log('onAction hooks complete'); });// ...
  system.hooks.onError.call('Fake Error');
  log('onError hooks complete'); }) ();Copy the code

After reading the above sample code, is there a deeper understanding. You can also imagine that the ability of framework applications based on plug-ins is very powerful. A complex application with rich hook interfaces can generate numerous plug-ins based on hooks, or even a community.

Think: How does it apply to business requirements?

So far, I think their understanding of plug-ins or more superficial, not in-depth. But as a result of writing this blog post, I do realize more about the power of the plug-in model.

So the question is, how to successfully apply the above model to the actual production environment, if not said, is basically only a “brain exercise”.

How do you apply the plug-in pattern to your business requirements? This I believe there is no standard answer, but I want to give their own thinking.

First of all, at the beginning of the design of an application, the designer should have a long-term vision, able to know where the application’s business direction is. What are the possible future directions? It is also necessary to define competency boundaries. After the big framework is set, the general business functions can be detailed and listed after knowing the application future business scenarios (of course, I think it also needs to keep some imagination space, which is often the most difficult point).

Next, scientific code design. Design what hook? And when and where does the hook trigger (the code triggers the space-time control problem)? Hook design is closely related to business requirements as well as the application lifecycle. React applications have init, Render, Update, DeStory phases for componentWillMount, componentDidMount, etc. Can each stage be refined into hooks, and what types of hooks are appropriate for different stages? All worth thinking about.

With a well-designed hook, how can a third party hook into an implementation plug-in? Is it loaded through configuration, or can it support dynamic loading? Does the plug-in allow dynamic unloading? Does the plug-in require permission control?

Above, all need in the concrete design, concrete thinking.

summary

When I was in high school, my physics teacher once said: “First-class students learn ideas, second-rate students learn methods, and third-rate students learn topics”, which impressed me very much. Learning one kind of thing, learning ideas is often the shortcut. Thought grasp, often will draw inferences by analogy.

I hope you can also understand the design method and thought principle behind plug-ins through the discussion of plug-in system in this article. No matter what framework or library plug-in API you come into contact with, you can use and think from a higher dimension.

When we master the idea of plug-ins, if we meet the right scenario, we must not hesitate to design and implement a plug-in system for specific business, the application will become more extensible, more powerful, of course, with the plug-in mechanism to enable the application will be upgraded from “application” to “platform”.

Above, if this article can give you enlightenment and help, will be the author’s honor ~