大型前端项目如何解耦各模块
插件机制
通过注册插件来扩展各个模块
// 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 调用方式。