refactor(editor): 移除 BaseService 废弃的 use/middleware 机制

- 删除已 @deprecated 的 BaseService.use 方法及其 middleware 通道

- 删除 utils/compose.ts 及对应测试(仅服务于 middleware,无其他引用)

- editor.ts 移除 safeOptions/safeParent 兜底,相关方法 options 改用形参默认值

- props.ts fillConfig 的 labelWidth 改为形参默认值,移除 typeof function 兜底

- 同步更新 5 份 service 方法文档,删除 ## use 章节
This commit is contained in:
roymondchen 2026-05-27 18:55:38 +08:00
parent d01a28ce76
commit de94a75803
12 changed files with 71 additions and 405 deletions

View File

@ -288,15 +288,11 @@
销毁 codeBlockService重置状态并移除所有事件监听和插件
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值

View File

@ -712,45 +712,11 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
移除所有事件监听清空state移除所有插件
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
- **示例:**
```js
import { editorService, getAddParent } from "@tmagic/editor";
import { ElMessageBox } from "element-plus";
editorService.use({
// 添加是否删除节点确认提示
async remove(node, next) {
await ElMessageBox.confirm("是否删除", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
next();
},
add(node, next) {
// text组件只能添加到container中
const parentNode = getAddParent(node);
if (node.type === "text" && parentNode?.type !== "container") {
return;
}
next();
},
});
```
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值

View File

@ -254,15 +254,11 @@
销毁propsService
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值

View File

@ -231,28 +231,11 @@ import { storageService } from '@tmagic/editor';
storageService.destroy();
```
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
- **示例:**
```js
import { storageService } from '@tmagic/editor';
storageService.use({
getItem(key, options, next) {
console.log('获取存储项:', key);
return next();
},
});
```
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值

View File

@ -179,29 +179,11 @@ import { uiService } from '@tmagic/editor';
uiService.destroy();
```
## use
使用中间件的方式扩展方法,上述方法中标记有`扩展支持: 是`的方法都支持使用use扩展
- **示例:**
```js
import { uiService } from '@tmagic/editor';
uiService.use({
async zoom(value, next) {
console.log('缩放前:', uiService.get('zoom'));
await next();
console.log('缩放后:', uiService.get('zoom'));
},
});
```
## usePlugin
- **详情:**
相对于[use](#use), usePlugin支持更加灵活更加细致的扩展, 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
usePlugin支持灵活细致的扩展 上述方法中标记有`扩展支持: 是`的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为before可以用于修改传入参数after可以用于修改返回的值

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
@ -19,45 +18,32 @@
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 = (
args: any[],
scope: any,
sourceMethod: any,
beforeMethodName: string,
afterMethodName: string,
fn: (args: any[], next?: Function | undefined) => void,
) => {
try {
let beforeArgs = args;
const doAction = (args: any[], scope: any, sourceMethod: any, beforeMethodName: string, afterMethodName: string) => {
let beforeArgs = args;
for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) {
beforeArgs = beforeMethod(...beforeArgs) || [];
for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) {
beforeArgs = beforeMethod(...beforeArgs) || [];
if (isError(beforeArgs)) throw beforeArgs;
if (isError(beforeArgs)) throw beforeArgs;
if (!Array.isArray(beforeArgs)) {
beforeArgs = [beforeArgs];
}
if (!Array.isArray(beforeArgs)) {
beforeArgs = [beforeArgs];
}
let returnValue: any = fn(beforeArgs, sourceMethod.bind(scope));
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
returnValue = afterMethod(returnValue, ...beforeArgs);
if (isError(returnValue)) throw returnValue;
}
return returnValue;
} catch (error) {
throw error;
}
let returnValue: any = sourceMethod.apply(scope, beforeArgs);
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
returnValue = afterMethod(returnValue, ...beforeArgs);
if (isError(returnValue)) throw returnValue;
}
return returnValue;
};
const doAsyncAction = async (
@ -66,40 +52,34 @@ const doAsyncAction = async (
sourceMethod: any,
beforeMethodName: string,
afterMethodName: string,
fn: (args: any[], next?: Function | undefined) => Promise<void> | void,
) => {
try {
let beforeArgs = args;
let beforeArgs = args;
for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) {
beforeArgs = (await beforeMethod(...beforeArgs)) || [];
for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) {
beforeArgs = (await beforeMethod(...beforeArgs)) || [];
if (isError(beforeArgs)) throw beforeArgs;
if (isError(beforeArgs)) throw beforeArgs;
if (!Array.isArray(beforeArgs)) {
beforeArgs = [beforeArgs];
}
if (!Array.isArray(beforeArgs)) {
beforeArgs = [beforeArgs];
}
let returnValue: any = await fn(beforeArgs, sourceMethod.bind(scope));
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
returnValue = await afterMethod(returnValue, ...beforeArgs);
if (isError(returnValue)) throw returnValue;
}
return returnValue;
} catch (error) {
throw error;
}
let returnValue: any = await sourceMethod.apply(scope, beforeArgs);
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
returnValue = await afterMethod(returnValue, ...beforeArgs);
if (isError(returnValue)) throw returnValue;
}
return returnValue;
};
/**
* Class进行扩展
* 1
* Class进行扩展
* Class中的每个方法都添加before after两个钩子
* Class添加一个usePlugin方法use方法可以传入一个包含before或者after方法的对象
* Class添加一个usePlugin方法usePlugin方法可以传入一个包含before或者after方法的对象
*
*
* Class EditorService extends BaseService {
@ -124,27 +104,9 @@ const doAsyncAction = async (
*
* before方法的参数中, before的return after的参数after第一个参数则是原方法的return值;
* return new Error();
*
* 2
* Class中的每个方法都添加中间件
* Class添加一个use方法use方法可以传入一个包含源对象方法名作为key值的对象
*
*
* Class EditorService extends BaseService {
* constructor() {
* super([ { name: 'add', isAsync: true },]);
* }
* add(value) { return result; }
* };
*
* const editorService = new EditorService();
* editorService.use({
* add(value, next) { console.log(value); next() },
* });
*/
export default class extends EventEmitter {
class BaseService extends EventEmitter {
private pluginOptionsList: Record<string, Function[]> = {};
private middleware: Record<string, Function[]> = {};
private taskList: (() => Promise<void>)[] = [];
private doingTask = false;
@ -161,14 +123,12 @@ export default class extends EventEmitter {
this.pluginOptionsList[beforeMethodName] = [];
this.pluginOptionsList[afterMethodName] = [];
this.middleware[propertyName] = [];
const fn = compose(this.middleware[propertyName], isAsync);
Object.defineProperty(scope, propertyName, {
value: isAsync
? async (...args: any[]) => {
if (!serialMethods.includes(propertyName)) {
return doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName, fn);
return doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName);
}
// 由于async await所以会出现函数执行到await时让出线程导致执行顺序出错例如调用了select(1) -> update -> select(2)这个时候就有可能出现update了2
@ -176,7 +136,7 @@ export default class extends EventEmitter {
const promise = new Promise<any>((resolve, reject) => {
this.taskList.push(async () => {
try {
const value = await doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName, fn);
const value = await doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName);
resolve(value);
} catch (e) {
reject(e);
@ -190,20 +150,11 @@ export default class extends EventEmitter {
return promise;
}
: (...args: any[]) => doAction(args, scope, sourceMethod, beforeMethodName, afterMethodName, fn),
: (...args: any[]) => doAction(args, scope, sourceMethod, beforeMethodName, afterMethodName),
});
});
}
/**
* @deprecated 使usePlugin代替
*/
public use(options: Record<string, Function>) {
for (const [methodName, method] of Object.entries(options)) {
if (typeof method === 'function') this.middleware[methodName].push(method);
}
}
public usePlugin(options: Record<string, Function>) {
for (const [methodName, method] of Object.entries(options)) {
if (typeof method === 'function' && !this.pluginOptionsList[methodName].includes(method)) {
@ -224,10 +175,6 @@ export default class extends EventEmitter {
for (const key of Object.keys(this.pluginOptionsList)) {
this.pluginOptionsList[key] = [];
}
for (const key of Object.keys(this.middleware)) {
this.middleware[key] = [];
}
}
private async doTask() {
@ -240,3 +187,5 @@ export default class extends EventEmitter {
this.doingTask = false;
}
}
export default BaseService;

View File

@ -66,25 +66,6 @@ import type { HistoryOpContext } from '@editor/utils/editor-history';
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
import { beforePaste, getAddParent } from '@editor/utils/operator';
/**
* BaseService / dispatch
* options null
*/
const safeOptions = <T extends object>(options: unknown): T => {
const empty = {};
if (!options || typeof options === 'function') return empty as T;
return options as T;
};
/**
* BaseService / dispatch
* parent null parent
*/
const safeParent = (parent: unknown): MContainer | null => {
if (!parent || typeof parent === 'function') return null;
return parent as MContainer;
};
class Editor extends BaseService {
public state: StoreState = reactive({
root: null,
@ -212,7 +193,7 @@ class Editor extends BaseService {
*
*/
public async getLayout(parent: MNode, node?: MNode | null): Promise<Layout> {
if (node && typeof node !== 'function' && isFixed(node.style || {})) return Layout.FIXED;
if (node && isFixed(node.style || {})) return Layout.FIXED;
if (parent.layout) {
return parent.layout;
@ -393,11 +374,8 @@ class Editor extends BaseService {
public async add(
addNode: AddMNode | MNode[],
parent?: MContainer | null,
options?: DslOpOptions,
{ doNotSelect = false, doNotSwitchPage = false }: DslOpOptions = {},
): Promise<MNode | MNode[]> {
const safeParentNode = safeParent(parent);
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
this.captureSelectionBeforeOp();
const stage = this.get('stage');
@ -420,7 +398,7 @@ class Editor extends BaseService {
if ((isPage(node) || isPageFragment(node)) && root) {
return this.doAdd(node, root);
}
const parentNode = safeParentNode ?? getAddParent(node);
const parentNode = parent ?? getAddParent(node);
if (!parentNode) throw new Error('未找到父元素');
return this.doAdd(node, parentNode);
}),
@ -476,9 +454,10 @@ class Editor extends BaseService {
return Array.isArray(addNode) ? newNodes : newNodes[0];
}
public async doRemove(node: MNode, options?: DslOpOptions): Promise<void> {
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
public async doRemove(
node: MNode,
{ doNotSelect = false, doNotSwitchPage = false }: DslOpOptions = {},
): Promise<void> {
const root = this.get('root');
if (!root) throw new Error('root不能为空');
@ -558,9 +537,10 @@ class Editor extends BaseService {
* @param options.doNotSelect false
* @param options.doNotSwitchPage false / true
*/
public async remove(nodeOrNodeList: MNode | MNode[], options?: DslOpOptions): Promise<void> {
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
public async remove(
nodeOrNodeList: MNode | MNode[],
{ doNotSelect = false, doNotSwitchPage = false }: DslOpOptions = {},
): Promise<void> {
this.captureSelectionBeforeOp();
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
@ -711,9 +691,7 @@ class Editor extends BaseService {
* @param options.doNotSwitchPage DSL API
* @returns void
*/
public async sort(id1: Id, id2: Id, options?: DslOpOptions): Promise<void> {
const { doNotSelect = false } = safeOptions<DslOpOptions>(options);
public async sort(id1: Id, id2: Id, { doNotSelect = false }: DslOpOptions = {}): Promise<void> {
this.captureSelectionBeforeOp();
const root = this.get('root');
@ -784,10 +762,8 @@ class Editor extends BaseService {
public async paste(
position: PastePosition = {},
collectorOptions?: TargetOptions,
options?: DslOpOptions,
{ doNotSelect = false, doNotSwitchPage = false }: DslOpOptions = {},
): Promise<MNode | MNode[] | void> {
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
if (!Array.isArray(config)) return;
@ -840,9 +816,10 @@ class Editor extends BaseService {
* @param options.doNotSwitchPage style DSL API
* @returns
*/
public async alignCenter(config: MNode | MNode[], options?: DslOpOptions): Promise<MNode | MNode[]> {
const { doNotSelect = false } = safeOptions<DslOpOptions>(options);
public async alignCenter(
config: MNode | MNode[],
{ doNotSelect = false }: DslOpOptions = {},
): Promise<MNode | MNode[]> {
const nodes = Array.isArray(config) ? config : [config];
const stage = this.get('stage');
@ -922,9 +899,11 @@ class Editor extends BaseService {
* @param options.doNotSelect false
* @param options.doNotSwitchPage false true
*/
public async moveToContainer(config: MNode, targetId: Id, options?: DslOpOptions): Promise<MNode | undefined> {
const { doNotSelect = false, doNotSwitchPage = false } = safeOptions<DslOpOptions>(options);
public async moveToContainer(
config: MNode,
targetId: Id,
{ doNotSelect = false, doNotSwitchPage = false }: DslOpOptions = {},
): Promise<MNode | undefined> {
this.captureSelectionBeforeOp();
const root = this.get('root');

View File

@ -97,9 +97,9 @@ class Props extends BaseService {
return this.state.propsConfigMap;
}
public async fillConfig(config: FormConfig, labelWidth?: string) {
public async fillConfig(config: FormConfig, labelWidth = '80px') {
return fillConfig(config, {
labelWidth: typeof labelWidth !== 'function' ? labelWidth : '80px',
labelWidth,
disabledDataSource: this.getDisabledDataSource(),
disabledCodeBlock: this.getDisabledCodeBlock(),
});

View File

@ -1,52 +0,0 @@
/**
* @param {Array} middleware
* @return {Function}
*/
export const compose = (middleware: Function[], isAsync: boolean) => {
if (!Array.isArray(middleware)) throw new TypeError('Middleware 必须是一个数组!');
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware 必须由函数组成!');
}
/**
* @param {Object} args
* @return {Promise}
* @api public
*/
return (args: any[], next?: Function) => {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i: number): Promise<void> | void {
if (i <= index) {
const error = new Error('next() 被多次调用');
if (isAsync) {
return Promise.reject(error);
}
throw error;
}
index = i;
let fn = middleware[i];
if (i === middleware.length && next) fn = next;
if (!fn) {
if (isAsync) {
return Promise.resolve();
}
return;
}
if (isAsync) {
try {
return Promise.resolve(fn(...args, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
try {
return fn(...args, dispatch.bind(null, i + 1));
} catch (err) {
throw err;
}
}
};
};

View File

@ -43,20 +43,6 @@ describe('BaseService 同步方法 + plugin', () => {
expect(result).toBe(30);
});
test('use 添加同步 middleware', () => {
const svc = new SyncService();
const order: string[] = [];
svc.use({
add(_value: number, next: Function) {
order.push('mw-before');
next();
order.push('mw-after');
},
});
svc.add(1);
expect(order).toEqual(['mw-before', 'mw-after']);
});
test('removePlugin 解除指定钩子', () => {
const svc = new SyncService();
const before = vi.fn((v: number) => [v]);
@ -66,7 +52,7 @@ describe('BaseService 同步方法 + plugin', () => {
expect(before).not.toHaveBeenCalled();
});
test('removeAllPlugins 清空所有 plugin/middleware', () => {
test('removeAllPlugins 清空所有 plugin', () => {
const svc = new SyncService();
const before = vi.fn((v: number) => [v]);
svc.usePlugin({ beforeAdd: before });

View File

@ -648,35 +648,6 @@ describe('moveLayer', () => {
});
});
describe('插件参数兜底', () => {
test('add 的 parent 形参传入函数时不抛错,仍走默认父节点逻辑', async () => {
editorService.set('root', cloneDeep(root));
await editorService.select(NodeId.NODE_ID);
// 模拟 BaseService 中间件机制在 parent 位置注入 dispatch 函数
const dispatchFn = () => {};
const newNode = await editorService.add({ type: 'text' }, dispatchFn as any);
// 默认行为:被加到了当前选中节点的父节点 (PAGE)
const addedId = Array.isArray(newNode) ? newNode[0].id : newNode.id;
const parentInfo = editorService.getParentById(addedId);
expect(parentInfo?.id).toBe(NodeId.PAGE_ID);
});
test('add 的 options 形参传入函数时不抛错doNotSelect 回落为默认值', async () => {
editorService.set('root', cloneDeep(root));
await editorService.select(NodeId.NODE_ID);
// 模拟 BaseService 中间件机制在 options 位置注入 dispatch 函数
const dispatchFn = () => {};
const newNode = await editorService.add({ type: 'text' }, null, dispatchFn as any);
// 默认行为:当前选中节点变成了新增节点
const addedId = Array.isArray(newNode) ? newNode[0].id : newNode.id;
expect(editorService.get('node')?.id).toBe(addedId);
});
});
describe('undo redo', () => {
beforeAll(() => editorService.set('root', cloneDeep(root)));

View File

@ -1,90 +0,0 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { describe, expect, test } from 'vitest';
import { compose } from '@editor/utils/compose';
describe('compose 同步', () => {
test('依次调用 middleware 并通过 next 串行', () => {
const order: number[] = [];
const m1 = (next: Function) => {
order.push(1);
next();
order.push(4);
};
const m2 = (next: Function) => {
order.push(2);
next();
order.push(3);
};
compose([m1, m2], false)([]);
expect(order).toEqual([1, 2, 3, 4]);
});
test('next() 多次调用抛错', () => {
const m = (next: Function) => {
next();
next();
};
expect(() => compose([m], false)([])).toThrow(/next\(\) 被多次调用/);
});
test('参数 fn 不是函数抛错', () => {
expect(() => compose([null as any], false)([])).toThrow(/Middleware 必须由函数组成/);
});
test('参数不是数组抛错', () => {
expect(() => compose('x' as any, false)([])).toThrow(/Middleware 必须是一个数组/);
});
test('支持 next 参数透传', () => {
const order: number[] = [];
const tail = () => order.push(99);
compose(
[
(next: Function) => {
order.push(1);
next();
},
],
false,
)([], tail);
expect(order).toEqual([1, 99]);
});
});
describe('compose 异步', () => {
test('返回 Promise按 next 顺序执行', async () => {
const order: number[] = [];
const m1 = async (next: Function) => {
order.push(1);
await next();
order.push(4);
};
const m2 = async (next: Function) => {
order.push(2);
await next();
order.push(3);
};
await compose([m1, m2], true)([]);
expect(order).toEqual([1, 2, 3, 4]);
});
test('next 多次调用 reject', async () => {
const m = async (next: Function) => {
await next();
await next();
};
await expect(compose([m], true)([])).rejects.toBeInstanceOf(Error);
});
test('middleware 抛同步错时 reject', async () => {
const m = () => {
throw new Error('boom');
};
await expect(compose([m as any], true)([])).rejects.toThrow('boom');
});
});