本系列教程是教大家如何根據開源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: 0. 閱讀開發文檔,瞭解相關屬性。
- fork倉庫到自己名下
- 本地修改並提交到自己的git倉庫
- 在自己的fork倉庫找到 “Pull request” 按鈕,提交
其他
頂部工具欄和右鍵菜單功能待續。
開源項目不易,歡迎大家一起參與,或資助服務器: