乐趣区

前端新手也能做大项目从零打造一个属于自己的在线Visio项目实战ReactJS-UmiJS-DvaJS-一

本系列教程是教大家如何根据开源 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 (<div className={styles.page}>
      <Headers />
      <div className={styles.body}>{props.children}</div>
    </div>
  );
};

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 (<div className={styles.page}>
        <div className={styles.tools}/>
        <div id="workspace" className={styles.full} />
        <div className={styles.props}>{}</div>
      </div>
    );
  }
}

export default Index;

实现左侧工具栏

1. 导入阿里字体图标 iconfont
在 src/pages/document.ejs 中引入我们需要的 iconfont

  <link href="//at.alicdn.com/t/font_1113798_0532l8oa6jqp.css" rel="stylesheet" />
  <link href="//at.alicdn.com/t/font_1331132_5lvbai88wkb.css" rel="stylesheet" />

其中,上面的是左侧工具栏所需要用到的图标;下面是右侧属性栏作为可供用户选中的节点图标库。可以替换成自己的地址(注意修改 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 (<div className={styles.page}>
        <div className={styles.tools}>
          {this.state.tools.map((item, index) => {
              return (<div key={index}>
                  <div className={styles.title}>{item.group}</div>
                  <div className={styles.buttons}>
                    {item.children.map((btn: any, i: number) => {
                        return (<a key={i} title={btn.name}>
                            <i className={'iconfont' + btn.icon} style={this.state.iconfont} />
                          </a>
                        )
                      })
                    }
                  </div>
                </div>
              )
            })
          }
        </div>
        <div id="workspace" className={styles.full} />
        <div className={styles.props}>{}</div>
      </div>
    );
  }
}

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 属性和事件

<a key={i} title={btn.name} draggable={true} onDragStart={(ev) => {this.onDrag(ev, btn) }}>
  <i className={'iconfont' + btn.icon} style={this.state.iconfont} />
</a>

4.2 定义 onDrag 函数

onDrag(event: React.DragEvent<HTMLAnchorElement>, 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<CanvasPropsProps>({onValuesChange({ onValuesChange, ...restProps}, changedValues, allValues) {if (onValuesChange) {onValuesChange(restProps, changedValues, allValues);
    }
  }
})(CanvasProps);

src/pages/index.tsx

<div className={styles.props}>
  <CanvasProps data={this.state.selected} onValuesChange={this.handlePropsChange} />
</div>
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

  1. 阅读开发文档,了解相关属性。
  2. fork 仓库到自己名下
  3. 本地修改并提交到自己的 git 仓库
  4. 在自己的 fork 仓库找到“Pull request”按钮,提交

其他

顶部工具栏和右键菜单功能待续。

开源项目不易,欢迎大家一起参与,或资助服务器:

退出移动版