perf: Optimize the rendering logic of front-end nodes (#2030)
This commit is contained in:
parent
77173de5c0
commit
3dcc31b3b9
@ -326,7 +326,7 @@ function getDetail() {
|
|||||||
saveTime.value = res.data?.update_time
|
saveTime.value = res.data?.update_time
|
||||||
workflowRef.value?.clearGraphData()
|
workflowRef.value?.clearGraphData()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
workflowRef.value?.renderGraphData(detail.value.work_flow)
|
workflowRef.value?.render(detail.value.work_flow)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,39 +3,24 @@ import ElementPlus from 'element-plus'
|
|||||||
import * as ElementPlusIcons from '@element-plus/icons-vue'
|
import * as ElementPlusIcons from '@element-plus/icons-vue'
|
||||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
import { HtmlResize } from '@logicflow/extension'
|
import { HtmlResize } from '@logicflow/extension'
|
||||||
|
|
||||||
import { h as lh } from '@logicflow/core'
|
import { h as lh } from '@logicflow/core'
|
||||||
import { createApp, h } from 'vue'
|
import { createApp, h } from 'vue'
|
||||||
import directives from '@/directives'
|
import directives from '@/directives'
|
||||||
import i18n from '@/locales'
|
import i18n from '@/locales'
|
||||||
import { WorkflowType } from '@/enums/workflow'
|
import { WorkflowType } from '@/enums/workflow'
|
||||||
import { nodeDict } from '@/workflow/common/data'
|
import { nodeDict } from '@/workflow/common/data'
|
||||||
|
import { isActive, connect, disconnect } from './teleport'
|
||||||
class AppNode extends HtmlResize.view {
|
class AppNode extends HtmlResize.view {
|
||||||
isMounted
|
isMounted
|
||||||
r
|
r?: any
|
||||||
app
|
component: any
|
||||||
|
app: any
|
||||||
|
root?: any
|
||||||
|
VueNode: any
|
||||||
constructor(props: any, VueNode: any) {
|
constructor(props: any, VueNode: any) {
|
||||||
super(props)
|
super(props)
|
||||||
|
this.component = VueNode
|
||||||
this.isMounted = false
|
this.isMounted = false
|
||||||
this.r = h(VueNode, {
|
|
||||||
properties: props.model.properties,
|
|
||||||
nodeModel: props.model
|
|
||||||
})
|
|
||||||
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r
|
|
||||||
})
|
|
||||||
this.app.use(ElementPlus, {
|
|
||||||
locale: zhCn
|
|
||||||
})
|
|
||||||
this.app.use(Components)
|
|
||||||
this.app.use(directives)
|
|
||||||
this.app.use(i18n)
|
|
||||||
for (const [key, component] of Object.entries(ElementPlusIcons)) {
|
|
||||||
this.app.component(key, component)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.model.properties.noRender) {
|
if (props.model.properties.noRender) {
|
||||||
delete props.model.properties.noRender
|
delete props.model.properties.noRender
|
||||||
} else {
|
} else {
|
||||||
@ -137,13 +122,79 @@ class AppNode extends HtmlResize.view {
|
|||||||
this.isMounted = true
|
this.isMounted = true
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
rootEl.appendChild(node)
|
rootEl.appendChild(node)
|
||||||
this.app?.mount(node)
|
this.renderVueComponent(node)
|
||||||
} else {
|
} else {
|
||||||
if (this.r && this.r.component) {
|
if (this.r && this.r.component) {
|
||||||
this.r.component.props.properties = this.props.model.getProperties()
|
this.r.component.props.properties = this.props.model.getProperties()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
super.componentWillUnmount()
|
||||||
|
this.unmount()
|
||||||
|
}
|
||||||
|
getComponentContainer() {
|
||||||
|
return this.root
|
||||||
|
}
|
||||||
|
protected targetId() {
|
||||||
|
return `${this.props.graphModel.flowId}:${this.props.model.id}`
|
||||||
|
}
|
||||||
|
protected renderVueComponent(root: any) {
|
||||||
|
this.unmountVueComponent()
|
||||||
|
this.root = root
|
||||||
|
const { model, graphModel } = this.props
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
if (isActive()) {
|
||||||
|
connect(this.targetId(), this.component, root, model, graphModel)
|
||||||
|
} else {
|
||||||
|
this.r = h(this.component, {
|
||||||
|
properties: this.props.model.properties,
|
||||||
|
nodeModel: this.props.model
|
||||||
|
})
|
||||||
|
this.app = createApp({
|
||||||
|
render() {
|
||||||
|
return this.r
|
||||||
|
},
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
getNode: () => model,
|
||||||
|
getGraph: () => graphModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.use(ElementPlus, {
|
||||||
|
locale: zhCn
|
||||||
|
})
|
||||||
|
this.app.use(Components)
|
||||||
|
this.app.use(directives)
|
||||||
|
this.app.use(i18n)
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIcons)) {
|
||||||
|
this.app.component(key, component)
|
||||||
|
}
|
||||||
|
this.app?.mount(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected unmountVueComponent() {
|
||||||
|
if (this.app) {
|
||||||
|
this.app.unmount()
|
||||||
|
this.app = null
|
||||||
|
}
|
||||||
|
if (this.root) {
|
||||||
|
this.root.innerHTML = ''
|
||||||
|
}
|
||||||
|
return this.root
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
if (isActive()) {
|
||||||
|
disconnect(this.targetId())
|
||||||
|
}
|
||||||
|
this.unmountVueComponent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppNodeModel extends HtmlResize.model {
|
class AppNodeModel extends HtmlResize.model {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core'
|
import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core'
|
||||||
import { createApp, h as vh } from 'vue'
|
import { createApp, h as vh } from 'vue'
|
||||||
|
import { isActive, connect, disconnect } from './teleport'
|
||||||
import CustomLine from './CustomLine.vue'
|
import CustomLine from './CustomLine.vue'
|
||||||
function isMouseInElement(element: any, e: any) {
|
function isMouseInElement(element: any, e: any) {
|
||||||
const rect = element.getBoundingClientRect()
|
const rect = element.getBoundingClientRect()
|
||||||
@ -15,7 +15,8 @@ const DEFAULT_WIDTH = 32
|
|||||||
const DEFAULT_HEIGHT = 32
|
const DEFAULT_HEIGHT = 32
|
||||||
class CustomEdge2 extends BezierEdge {
|
class CustomEdge2 extends BezierEdge {
|
||||||
isMounted
|
isMounted
|
||||||
|
customLineApp?: any
|
||||||
|
root?: any
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.isMounted = false
|
this.isMounted = false
|
||||||
@ -28,6 +29,64 @@ class CustomEdge2 extends BezierEdge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 渲染vue组件
|
||||||
|
* @param root
|
||||||
|
*/
|
||||||
|
protected renderVueComponent(root: any) {
|
||||||
|
this.unmountVueComponent()
|
||||||
|
this.root = root
|
||||||
|
const { graphModel } = this.props
|
||||||
|
if (root) {
|
||||||
|
if (isActive()) {
|
||||||
|
connect(
|
||||||
|
this.targetId(),
|
||||||
|
CustomLine,
|
||||||
|
root,
|
||||||
|
this.props.model,
|
||||||
|
graphModel,
|
||||||
|
(node: any, graph: any) => {
|
||||||
|
return { model: node, graph }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.customLineApp = createApp({
|
||||||
|
render: () => vh(CustomLine, { model: this.props.model })
|
||||||
|
})
|
||||||
|
this.customLineApp?.mount(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected targetId() {
|
||||||
|
return `${this.props.graphModel.flowId}:${this.props.model.id}`
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 组件即将卸载勾子
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (super.componentWillUnmount) {
|
||||||
|
super.componentWillUnmount()
|
||||||
|
}
|
||||||
|
if (isActive()) {
|
||||||
|
console.log('unmount')
|
||||||
|
disconnect(this.targetId())
|
||||||
|
}
|
||||||
|
this.unmountVueComponent()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 卸载vue
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected unmountVueComponent() {
|
||||||
|
if (this.customLineApp) {
|
||||||
|
this.customLineApp.unmount()
|
||||||
|
this.customLineApp = null
|
||||||
|
}
|
||||||
|
if (this.root) {
|
||||||
|
this.root.innerHTML = ''
|
||||||
|
}
|
||||||
|
return this.root
|
||||||
|
}
|
||||||
|
|
||||||
getEdge() {
|
getEdge() {
|
||||||
const { model } = this.props
|
const { model } = this.props
|
||||||
@ -57,14 +116,11 @@ class CustomEdge2 extends BezierEdge {
|
|||||||
height: customHeight
|
height: customHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
render: () => vh(CustomLine, { model: this.props.model })
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const s = document.getElementById(id)
|
const s = document.getElementById(id)
|
||||||
if (s && !this.isMounted) {
|
if (s && !this.isMounted) {
|
||||||
app.mount(s)
|
|
||||||
this.isMounted = true
|
this.isMounted = true
|
||||||
|
this.renderVueComponent(s)
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
|
|||||||
80
ui/src/workflow/common/teleport.ts
Normal file
80
ui/src/workflow/common/teleport.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { BaseEdgeModel, BaseNodeModel, GraphModel } from '@logicflow/core'
|
||||||
|
import { defineComponent, h, reactive, isVue3, Teleport, markRaw, Fragment } from 'vue-demi'
|
||||||
|
|
||||||
|
let active = false
|
||||||
|
const items = reactive<{ [key: string]: any }>({})
|
||||||
|
|
||||||
|
export function connect(
|
||||||
|
id: string,
|
||||||
|
component: any,
|
||||||
|
container: HTMLDivElement,
|
||||||
|
node: BaseNodeModel | BaseEdgeModel,
|
||||||
|
graph: GraphModel,
|
||||||
|
get_props?: any
|
||||||
|
) {
|
||||||
|
if (!get_props) {
|
||||||
|
get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => {
|
||||||
|
return { nodeModel: node, graph }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
items[id] = markRaw(
|
||||||
|
defineComponent({
|
||||||
|
render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]),
|
||||||
|
provide: () => ({
|
||||||
|
getNode: () => node,
|
||||||
|
getGraph: () => graph
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disconnect(id: string) {
|
||||||
|
if (active) {
|
||||||
|
delete items[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isActive() {
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTeleport(): any {
|
||||||
|
if (!isVue3) {
|
||||||
|
throw new Error('teleport is only available in Vue3')
|
||||||
|
}
|
||||||
|
active = true
|
||||||
|
|
||||||
|
return defineComponent({
|
||||||
|
props: {
|
||||||
|
flowId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
return () => {
|
||||||
|
const children: Record<string, any>[] = []
|
||||||
|
Object.keys(items).forEach((id) => {
|
||||||
|
// https://github.com/didi/LogicFlow/issues/1768
|
||||||
|
// 多个不同的VueNodeView都会connect注册到items中,因此items存储了可能有多个flowId流程图的数据
|
||||||
|
// 当使用多个LogicFlow时,会创建多个flowId + 同时使用KeepAlive
|
||||||
|
// 每一次items改变,会触发不同flowId持有的setup()执行,由于每次setup()执行就是遍历items,因此存在多次重复渲染元素的问题
|
||||||
|
// 即items[0]会在Page1的setup()执行,items[0]也会在Page2的setup()执行,从而生成两个items[0]
|
||||||
|
|
||||||
|
// 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据
|
||||||
|
// 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0]
|
||||||
|
if (id.startsWith(props.flowId)) {
|
||||||
|
children.push(items[id])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return h(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
children.map((item) => h(item))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
<div className="workflow-app" id="container"></div>
|
<div className="workflow-app" id="container"></div>
|
||||||
<!-- 辅助工具栏 -->
|
<!-- 辅助工具栏 -->
|
||||||
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
|
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
|
||||||
|
<TeleportContainer :flow-id="flowId" />
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LogicFlow from '@logicflow/core'
|
import LogicFlow from '@logicflow/core'
|
||||||
@ -13,10 +14,12 @@ import '@logicflow/extension/lib/style/index.css'
|
|||||||
import '@logicflow/core/dist/style/index.css'
|
import '@logicflow/core/dist/style/index.css'
|
||||||
import { initDefaultShortcut } from '@/workflow/common/shortcut'
|
import { initDefaultShortcut } from '@/workflow/common/shortcut'
|
||||||
import Dagre from '@/workflow/plugins/dagre'
|
import Dagre from '@/workflow/plugins/dagre'
|
||||||
|
import { getTeleport } from '@/workflow/common/teleport'
|
||||||
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
|
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
|
||||||
|
|
||||||
defineOptions({ name: 'WorkFlow' })
|
defineOptions({ name: 'WorkFlow' })
|
||||||
|
const TeleportContainer = getTeleport()
|
||||||
|
const flowId = ref('')
|
||||||
type ShapeItem = {
|
type ShapeItem = {
|
||||||
type?: string
|
type?: string
|
||||||
text?: string
|
text?: string
|
||||||
@ -56,9 +59,6 @@ const render = (data: any) => {
|
|||||||
lf.value.render(data)
|
lf.value.render(data)
|
||||||
}
|
}
|
||||||
const renderGraphData = (data?: any) => {
|
const renderGraphData = (data?: any) => {
|
||||||
if (data) {
|
|
||||||
graphData.value = data
|
|
||||||
}
|
|
||||||
const container: any = document.querySelector('#container')
|
const container: any = document.querySelector('#container')
|
||||||
if (container) {
|
if (container) {
|
||||||
lf.value = new LogicFlow({
|
lf.value = new LogicFlow({
|
||||||
@ -89,11 +89,14 @@ const renderGraphData = (data?: any) => {
|
|||||||
strokeWidth: 1
|
strokeWidth: 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
lf.value.on('graph:rendered', () => {
|
||||||
|
flowId.value = lf.value.graphModel.flowId
|
||||||
|
})
|
||||||
initDefaultShortcut(lf.value, lf.value.graphModel)
|
initDefaultShortcut(lf.value, lf.value.graphModel)
|
||||||
lf.value.batchRegister([...Object.keys(nodes).map((key) => nodes[key].default), AppEdge])
|
lf.value.batchRegister([...Object.keys(nodes).map((key) => nodes[key].default), AppEdge])
|
||||||
lf.value.setDefaultEdgeType('app-edge')
|
lf.value.setDefaultEdgeType('app-edge')
|
||||||
|
|
||||||
lf.value.render(graphData.value)
|
lf.value.render(data ? data : {})
|
||||||
|
|
||||||
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
|
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
|
||||||
id_list.forEach((id: string) => {
|
id_list.forEach((id: string) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user