本系列教程是教大家如何根据开源js绘图库,打造一个属于自己的在线绘图软件。当然,也可以看着是这个绘图库的开发教程。如果你觉得好,欢迎点个赞,让我们更有动力去做好!
本系列教程重点介绍如何开发自己的绘图软件,因此,react基础和框架不在此介绍。可以推荐react官网学习,或《React全家桶免费视频》。
本系列教程源码地址:Github
一、搭建react框架环境
这里,我们选择阿里的UmiJS + DvaJS + Ant.Desgin 轻应用框架。
1. 安装UmiJS
// 推荐使用yarn
npm install yarn -g
yarn global add umi
2. 安装UmiJS手脚架
mkdir topology-react
yarn create umi
// 创建项目文件后,安装依赖包
yarn
这里,我们选择typescript,dva等(dll可以不用,已落伍)。
3. 把css改成less
A. typings.d.ts加入less
declare module '*.less';
B. global.css改成global.less,并引入antd主题
@import '~antd/es/style/themes/default.less';
html,
body,
#root {
height: 100%;
}
.colorWeak {
filter: invert(80%);
}
.ant-layout {
min-height: 100vh;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
ol {
list-style: none;
}
@media (max-width: @screen-xs) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}
C. 其他css改成less
layouts和pages下的css改成less,并修改tsx中的引用。
4. 修改默认的单页面模板文件
根据UmiJS的约定,我们可以给src/pages下新增一个名为document.ejs的模板文件,代替缺省模板。新模板内容,参考源码。
具体参考:UmiJS的HTML模板文档
5. 运行
npm start就可以看到默认UmiJS界面。
代码:tag: umi
其中,layouts下的index.tsx为全局模板文件;pages为路由模块。
二、修改页面布局为上下导航、左中右布局
拷贝静态资源文件
在typings.d.ts中添加其他图片文件扩展名
declare module '*.ico';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
新建一个public文件夹,放入静态资源。这里没有使用assets,主要是public里面放一些独立的静态资源。
新建顶部导航菜单栏
我们在layouts里新建一个Headers类,作为导航菜单,添加到BasicLayout里面。代码如下:
import React from 'react';
import styles from './index.less';
import Headers from './headers';
const BasicLayout: React.FC = props => {
return (
{props.children}
);
};
export default BasicLayout;
导航栏菜单调用onMenuClick和对话框详见源码。
工作区左中右三栏布局
我们修改pages下index.tsx,使其为左中右3栏,如下:
import React from 'react';
import styles from './index.less';
import { Tools } from '@/utils/tools';
class Index extends React.Component<{}> {
state = {
tools: Tools,
iconfont: { fontSize: '.24rem' }
};
render() {
return (
{}
);
}
}
export default Index;
实现左侧工具栏
1. 导入阿里字体图标iconfont
在src/pages/document.ejs中引入我们需要的iconfont
其中,上面的是左侧工具栏所需要用到的图标;下面是右侧属性栏作为可供用户选中的节点图标库。可以替换成自己的地址(注意修改Tools里的数据就好)。
2.自定义左侧工具栏图标列表
我们在src下新建一个utils目录,与UmiJS约定规则目录区分开,作为我们自定义功能模块。新增一个tools.tsx文件,把我们左侧工具栏图标列表的数组定义在此。
其中,tools.tsx功能如下:
然后,在src/pages/index.tsx中导入,并循环遍历显示左侧工具栏图标:(这里,并没有单独定义一个左侧工具栏类,大家根据自己习惯就好,没有强制规定,也不需要极端)
import React from 'react';
import styles from './index.less';
import { Tools } from '@/utils/tools';
class Index extends React.Component<{}> {
state = {
tools: Tools,
iconfont: { fontSize: '.24rem' }
};
render() {
return (
);
}
}
export default Index;
导入画布(重点、重点、重点)
这里就是重点功能了,需要依据官方开发文档使用。
1. 安装画布核心库
我们在package.json文件夹下新增:
"topology-activity-diagram": "^0.0.4",
"topology-class-diagram": "^0.0.1",
"topology-core": "^0.0.10",
"topology-flow-diagram": "^0.0.1",
"topology-sequence-diagram": "^0.0.4"
其中,topology-core是核心库,其他4个是扩展图形库;我们可以根据api开发文档,实现自己的图形库,并可选择共享,让大家一起使用。这是topology的可扩展性。
然后,执行yarn下载安装依赖库。
2. 注册扩展图形库
核心库仅包含最简单最基础的图形,其他丰富的图形库需要安装依赖包,并在topology-core里注册。这里我们定义一个canvasRegister的注册函数,如下:
// 先导入库
import { Topology } from 'topology-core';
import { Options } from 'topology-core/options';
import { registerNode } from 'topology-core/middles';
import {
flowData,
flowDataAnchors,
flowDataIconRect,
flowDataTextRect,
flowSubprocess,
flowSubprocessIconRect,
flowSubprocessTextRect,
flowDb,
flowDbIconRect,
flowDbTextRect,
flowDocument,
flowDocumentAnchors,
flowDocumentIconRect,
flowDocumentTextRect,
flowInternalStorage,
flowInternalStorageIconRect,
flowInternalStorageTextRect,
flowExternStorage,
flowExternStorageAnchors,
flowExternStorageIconRect,
flowExternStorageTextRect,
flowQueue,
flowQueueIconRect,
flowQueueTextRect,
flowManually,
flowManuallyAnchors,
flowManuallyIconRect,
flowManuallyTextRect,
flowDisplay,
flowDisplayAnchors,
flowDisplayIconRect,
flowDisplayTextRect,
flowParallel,
flowParallelAnchors,
flowComment,
flowCommentAnchors
} from 'topology-flow-diagram';
import {
activityFinal,
activityFinalIconRect,
activityFinalTextRect,
swimlaneV,
swimlaneVIconRect,
swimlaneVTextRect,
swimlaneH,
swimlaneHIconRect,
swimlaneHTextRect,
fork,
forkHAnchors,
forkIconRect,
forkTextRect,
forkVAnchors
} from 'topology-activity-diagram';
import {
simpleClass,
simpleClassIconRect,
simpleClassTextRect,
interfaceClass,
interfaceClassIconRect,
interfaceClassTextRect
} from 'topology-class-diagram';
import {
lifeline,
lifelineAnchors,
lifelineIconRect,
lifelineTextRect,
sequenceFocus,
sequenceFocusAnchors,
sequenceFocusIconRect,
sequenceFocusTextRect
} from 'topology-sequence-diagram';
// 使用
canvasRegister() {
registerNode('flowData', flowData, flowDataAnchors, flowDataIconRect, flowDataTextRect);
registerNode('flowSubprocess', flowSubprocess, null, flowSubprocessIconRect, flowSubprocessTextRect);
registerNode('flowDb', flowDb, null, flowDbIconRect, flowDbTextRect);
registerNode('flowDocument', flowDocument, flowDocumentAnchors, flowDocumentIconRect, flowDocumentTextRect);
registerNode(
'flowInternalStorage',
flowInternalStorage,
null,
flowInternalStorageIconRect,
flowInternalStorageTextRect
);
registerNode(
'flowExternStorage',
flowExternStorage,
flowExternStorageAnchors,
flowExternStorageIconRect,
flowExternStorageTextRect
);
registerNode('flowQueue', flowQueue, null, flowQueueIconRect, flowQueueTextRect);
registerNode('flowManually', flowManually, flowManuallyAnchors, flowManuallyIconRect, flowManuallyTextRect);
registerNode('flowDisplay', flowDisplay, flowDisplayAnchors, flowDisplayIconRect, flowDisplayTextRect);
registerNode('flowParallel', flowParallel, flowParallelAnchors, null, null);
registerNode('flowComment', flowComment, flowCommentAnchors, null, null);
// activity
registerNode('activityFinal', activityFinal, null, activityFinalIconRect, activityFinalTextRect);
registerNode('swimlaneV', swimlaneV, null, swimlaneVIconRect, swimlaneVTextRect);
registerNode('swimlaneH', swimlaneH, null, swimlaneHIconRect, swimlaneHTextRect);
registerNode('forkH', fork, forkHAnchors, forkIconRect, forkTextRect);
registerNode('forkV', fork, forkVAnchors, forkIconRect, forkTextRect);
// class
registerNode('simpleClass', simpleClass, null, simpleClassIconRect, simpleClassTextRect);
registerNode('interfaceClass', interfaceClass, null, interfaceClassIconRect, interfaceClassTextRect);
// sequence
registerNode('lifeline', lifeline, lifelineAnchors, lifelineIconRect, lifelineTextRect);
registerNode('sequenceFocus', sequenceFocus, sequenceFocusAnchors, sequenceFocusIconRect, sequenceFocusTextRect);
}
3. 声明、定义画布对象
我们给src/pages/index.tsx下的Index类定义两个成员变量:canvas和canvasOptions
class Index extends React.Component<{}> {
canvas: Topology;
canvasOptions: Options = {};
state = {
tools: Tools,
iconfont: { fontSize: '.24rem' }
};
...
}
注意,这里并没有定义在state中,因为state用于内部的UI上数据显示和交互,我们的画布是属于一个内部非ui交互的数据。
然后,我们在dom加载完成后componentDidMount里(确保画布的父元素存在)实例化画布:
componentDidMount() {
this.canvasRegister();
this.canvasOptions.on = this.onMessage;
this.canvas = new Topology('topology-canvas', this.canvasOptions);
}
其中,canvasOptions.on为画布的消息回调函数,目前为止,暂时用不到。
4. 添加左侧工具栏拖曳事件,使能够拖放图形
4.1 给图标按钮添加drag属性和事件
{ this.onDrag(ev, btn) }}>
4.2 定义onDrag函数
onDrag(event: React.DragEvent, node: any) {
event.dataTransfer.setData('Text', JSON.stringify(node.data));
}
至此,画布的基本操作就完成了。
定义右边属性栏
1. 创建一个简单的属性栏类
同样,我们创建一个src/pages/components文件夹,放我们的组件;然后创建一个canvasProps.tsx文件。
定义props属性接口:
export interface CanvasPropsProps {
form: FormComponentProps['form'];
data: {
node?: Node,
line?: Line,
multi?: boolean
};
onValuesChange: (props: any, changedValues: any, allValues: any) => void;
}
其中,node不为空表示node节点属性;line不为空表示line连线属性;multi表示多选。
其他内容就是react的表单输入,具体看源码。(这里,我们使用的是ant.design的表单)
2. 定义change事件
我们还是通过ant.design的方式,定义表单的change事件:
src/pages/components/canvasProps.tsx
export default Form.create({
onValuesChange({ onValuesChange, ...restProps }, changedValues, allValues) {
if (onValuesChange) {
onValuesChange(restProps, changedValues, allValues);
}
}
})(CanvasProps);
src/pages/index.tsx
handlePropsChange = (props: any, changedValues: any, allValues: any) => {
if (changedValues.node) {
// 遍历查找修改的属性,赋值给原始Node
// this.state.selected.node = Object.assign(this.state.selected.node, changedValues.node);
for (const key in changedValues.node) {
if (Array.isArray(changedValues.node[key])) {
} else if (typeof changedValues.node[key] === 'object') {
for (const k in changedValues.node[key]) {
this.state.selected.node[key][k] = changedValues.node[key][k];
}
} else {
this.state.selected.node[key] = changedValues.node[key];
}
}
// 通知属性更新,刷新
this.canvas.updateProps(this.state.selected.node);
}
}
简单的属性修改示例就完成了。更多属性,欢迎大家补充并提交GitHub的pr:
- 阅读开发文档,了解相关属性。
- fork仓库到自己名下
- 本地修改并提交到自己的git仓库
- 在自己的fork仓库找到 “Pull request” 按钮,提交
其他
顶部工具栏和右键菜单功能待续。
开源项目不易,欢迎大家一起参与,或资助服务器: