【前端新手也能作大項目】:從零打造一個屬於本身的在線Visio項目實戰【ReactJS + UmiJS + DvaJS】 (一)

本系列教程是教你們如何根據開源js繪圖庫,打造一個屬於本身的在線繪圖軟件。固然,也能夠看着是這個繪圖庫的開發教程。若是你以爲好,歡迎點個贊,讓咱們更有動力去作好!css

本系列教程重點介紹如何開發本身的繪圖軟件,所以,react基礎和框架不在此介紹。能夠推薦react官網學習,或《React全家桶免費視頻》。html

本系列教程源碼地址:Githubnode

1、搭建react框架環境

這裏,咱們選擇阿里的UmiJS + DvaJS + Ant.Desgin 輕應用框架。react

1. 安裝UmiJS

// 推薦使用yarn
npm install yarn -g

yarn global add umi

2. 安裝UmiJS手腳架

mkdir topology-react
 yarn create umi
 
 // 建立項目文件後,安裝依賴包
 yarn

這裏,咱們選擇typescript,dva等(dll能夠不用,已落伍)。git

3. 把css改爲less

A. typings.d.ts加入lesses6

declare module '*.less';

B. global.css改爲global.less,並引入antd主題github

@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中的引用。web

4. 修改默認的單頁面模板文件

根據UmiJS的約定,咱們能夠給src/pages下新增一個名爲document.ejs的模板文件,代替缺省模板。新模板內容,參考源碼。typescript

具體參考:UmiJS的HTML模板文檔npm

5. 運行

npm start就能夠看到默認UmiJS界面。
代碼:tag: umi

其中,layouts下的index.tsx爲全局模板文件;pages爲路由模塊。

2、修改頁面佈局爲上下導航、左中右佈局

拷貝靜態資源文件

在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;

le5le-topology - 左中右3欄佈局

實現左側工具欄

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功能以下:
file

而後,在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」 按鈕,提交

file

其餘

頂部工具欄和右鍵菜單功能待續。

開源項目不易,歡迎你們一塊兒參與,或資助服務器:

file

相關文章
相關標籤/搜索