1. 为什么我决定把“写代码”这件事交出去
1.1 一个让我彻底改变工作流的契机
去年下半年,我手上同时压着三个内部工具的需求。一个是运营团队要的数据看板,一个是客服组要的工单统计面板,还有一个是给管理层看的周报汇总页。三个东西本质上都是同一类活:拉数据、做展示、加筛选、配图表。功能不复杂,但每个都要单独搭一套前端,从路由到状态管理到图表库选型,重复劳动量大得让人抓狂。
那段时间我试过一个做法:把每个看板的页面结构、组件拆分、数据接口定义全部写成详细的注释和伪代码,然后让AI帮我补全具体实现。结果出乎意料地好用。我负责定义“这个页面长什么样、数据从哪来、交互怎么走”,AI负责把那些我闭着眼睛都能写但写起来很烦的模板代码填进去。一个原本需要两天才能搭完的看板,压缩到了半天。
这件事让我开始认真思考一个问题:在Dashboard这类高度模式化的项目里,我到底应该把精力花在哪里?是花在写那些千篇一律的表格渲染、图表配置、响应式布局上,还是花在定义数据流转逻辑、交互规则、模块边界上?
答案很明显。代码可以由AI来写,但逻辑必须由我来定。这就是我这套“可插拔轻量级Dashboard”实践的起点。
1.2 这套方案到底解决了什么问题
先说清楚这套东西是什么。它是一个基于配置驱动的Dashboard搭建方案,核心思路是把看板拆成三个独立层:
- 数据层:负责定义数据源、请求方式、字段映射
- 逻辑层:负责定义筛选联动、模块间通信、状态流转
- 展示层:负责定义布局、组件类型、样式主题
三层之间通过一套约定好的接口协议通信,任何一层都可以单独替换或扩展。你想换个图表库?只动展示层的组件映射就行。你想把REST接口换成GraphQL?只动数据层的适配器就行。你想加一个新的筛选维度?在逻辑层注册一个事件通道就行。
它适合谁呢?我总结了三类人:
第一类是中后台开发者,手上有一堆类似的管理面板、数据看板要做,不想每次都从头搭架子。第二类是全栈但偏后端的工程师,前端只会写基础的东西,需要一个能快速出活又不至于太丑的方案。第三类是对AI辅助编程已经有一定经验,想找一个具体场景把“AI写代码、人定逻辑”这个模式跑通的人。
不适合谁?如果你的看板需要极其复杂的自定义交互、大量非标准化的可视化效果,那这套轻量方案可能会束缚你。它的定位是“80%的常见看板场景”,不是万能银弹。
1.3 为什么是“可插拔”和“轻量级”这两个关键词
“可插拔”这个词我用得很谨慎。市面上很多号称可插拔的方案,实际上是把耦合点藏得更深了,你换一个组件要改五个地方。我这套方案的可插拔性体现在一个很具体的设计上:每个模块都是一个独立的自描述单元。
什么意思?一个模块包含自己的元信息(叫什么名字、占多大位置、需要什么数据)、自己的渲染逻辑(用什么组件画)、自己的数据消费声明(订阅哪些数据源、监听哪些事件)。主框架只负责根据元信息把模块放到正确的位置,把正确的数据喂给它,其他的什么都不管。
“轻量级”则体现在依赖上。整个方案的核心运行时不超过500行代码,不依赖任何重型框架。你可以把它理解成一个“看板操作系统”,它只提供最基础的调度能力,具体的功能全部由模块自己带进来。这样做的好处是,你不需要为了用一个看板去学一整套框架的API,只需要理解模块的生命周期和通信协议就够了。
2. 整体架构设计与核心思路拆解
2.1 三层分离架构的底层逻辑
我先把整体架构说清楚。这套Dashboard的核心是一个事件驱动的模块调度器,它维护着三个注册表:
- 数据源注册表:记录每个数据源的名称、请求函数、缓存策略、刷新间隔
- 模块注册表:记录每个模块的ID、类型、布局信息、数据依赖声明
- 事件通道注册表:记录每个事件通道的名称、当前值、订阅者列表
当页面初始化时,调度器按顺序做几件事:先根据数据源注册表发起初始请求,然后根据模块注册表把模块实例化并挂载到布局容器里,最后根据事件通道注册表建立模块间的通信链路。之后所有的更新都是事件驱动的:某个筛选器变了,它往对应的事件通道推一个新值,订阅了这个通道的模块收到通知后重新拉数据或重新渲染。
这个设计的核心考量是解耦。模块之间不直接通信,都通过事件通道中转。数据源不关心谁在消费它,只负责按约定返回数据。布局容器不关心模块内部怎么实现,只负责按元信息摆放位置。每一层都可以独立测试、独立替换。
注意:事件通道的设计要避免“事件风暴”。我的经验是,通道数量控制在10个以内,每个通道的语义要足够粗粒度。比如“时间范围变化”是一个通道,“当前选中的部门”是另一个通道。不要为每个筛选器都建一个通道,那样会导致订阅关系爆炸。
2.2 模块协议:一个模块需要声明什么
每个模块在注册时,需要提供一个符合协议的描述对象。我用一个简化的例子来说明:
{ id: 'sales-trend', name: '销售趋势', layout: { col: 1, row: 1, span: 2 }, data: { source: 'salesApi', params: ['dateRange', 'department'], transform: (raw) => raw.list.map(item => ({ date: item.stat_date, value: item.amount })) }, events: { listens: ['dateRange', 'department'], emits: [] }, render: (container, data) => { // 具体的渲染逻辑,可以用任何图表库 } }这个描述对象里,data.params声明了这个模块依赖哪些事件通道的值作为请求参数。当这些通道的值发生变化时,调度器会自动重新调用数据源,拿到新数据后调用transform做转换,最后把结果传给render。
layout字段告诉布局容器这个模块占几列几行。我用的是简单的网格布局,12列栅格,模块通过span指定宽度。这个设计足够覆盖大部分看板场景,又不会引入复杂的布局计算。
render函数是唯一跟具体UI库耦合的地方。你可以用ECharts、用Chart.js、甚至直接用原生DOM画。调度器只负责把数据和容器传进来,怎么画是你的事。
2.3 数据源适配器的设计要点
数据源适配器是这套方案里最容易被低估的部分。很多人觉得“不就是发个请求吗”,但实际上,看板场景下的数据请求有几个特殊需求:
第一是缓存。同一个数据源可能被多个模块消费,如果每个模块都发一次请求,既浪费带宽又可能导致数据不一致。我的做法是在适配器层做请求去重和短期缓存。同一个数据源在500毫秒内的多次请求合并成一次,结果缓存30秒(可配置)。
第二是错误处理。看板上的某个模块数据拉取失败,不应该影响其他模块。适配器需要把错误封装成模块能理解的形式,让模块自己决定是显示错误提示还是用上一次的数据兜底。
第三是参数序列化。事件通道里的值可能是对象、数组、日期范围,适配器需要把它们统一序列化成请求参数。我写了一个简单的序列化规则:日期转时间戳、数组转逗号分隔、对象转JSON字符串。这个规则可以通过配置覆盖。
const dataSources = { salesApi: { request: async (params) => { const res = await fetch('/api/sales?' + serialize(params)); return res.json(); }, cache: 30000, dedupe: 500, fallback: { list: [] } } }2.4 为什么不用现成的低代码平台
这个问题我被问过很多次。市面上确实有很多低代码看板搭建工具,拖拖拽拽就能出效果。但我最终选择自己搭一套,原因有三个:
第一是可控性。低代码平台的黑盒程度太高,出了问题你只能等平台方修。而且当你想实现一个平台没提供的功能时,要么放弃,要么用很别扭的方式绕过去。自己搭的方案,每一行代码你都能改。
第二是AI协作的便利性。低代码平台的配置是图形化的,AI很难直接参与。而我这套方案的核心是JavaScript对象和函数,AI可以很自然地理解“这个模块需要什么数据、怎么渲染”,然后帮你补全代码。我实测下来,用AI生成一个模块的完整实现,平均只需要3到5轮对话。
第三是学习成本。低代码平台看似上手快,但当你需要做稍微复杂一点的事情时,学习曲线会陡然上升。而这套方案的核心概念只有三个:数据源、模块、事件通道。理解这三个概念之后,剩下的就是写普通的JavaScript。
3. 核心细节解析与实操要点
3.1 模块生命周期与挂载流程
一个模块从注册到渲染到销毁,经历以下几个阶段:
注册阶段:模块把自己的描述对象交给调度器,调度器校验必填字段(id、layout、render),然后存入模块注册表。这个阶段不涉及任何DOM操作。
解析阶段:调度器遍历所有模块,收集它们声明的数据源依赖和事件通道依赖。对于每个数据源,检查是否已经注册;对于每个事件通道,检查是否存在。如果发现未注册的依赖,抛出明确的错误信息,而不是静默失败。
挂载阶段:调度器根据layout信息计算每个模块在网格中的位置,创建容器元素,然后调用模块的render函数。如果模块声明了数据依赖,调度器会先发起数据请求,拿到数据后再调用render。
更新阶段:当某个事件通道的值变化时,调度器找出所有订阅了该通道的模块,重新执行数据请求和渲染流程。这里有一个优化点:如果模块的render函数支持增量更新(比如ECharts的setOption),可以在模块描述里声明updateMode: 'patch',调度器会传入上一次的数据和容器,让模块自己做差异更新。
销毁阶段:当模块被移除时,调度器调用模块的destroy函数(如果有),清理定时器、事件监听、图表实例等资源。
实操心得:render函数的第一个参数是容器元素,第二个参数是数据对象。我建议在render函数内部不要直接操作全局状态,所有需要的数据都通过参数传入。这样模块才是真正可测试、可复现的。
3.2 事件通道的命名规范与使用技巧
事件通道是模块间通信的唯一途径,所以命名规范很重要。我总结了一套简单的规则:
- 通道名用驼峰命名,语义要具体但不过于细碎
- 值类型要稳定,不要一个通道有时候传字符串有时候传对象
- 通道的初始值要在注册时指定,避免模块首次渲染时拿到undefined
举个例子,一个典型的看板可能有这些通道:
| 通道名 | 类型 | 初始值 | 说明 |
|---|---|---|---|
| dateRange | [number, number] | [今天零点, 现在] | 时间范围 |
| department | string | 'all' | 选中的部门 |
| metric | string | 'sales' | 当前查看的指标 |
| refreshToken | number | 0 | 手动刷新触发器 |
refreshToken这个通道很有意思。它的值就是一个递增的数字,每次用户点击刷新按钮就加一。订阅了这个通道的模块会重新拉数据。这种“信号型”通道在需要手动触发更新的场景下很好用。
另一个技巧是通道的派生。有时候一个通道的值可以从其他通道计算出来,不需要单独维护。比如“当前季度的起止时间”可以从dateRange派生。我的做法是在调度器里支持注册派生通道,指定它的计算函数和依赖的源通道。当源通道变化时,派生通道自动更新。
3.3 数据转换层的设计
数据从接口返回到传给render函数之间,有一个转换层。这个层的职责是把后端返回的原始数据整理成模块需要的格式。我见过很多项目把转换逻辑散落在各个模块的render函数里,导致同样的转换逻辑重复写好几遍。
我的做法是在模块描述里声明transform函数,调度器在数据请求完成后自动调用它。transform函数接收原始数据,返回转换后的数据。如果transform抛出异常,调度器会捕获并记录日志,同时把原始数据传给render函数作为兜底。
transform: (raw) => { if (!raw || !raw.list) return { items: [] }; return { items: raw.list.map(item => ({ label: item.name, value: Number(item.value) || 0, percent: raw.total ? (item.value / raw.total * 100).toFixed(1) : '0.0' })), total: raw.total || 0 }; }这个设计的好处是,转换逻辑可以被单独测试。你可以写一个单元测试,输入模拟的原始数据,断言转换后的输出。这比在render函数里调试要高效得多。
3.4 布局系统的实现细节
布局系统我用的是最朴素的12列栅格。每个模块通过layout: { col, row, span }声明自己的位置和宽度。col和row是从1开始的网格坐标,span是占用的列数。
调度器在挂载阶段会做一次布局计算:遍历所有模块,按row分组,同一行内的模块按col排序,然后计算每个模块的实际像素位置。如果同一行内的模块span总和超过12,调度器会发出警告并自动换行。
这个布局系统的优点是简单直观,缺点是灵活性有限。如果你需要更复杂的布局(比如嵌套网格、绝对定位),可以在模块的render函数里自己实现,调度器只负责给它一个容器。
注意:布局计算要在窗口resize时重新执行。我建议用ResizeObserver监听容器尺寸变化,而不是监听window的resize事件。前者更精确,后者在移动端会有各种问题。
3.5 主题与样式的处理方式
主题这块我走的是CSS变量路线。调度器在初始化时读取主题配置,把颜色、字体、间距等变量写入根元素的style属性。模块的render函数里直接用var(--color-primary)这样的变量,不需要关心当前是什么主题。
const themes = { light: { '--color-bg': '#ffffff', '--color-text': '#1a1a1a', '--color-primary': '#2563eb', '--color-border': '#e5e7eb' }, dark: { '--color-bg': '#1a1a1a', '--color-text': '#f5f5f5', '--color-primary': '#60a5fa', '--color-border': '#374151' } }切换主题时,调度器只需要更新根元素的CSS变量,所有模块的样式会自动跟着变。这个方案的好处是零成本适配,不需要每个模块都写主题切换逻辑。
4. 实操过程与核心环节实现
4.1 从零搭建一个看板的完整流程
假设我要做一个销售数据看板,包含三个模块:顶部筛选栏、销售趋势折线图、部门销售排行表格。我按以下步骤操作:
第一步:定义数据源。我需要两个接口:一个返回销售趋势数据,一个返回部门排行数据。在数据源注册表里声明:
const dataSources = { salesTrend: { request: (params) => fetch(`/api/trend?start=${params.dateRange[0]}&end=${params.dateRange[1]}&dept=${params.department}`).then(r => r.json()), cache: 60000 }, deptRank: { request: (params) => fetch(`/api/rank?start=${params.dateRange[0]}&end=${params.dateRange[1]}`).then(r => r.json()), cache: 60000 } }第二步:定义事件通道。筛选栏需要控制时间范围和部门,所以注册两个通道:
const channels = { dateRange: { initial: [startOfToday(), now()] }, department: { initial: 'all' } }第三步:实现筛选栏模块。这个模块不消费数据,只负责发出事件:
{ id: 'filter-bar', layout: { col: 1, row: 1, span: 12 }, events: { emits: ['dateRange', 'department'] }, render: (container, _, emit) => { // 渲染日期选择器和部门下拉框 // 用户操作时调用 emit('dateRange', newRange) } }第四步:实现趋势图模块。它订阅两个通道,消费salesTrend数据源:
{ id: 'trend-chart', layout: { col: 1, row: 2, span: 8 }, data: { source: 'salesTrend', params: ['dateRange', 'department'], transform: (raw) => raw.list.map(i => [i.date, i.amount]) }, events: { listens: ['dateRange', 'department'] }, render: (container, data) => { // 用ECharts画折线图 } }第五步:实现排行表格模块。它只订阅dateRange,消费deptRank数据源:
{ id: 'dept-table', layout: { col: 9, row: 2, span: 4 }, data: { source: 'deptRank', params: ['dateRange'], transform: (raw) => raw.list }, events: { listens: ['dateRange'] }, render: (container, data) => { // 渲染表格 } }第六步:启动调度器。把上面所有配置传给调度器,调用init()方法,看板就跑起来了。
整个流程下来,我写的代码里没有一行是重复的模板代码。每个模块只关心自己的数据需求和渲染逻辑,通信和调度全部由框架处理。
4.2 用AI辅助生成模块代码的实操记录
这是我重点想分享的部分。我的工作流是这样的:
第一轮对话:我把模块的描述对象骨架给AI,包括id、layout、data.source、events,但render函数留空。我告诉AI:“这是一个看板模块的描述对象,请帮我实现render函数,用ECharts画一个折线图,数据格式是[[日期, 数值], ...]。”
AI返回的代码通常能用,但会有一些小问题,比如没有处理容器尺寸变化、没有销毁图表实例。
第二轮对话:我把问题反馈给AI:“render函数在模块更新时会被重复调用,需要先销毁上一次的图表实例。另外容器尺寸变化时图表要自适应。”AI会补上dispose和resize的逻辑。
第三轮对话:我让AI加上加载状态和错误状态的显示。AI会在render函数里加一些条件判断,根据data的状态显示不同的内容。
三轮下来,一个完整的模块实现就出来了。我做的事情是:定义接口、审查逻辑、提出改进点。AI做的事情是:写具体的实现代码。这个分工的效率比我全部自己写要高很多,而且AI写的代码风格一致,不会出现“这个模块用这种写法、那个模块用那种写法”的问题。
实操心得:给AI的描述越具体,返回的代码质量越高。不要只说“画一个折线图”,要说“用ECharts的line系列,x轴是日期类型,y轴是数值类型,数据格式是二维数组,需要支持tooltip和dataZoom”。这些细节AI不会自己猜,你说了它才会做。
4.3 参数计算与配置项的选择依据
在搭建过程中,有几个参数需要根据实际情况计算和选择:
缓存时间:我默认设30秒。这个值的依据是,看板数据通常不需要秒级实时性,30秒的缓存可以显著减少请求量,同时用户感知不到明显的延迟。如果某个数据源变化频繁(比如实时监控),可以单独设为5秒或关闭缓存。
请求去重窗口:默认500毫秒。这个值的依据是,用户操作筛选器时可能会连续触发多次变化(比如拖动日期选择器),500毫秒的窗口可以把这些连续变化合并成一次请求。
布局断点:我设了三个断点:小于768px时所有模块占满12列,768到1200px时占6列,大于1200px时按声明的span显示。这个断点划分参考了常见设备的屏幕宽度分布。
事件通道的初始值:日期范围默认是“今天零点到当前时间”,部门默认是“全部”。这些初始值要保证模块首次渲染时能拿到有意义的数据,而不是空值导致报错。
4.4 一个完整模块的代码实现示例
下面是一个完整的趋势图模块实现,包含了数据转换、图表渲染、更新处理、销毁清理:
const trendChartModule = { id: 'trend-chart', name: '销售趋势', layout: { col: 1, row: 2, span: 8 }, data: { source: 'salesTrend', params: ['dateRange', 'department'], transform: (raw) => { if (!raw || !Array.isArray(raw.list)) return []; return raw.list.map(item => [item.date, Number(item.amount) || 0]); } }, events: { listens: ['dateRange', 'department'], emits: [] }, render: (container, data, emit, context) => { let chart = context.chartInstance; if (!chart) { chart = echarts.init(container); context.chartInstance = chart; window.addEventListener('resize', () => chart.resize()); } chart.setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'time' }, yAxis: { type: 'value' }, series: [{ type: 'line', smooth: true, data: data, areaStyle: { opacity: 0.1 } }] }); }, destroy: (context) => { if (context.chartInstance) { context.chartInstance.dispose(); context.chartInstance = null; } } }这个实现里,context是调度器传给模块的上下文对象,用来存放模块自己的状态(比如图表实例)。这样模块不需要用全局变量,也不会和其他模块冲突。
5. 常见问题与排查技巧实录
5.1 模块不更新或更新异常
这是最常见的问题。排查思路按以下顺序进行:
第一,检查事件通道是否注册。如果模块订阅了一个不存在的通道,调度器会在初始化时报错。但如果你是在运行时动态添加通道,可能不会触发校验。我的做法是在调度器的emit方法里加一个检查:如果通道不存在,打印警告并忽略这次emit。
第二,检查params声明是否正确。模块的data.params数组里列出的通道名,必须和实际注册的通道名完全一致。大小写错误、拼写错误都会导致参数取不到值。我建议在开发模式下,调度器对每个params项做一次存在性检查,不存在就报错。
第三,检查transform是否抛异常。如果transform函数报错,调度器会捕获并记录,但模块可能拿到的是undefined。我建议在transform里加防御性判断,对空数据返回一个合理的默认值。
第四,检查render函数是否支持重复调用。有些图表库在重复init时会报错,需要在render函数里判断实例是否已存在。用context缓存实例是一个好办法。
5.2 数据请求失败的处理策略
看板上的数据请求失败是常态,不能让它导致整个页面崩溃。我的处理策略分三级:
第一级:模块级兜底。每个数据源可以配置fallback值,请求失败时返回这个值。模块拿到fallback数据后正常渲染,只是数据可能是空的或过期的。
第二级:错误提示。如果模块希望显示错误状态,可以在render函数里检查data是否包含错误标记。调度器在请求失败时会把错误信息放在data的__error字段里。
第三级:全局降级。如果多个数据源同时失败,可能是网络问题。调度器可以触发一个全局的“离线模式”,暂停所有请求,显示一个统一的提示条。
注意:不要在每个模块里单独写错误处理逻辑,那样会导致代码重复且不一致。把错误处理放在调度器层,模块只需要根据data的状态决定怎么展示。
5.3 性能问题的排查与优化
看板性能问题通常表现为:首次加载慢、筛选操作卡顿、内存占用高。我遇到过几次,排查下来主要是这几个原因:
原因一:请求过多。每个模块独立发请求,没有去重和缓存。解决办法是在数据源层加缓存和去重,我前面已经说过了。
原因二:渲染过重。某个模块的render函数里做了大量计算,或者图表配置过于复杂。解决办法是把计算逻辑移到transform里,render只负责画图。另外,对于不需要实时更新的模块,可以设置updateMode: 'manual',只有手动触发时才重新渲染。
原因三:事件监听未清理。模块销毁时没有移除window上的resize监听,导致内存泄漏。解决办法是在destroy函数里清理所有监听器。我建议调度器提供一个onDestroy钩子,模块把清理逻辑注册进去。
原因四:图表实例未销毁。ECharts实例不销毁会一直占用内存。解决办法是在destroy函数里调用dispose。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模块不显示 | layout配置错误 | 检查col/row/span是否超出网格范围 | 修正layout配置 |
| 数据不更新 | 事件通道未触发 | 在emit处打日志 | 检查订阅关系 |
| 图表重叠 | 容器尺寸为0 | 检查父容器是否有宽高 | 给容器设置最小高度 |
| 请求重复发送 | 缓存未生效 | 检查cache配置 | 确认数据源名称一致 |
| 主题切换无效 | CSS变量未更新 | 检查根元素style | 确认变量名一致 |
| 移动端布局错乱 | 断点未适配 | 检查媒体查询 | 调整断点或span |
5.5 几个我踩过的坑
坑一:在render函数里直接修改data。有一次我在render里对data做了排序,结果下一次更新时数据顺序乱了。教训是render函数应该是纯函数,只读不写。
坑二:事件通道的值类型不一致。dateRange通道有时候传数组,有时候传对象,导致订阅者处理逻辑复杂。教训是通道的值类型一旦确定就不要改。
坑三:模块ID重复。两个模块用了同一个ID,调度器后注册的覆盖了先注册的。教训是调度器应该在注册时检查ID唯一性,重复就报错。
坑四:忘记处理空数据。接口返回空数组时,图表渲染报错。教训是transform里要对空数据做处理,返回一个合理的默认结构。
坑五:resize监听未防抖。窗口拖动时resize触发太频繁,图表重绘卡顿。教训是resize处理要加防抖,我一般设200毫秒。
6. 扩展思路与个人体会
6.1 这套方案还能怎么扩展
目前这套方案覆盖了看板搭建的核心需求,但还有几个方向可以继续扩展:
方向一:模块市场。把常用的模块(折线图、柱状图、表格、指标卡、筛选栏)做成标准件,新项目直接引入。更进一步,可以做一个模块注册中心,团队内部共享模块。
方向二:配置持久化。把看板的配置(模块列表、布局、数据源映射)存到后端,支持用户自定义看板。用户拖拽调整布局后,配置保存到数据库,下次打开时恢复。
方向三:服务端渲染。对于首屏性能要求高的场景,可以在服务端预执行数据请求和transform,把结果嵌入HTML,客户端只负责渲染。
方向四:AI自动生成模块。结合大语言模型,用户用自然语言描述需求,AI生成模块描述对象和render函数,人工审核后注册到看板。这个方向我已经在尝试,目前的效果是:简单模块(指标卡、表格)可以全自动生成,复杂模块(联动图表)还需要人工调整。
6.2 关于“AI写代码、人定逻辑”的一些真实感受
这套方案跑通之后,我最大的感受是:AI不会取代开发者,但会改变开发者的工作重心。以前我花80%的时间写代码,20%的时间想逻辑。现在我花20%的时间写代码(主要是定义接口和审查AI的输出),80%的时间想逻辑(数据怎么流转、模块怎么拆分、边界怎么定义)。
这个转变对能力的要求其实更高了。因为写代码是确定性劳动,你想清楚了就能写出来。但定逻辑是不确定性劳动,你需要考虑各种边界情况、异常场景、扩展需求。AI可以帮你写代码,但没法帮你做这些判断。
另一个感受是:好的架构设计比好的代码实现更重要。在这套方案里,调度器的代码只有几百行,但它的设计决定了整个系统的可扩展性。如果调度器的接口设计得不好,模块再怎么写都别扭。所以我现在会把更多精力花在接口设计上,而不是具体实现上。
6.3 给想尝试这套方案的人的建议
如果你打算在自己的项目里尝试这套方案,我有几个建议:
从小处开始。不要一上来就搭一个完整的看板系统,先做一个最简单的模块,跑通数据源、事件通道、渲染的完整链路。确认这套模式符合你的需求后,再逐步扩展。
重视接口设计。模块描述对象的字段一旦确定,后续所有模块都要遵循。所以在设计阶段要多花时间,考虑清楚哪些字段是必须的、哪些是可选的、字段的类型和语义是什么。
建立模块库。把常用的模块沉淀下来,形成团队内部的模块库。这样新项目可以直接复用,不需要每次从头写。
保持轻量。这套方案的核心价值在于轻量和灵活。不要为了追求功能全面而引入复杂的依赖和抽象。每加一个功能,都要问自己:这个功能是必须的吗?有没有更简单的实现方式?
拥抱AI但保持审查。AI生成的代码一定要审查,特别是涉及数据安全和业务逻辑的部分。我的做法是:AI生成的代码必须通过单元测试才能合并,关键模块还要人工review。
这套方案我用了大半年,迭代了三个版本,目前支撑着内部七八个看板的运行。它的代码量不大,但确实解决了我“重复搭看板”的痛点。如果你也有类似的困扰,不妨试试这个思路。代码交给AI写,逻辑自己定,把精力花在真正有价值的地方。