大型前端项目如何解耦各模块

96 阅读1分钟

大型前端项目如何解耦各模块

插件机制

通过注册插件来扩展各个模块

// plugin
export type SyncPluginCallback<T = any, R = void> = (p: T, modifier: PluginModifier) => R;
export type AsyncPluginCallback<T = any, R = void> = (p: T, modifier: PluginModifier) => Promise<R>;
export interface PluginModifier {
  stop(): void;
}

export interface PluginNotifyModifier {
  onStop(): void;
}

export interface PluginModifierResp {
  stop: boolean;
}

export type PluginCallback<T = any, R = void> = SyncPluginCallback<T, R> | AsyncPluginCallback<T, R>;

export enum PluginHookType {
  SYNC,
  ASYNC,
}

export type PluginRegister<Ctx, PluginInstanceValue = void> =
  | ((ctx: Ctx) => PluginInstanceValue)
  | {
      key: string;
      register(ctx: Ctx): PluginInstanceValue;
    };
export type PluginsMap<Ctx, PluginInstanceValue = void> = Map<
  PluginRegister<Ctx, PluginInstanceValue> | string,
  PluginInstanceValue
>;
export function registerHooks<
  T extends {
    plugins: PluginsMap<any, any>;
  },
  R = void,
>(this: T, plugins: PluginRegister<T, R>[]) {
  plugins.forEach((i) => {
    const isFn = typeof i === "function";
    const res = isFn ? i(this) : i.register(this);
    res && this.plugins.set(isFn ? i : i.key, res);
  });
}

export function queryPlugin<
  Ctx extends {
    plugins: PluginsMap<any, any>;
  },
  R,
>(this: Ctx, p: PluginRegister<Ctx, R>) {
  const res = this.plugins.get(p) as R | undefined;
  if (!res) {
    throw Error(`${p} is not found`);
  }
  return res as R;
}

export function destroyPlugin<
  Ctx extends {
    plugins: PluginsMap<any, any>;
  },
>(this: Ctx) {
  for (const [_, v] of this.plugins) {
    v && v.destroy && v.destroy();
  }
}

export function modifierHooksExecute(fn: (m: PluginNotifyModifier) => void): PluginModifierResp {
  let stop = false;
  fn({ onStop: () => (stop = true) });
  return {
    stop,
  };
}

type LifeCycle = "on" | "before" | "after";

export type HookLifeCycle<T = any, ExcludeKey = void, R = void> = {
  [k in Exclude<LifeCycle, ExcludeKey>]: PluginHook<T, R>;
};

interface PluginItem<T, R> {
  type: PluginHookType;
  name: string;
  fn: PluginCallback<T, R>;
}
export class PluginHook<T = any, R = void> {
  private pluginMap: PluginItem<T, R>[];
  name: string;
  constructor(name: string) {
    this.name = name;
    this.pluginMap = [];
  }
  private _on(name: string, type: PluginHookType, fn: PluginCallback<T, R>) {
    this.pluginMap.push({ name, type, fn });
  }
  subscribe(name: string, fn: PluginCallback<T, R>) {
    this._on(name, PluginHookType.SYNC, fn);
  }

  asyncSubscribe(name: string, fn: PluginCallback<T, R>) {
    this._on(name, PluginHookType.ASYNC, fn);
  }

  unsubscribe(fn: PluginCallback<T, R>) {
    const idx = this.pluginMap.findIndex((n) => n.fn === fn);
    if (idx === -1) return;
    this.pluginMap.splice(idx, 1);
  }

  notify(p: T, modifier?: PluginNotifyModifier) {
    if (!this.pluginMap.length) return;
    let stop = false;
    const params = {
      stop: () => {
        stop = true;
        modifier?.onStop();
      },
    };
    for (const item of this.pluginMap) {
      item.fn(p, params);
      if (stop) break;
    }
  }

  async asyncNotify(p: T, modifier?: PluginNotifyModifier) {
    if (!this.pluginMap.length) return;
    let stop = false;
    const params = {
      stop: () => {
        stop = true;
        modifier?.onStop();
      },
    };
    for (const item of this.pluginMap) {
      await item.fn(p, params);
      if (stop) break;
    }
  }

  destroy() {
    this.pluginMap = [];
  }
}

// usage
function hooks() {
  return Object.freeze({
    init: new PluginHook<ModuleA>("init"),
    // ... other hooks
  } as const); // use `as const` to force type narrowing and avoid defining a `hooks` interface separately
}
class ModuleA {
  hooks = hooks();
  private registerHooks = registerHooks;
  plugins: PluginsMap<ModuleA, any> = new Map();
  queryPlugin = queryPlugin;
  private destroyPlugin = destroyPlugin;
  constructor({plugins = []}: {plugins: PluginRegister<ModuleA>[]}) {
    this.registerHooks(plugins)
    this.hooks.init.notify(this)
  }
}

function pluginA(ctx: ModuleA) {
  // todo
  return {
    say() {
      console.log('module a plugin a')
    }
  }
}

const ma = new ModuleA({
  plugins: [
    pluginA
  ]
})

// get plugin a
ma.queryPlugin(pluginA).say()

通过上述代码我们介绍了 module A 模块,然后我们通过插件以一种非侵入的方式使得 module A 通过 plugin A 扩展了它的能力

回调

通过回调我们可以提前配置好模块所需要的一些能力比如,我要实现动态的参数注入

class ModuleA {
  constructor(private options: {
    name: () => string;
  }) {

  }

  hi() {
    console.log(`i am ${this.options.name()}`)
  }
}

const a = new ModuleA({
  name: () => {
    return Math.random() > 0.5 ? 'Echo' : 'Nike'
  }
})
a.hi() // i am Echo
a.hi() // i am Nike

事件订阅发布

在第一个例子中我们列出了插件机制,其实插件机制是基于事件订阅发布之上建立的,所以在这个例子中我们来看可以订阅的插件

// get case 1 moduleA

const ma = new ModuleA()

ma.hooks.init.subscribe('functionA', (ctx) => {
  console.log('module a init')
})

通过上面三个例子我们可以看到一些非侵入性的实现方式,第二种没有第一种以及第三种灵活,但是它拥有稳定的 api 调用方式。