mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-09-03 06:16:11 +08:00
feat(editor): 新增依赖收集器
This commit is contained in:
parent
3b3fbb288d
commit
35f9a59f44
328
packages/editor/src/services/dep.ts
Normal file
328
packages/editor/src/services/dep.ts
Normal file
@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2023 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 { reactive } from 'vue';
|
||||
|
||||
import { MNode } from '@tmagic/schema';
|
||||
|
||||
type IsTarget = (key: string | number, value: any) => boolean;
|
||||
|
||||
interface TargetOptions {
|
||||
isTarget: IsTarget;
|
||||
id: string | number;
|
||||
type?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Dep {
|
||||
[key: string | number]: {
|
||||
name: string;
|
||||
keys: (string | number)[];
|
||||
};
|
||||
}
|
||||
|
||||
interface TargetList {
|
||||
[key: string]: {
|
||||
[key: string | number]: Target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要收集依赖的目标
|
||||
* 例如:一个代码块可以为一个目标
|
||||
*/
|
||||
export class Target extends EventEmitter {
|
||||
/**
|
||||
* 如何识别目标
|
||||
*/
|
||||
public isTarget: IsTarget;
|
||||
/**
|
||||
* 目标id,不可重复
|
||||
* 例如目标是代码块,则为代码块id
|
||||
*/
|
||||
public id: string | number;
|
||||
/**
|
||||
* 目标名称,用于显示在依赖列表中
|
||||
*/
|
||||
public name: string;
|
||||
/**
|
||||
* 不同的目标可以进行分类,例如代码块,数据源可以为两个不同的type
|
||||
*/
|
||||
public type = 'default';
|
||||
/**
|
||||
* 依赖详情
|
||||
* 实例:{ 'node_id': { name: 'node_name', keys: [ created, mounted ] } }
|
||||
*/
|
||||
public deps = reactive<Dep>({});
|
||||
|
||||
constructor(options: TargetOptions) {
|
||||
super();
|
||||
this.isTarget = options.isTarget;
|
||||
this.id = options.id;
|
||||
this.name = options.name;
|
||||
if (options.type) {
|
||||
this.type = options.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新依赖
|
||||
* @param node 节点配置
|
||||
* @param key 哪个key配置了这个目标的id
|
||||
*/
|
||||
public updateDep(node: MNode, key: string | number) {
|
||||
const dep = this.deps[node.id] || {
|
||||
name: node.name,
|
||||
keys: [],
|
||||
};
|
||||
|
||||
if (node.name) {
|
||||
dep.name = node.name;
|
||||
}
|
||||
|
||||
this.deps[node.id] = dep;
|
||||
|
||||
if (dep.keys.indexOf(key) === -1) {
|
||||
dep.keys.push(key);
|
||||
}
|
||||
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除依赖
|
||||
* @param node 哪个节点的依赖需要移除,如果为空,则移除所有依赖
|
||||
* @param key 节点下哪个key需要移除,如果为空,则移除改节点下的所有依赖key
|
||||
* @returns void
|
||||
*/
|
||||
public removeDep(node?: MNode, key?: string | number) {
|
||||
if (!node) {
|
||||
Object.keys(this.deps).forEach((depKey) => {
|
||||
delete this.deps[depKey];
|
||||
});
|
||||
this.emit('change');
|
||||
return;
|
||||
}
|
||||
|
||||
const dep = this.deps[node.id];
|
||||
|
||||
if (!dep) return;
|
||||
|
||||
if (key) {
|
||||
const index = dep.keys.indexOf(key);
|
||||
dep.keys.splice(index, 1);
|
||||
|
||||
if (dep.keys.length === 0) {
|
||||
delete this.deps[node.id];
|
||||
}
|
||||
} else {
|
||||
delete this.deps[node.id];
|
||||
}
|
||||
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定节点下的指定key是否存在在依赖列表中
|
||||
* @param node 哪个节点
|
||||
* @param key 哪个key
|
||||
* @returns boolean
|
||||
*/
|
||||
public hasDep(node: MNode, key: string | number) {
|
||||
const dep = this.deps[node.id];
|
||||
|
||||
return Boolean(dep?.keys.find((d) => d === key));
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
export class Watcher extends EventEmitter {
|
||||
public targets = reactive<TargetList>({});
|
||||
|
||||
/**
|
||||
* 获取指定类型中的所有target
|
||||
* @param type 分类
|
||||
* @returns Target[]
|
||||
*/
|
||||
public getTargets(type = 'default') {
|
||||
return this.targets[type] || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的目标
|
||||
* @param target Target
|
||||
*/
|
||||
public addTarget(target: Target) {
|
||||
const targets = this.getTargets(target.type) || {};
|
||||
this.targets[target.type] = targets;
|
||||
targets[target.id] = target;
|
||||
|
||||
this.emit('add-target', target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定id的target
|
||||
* @param id target id
|
||||
* @returns Target
|
||||
*/
|
||||
public getTarget(id: string | number) {
|
||||
const allTargets = Object.values(this.targets);
|
||||
for (const targets of allTargets) {
|
||||
if (targets[id]) {
|
||||
return targets[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在指定id的target
|
||||
* @param id target id
|
||||
* @returns boolean
|
||||
*/
|
||||
public hasTarget(id: string | number) {
|
||||
const allTargets = Object.values(this.targets);
|
||||
for (const targets of allTargets) {
|
||||
if (targets[id]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定id的target
|
||||
* @param id target id
|
||||
*/
|
||||
public removeTarget(id: string | number) {
|
||||
const allTargets = Object.values(this.targets);
|
||||
for (const targets of allTargets) {
|
||||
if (targets[id]) {
|
||||
targets[id].destroy();
|
||||
delete targets[id];
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('remove-target');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定分类的所有target
|
||||
* @param type 分类
|
||||
* @returns void
|
||||
*/
|
||||
public removeTargets(type = 'default') {
|
||||
const targets = this.targets[type];
|
||||
|
||||
if (!targets) return;
|
||||
|
||||
for (const target of Object.values(targets)) {
|
||||
target.destroy();
|
||||
}
|
||||
|
||||
delete this.targets[type];
|
||||
|
||||
this.emit('remove-target');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除所有target
|
||||
*/
|
||||
public clearTargets() {
|
||||
Object.keys(this.targets).forEach((key) => {
|
||||
delete this.targets[key];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集依赖
|
||||
* @param nodes 需要收集的节点
|
||||
* @param deep 是否需要收集子节点
|
||||
*/
|
||||
public collect(nodes: MNode[], deep = false) {
|
||||
Object.values(this.targets).forEach((targets) => {
|
||||
Object.values(targets).forEach((target) => {
|
||||
nodes.forEach((node) => {
|
||||
target.removeDep(node);
|
||||
this.collectItem(node, target, deep);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除依赖
|
||||
* @param nodes 需要清除依赖的节点
|
||||
*/
|
||||
public clear(nodes?: MNode[]) {
|
||||
Object.values(this.targets).forEach((targets) => {
|
||||
Object.values(targets).forEach((target) => {
|
||||
if (nodes) {
|
||||
nodes.forEach((node) => {
|
||||
target.removeDep(node);
|
||||
|
||||
if (Array.isArray(node.items)) {
|
||||
this.clear(node.items);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
target.removeDep();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private collectItem(node: MNode, target: Target, deep = false) {
|
||||
const collectTarget = (config: Record<string | number, any>, prop = '') => {
|
||||
const doCollect = (key: string, value: any) => {
|
||||
const keyIsItems = key === 'items';
|
||||
const fullKey = prop ? `${prop}.${key}` : key;
|
||||
|
||||
if (target.isTarget(key, value)) {
|
||||
target.updateDep(node, fullKey);
|
||||
} else if (!keyIsItems && Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
collectTarget(item, `${fullKey}.${index}`);
|
||||
});
|
||||
} else if (Object.prototype.toString.call(value) === '[object Object]') {
|
||||
collectTarget(value, fullKey);
|
||||
}
|
||||
|
||||
if (keyIsItems && deep && Array.isArray(value)) {
|
||||
value.forEach((child) => {
|
||||
this.collectItem(child, target, deep);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(config).forEach(([key, value]) => {
|
||||
doCollect(key, value);
|
||||
});
|
||||
};
|
||||
|
||||
collectTarget(node);
|
||||
}
|
||||
}
|
||||
|
||||
export type DepService = Watcher;
|
||||
|
||||
export default new Watcher();
|
21
packages/editor/src/utils/dep.ts
Normal file
21
packages/editor/src/utils/dep.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import { CodeBlockContent, HookType, Id } from '@tmagic/schema';
|
||||
|
||||
import { Target } from '../services/dep';
|
||||
import { HookData } from '../type';
|
||||
|
||||
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent) =>
|
||||
new Target({
|
||||
type: 'code-block',
|
||||
id,
|
||||
name: codeBlock.name,
|
||||
isTarget: (key: string | number, value: any) => {
|
||||
if (value?.hookType === HookType.CODE && !isEmpty(value.hookData)) {
|
||||
const index = value.hookData.findIndex((item: HookData) => item.codeId === id);
|
||||
return Boolean(index > -1);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
227
packages/editor/tests/unit/services/dep.spec.ts
Normal file
227
packages/editor/tests/unit/services/dep.spec.ts
Normal file
@ -0,0 +1,227 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import depService, { Target, Watcher } from '@editor/services/dep';
|
||||
|
||||
describe('Watcher', () => {
|
||||
test('instance', () => {
|
||||
const watcher = new Watcher();
|
||||
expect(watcher).toBeInstanceOf(Watcher);
|
||||
});
|
||||
});
|
||||
|
||||
describe('depService', () => {
|
||||
const defaultTarget = new Target({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
isTarget: () => true,
|
||||
});
|
||||
|
||||
const target = new Target({
|
||||
type: 'target',
|
||||
id: 2,
|
||||
name: 'test',
|
||||
isTarget: () => true,
|
||||
});
|
||||
|
||||
test('default target type', () => {
|
||||
expect(defaultTarget.type).toBe('default');
|
||||
expect(target.type).toBe('target');
|
||||
});
|
||||
|
||||
test('addTarget', () => {
|
||||
depService.addTarget(target);
|
||||
|
||||
expect(depService.getTarget(1)).toBeUndefined();
|
||||
expect(depService.getTarget(2)?.id).toBe(2);
|
||||
expect(Object.keys(depService.getTargets())).toHaveLength(0);
|
||||
expect(Object.keys(depService.getTargets('target'))).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('clearTargets', () => {
|
||||
depService.clearTargets();
|
||||
|
||||
depService.addTarget(target);
|
||||
|
||||
expect(depService.hasTarget(2)).toBeTruthy();
|
||||
|
||||
depService.clearTargets();
|
||||
|
||||
expect(depService.hasTarget(2)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('hasTarget', () => {
|
||||
depService.clearTargets();
|
||||
|
||||
depService.addTarget(target);
|
||||
|
||||
expect(depService.hasTarget(1)).toBeFalsy();
|
||||
expect(depService.hasTarget(2)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('removeTarget', () => {
|
||||
depService.clearTargets();
|
||||
depService.addTarget(target);
|
||||
expect(depService.hasTarget(2)).toBeTruthy();
|
||||
depService.removeTarget(2);
|
||||
expect(depService.hasTarget(2)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('removeTargets', () => {
|
||||
depService.clearTargets();
|
||||
depService.addTarget(defaultTarget);
|
||||
depService.addTarget(target);
|
||||
expect(depService.hasTarget(1)).toBeTruthy();
|
||||
expect(depService.hasTarget(2)).toBeTruthy();
|
||||
depService.removeTargets('target');
|
||||
expect(depService.hasTarget(1)).toBeTruthy();
|
||||
expect(depService.hasTarget(2)).toBeFalsy();
|
||||
|
||||
depService.removeTargets('target1');
|
||||
});
|
||||
|
||||
test('collect', () => {
|
||||
depService.clearTargets();
|
||||
|
||||
depService.addTarget(
|
||||
new Target({
|
||||
type: 'target',
|
||||
id: 'collect_1',
|
||||
name: 'test',
|
||||
isTarget: (key: string | number, value: any) => key === 'text' && value === 'text1',
|
||||
}),
|
||||
);
|
||||
|
||||
depService.addTarget(
|
||||
new Target({
|
||||
type: 'target',
|
||||
id: 'collect_2',
|
||||
name: 'test2',
|
||||
isTarget: (key: string | number, value: any) => key === 'text1' && value === 'text',
|
||||
}),
|
||||
);
|
||||
|
||||
depService.collect([
|
||||
{
|
||||
id: 'node_1',
|
||||
name: 'node',
|
||||
text: 'text1',
|
||||
text1: 'text',
|
||||
object: {
|
||||
text1: 'text',
|
||||
},
|
||||
array: [
|
||||
{
|
||||
object: {
|
||||
text1: 'text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const target1 = depService.getTarget('collect_1');
|
||||
const target2 = depService.getTarget('collect_2');
|
||||
|
||||
expect((target1?.deps || {}).node_1.name).toBe('node');
|
||||
expect((target2?.deps || {}).node_1.name).toBe('node');
|
||||
expect((target1?.deps || {}).node_1.keys).toHaveLength(1);
|
||||
expect((target2?.deps || {}).node_1.keys).toHaveLength(3);
|
||||
|
||||
depService.collect([
|
||||
{
|
||||
id: 'node_1',
|
||||
name: 'node',
|
||||
text: 'text',
|
||||
text1: 'text',
|
||||
object: {
|
||||
text1: 'text1',
|
||||
},
|
||||
array: [
|
||||
{
|
||||
object: {
|
||||
text1: 'text1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect((target1?.deps || {}).node_1).toBeUndefined();
|
||||
expect((target2?.deps || {}).node_1.keys).toHaveLength(1);
|
||||
|
||||
depService.collect([
|
||||
{
|
||||
id: 'node_1',
|
||||
name: 'node',
|
||||
text: 'text',
|
||||
text1: 'text',
|
||||
},
|
||||
]);
|
||||
|
||||
expect((target1?.deps || {}).node_1).toBeUndefined();
|
||||
expect((target2?.deps || {}).node_1.keys[0]).toBe('text1');
|
||||
|
||||
depService.clear([
|
||||
{
|
||||
id: 'node_1',
|
||||
name: 'node',
|
||||
},
|
||||
]);
|
||||
|
||||
expect((target1?.deps || {}).node_1).toBeUndefined();
|
||||
expect((target2?.deps || {}).node_1).toBeUndefined();
|
||||
});
|
||||
|
||||
test('collect deep', () => {
|
||||
depService.clearTargets();
|
||||
|
||||
depService.addTarget(
|
||||
new Target({
|
||||
type: 'target',
|
||||
id: 'collect_1',
|
||||
name: 'test',
|
||||
isTarget: (key: string | number, value: any) => key === 'text' && value === 'text1',
|
||||
}),
|
||||
);
|
||||
|
||||
depService.collect(
|
||||
[
|
||||
{
|
||||
id: 'node_1',
|
||||
name: 'node',
|
||||
text: 'text1',
|
||||
items: [
|
||||
{
|
||||
id: 'node_2',
|
||||
name: 'node2',
|
||||
text: 'text1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
const target1 = depService.getTarget('collect_1');
|
||||
|
||||
expect((target1?.deps || {}).node_1.name).toBe('node');
|
||||
expect((target1?.deps || {}).node_2.name).toBe('node2');
|
||||
|
||||
depService.clear([
|
||||
{
|
||||
id: 'node_1',
|
||||
name: 'node',
|
||||
items: [
|
||||
{
|
||||
id: 'node_2',
|
||||
name: 'node2',
|
||||
text: 'text1',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect((target1?.deps || {}).node_1).toBeUndefined();
|
||||
expect((target1?.deps || {}).node_2).toBeUndefined();
|
||||
});
|
||||
});
|
54
packages/editor/tests/unit/utils/dep.spec.ts
Normal file
54
packages/editor/tests/unit/utils/dep.spec.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import * as dep from '@editor/utils/dep';
|
||||
|
||||
describe('dep', () => {
|
||||
test('createCodeBlockTarget', () => {
|
||||
const target = dep.createCodeBlockTarget('code_5316', {
|
||||
name: 'code',
|
||||
content: () => false,
|
||||
params: [],
|
||||
});
|
||||
|
||||
expect(target.id).toBe('code_5316');
|
||||
expect(target.name).toBe('code');
|
||||
expect(target.type).toBe('code-block');
|
||||
|
||||
const isTarget = target.isTarget('created', {
|
||||
hookType: 'code',
|
||||
hookData: [
|
||||
{
|
||||
codeId: 'code_5336',
|
||||
params: {
|
||||
studentName: 'lisa',
|
||||
age: 14,
|
||||
},
|
||||
},
|
||||
{
|
||||
codeId: 'code_5316',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(isTarget).toBeTruthy();
|
||||
|
||||
const target1 = dep.createCodeBlockTarget('1', {
|
||||
name: 'code',
|
||||
content: () => false,
|
||||
params: [],
|
||||
});
|
||||
|
||||
const isTarget1 = target1.isTarget('created', {
|
||||
hookType: 'code',
|
||||
hookData: [
|
||||
{
|
||||
codeId: 'code_5316',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(isTarget1).toBeFalsy();
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user