poplark

Hi, 👋 ! 如果您发现有什么错误的地方,点这里可以提 issue,🤝🤝

View My GitHub Profile

20 November 2020

具备插件机制的 Typescript 库

by poplark

写一个具备插件机制的 Typescript 库

最近在做公司某产品第二版的 sdk,使 sdk 提供更具有面向对象的 API。在我做该产品 sdk 的第一版时,发现其中有一些 API 调用很少,很多用户没有使用场景,那么这让我有了在第二版中引入插件机制的想法,sdk 提供最基础的 API,一些不常调用的 API 可以通过插件让用户选择性的加载。虽然实现时碰到一些问题,通过折腾最终倒是找到了一个自认为可行的方案。

具体场景是这样的: ` 假设有 Base, Inherit, Plugin 三个类,Inherit 类继承自 Base 类,Plugin 作为插件类,希望当用户加载插件之后,可以直接通过 Inherit 对象直接调用 Plugin 类中提供的方法。 `

那么就会面临下面几个问题:

  1. 如何使 Inherit 类似于继承 Plugin 类一样,直接使用 Plugin 中的 成员方法 ?
  2. 如何使 Inherit 类似于继承 Plugin 类一样,直接使用 Plugin 中的 成员变量 ?
  3. 如何扩展 Inherit 的类型?

在加载插件上,我参考了 Vue 中的 use API Vue.use(PluginObject),以及 PluginObject 需要提供 install 方法,只是具体的内部实现上不太一样。对于上面几个问题我这里的实现是:

1. 如何使 Inherit 类似于继承 Plugin 类一样,直接使用 Plugin 中的 成员方法 ?

将 Plugin 原型上的方法直接挂到 Inherit 的原型上,参考代码:

export function applyMixins(inheritCtor: Function, pluginCtor: Function): void {
  const keys = Object.getOwnPropertyNames(inheritCtor.prototype);
  Object.getOwnPropertyNames(pluginCtor.prototype).forEach((name) => {
    if (name === 'constructor') {
      // 构造函数,直接跳过
      return;
    }
    if (keys.includes(name)) {
      console.warn(`${name} is conflict, will ignore`);
      return;
    }
    inheritCtor.prototype[name] = pluginCtor.prototype[name];
  });
}

class Inherit extends Base {
  static use(plugin: PluginObject): void {
    LocalStream.plugins[plugin.name] = plugin;
    applyMixins(LocalStream, plugin.install);
  }
}

2. 如何使 Inherit 类似于继承 Plugin 类一样,直接使用 Plugin 中的 属性 ?

对于每个 Inherit 对象都需要有一份 Plugin 上的属性的话,我这里采取的是类似于在 Inherit 的构建函数里,调用 Plugin 的构建方法,不过调用时是将 Plugin 的构建方法作为纯函数,并通过 call 改变其运行时的上下文,参考代码:

class Inherit extends Base {
  static plugins: PluginObject[] = [];
  constructor() {
    super();
    this.initPlugins();
  }

  initPlugins(): void {
    for (const key in Inherit.plugins) {
      const plugin = Inherit.plugins[key];
      plugin.install.call(this);
    }
  }
}

3. 如何扩展 Inherit 的类型?

这里可以使用 Interface extends Class,参考代码:

declare module "xxx-lib" {
  interface Inherit extends Plugin {}
}

具体理论依据为:TypeScript - Understanding TypeScript 中这样描述 In TypeScript, interfaces can also extend classes, but only in a way that involves inheritance. When an interface extends a class, the interface includes all class members (public and private), but without the class’ implementations.

特别在处理这个问题时,尝试了很多种方法,最终确定是可以这么搞。

最后贴上一段相对完整的示例代码:

主库代码

export interface PluginObject {
  name: string;
  install: Function;
}

export function applyMixins(derivedCtor: Function, pluginCtor: Function): void {
  const keys = Object.getOwnPropertyNames(derivedCtor.prototype);
  Object.getOwnPropertyNames(pluginCtor.prototype).forEach((name) => {
    if (name === 'constructor') {
      // 构造函数,直接跳过
      return;
    }
    if (keys.includes(name)) {
      console.warn(`${name} is conflict, will ignore`);
      return;
    }
    derivedCtor.prototype[name] = pluginCtor.prototype[name];
  });
}

export class Base {
  // ....
}

export class Inherit extends Base {
  /**
   * @internal
   */
  static plugins: {
    [key: string]: PluginObject;
  } = {};

  /**
   * 加载插件,使用插件功能
   * @param plugin - 插件
   */
  static use(plugin: PluginObject): void {
      Inherit.plugins[item.name] = plugin;
      // 为 Inherit 的补上 Plugin 原型中的方法
      applyMixins(Inherit, plugin.install);
    });
  }
  /**
   * @internal
   */
  initPlugins(): void {
    for (const key in Inherit.plugins) {
      // 为 Inherit 的对象补上 Plugin 中的成员变量
      Inherit.plugins[key].install.call(this);
    }
  }

  constructor() {
    super();
    this.initPlugins();
  }
}

插件库代码

declare module "@my/lib" {
  interface Inherit extends Plugin {}
}

export class Plugin {
  // ...
}
const plugin: PluginObject = {
  name: 'plugin-name',
  install: Plugin,
};

export default plugin;
tags: typescript - plugin