本系列教程是教你們如何根據開源js繪圖庫,打造一個屬於本身的在線繪圖軟件。固然,也能夠看着是這個繪圖庫的開發教程。若是你以爲好,歡迎點個贊,讓咱們更有動力去作好!css
本系列教程重點介紹如何開發本身的繪圖軟件,所以,react基礎和框架不在此介紹。能夠推薦react官網學習,或《React全家桶免費視頻》。html
本系列教程源碼地址:Githubnode
這裏,咱們選擇阿里的UmiJS + DvaJS + Ant.Desgin 輕應用框架。react
// 推薦使用yarn npm install yarn -g yarn global add umi
mkdir topology-react yarn create umi // 建立項目文件後,安裝依賴包 yarn
這裏,咱們選擇typescript,dva等(dll能夠不用,已落伍)。git
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
根據UmiJS的約定,咱們能夠給src/pages下新增一個名爲document.ejs的模板文件,代替缺省模板。新模板內容,參考源碼。typescript
具體參考:UmiJS的HTML模板文檔npm
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:
頂部工具欄和右鍵菜單功能待續。
開源項目不易,歡迎你們一塊兒參與,或資助服務器: