与宿主集成
查看器与设计器只管显示与编辑,不发网络请求,也不碰存储与权限。把它们嵌进业务系统时,下面这些由宿主来做:
| 事情 | 宿主做什么 | 元素给什么 |
|---|---|---|
| 流程的保存 | 存流程定义,或换成引擎定义交给工作流引擎;管版本与发布 | schema、schema-change、save、dirty、markSaved(),预设的 toWorkflowDefinition |
| 运行状态 | 从后端取实例的执行情况,换成节点的状态 | 查看器的 statuses、setNodeStatuses()、highlightNodes() |
| 权限 | 决定谁能看、谁能改,服务端再复核 | readonly、panels、extensions |
| 校验 | 保存前拦住有错误的定义,服务端复核 | validate()、issues-change、validateDocument |
| 业务数据 | 受理人、表单这类要查宿主数据才能选的属性 | 设计器层扩展的属性编辑器 |
打开与保存
流程定义是一份完整的 JSON,原样存、原样取。对接 XiHan 工作流引擎时存的是引擎定义:打开时把取回的定义交给 fromWorkflowDefinition,结果赋给 schema;保存时把 schema 交给 toWorkflowDefinition,再交给后端。不用引擎时直接存取流程定义本身。
import type { XihanWorkflowDefinition } from '@xihan-flow/core/presets/xihan-workflow'
import type { XhFlowDesigner } from '@xihan-flow/designer'
import type { FlowNodeStatus, XhFlowViewer } from '@xihan-flow/runtime'
import { readDocument, validateDocument } from '@xihan-flow/core'
import { createXihanWorkflow, fromWorkflowDefinition, toWorkflowDefinition } from '@xihan-flow/core/presets/xihan-workflow'
export async function openWorkflow(designer: XhFlowDesigner, code: string): Promise<void> {
const url = `/api/workflows/${encodeURIComponent(code)}`
const response = await fetch(url)
designer.schema = fromWorkflowDefinition(await response.json() as XihanWorkflowDefinition)
designer.addEventListener('save', async () => {
if (designer.validate().some(issue => issue.severity === 'error'))
return
const saving = designer.schema
const saved = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(toWorkflowDefinition(saving)),
})
// 保存期间又改过的,不记为已保存
if (saved.ok && designer.schema === saving)
designer.markSaved()
})
}- 保存前用
validate()拦住错误级的问题,警告不拦。 - 保存是异步的,用户可能在等待期间接着改。示例在发出请求时记下当时的定义,成功后只有定义没再变才调用
markSaved()。
设计器在内存里改流程,页面关掉修改就没了。有未保存的修改时提醒用户:
export function guardUnsaved(designer: XhFlowDesigner): () => void {
const onBeforeUnload = (event: BeforeUnloadEvent): void => {
if (designer.dirty)
event.preventDefault()
}
window.addEventListener('beforeunload', onBeforeUnload)
return () => window.removeEventListener('beforeunload', onBeforeUnload)
}显示运行状态
业务页面用查看器显示某个实例走到了哪里:宿主从后端取实例里各节点的执行情况,换成「节点 id → 状态」交给 statuses,走过的节点可以再高亮出来。查看器为 running、completed、waiting、faulted、canceled、compensated 各画了一种样子,后端的状态名不同时在宿主这边换好,详见查看器。
/** 宿主接口返回的一条节点执行记录。 */
interface NodeRecord {
readonly nodeId: string
readonly status: FlowNodeStatus
}
export async function showInstance(viewer: XhFlowViewer, instanceId: string): Promise<void> {
const response = await fetch(`/api/workflow-instances/${encodeURIComponent(instanceId)}/nodes`)
const records = await response.json() as NodeRecord[]
viewer.statuses = Object.fromEntries(records.map(record => [record.nodeId, record.status]))
viewer.highlightNodes(records.map(record => record.nodeId))
}实例还在运行时,宿主按自己的节奏重新取,用 setNodeStatuses() 只更新变了的节点。
权限
- 只能看的用户:设计器设成
readonly,仍可选择、浏览与复制;只需要看的页面直接用查看器,它更轻。 - 只让调整局部的场景,用
panels去掉物料等区,用commandLayout精简命令,用自定义属性编辑器限定可选的值。 - 这些都只在前端生效。流程的保存、发布与删除要在服务端判断权限,保存时服务端也应复核流程本身。
服务端复核
@xihan-flow/core 与预设不依赖 DOM,存流程定义的宿主可以在 Node.js 后端直接读入并校验收到的定义,结论与设计器一致:
/** 服务端收到流程定义时复核:读入不了或有错误级问题就拒收,返回问题码与路径。 */
export function reviewWorkflow(body: string): { valid: boolean, errors: string[] } {
const read = readDocument(JSON.parse(body))
if (!read.ok)
return { valid: false, errors: read.issues.map(issue => `${issue.code} @ ${issue.path.join('.')}`) }
const errors = validateDocument(read.document, { extensions: [createXihanWorkflow()] })
.filter(issue => issue.severity === 'error')
.map(issue => `${issue.code} @ ${issue.path.join('.')}`)
return { valid: errors.length === 0, errors }
}存引擎定义、后端是 XiHan.Framework 的,由引擎在发布时按它自己的规则把关,前端在提交前用 validate() 先拦一道。
在前端框架里使用
查看器与设计器是原生 Web Components,任何框架都能用:在应用启动时调用一次 defineFlowViewer() 或 defineFlowDesigner(),之后像普通元素一样放进模板。流程定义、扩展这类对象要交给元素的属性,不要写成 attribute。
在 Vue 里,先告诉编译器 xh- 开头的标签是自定义元素,免得被当成组件去解析:
// vite.config.ts 里 @vitejs/plugin-vue 的选项
vue({
template: {
compilerOptions: {
isCustomElement: tag => tag.startsWith('xh-'),
},
},
})对象用 .prop 修饰交给属性,事件用 @ 监听:
<script setup lang="ts">
/*
* Copyright (c) 2021-Present XiHanFun and contributors.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*/
// 在 Vue 里使用查看器:对象经 .prop 交给元素的属性,事件用 @ 监听;点一个节点把它标为已完成。
import type { FlowNodeStatus, FlowViewerEventMap } from '@xihan-flow/runtime'
import { defineFlowViewer } from '@xihan-flow/runtime'
import { ref } from 'vue'
import { extensions, schema } from '../schemas/approval'
defineFlowViewer()
const statuses = ref<Record<string, FlowNodeStatus>>({ start: 'completed', dept: 'running' })
const message = ref('点一个节点把它标为已完成')
function onNodeClick(event: Event): void {
const { id } = (event as FlowViewerEventMap['node-click']).detail
statuses.value = { ...statuses.value, [id]: 'completed' }
message.value = `已把 ${id} 标为已完成`
}
</script>
<template>
<p>{{ message }}</p>
<xh-flow-viewer :extensions.prop="extensions" :schema.prop="schema" :statuses.prop="statuses" @node-click="onNodeClick" />
</template>React 19 起会把对象直接赋给自定义元素的同名属性;事件可以用 ref 拿到元素后 addEventListener 监听。
文案与样式
- 内建文案缺省是英文。全站换语言时把同一语言的流程语言包(如
flowZhCN、flowJaJP)与 XiHan.UI 的语言包合并后交给setXhConfig,写法见快速开始;单个实例的文案用它的translations覆写。预设的节点类型与属性另有文案,见 XiHan 工作流引擎预设。 - 样式只用 XiHan.UI 的设计令牌,元素跟着宿主的 UI 主题变化,宿主不用另写颜色。设计器与查看器铺满自身,宿主给元素定宽高。
一个页面里放多个实例
元素不往 window 上挂对象,也不读写固定键名的本地存储,同一页面里的多个查看器、多个设计器互不影响,各自的扩展也只对自己生效。只有语言包是全局的:setXhConfig 对页面上所有元素生效。
