背景
自从上家公司被裁后,家里蹲了近两个月,苦于自己学历大专,又不是对口专业,所以投递的简历基本上要么已读不回、要么不读。偶然的一次机会,朋友圈有人推荐需要招前端的项目,奔着试试的心态就投递了简历,结果挺意外的约到了面试机会。后续面试过程中了解到当前项目是工业互联网领域的代表性项目。在二面时聊到了如果我进到项目组会交给我一个重难点类似于实现xmind各种交互,要我自己可以hold住。
尝试
考虑到当下市场环境的的确确太差了,不出意外的我进了这个项目组,熟悉项目过程中,发现他们之前应该调研过xmind的类似实现方式就是使用kityminder,对于这个库我所熟知的寥寥无几,而项目组的开发方式是使用桌面云来进行开发,github无法进行访问,下班回家后我在github上面找到相应的开源库,了解到如果需要对这种脑图进行二次开发,则必须要用到kityminder-core与kitymidner-editor两个核心包。
这个项目用的umi.js框架搭建的 umijs.org/
迁移
迁移方式结合umi.js当中的配置项headScripts引入编译后js资源kitminder.core.min.js和kity.min.js以及hotbox.js。至于kityminder-editor我直接是通过放在src目录下的lib文件夹,原因是因为kityminder-editor的js资源会用到angular与jquey等资源,而项目当中用到的是react框架,而其核心主要是svg来绘制脑图所以就没必要引入过多没必要的资源。引入方式示例可以参考github.com/fex-team/ki...
初始化
脑图初始化通过KMEditor类传入dom节点来为dom元素绑定一系列的类方法来实现
js
function KMEditor(selector) {
this.selector = selector;
for (var i = 0; i < runtimes.length; i++) {
if (typeof runtimes[i] == 'function') {
runtimes[i].call(this, this);
}
}
}
KMEditor.assemble = assemble;
assemble(require('./runtime/container'));
assemble(require('./runtime/fsm'));
assemble(require('./runtime/minder'));
assemble(require('./runtime/receiver'));
assemble(require('./runtime/hotbox'));
assemble(require('./runtime/input'));
assemble(require('./runtime/clipboard-mimetype'));
assemble(require('./runtime/clipboard'));
assemble(require('./runtime/drag'));
assemble(require('./runtime/node'));
assemble(require('./runtime/history'));
assemble(require('./runtime/jumping'));
assemble(require('./runtime/priority'));
assemble(require('./runtime/progress'));
二次改造重难点Render、Layout
- kity.js暴露了模块的创建createClass和扩展extendClass(如Minder、MinderNode)
- Minder、MinderNode分别暴露了很多API具体可参考github.com/fex-team/ki...
- 节点创建:
js
createNode: function(textOrData, parent, index) {
var node = new MinderNode(textOrData);
this.fire('nodecreate', {
node: node,
parent: parent,
index: index
});
this.appendNode(node, parent, index);
return node;
},
单个节点渲染:
js
renderNode: function(node) {
var rendererClasses = this._rendererClasses;
var i, latestBox, renderer;
if (!node._renderers) {
createRendererForNode(node, rendererClasses);
}
this.fire('beforerender', {
node: node
});
node._contentBox = new kity.Box();
node._renderers.forEach(function(renderer) {
// 判断当前上下文是否应该渲染
if (renderer.shouldRender(node)) {
// 应该渲染,但是渲染图形没创建过,需要创建
if (!renderer.getRenderShape()) {
renderer.setRenderShape(renderer.create(node));
if (renderer.bringToBack) {
node.getRenderContainer().prependShape(renderer.getRenderShape());
} else {
node.getRenderContainer().appendShape(renderer.getRenderShape());
}
}
// 强制让渲染图形显示
renderer.getRenderShape().setVisible(true);
// 更新渲染图形
latestBox = renderer.update(renderer.getRenderShape(), node, node._contentBox);
if (typeof(latestBox) == 'function') latestBox = latestBox();
// 合并渲染区域
if (latestBox) {
node._contentBox = node._contentBox.merge(latestBox);
renderer.contentBox = latestBox;
}
}
// 如果不应该渲染,但是渲染图形创建过了,需要隐藏起来
else if (renderer.getRenderShape()) {
renderer.getRenderShape().setVisible(false);
}
});
this.fire('noderender', {
node: node
});
}
};
}
this.fire('noderender', { node: node });表示注册了一个事件noderender,并传入一个node节点,如果想在渲染完成后布局前执行一些操作可以监听noderender事件,Minder.on('noderender', { node })
节点批量渲染:
js
renderNodeBatch: function(nodes) {
var rendererClasses = this._rendererClasses;
var lastBoxes = [];
var rendererCount = 0;
var i, j, renderer, node;
if (!nodes.length) return;
for (j = 0; j < nodes.length; j++) {
node = nodes[j];
if (!node._renderers) {
createRendererForNode(node, rendererClasses);
}
node._contentBox = new kity.Box();
this.fire('beforerender', {
node: node
});
}
// 所有节点渲染器数量是一致的
rendererCount = nodes[0]._renderers.length;
for (i = 0; i < rendererCount; i++) {
// 获取延迟盒子数据
for (j = 0; j < nodes.length; j++) {
if (typeof(lastBoxes[j]) == 'function') {
lastBoxes[j] = lastBoxes[j]();
}
if (!(lastBoxes[j] instanceof kity.Box)) {
lastBoxes[j] = new kity.Box(lastBoxes[j]);
}
}
for (j = 0; j < nodes.length; j++) {
node = nodes[j];
renderer = node._renderers[i];
// 合并盒子
if (lastBoxes[j]) {
node._contentBox = node._contentBox.merge(lastBoxes[j]);
renderer.contentBox = lastBoxes[j];
}
// 判断当前上下文是否应该渲染
if (renderer.shouldRender(node)) {
// 应该渲染,但是渲染图形没创建过,需要创建
if (!renderer.getRenderShape()) {
renderer.setRenderShape(renderer.create(node));
if (renderer.bringToBack) {
node.getRenderContainer().prependShape(renderer.getRenderShape());
} else {
node.getRenderContainer().appendShape(renderer.getRenderShape());
}
}
// 强制让渲染图形显示
renderer.getRenderShape().setVisible(true);
// 更新渲染图形
lastBoxes[j] = renderer.update(renderer.getRenderShape(), node, node._contentBox);
}
// 如果不应该渲染,但是渲染图形创建过了,需要隐藏起来
else if (renderer.getRenderShape()) {
renderer.getRenderShape().setVisible(false);
lastBoxes[j] = null;
}
}
}
for (j = 0; j < nodes.length; j++) {
this.fire('noderender', {
node: nodes[j]
});
}
},
节点渲染完成后则会开始进行布局操作,而布局会根据模板来决定,kityminder提供了以下几种模板(default、filetree、right、fish-bone等);而Layout同样提供了几种不同的布局方式(btree、filetree、fish-bone-master等);一般项目当中设置模板会通过Minder.useTemplate()来进行设置,而布局则通过setLayout()来实现。
布局:
js
Layout.register(name, kity.createClass({
base: Layout,
doLayout: function(parent, children) {
var pbox = parent.getContentBox();
if (axis == 'x') {
parent.setVertexOut(new kity.Point(pbox[name], pbox.cy));
parent.setLayoutVectorOut(new kity.Vector(dir, 0));
} else {
parent.setVertexOut(new kity.Point(pbox.cx, pbox[name]));
parent.setLayoutVectorOut(new kity.Vector(0, dir));
}
if (!children.length) {
return false;
}
children.forEach(function(child) {
var cbox = child.getContentBox();
child.setLayoutTransform(new kity.Matrix());
if (axis == 'x') {
child.setVertexIn(new kity.Point(cbox[oppsite[name]], cbox.cy));
child.setLayoutVectorIn(new kity.Vector(dir, 0));
} else {
child.setVertexIn(new kity.Point(cbox.cx, cbox[oppsite[name]]));
child.setLayoutVectorIn(new kity.Vector(0, dir));
}
});
this.align(children, oppsite[name]);
this.stack(children, oppsite[axis]);
var bbox = this.getBranchBox(children);
var xAdjust = 0, yAdjust = 0;
if (axis == 'x') {
xAdjust = pbox[name];
xAdjust += dir * parent.getStyle('margin-' + name);
xAdjust += dir * children[0].getStyle('margin-' + oppsite[name]);
yAdjust = pbox.bottom;
yAdjust -= pbox.height / 2;
yAdjust -= bbox.height / 2;
yAdjust -= bbox.y;
} else {
xAdjust = pbox.right;
xAdjust -= pbox.width / 2;
xAdjust -= bbox.width / 2;
xAdjust -= bbox.x;
yAdjust = pbox[name];
yAdjust += dir * parent.getStyle('margin-' + name);
yAdjust += dir * children[0].getStyle('margin-' + oppsite[name]);
}
this.move(children, xAdjust, yAdjust);
},
}));
布局的计算方式也是通过父盒子与子盒子的box来设置,如果要改变默认的节点布局方式要考虑两点:
- 将当前节点相对于父节点如何transform变换
- 将变换后的节点与父节点进行连线,设置pathData