perf: Optimize the rendering logic of front-end nodes (#2030)
This commit is contained in:
parent
77173de5c0
commit
3dcc31b3b9
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
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>
|
||||
<!-- 辅助工具栏 -->
|
||||
<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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user