Flow Render: 像调用异步函数一样渲染 UI 组件

Flow Render 提供了一种基于 Promise 的 UI 渲染方式,让你可以像调用异步函数一样渲染组件,并等待用户交互结果

它将分散的状态、回调和组件层级重新组织为线性的 async/await 控制流,让复杂的交互逻辑变得直观且易于维护。

ts 复制代码
const result = await render(Component)

✨ 核心特性

  • Promise 驱动的 UI 渲染:像调用异步函数一样等待组件的结果
  • 支持任意组件 Promise 化:新组件或现有组件都能接入,无需侵入式改造
  • 控制流集中管理:交互逻辑按顺序书写,避免状态分散和回调嵌套
  • 支持上下文完整继承:继承 theme、i18n、store 等应用上下文
  • 实例隔离,用完即销毁:每次渲染都是独立实例,互不干扰,组件状态自动重置
  • 支持全局与局部渲染:既可挂载在应用根节点,也可绑定到局部组件生命周期

📦 Framework Support

Framework Package
React @flow-render/react (也支持 React Native)
Vue @flow-render/vue
Preact @flow-render/preact
Svelte @flow-render/svelte
Solid @flow-render/solid

🚀 快速开始(React)

第一步:安装

bash 复制代码
npm i @flow-render/react

第二部:挂载容器

在应用根节点放一个 <Viewport/>,所有动态渲染的组件都会出现在这里。

tsx 复制代码
import { Viewport } from '@flow-render/react'

function App () {
  return (
    <>
      <YourApp/>
      <Viewport/> {/* 动态组件都渲染在这里 */}
    </>
  )
}

第三步:定义组件

Flow Render 支持两种编写组件的模式,你可以根据场景自由选择。

执行器模式(推荐)

组件内部直接声明并使用 resolve / reject 回调,类似 new Promise((resolve, reject)=>...) 的执行器风格。

tsx 复制代码
import { type PromiseResolvers } from '@flow-render/react'

interface Props extends PromiseResolvers<boolean> {
  title: string
}

function ConfirmDialog ({ title, resolve, reject }: Props) {
  return (
    <dialog open>
      <div>{title}</div>
      <div>
        <button onClick={() => resolve(true)}>是</button>
        <button onClick={() => resolve(false)}>否</button>
        <button onClick={() => reject(new Error('取消'))}>取消</button>
      </div>
    </dialog>
  )
}

渲染时自动注入回调:

tsx 复制代码
import { render } from '@flow-render/react'

const result = await render(ConfirmDialog, {
  title: '你确定吗?'
})

适配器模式(灵活强大)

适配器模式让你可以将任意组件的 props 与 Promise 动态关联。你只需提供一个函数,该函数接收 resolve 和 reject,并返回组件的 props。这种方式不仅兼容现有组件,还能实现更复杂的逻辑,例如根据外部数据决定 props、条件渲染、动态绑定等。

tsx 复制代码
interface Props {
  title: string
  onYes: () => void
  onNo: () => void
  onCancel: () => void
}

function ConfirmDialog (props: Props) {
  return (
    <dialog open>
      <div>{props.title}</div>
      <div>
        <button onClick={props.onYes}>是</button>
        <button onClick={props.onNo}>否</button>
        <button onClick={props.onCancel}>取消</button>
      </div>
    </dialog>
  )
}

适配器模式渲染时,可以通过适配器函数建立 Promise 与组件回调的关联:

tsx 复制代码
import { render } from '@flow-render/react'

const result = await render(ConfirmDialog, (resolve, reject) => {
  return {
    title: '你确定吗?',
    onYes: () => resolve(true),
    onNo: () => resolve(false),
    onCancel: () => reject(),
  }
})

全局渲染器(默认)

默认情况下,render() 渲染出的动态组件生命周期不跟随调用它的组件 ,而是跟随全局 Viewport

这意味着:

  • 即使触发渲染的组件已卸载,动态组件仍可继续存在
  • 适合全局弹窗、确认框、选择器、异步引导流程等场景

若希望动态组件在当前页面或当前组件卸载时自动销毁,请使用局部渲染器


局部渲染器

使用 useRenderer() 可以创建一个与当前组件生命周期绑定的局部渲染器。

适用场景:

  • 页面级弹窗
  • 需跟随局部区域销毁的交互
  • 希望自定义渲染位置
tsx 复制代码
import { useRenderer } from '@flow-render/react'

function Page () {
  const [render, Viewport] = useRenderer()

  return (
    <div>
      <button onClick={() => render(ConfirmDialog)}>打开</button>
      <Viewport/>
    </div>
  )
}

Page 卸载时,局部渲染器中未完成的渲染任务也会一并结束。


自定义渲染器

开发组件库业务子系统 时,你可能希望对外暴露自己的渲染入口,而不是让用户依赖默认渲染器。此时可使用 createRenderer() 创建独立实例。

tsx 复制代码
// your-lib/index.ts

import { createRenderer } from '@flow-render/react'

const [render, Viewport] = createRenderer()

export function LibProvider (props) {
  return (
    <>
      {props.children}
      <Viewport/>
    </>
  )
}

export function openDialog () {
  return render(Dialog)
}

这样用户使用时只需接入库提供的 Provider 和对应的方法,无需了解关于 Flow Render 的任何逻辑:

tsx 复制代码
import { LibProvider, openDialog } from 'your-lib'

function App () {
  return (
    <LibProvider>
      <UserApp/>
      <button onClick={() => openDialog()}>打开</button>
    </LibProvider>
  )
}

这样便将渲染能力封装在库内部,对外提供更稳定、统一的 API。


取消渲染

手动取消渲染

某些高级场景下,你可能需要从外部中断 UI 流程,例如:

  • 超时自动关闭
  • 路由切换时终止
  • 用户主动取消整个流程

由于 render() 返回标准 Promise,你可以在适配器中自行暴露取消能力:

tsx 复制代码
let cancel: () => void

const promise = render(Component, (resolve, reject) => {
  cancel = () => reject(new Error('Cancelled'))

  return {
    resolve,
    reject,
  }
})

// 需要时调用
cancel()

自动取消渲染

Viewport 卸载时(例如全局 Viewport 随应用销毁,或局部 Viewport 随组件销毁),所有未完成的渲染任务会自动 reject。如有必要可以通过 isCancelError 判断错误是否由自动取消引起。

tsx 复制代码
import { render, isCancelError } from '@flow-render/react'

try {
  await render(Component)
} catch (error) {
  if (isCancelError(error)) {
    // 处理自动取消
    return
  }

  throw error
}

适用场景

Flow Render 特别适合以下交互:

  • 确认框 / 提示框
  • 表单弹窗
  • 选择器
  • 向导流程
  • 登录拦截
  • 权限确认
  • 任何需要"等待用户完成某一步再继续"的 UI 逻辑

例如,你可以将原本分散的交互写成线性流程:

tsx 复制代码
async function postForm () {
  // 第一步:确认
  const confirmed = await render(ConfirmDialog, {
    title: '确认提交?'
  })

  if (!confirmed) {
    return
  }

  // 第二步:填写表单
  const formData = await render(FormDialog)

  // 第三步:提交
  await submit(formData)
}

相比传统的状态驱动写法,这种方式更易阅读、复用和维护。


设计理念

Flow Render 并非要替代框架原有的组件模型,而是为异步 UI 交互流程提供更自然的表达方式:

  1. 按需动态渲染
  2. 展示 UI 并等待用户操作
  3. 获取结果后继续后续逻辑

这几件事可以组织在同一段 async / await 代码中。

对于跨组件、跨层级、跨流程的交互,这种写法往往更直观。


Github: github.com/flow-render...

相关推荐
小小程序员mono2 小时前
JS 与 Vue Router 导航方式对比
开发语言·javascript·vue.js
Sgf2272 小时前
如何阅读 React 源码:系统化学习指南
前端·react.js·前端框架
李剑一2 小时前
解决 Cesium 网络卡顿!5 分钟加载天地图,内网也能流畅用,附完整代码
前端·vue.js·cesium
肉肉不吃 肉3 小时前
Vue Router 路由模式
前端·javascript·vue.js
北寻北爱3 小时前
Vue-Router
前端·javascript·vue.js
天天向上10243 小时前
vue 页面内实现el-table和div自动滚动
前端·javascript·vue.js
wing983 小时前
用 AI 实现图片懒加载,这也太简单了!
前端·vue.js·图片资源
sure2824 小时前
React Native中自定义TabBar
前端·react native·react.js
前端小菜鸟也有人起4 小时前
Vue2父子组件通信方法总结
javascript·vue.js·ecmascript