2025年8月08日·阅读约1分钟

在生成的 UI 中安全地使用自定义 Vue 组件并支持重生成

了解如何在不破坏重新生成的情况下向生成的 UI 添加自定义 Vue 组件,使用隔离模式、清晰边界和简单交接规则。

在生成的 UI 中安全地使用自定义 Vue 组件并支持重生成

在生成的 UI 上直接修改会破坏什么

生成的 UI 是为重建而设计的。在像 AppMaster 这样的可视化平台中,Vue3 Web 应用代码由构建器生成。重新生成(regeneration)是保持屏幕、逻辑和数据模型一致的方式。

问题很简单:如果你直接编辑生成的文件,下一次重新生成可能会覆盖你的修改。

这就是团队选择自定义代码的原因。内建的 UI 模块通常能覆盖常见表单和表格,但真实应用经常需要一些特殊部件:复杂图表、地图拾取器、富文本编辑器、签名板或拖放排期器。这些都是添加自定义 Vue 组件的好理由,只要你把它们当作插件而不是直接修改生成文件即可。

当自定义代码与生成代码混合时,故障往往会延迟出现并且难以定位。你可能直到下一次 UI 变更触发重新生成,或直到同事在可视化编辑器里调整屏幕时才发现问题。常见问题包括:

  • 你的自定义标记被重新生成的模板覆盖而消失。
  • 导入或注册因文件名或结构改变而失效。
  • 一次“小修复”在每次部署时都变成合并冲突。
  • 生成逻辑与自定义逻辑出现漂移,边缘情况开始出错。
  • 升级变得危险,因为你不知道会替换掉哪些内容。

目标不是避免定制,而是让重建可预测。如果你在生成的屏幕和自定义小部件之间保持干净的边界,重新生成就会变成常规操作而不是灾难。

一个能保持重新生成安全的边界规则

如果你想定制同时不丢失工作成果,请遵循一条规则:永远不要编辑生成的文件。把它们当作只读输出,就像编译产物一样。

把你的 UI 想象为两个区域:

  • 生成区(Generated zone):由生成器输出的页面、布局和屏幕。
  • 自定义区(Custom zone):你手写的 Vue 组件,放在单独的文件夹中。

生成的 UI 应该消耗你的自定义组件,而不是用于构建这些组件。

为了让这长期可行,把“边界”做窄且清晰。一个自定义小部件应该像一个小产品一样,拥有清晰的契约:

  • 传入 props:只传它渲染所需的内容。
  • 发出事件:只发出页面需要响应的事件。

避免在小部件内部随意读取全局状态或调用无关接口,除非那确实是契约的一部分。

在 AppMaster 风格的生成 Vue3 屏幕中,这通常意味着你在生成的屏幕里做最少的连线工作:传递 props 并处理事件。那些连线可能会在重新生成时改变,但它们通常小且易于重做。真正的工作应该安全地保存在自定义区。

与 Vue3 配合良好的隔离模式

目标很明确:重新生成应该可以自由替换生成的文件,而你的小部件代码保持不变。

一个实用的方法是把定制小部件作为一个小型内部模块:组件、样式和辅助工具放在同一位置。在生成的 Vue3 应用中,这通常意味着自定义代码位于生成页面之外,并作为依赖被导入。

包装器组件非常有用。让包装器与生成的应用对话:读取页面现有的数据结构,对其规范化,然后向小部件传入干净的 props。如果生成的数据结构后来改变,你通常只需更新包装器,而不是重写小部件。

一些经验证的模式包括:

  • 把小部件当作黑盒:props 进,事件出。
  • 使用包装器来映射 API 响应、日期和 ID 到小部件友好的格式。
  • 保持样式作用域化,避免生成页面意外覆盖你的组件样式。
  • 不要依赖父级 DOM 结构或页面特定的类名。

在样式方面,优先使用 scoped CSS(或 CSS Modules)并对共享类进行命名空间处理。如果小部件需要匹配应用主题,传入主题 token(颜色、间距、字体大小)作为 props,而不是直接导入页面样式。

插槽(slots)在保持小且可选时是安全的,例如显示“空状态”信息。如果插槽开始控制核心布局或行为,那说明你把小部件又移回了生成层——这正是重新生成带来痛苦的地方。

设计稳定的组件契约(props 与事件)

让重新生成无痛的最安全方法是把每个小部件当作稳定接口来对待。生成的屏幕会变化,你的组件不应该频繁变化。

从输入(props)开始。保持数量少、可预测并易于校验。优先使用简单的基本类型和你控制的普通对象。添加默认值,这样当页面尚未传值时组件也能良好表现。如果某些值可能格式不正确(例如 ID、日期字符串、类似枚举的值),对其校验并优雅失败:显示空状态而不是崩溃。

输出方面,标准化事件,使小部件对整个应用显得一致。常见且可靠的一组事件有:

  • update:modelValue 用于支持 v-model
  • change 用于用户确认的变化(不是每次按键)
  • error 当组件无法完成其工作时
  • ready 当异步工作完成且小部件可用时

如果涉及异步工作,就把它作为契约的一部分。暴露 loadingdisabled props,并考虑 errorMessage 来表示服务端失败。如果组件自己负责拉取数据,也要发出 errorready,以便父组件可以响应(展示提示、记录日志或降级 UI)。

可访问性(Accessibility)期望

在契约中内建可访问性。接受 label(或 ariaLabel)prop,记录键盘行为,并在操作后保持焦点可预测。

例如,仪表板上的时间线小部件应支持方向键在项目间切换、按 Enter 打开详情,并在对话框关闭时把焦点返回到打开对话框的控件。这使得小部件可以在多个重新生成的屏幕间复用而无需额外改动。

逐步操作:在不修改生成文件的情况下添加定制小部件

Ship one bespoke widget
在真实页面添加时间线或图表小部件,而无需冒险手动编辑。
构建仪表板

从小处开始:选择一个用户关心的页面和一个能显著提升体验的小部件。把第一次改动控制得很窄,可以更容易观察重新生成影响的范围。

  1. 在生成区外创建组件。 将它放在受你管理并纳入版本控制的文件夹(通常为 customextensions)。

  2. 保持公共接口小巧。 少量 props 进、少量事件出。不要传整个页面状态。

  3. 添加一个你也拥有的薄包装器。 它的工作是把“生成页面数据”翻译成小部件契约。

  4. 通过受支持的扩展点集成。 以不需要编辑生成文件的方式引用包装器。

  5. 重新生成并验证。 你的自定义文件夹、包装器和组件应该保持不变并能编译通过。

保持边界清晰。小部件关注显示与交互;包装器负责映射数据并转发动作。业务规则应保留在应用的逻辑层(后端或共享流程),不要埋在小部件内部。

一个有用的自查:若现在发生重新生成,是否能让同事重建应用并得到相同结果而不需要手动重做修改?如果可以,说明你的模式是稳健的。

把逻辑放在哪里以保持 UI 可维护

自定义小部件应该主要关心外观和用户输入的响应。把太多业务规则塞进小部件会降低可复用性、测试性和可变更性。

一个好的默认做法是:把业务逻辑放在页面或功能层,小部件保持“哑”状态。页面决定小部件获得什么数据以及小部件触发事件后发生什么。小部件负责渲染并报告用户意图。

当你确实需要靠近小部件的逻辑(格式化、小状态、客户端校验)时,把它隐藏在一个小的服务层后面。在 Vue3 中,这可以是一个模块、一个 composable 或一个具有清晰 API 的 store。小部件导入该 API,而不是随意引用应用内部。

一个实用的划分:

  • 小部件(组件):UI 状态、输入处理、视觉表现,发出 selectchangeretry 等事件。
  • 服务 / composable:数据整形、缓存、把 API 错误映射为用户消息。
  • 页面 / 容器:业务规则、权限、决定何时加载与保存数据。
  • 生成的应用部分:保持不动;向内传入数据并监听事件。

避免在小部件内部进行直接 API 调用,除非那确实是该小部件的契约。如果它负责获取数据,请令其明显(例如命名为 CustomerSearchWidget 并把调用代码集中在一个服务里)。否则,把 itemsloadingerror 等作为 props 传入。

错误信息应面向用户且一致。不要直接显示原始服务器文本,而是把错误映射到应用常用的一小套提示,例如“无法加载数据。请重试。”并在可能时提供重试操作,同时把详细错误记录到组件外部。

示例:自定义的 ApprovalBadge 小部件不应决定发票是否可审批。让页面计算出 statuscanApprove。徽章只发出 approve,页面执行真正的规则并调用后端(例如你在 AppMaster 中建模的 API),然后将清晰的成功或错误状态传回 UI。

在下一次重新生成后造成痛苦的常见错误

Mix no-code and Vue3
使用可视化 UI 构建器,然后将定制的 Vue 组件作为插件接入。
构建 Web 应用

大多数问题并不源自 Vue 本身,而是将自定义工作混入生成器拥有的地方,或依赖那些很可能改变的细节。

最常把小部件变成需要反复清理的问题的错误有:

  • 直接编辑生成的 Vue 文件并忘记记录改动。
  • 使用全局 CSS 或宽泛选择器,在标记变动时悄然影响其他页面。
  • 直接读取或修改生成的状态形状,使得一个无害的重命名会破坏小部件。
  • 把太多页面特定假设打包进一个组件。
  • 在没有迁移计划的情况下更改组件 API(props/事件)。

一个常见场景:你添加了一个自定义表格小部件并且运行良好。一个月后,生成的布局变化导致你的全局 .btn 规则影响到登录页或管理页面。或者数据对象从 user.name 变为 user.profile.name,导致小部件静默失败。问题不在小部件本身,而在于依赖不稳定的细节。

两种习惯能防止大部分问题:

首先,把生成的代码当作只读,保持自定义文件独立并有明确的导入边界。

其次,保持组件契约小而明确。如果需要演进,添加简单的版本属性(例如 apiVersion)或在一个发布周期内同时支持新旧 prop 形状。

发布前的快速检查清单

Use built-ins first
优先使用内建的认证和支付模块,然后在关键处自定义 UI。
使用模块

在把定制小部件合并到生成的 Vue3 应用前,做个快速现实检验。它应该能在下一次重新生成后幸存,且他人能复用它。

  • 重新生成测试: 运行完整的重新生成并重建。如果你不得不重新编辑生成的文件,边界就是错的。
  • 清晰的输入与输出: props 进、事件出。避免像抓取外部 DOM 或假定特定页面 store 这样的隐式依赖。
  • 样式隔离: 作用域化样式并使用清晰的类前缀(例如 timeline-)。
  • 所有状态均有表现: loading、error 与 empty 状态应存在且合理。
  • 可复用而不复制: 验证是否可以通过改变 props 与事件处理器把它放到第二个页面,而不是复制内部实现。

一个验证方法是:想象把小部件放到管理页面和客户门户页面。如果两处仅通过 props 和事件处理就能工作,说明设计安全。

一个现实的例子:在仪表板中添加时间线小部件

支持团队往往想要一个页面来讲述工单的故事:状态变化、内部备注、客户回复以及付款或交付事件。时间线小部件很合适,但你不希望编辑生成的文件并在下一次重新生成后丢失工作。

安全做法是把小部件隔离在生成 UI 之外,并通过一个薄包装器把它放入页面。

小部件契约

保持简单且可预测。例如,包装器传入:

  • ticketId(字符串)
  • range(最近 7 天、最近 30 天、自定义)
  • mode(紧凑或详细)

小部件发出:

  • select 当用户点击某个事件
  • changeFilters 当用户调整范围或模式

这样小部件不需要了解仪表板页面、数据模型或请求如何发起。它渲染时间线并报告用户操作。

包装器如何将其连接到页面

包装器位于仪表板旁边,把页面数据翻译成契约。它从页面状态读取当前工单 ID,将 UI 过滤条件转换为 range,并把后端记录映射为小部件期望的事件格式。

当小部件发出 select 时,包装器可以打开详情面板或触发页面动作;当发出 changeFilters 时,包装器更新页面过滤并刷新数据。

当仪表板 UI 被重新生成时,小部件保持不变,因为它位于生成文件之外。通常只有在页面重命名字段或改变存储过滤方式时才需要更新包装器。

测试与发布习惯以防止意外

Practice safe regeneration
从第一天起就以小型小部件为先构建页面并测试重新生成。
开始项目

自定义组件通常以平凡的方式失败:prop 形状改变、事件不再触发,或生成页面比小部件预期的更频繁重渲染。以下习惯能早期捕获这些问题。

本地测试:及早发现边界破裂

把生成的 UI 与小部件之间的边界当作一个 API。先在隔离环境中测试小部件,使用与契约相符的硬编码 props。

用“正常路径” props 和缺失值渲染它。模拟关键事件(保存、取消、选择)并确认父组件正确处理。测试慢速数据和小屏幕。验证除非契约要求,否则小部件不写入全局状态。

如果你在 AppMaster 的 Vue3 Web 应用上构建,请在重新生成任何东西之前先做这些检查。诊断边界问题时同时改动两项会更难排查。

重新生成后的回归:先检查什么

每次重新生成后,优先检查触点:是否仍然传递相同的 props,页面是否仍然处理相同的事件?通常故障最先在这里暴露。

保持引入方式可预测。避免依赖可能移动的脆弱导入路径。为自定义组件使用一个稳定的入口点。

在生产环境中,在小部件内部添加轻量的日志与错误捕获:

  • 挂载时记录关键 props(经清洗)
  • 契约违规(缺失必需 prop、类型错误)
  • 失败的 API 调用并带短错误码
  • 意外的空状态

当出现问题时,你希望能快速判定:是重新生成改变了输入,还是小部件自身发生了变化?

下一步:让模式在全应用中可重复

一旦第一个小部件可行,真正的收益是把该模式变成可重复的流程,而不是一次性的解决方案。

为小部件契约创建一份简短的内部规范并把它写在团队文档里。保持简单:命名方式、必需与可选 props、一个小事件集、错误行为以及明确的归属(哪些属于生成 UI,哪些属于自定义文件夹)。

也把边界规则用白话写出来:不要编辑生成文件,保持自定义代码隔离,仅通过 props 与事件传递数据。它能阻止那种“临时修复”变成永久维护负担的情况。

在构建第二个小部件前,做一次小规模的重新生成试验。发布第一个小部件,然后在正常变更中至少重新生成两次(例如标签变更、布局变更、新字段),确认没有破坏发生。

如果你使用 AppMaster,通常更好的做法是把大部分 UI 与逻辑保留在可视化编辑器(UI 构建器、Business Process Editor 和 Data Designer)中。把自定义 Vue 组件保留给那些编辑器无法表达的真正定制化小部件,例如特殊的时间线、交互复杂的图表或异常输入控件。要开始实践这种方式,AppMaster 在 appmaster.io 上的设计以重新生成为中心,使得把自定义小部件隔离开来成为一种自然的工作流程。

常见问题

Why do my UI changes disappear after I regenerate the app?

编辑生成的 Vue 文件存在风险,因为重新生成可能会完全覆盖这些文件。即使某次改动幸存,一次在可视化构建器中的小改动也可能重建模板并抹去你的手动修改。

How can I customize a generated UI without losing work on the next regenerate?

把所有手写的 Vue 代码放在一个单独且受你管理的文件夹(比如 customextensions),并作为依赖导入。把生成的页面当作只读输出,只通过一个小而稳定的接口与组件连接。

What is a wrapper component and why does it help with generated screens?

包装器是你拥有的一个薄组件,位于生成页面和你的小部件之间。它把页面的数据结构翻译成干净的 props,并把小部件的事件转换为页面动作,这样当生成的数据变化时通常只需更新包装器即可。

What’s the safest way to design props and events for a reusable widget?

保持契约小而清晰:少量 props 提供组件需要的数据,少量事件用于报告用户意图。优先使用简单值与你可控的对象,添加默认值,对输入进行校验,并在出错时以空状态优雅降级而不是抛出异常。

When should I emit `update:modelValue` vs `change` from a custom component?

update:modelValue 适用于组件表现为表单控件并希望支持 v-model 的场景。change 更适合“已确认”的操作,例如用户点击保存或完成选择,这样父组件不会处理每一次按键事件。

How do I prevent my widget’s CSS from breaking other generated pages?

将你的组件样式作用域化并使用明确的类前缀,防止重新生成的页面意外覆盖你的 CSS。如果需要匹配应用主题,传入主题 token(颜色、间距、字号)作为 props,而不是依赖页面级样式。

Where should business logic live if I add custom UI components?

默认把业务规则放在组件外面。让页面或后端决定权限、校验规则与保存行为,而组件专注于渲染和交互,并通过事件(如 selectretryapprove)上报用户意图。

What are the most common reasons custom components break after regeneration?

避免依赖不稳定的细节,例如生成的文件路径、父 DOM 结构或内部状态对象形状。如果确实需要这些,应该把它们隐藏在包装器中,这样诸如 user.name 变成 user.profile.name 时不至于迫使你重写小部件。

What should I test before and after I regenerate to catch issues early?

先在隔离环境中使用硬编码 props 测试小部件,包括正常路径与缺失或格式错误的值。重新生成后优先检查两个关键点:页面是否仍然传递相同的 props,以及页面是否仍然处理相同的事件。

When is it worth building a bespoke Vue widget instead of using built-in UI blocks?

并非所有界面都需要自定义代码;当可视化构建器无法表达的需求出现时才值得使用自定义组件,例如复杂图表、地图拾取器、签名板或拖放排期器。如果需求能通过调整生成的 UI 与流程实现,长期维护通常更容易。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧