/* * Tencent is pleased to support the open source community by making TMagicEditor available. * * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { EventEmitter } from 'events'; import { compose } from '@editor/utils/compose'; const methodName = (prefix: string, name: string) => `${prefix}${name[0].toUpperCase()}${name.substring(1)}`; const isError = (error: any): boolean => Object.prototype.toString.call(error) === '[object Error]'; const doAction = async ( args: any[], scope: any, sourceMethod: any, beforeMethodName: string, afterMethodName: string, fn: (args: any[], next?: Function | undefined) => Promise, ) => { try { let beforeArgs = args; for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) { let beforeReturnValue = (await beforeMethod(...beforeArgs)) || []; if (isError(beforeReturnValue)) throw beforeReturnValue; if (!Array.isArray(beforeReturnValue)) { beforeReturnValue = [beforeReturnValue]; } beforeArgs = beforeArgs.map((v: any, index: number) => { if (typeof beforeReturnValue[index] === 'undefined') return v; return beforeReturnValue[index]; }); } let returnValue: any = await fn(beforeArgs, sourceMethod.bind(scope)); for (const afterMethod of scope.pluginOptionsList[afterMethodName]) { returnValue = await afterMethod(...beforeArgs, returnValue); if (isError(returnValue)) throw returnValue; } return returnValue; } catch (error) { throw error; } }; /** * 提供两种方式对Class进行扩展 * 方法1: * 给Class中的每个方法都添加before after两个钩子 * 给Class添加一个usePlugin方法,use方法可以传入一个包含before或者after方法的对象 * * 例如: * Class EditorService extends BaseService { * constructor() { * super(['add']); * } * add(value) { return result; } * }; * * const editorService = new EditorService(); * * editorService.usePlugin({ * beforeAdd(value) { return [value] }, * afterAdd(value, result) {}, * }); * * editorService.add(); 最终会变成 () => { * editorService.beforeAdd(); * editorService.add(); * editorService.afterAdd(); * } * * 调用时的参数会透传到before方法的参数中, 然后before的return 会作为原方法的参数和after的参数,after最后一个参数则是原方法的return值; * 如需终止后续方法调用可以return new Error(); * * 方法2: * 给Class中的每个方法都添加中间件 * 给Class添加一个use方法,use方法可以传入一个包含源对象方法名作为key值的对象 * * 例如: * Class EditorService extends BaseService { * constructor() { * super(['add']); * } * add(value) { return result; } * }; * * const editorService = new EditorService(); * editorService.use({ * add(value, next) { console.log(value); next() }, * }); */ export default class extends EventEmitter { private pluginOptionsList: Record = {}; private middleware: Record = {}; private taskList: (() => Promise)[] = []; private doingTask = false; constructor(methods: string[] = [], serialMethods: string[] = []) { super(); methods.forEach((propertyName: string) => { const scope = this as any; const sourceMethod = scope[propertyName]; const beforeMethodName = methodName('before', propertyName); const afterMethodName = methodName('after', propertyName); this.pluginOptionsList[beforeMethodName] = []; this.pluginOptionsList[afterMethodName] = []; this.middleware[propertyName] = []; const fn = compose(this.middleware[propertyName]); Object.defineProperty(scope, propertyName, { value: async (...args: any[]) => { if (!serialMethods.includes(propertyName)) { return doAction(args, scope, sourceMethod, beforeMethodName, afterMethodName, fn); } // 由于async await,所以会出现函数执行到await时让出线程,导致执行顺序出错,例如调用了select(1) -> update -> select(2),这个时候就有可能出现update了2; // 这里保证函数调用严格按顺序执行; const promise = new Promise((resolve, reject) => { this.taskList.push(async () => { try { const value = await doAction(args, scope, sourceMethod, beforeMethodName, afterMethodName, fn); resolve(value); } catch (e) { reject(e); } }); }); if (!this.doingTask) { this.doTask(); } return promise; }, }); }); } public use(options: Record) { Object.entries(options).forEach(([methodName, method]: [string, Function]) => { if (typeof method === 'function') this.middleware[methodName].push(method); }); } public usePlugin(options: Record) { Object.entries(options).forEach(([methodName, method]: [string, Function]) => { if (typeof method === 'function') this.pluginOptionsList[methodName].push(method); }); } private async doTask() { this.doingTask = true; let task = this.taskList.shift(); while (task) { await task(); task = this.taskList.shift(); } this.doingTask = false; } }