perf: Optimize the rendering logic of front-end nodes (#2030)

This commit is contained in:
shaohuzhang1 2025-01-14 18:35:54 +08:00 committed by GitHub
parent 77173de5c0
commit 3dcc31b3b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 227 additions and 37 deletions

View File

@ -27,11 +27,11 @@
<el-button icon="Plus" @click="showPopover = !showPopover"> 添加组件 </el-button>
<el-button @click="clickShowDebug" :disabled="showDebug">
<AppIcon iconName="app-play-outlined" class="mr-4"></AppIcon>
{{ $t('common.debug')}}</el-button
{{ $t('common.debug') }}</el-button
>
<el-button @click="saveApplication(true)">
<AppIcon iconName="app-save-outlined" class="mr-4"></AppIcon>
{{ $t('common.save')}}
{{ $t('common.save') }}
</el-button>
<el-button type="primary" @click="publicHandle"> 发布 </el-button>
@ -326,7 +326,7 @@ function getDetail() {
saveTime.value = res.data?.update_time
workflowRef.value?.clearGraphData()
nextTick(() => {
workflowRef.value?.renderGraphData(detail.value.work_flow)
workflowRef.value?.render(detail.value.work_flow)
})
})
}

View File

@ -3,39 +3,24 @@ import ElementPlus from 'element-plus'
import * as ElementPlusIcons from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import { HtmlResize } from '@logicflow/extension'
import { h as lh } from '@logicflow/core'
import { createApp, h } from 'vue'
import directives from '@/directives'
import i18n from '@/locales'
import { WorkflowType } from '@/enums/workflow'
import { nodeDict } from '@/workflow/common/data'
import { isActive, connect, disconnect } from './teleport'
class AppNode extends HtmlResize.view {
isMounted
r
app
r?: any
component: any
app: any
root?: any
VueNode: any
constructor(props: any, VueNode: any) {
super(props)
this.component = VueNode
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) {
delete props.model.properties.noRender
} else {
@ -137,13 +122,79 @@ class AppNode extends HtmlResize.view {
this.isMounted = true
const node = document.createElement('div')
rootEl.appendChild(node)
this.app?.mount(node)
this.renderVueComponent(node)
} else {
if (this.r && this.r.component) {
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 {

View File

@ -1,6 +1,6 @@
import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core'
import { createApp, h as vh } from 'vue'
import { isActive, connect, disconnect } from './teleport'
import CustomLine from './CustomLine.vue'
function isMouseInElement(element: any, e: any) {
const rect = element.getBoundingClientRect()
@ -15,7 +15,8 @@ const DEFAULT_WIDTH = 32
const DEFAULT_HEIGHT = 32
class CustomEdge2 extends BezierEdge {
isMounted
customLineApp?: any
root?: any
constructor() {
super()
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() {
const { model } = this.props
@ -57,14 +116,11 @@ class CustomEdge2 extends BezierEdge {
height: customHeight
}
const app = createApp({
render: () => vh(CustomLine, { model: this.props.model })
})
setTimeout(() => {
const s = document.getElementById(id)
if (s && !this.isMounted) {
app.mount(s)
this.isMounted = true
this.renderVueComponent(s)
}
}, 0)

View 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))
)
}
}
})
}

View File

@ -2,6 +2,7 @@
<div className="workflow-app" id="container"></div>
<!-- 辅助工具栏 -->
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
<TeleportContainer :flow-id="flowId" />
</template>
<script setup lang="ts">
import LogicFlow from '@logicflow/core'
@ -13,10 +14,12 @@ import '@logicflow/extension/lib/style/index.css'
import '@logicflow/core/dist/style/index.css'
import { initDefaultShortcut } from '@/workflow/common/shortcut'
import Dagre from '@/workflow/plugins/dagre'
import { getTeleport } from '@/workflow/common/teleport'
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
defineOptions({ name: 'WorkFlow' })
const TeleportContainer = getTeleport()
const flowId = ref('')
type ShapeItem = {
type?: string
text?: string
@ -56,9 +59,6 @@ const render = (data: any) => {
lf.value.render(data)
}
const renderGraphData = (data?: any) => {
if (data) {
graphData.value = data
}
const container: any = document.querySelector('#container')
if (container) {
lf.value = new LogicFlow({
@ -89,11 +89,14 @@ const renderGraphData = (data?: any) => {
strokeWidth: 1
}
})
lf.value.on('graph:rendered', () => {
flowId.value = lf.value.graphModel.flowId
})
initDefaultShortcut(lf.value, lf.value.graphModel)
lf.value.batchRegister([...Object.keys(nodes).map((key) => nodes[key].default), AppEdge])
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>) => {
id_list.forEach((id: string) => {