tree、编辑器优化记录

引言

本篇记录最近工作中工程优化的收获,通过优化部门选择器,感觉对树的前中后序遍历理解更深入了;而对编辑器的优化,我发现目前的编辑器像 draftjs、slatejs 等这些比较受欢迎的富文本库对中文的编辑不是那么友好,在解决拼音输入导致页面报错问题的过程中,调试了下 draftjs 0.11.7 的源码,学习了一个新的浏览器 API:MutationObserver,并对 Selection 有了初步的了解

部门算法优化:

源数据大概长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
const tree = [
{
id: 187547,
name: "开发部",
parentId: null,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-05-08T14:37:31.000Z",
},
{
id: 189660,
name: "qiao部门",
parentId: null,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-05-12T07:30:36.000Z",
},
{
id: 191365,
name: "测试部",
parentId: null,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-05-08T14:37:21.000Z",
},
{
id: 199560,
name: "功能测试",
parentId: 191365,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-06-24T11:04:32.000Z",
},
{
id: 199561,
name: "性能",
parentId: 191365,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-06-24T11:05:10.000Z",
},
{
id: 201346,
name: "实习生",
parentId: 199560,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-06-24T11:05:26.000Z",
},
{
id: 201347,
name: "实习生-1",
parentId: 201346,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-06-24T11:05:26.000Z",
},
{
id: 201348,
name: "实习生-1-1",
parentId: 201347,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-06-24T11:05:26.000Z",
},
{
id: 201349,
name: "实习生-1-1-1",
parentId: 201348,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-06-24T11:05:26.000Z",
},
{
id: 20134110,
name: "实习生-1-1-1-1",
parentId: 201349,
deletedByApi: 0,
type: 1,
belongOrgId: "officialqa",
createdAt: "2020-06-24T11:05:26.000Z",
},
];

算法实现(重点注释):

参考JS 树结构操作:查找、遍历、筛选、树结构和列表结构相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
* 前序遍历
* 处理每个节点的所有父节点parentIds、以及层数trueLevel
* @param {array} departmentsTree 树结构数组
* @param {Map} idMap
*/
function handleParents(departmentsTree, idMap) {
let node;
const list = [...departmentsTree];
// 先处理子树的根节点
while ((node = list.shift())) {
const parent = idMap.get(node.parentId);

node.parentIds = node.parentId ? [node.parentId] : [];
if (parent && parent.parentIds && parent.parentIds.length) {
node.parentIds.push(...parent.parentIds);
}
node.trueLevel = node.parentIds.length + 1;

if (node.children) {
// 将子节点从左到右追加到队头
list.unshift(...node.children);
}
}
}

/**
* 后序遍历
* 处理每个节点的所有子节点childrenIds
* @param {*} departments
*/
function handleChildrens(departments) {
let node;
let list = [...departments];
let i = 0;
while ((node = list[i])) {
let childCount = (node.children || []).length;
/**
* 1、如果该节点直接是叶节点,则 !childCount === true
* 2、回溯的节点
* node.children[childCount - 1]获取当前节点的最后一个子节点
* list[i - 1] 拿到上一个节点
* 根据后序遍历特点,[左-右-根] 特点
* node.children[childCount - 1] === list[i - 1]判断是不是回溯到了某一子树的根节点
*/
if (!childCount || node.children[childCount - 1] === list[i - 1]) {
const child = node.children || [];
node.childrenIds = child.reduce(
(allChildren, { id, childrenIds = [] }) => {
return [...allChildren, id, ...childrenIds];
},
[]
);

/**
* 这一步是关键!
* 每处理完一个子节点,数组就指向下一个索引,这个索引是刚刚处理完的子节点的父节点
*/
i++;
} else {
/**
* 如果该节点有子节点,就把这些子节点都放在该节点前面
* [node1,node2,current] => [node1, node2, ...currentChilden, current]
*/
list.splice(i, 0, ...node.children);
}
}
}

function makeDepartmentsTree(departments, needChildren) {
const idMap = departments.reduce((map, node) => {
map.set(node.id, node);
return map;
}, new Map());

let tree = [];

// 构建部门树
departments.forEach((department) => {
if (needChildren) {
const parent = idMap.get(department.parentId);
if (!department.directChildrenIds) {
department.directChildrenIds = [];
}
if (parent) {
parent.children = [...(parent.children || [])];
parent.directChildrenIds = [...(parent.directChildrenIds || [])];

if (!parent.directChildrenIds.includes(department.id)) {
parent.directChildrenIds.push(department.id);
parent.children.push(department);
}
} else {
tree.push(department);
}
}
});

handleParents(tree, idMap);
handleChildrens(tree);

return Object.fromEntries(idMap.entries());
}

/**
* build departments to tree structure
* @param {array} departmentList - 部门列表
*/
export function buildTreeDepartments(departmentList) {
if (!Array.isArray(departmentList)) {
return departmentList;
}

//按照创建时间排序
departmentList = departmentList.sort((a, b) => {
return new Date(a.createdAt).valueOf() - new Date(b.createdAt).valueOf();
});

return makeDepartmentsTree(departmentList, needChildren);
}

Draftjs 拼音输入法报错导致页面白屏

解决方法: 1.在 Draftjs 提供的 Editor 组件外层,包一层 div 2.在这个 div 上绑定 onCompositionStart 这个方法,拼音输入法时会触发 handleCompositionstart 这个函数,在这里先把文本给删除了再把这个编辑器状态更新,可以解决一口气操作拼音输入法的报错问题

compositionstart是一个 web API,当用户使用拼音输入法开始输入汉字时,这个事件就会被触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
handleCompositionstart = () => {
const { editorState: prevEditorState } = this.props;
try {
const selection = prevEditorState.getSelection();
const contentState = prevEditorState.getCurrentContent();
// 将选中文字部分置空
const nextContentState = Modifier.replaceText(
contentState,
selection,
"",
void 0
);
this.props.onChange(
EditorState.push(prevEditorState, nextContentState, "insert-characters")
);
} catch (e) {
console.error(e);
}
};

// onCompositionStart这是重点
<div onCompositionStart={this.handleCompositionstart}>
<Editor
editorState={editorState}
customStyleMap={styleMap}
blockRenderMap={DefaultDraftBlockRenderMap.merge(blockRenderMap)}
onChange={onChange}
onBlur={this.onHideFlyingOption}
handleKeyCommand={handleKeyCommand}
/>
</div>;

就目前的调查来看,目前 0.11.7 中,Draftjs 对拼音的输入是通过 MutationObserver 这个 API 来监控文档变化的,然后会在这里处理选区Selection相关收集和操作。但是升级到最新的 0.11.7 版本,发现这个版本对拼音输入法的修改收集可能存在问题,具体是哪里的问题,我不确定也不会改,降级到 0.10.5 版本,发现 0.11.7 这里的问题都不存在了,所以,最终,我选择将 Daraftjs 升级到 0.10.5,加上上面的 onCompositionStart 方法解决了报错白屏的问题。

但是升级的同时,Daraftjs 内部本身会报错,这是由于之前我们的版本是 V8,V10 的版本对一些 API 想要废弃,所以在开发环境出现一堆 warning

一开始搜DraftEntity.get,项目里并没有搜到结果,后面仔细看发现/draft-js/src/Draft.js 这里的导出重命名了

所以导致我搜索关键字错误了,实际上,就是所有用到 Entity.get 的调用都需要升级,升级后,warning 就都解决了。

MutationObserver 用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function callback(mutationList, observer) {
mutationList.forEach((mutation) => {
switch (mutation.type) {
case "childList":
/* 从树上添加或移除一个或更多的子节点;参见 mutation.addedNodes 与
mutation.removedNodes */
break;
case "attributes":
/* mutation.target 中某节点的一个属性值被更改;该属性名称在 mutation.attributeName 中,
该属性之前的值为 mutation.oldValue */
break;
}
});
}

var targetNode = document.querySelector("#someElement");
var observerOptions = {
childList: true, // 观察目标子节点的变化,是否有添加或者删除
attributes: true, // 观察属性变动
subtree: true, // 观察后代节点,默认为 false
};

var observer = new MutationObserver(callback);
observer.observe(targetNode, observerOptions);

因为不熟悉 MutationObserver 的使用,在搜索文章的过程中,发现给文档加水印时,为了不让用户把水印隐藏,刚好这个方法能派上用场,web 页面水印传送门