架构概览
Kupola 是一个零运行时依赖的声明式 UI 组件系统。核心由 5 个引擎模块协作驱动。
kupolaBootstrap()
DOMContentLoaded / setTimeout(0)
↓
kupolaData
Proxy 双向绑定
data-bind
kupolaTheme
暗/亮主题切换
data-theme
ComponentInit
MutationObserver 自动发现
data-dropdown
五大核心引擎
| 模块 | 文件 | 职责 | 行数 |
|---|---|---|---|
| Initializer | js/initializer.js | MutationObserver 组件自动发现 + 生命周期管理 | ~200 |
| DataBind | js/data-bind.js | Proxy 响应式绑定 + EventBus + Store + ref() | ~1360 |
| KupolaCore | js/kupola-core.js | 组件注册表 + Bootstrap 编排 + Mixin | ~130 |
| GlobalEvents | js/global-events.js | 统一事件委托 + 作用域清理 | ~280 |
| Theme | js/theme.js | 暗/亮主题 + 11 种品牌色 | ~200 |
目录结构
kupola/
├── js/ # 核心 JS 模块(ES modules)
│ ├── initializer.js # 组件自动发现引擎
│ ├── kupola-core.js # 组件注册 + Bootstrap 编排
│ ├── component.js # KupolaComponent 基类
│ ├── registry.js # KupolaComponentRegistry 类注册表
│ ├── kupola-lifecycle.js# 页面级生命周期钩子
│ ├── data-bind.js # Proxy 数据绑定 + EventBus + Store + ref()
│ ├── global-events.js # 全局事件管理(作用域清理)
│ ├── theme.js # 主题/品牌色引擎
│ ├── icons.js # SVG 图标系统
│ ├── validation.js # 表单验证引擎
│ ├── form.js # KupolaForm 高级表单
│ ├── utils.js # Tree-shakable 工具库(命名导出)
│ ├── depends.js # useDeps 声明式数据依赖 + HTTP 插件
│ ├── table.js # KupolaTable 复杂表格
│ └── [component].js # 各 UI 组件(dropdown, modal, ...)
├── css/ # 样式源文件
│ ├── kupola.css # 入口(@import 汇总)
│ ├── scaffold.css # 基础重置 + 排版
│ ├── colors-and-type.css# CSS 变量 + 色彩系统
│ ├── theme-dark.css # 暗色主题变量
│ ├── theme-light.css # 亮色主题变量
│ ├── brand-themes.css # 11 种品牌色
│ ├── components.css # 组件样式
│ ├── components-ext.css # 扩展组件样式
│ └── table.css # 表格组件样式
├── src/index.js # Rollup/Vite 入口,re-export 所有 API
├── adapters/ # HTTP 客户端适配器(axios, navios-http)
├── plugins/ # Vite 插件
├── types/kupola.d.ts # TypeScript 声明
├── scripts/ # 构建脚本
└── dist/ # 构建产物(gitignored)
启动流程
Kupola 在页面加载时自动 bootstrap,按严格顺序初始化各子系统。
DOMContentLoaded
│
▼
kupolaBootstrap() ← js/kupola-core.js
│
├── 1. kupolaData.loadPersisted() 加载 localStorage 持久化数据
├── 2. kupolaData.bind() 扫描 data-bind 元素,建立 Proxy 绑定
├── 3. initTheme() 读取 data-theme,注入 CSS 变量
├── 4. kupolaInitializer.initializeAll() 函数式组件初始化
│ └── MutationObserver 监听 DOM 变化
└── 5. kupolaRegistry.bootstrap() 类式组件实例化
└── 自动发现 + 懒加载
关键点: 函数式组件(init 函数)先于类式组件(KupolaComponent 子类)初始化。这是因为函数式组件通常提供基础设施(如 tooltip),类式组件可能依赖它们。
动态内容刷新
// CDN / UMD
Kupola.refresh(scopeElement);
Kupola.cleanup(element);
// ESM
import { refresh, cleanup } from 'kupola';
await refresh(scopeElement);
cleanup(element);
组件初始化器
js/initializer.js — 基于 MutationObserver 的声明式组件自动发现系统。
工作原理
register('dropdown', initFn, cleanupFn, { dataAttribute: 'data-dropdown' })
│
▼
ComponentInitializerRegistry
├── initializers: Map<name, initFn>
├── cleanupFunctions: Map<name, cleanupFn>
├── processedElements: WeakSet ← 防重复初始化
└── _dataAttrs: ['data-dropdown', 'data-modal', ...]
│
▼ MutationObserver callback
扫描新增 DOM → 匹配 selector → 调用 initFn(el) → 存入 processedElements
注册模式
// 函数式注册(大多数组件)
kupolaInitializer.register('data-tooltip', initTooltip, cleanupTooltip, {
dataAttribute: 'data-tooltip'
});
// 类式注册(通过 registry.js)
class MyWidget extends KupolaComponent { ... }
registerComponent('my-widget', MyWidget);
注意: processedElements 是 WeakSet,元素被 GC 回收时自动移除。不要在外部缓存 DOM 元素引用来绕过此机制。
数据绑定引擎
js/data-bind.js — 1360 行,Kupola 最大的单一模块。包含 4 个子系统。
| 子系统 | 职责 | 关键数据结构 |
|---|---|---|
| KupolaDataBind | Proxy 双向绑定 + DOM 同步 | PathTrie(键路径索引) |
| KupolaEventBus | 发布/订阅事件总线 | Map<event, handlers[]> |
| KupolaStore | 状态管理(mutations/actions) | 依赖 kupolaData 实例 |
| ref() | 轻量响应式引用 | Proxy { value, _subscribers } |
绑定流程
kupolaData.set('user.name', '张三')
│
▼
Proxy set trap
│
├── PathTrie 查找依赖此路径的 DOM 元素
│
├── 遍历绑定元素
│ ├── data-bind="text:user.name" → el.textContent = '张三'
│ ├── data-bind="value:user.name" → el.value = '张三'
│ └── data-bind="html:user.name" → sanitizeHtml() → innerHTML
│
└── 通知 observers
HTML 安全
sanitizeHtml() 在每次 innerHTML 赋值前执行,移除 <script>、<iframe>、on* 事件属性和 javascript: URI。
事件系统
js/global-events.js — 统一事件管理,支持作用域批量清理。
import { on, once, off, emit, offByScope } from 'kupola';
// 绑定事件(返回 { unsubscribe })
const sub = on(document, 'click', handler, { scope: 'page:users' });
// 一次性事件
once(window, 'resize', handler);
// 按作用域批量清理(页面卸载时)
offByScope('page:users'); // 清理所有 scope='page:users' 的监听器
内部数据结构
GlobalEvents
├── _listeners: Map<eventKey, Listener[]>
│ └── Listener { id, target, eventName, handler, scope, wrappedHandler }
└── _scopeListeners: Map<scope, listenerId[]>
└── offByScope() → 批量 removeEventListener
设计决策: offByScope 是防止内存泄漏的关键机制。页面 init 模块通过 scope 参数注册所有事件,卸载时一次调用即可全部清理。
组件基类
js/component.js — KupolaComponent 提供类式组件的标准生命周期。
KupolaComponent
├── constructor(element)
│ ├── _parseProps() 读取 data-prop-* 属性
│ ├── _parseSlots() 读取 slot 元素
│ └── lifecycle = new KupolaLifecycle()
│
├── setup() 子类重写:初始化逻辑
├── render() 子类重写:渲染 DOM
├── mount() 挂载到 DOM
├── beforeUnmount() 子类重写:清理
└── destroy() 销毁实例
Mixin 系统:
defineMixin('validatable', { validate() {...} })
applyMixin(MyComponent, 'validatable')
Props 解析
<div data-my-widget data-prop-title="Hello" data-prop-count="42">
→ this.props = { title: 'Hello', count: 42 } // JSON.parse 自动转换
主题引擎
js/theme.js — CSS 变量驱动的主题切换,无 JS 样式计算。
setTheme('dark')
│
▼
document.documentElement.setAttribute('data-theme', 'dark')
│
▼
CSS [data-theme="dark"] { --bg-base-default: #1a1a2e; ... }
CSS [data-theme="light"] { --bg-base-default: #ffffff; ... }
│
▼
所有组件自动继承新变量(零 JS 开销)
品牌色
11 种品牌色通过 setBrand('zengqing') 切换,修改 --bg-brand 系列变量。品牌色独立于主题,可以自由组合。
数据依赖系统
js/depends.js — 声明式数据获取 + 可插拔 HTTP 客户端。
架构分层
useDeps({ users: { url: '/api/users', staleTime: 60000 } })
│
▼
Scheduler ← 调度器(去重 + 并发控制)
│
▼
CacheManager ← LRU 缓存(staleTime / maxAge)
│
▼
HTTP Client Plugin ← 可插拔(默认 fetch,可替换为 Axios)
│
▼
DependsSource ← 数据源抽象
├── FetchedSource (HTTP)
├── StorageSource (localStorage)
├── WebSocketSource (WebSocket)
└── StaticSource (静态数据)
HTTP 客户端插件
// 适配器模式 — 拦截器完全保留
import { configureHttpClient } from 'kupola';
import { createAxiosAdapter } from 'kupola/adapters/axios';
configureHttpClient(createAxiosAdapter(axiosInstance));
CSS 架构
13 个源文件,通过 @import 汇总,构建时合并 + 压缩。
分层结构
| 层级 | 文件 | 职责 |
|---|---|---|
| 基础 | scaffold.css | 重置 + 排版 + 基础元素 |
| 变量 | colors-and-type.css | CSS 自定义属性(颜色/字体/间距) |
| 主题 | theme-dark.css / theme-light.css | 暗/亮主题变量覆盖 |
| 品牌 | brand-themes.css | 11 种品牌色变量 |
| 组件 | components.css / components-ext.css | ds-* 组件样式 |
| 表格 | table.css | KupolaTable 专用样式 |
| 状态 | states.css | is-active / is-disabled 等状态类 |
| 响应式 | responsive.css | 断点适配 |
| 动画 | animations.css | 过渡 + 关键帧 |
| 无障碍 | accessibility.css | WCAG AA 焦点/对比度 |
| 工具 | utilities.css | .text-sm, .flex, .gap-* 等 |
CSS 命名约定: 所有组件类以 ds- 前缀,BEM 风格:ds-card__title、ds-btn--primary。CSS 变量以 -- 开头,不带 ds-。
构建管线
双构建系统:Vite(主)+ Rollup(替代),CSS 独立构建脚本。
JS 构建
src/index.js → Rollup/Vite
│
├── ESM → dist/kupola.esm.js (550 KB)
├── CJS → dist/kupola.cjs.js (554 KB)
├── UMD → dist/kupola.umd.js (582 KB)
└── UMD min → dist/kupola.min.js (317 KB, gzip 75 KB)
Copy 插件:
types/kupola.d.ts → dist/types/
icons/ → dist/icons/
css/*.css → dist/css/
plugins/ → dist/plugins/
CSS 构建
scripts/build-css.cjs
读取 css/kupola.css
解析 @import url('...')
递归内联所有 CSS 文件
│
├── dist/kupola.css (236 KB, 合并未压缩)
└── dist/kupola.min.css (174 KB, 压缩)
注意: Rollup 的 rollup-plugin-postcss 不支持解析 @import,因此 CSS 构建使用独立的 Node.js 脚本,不走 Rollup。
Tree-Shaking 策略
所有模块使用 ES Module 命名导出,bundler 可裁剪未使用代码。
sideEffects 声明
// package.json
"sideEffects": [
"./js/icons.js", // 注册全局图标(有副作用)
"./js/theme-standalone.js", // 独立主题初始化
"*.css" // CSS 文件不可裁剪
]
工具库命名导出
// utils.js 导出 8 个命名空间 + 2 个独立函数
export { stringUtils, arrayUtils, objectUtils, numberUtils,
dateUtils, validatorUtils, cryptoUtils, preloadUtils,
debounce, throttle,
KupolaUtils // 向后兼容的聚合对象
};
效果: 如果用户只导入 stringUtils,其他 7 个命名空间的代码会被 bundler 裁剪。KupolaUtils 聚合对象保留向后兼容但不会阻止裁剪。
新增组件指南
向 Kupola 添加新组件的标准流程。
步骤 1:创建组件文件
// js/my-component.js
import { kupolaInitializer } from './initializer.js';
import { on, offByScope } from './global-events.js';
export function initMyComponent(el) {
// 初始化逻辑
on(el, 'click', handler, { scope: el });
}
export function cleanupMyComponent(el) {
offByScope(el);
}
// 注册到初始化器
kupolaInitializer.register('my-component', initMyComponent, cleanupMyComponent, {
dataAttribute: 'data-my-component'
});
步骤 2:添加 CSS
/* css/components.css 或 css/components-ext.css */
.ds-my-component {
background: var(--bg-base-secondary);
border: 1px solid var(--border-neutral-l1);
border-radius: var(--radius-4);
}
步骤 3:注册导出
// src/index.js 添加
export { initMyComponent, cleanupMyComponent } from '../js/my-component.js';
步骤 4:添加类型
// types/kupola.d.ts 添加
export function initMyComponent(root?: Element): void;
export function cleanupMyComponent(el: Element): void;
步骤 5:验证
npm run build:rollup # 确认 Rollup 构建通过
npm run build:css # 确认 CSS 构建通过
npm run lint # 确认 ESLint 通过
编码约定
| 约定 | 说明 |
|---|---|
| ES Modules only | 所有核心模块使用 import/export,禁止 require() |
| 零外部依赖 | 核心模块不引入 npm 包,适配器除外 |
| JSDoc 注释 | 所有 public API 必须有 JSDoc |
| camelCase JS | JS 变量/函数 camelCase,CSS/HTML kebab-case |
| data-* 声明式 | 组件通过 data-* 属性初始化,禁止硬编码选择器 |
| cleanup 函数 | 每个 init 函数必须有对应的 cleanup,防内存泄漏 |
| scope 清理事件 | 使用 GlobalEvents 的 scope 参数,页面卸载时批量清理 |
| textContent 优先 | 避免 innerHTML,必须使用时先 sanitizeHtml() |
| 2 空格缩进 | ESLint 强制,Prettier 格式化 |
提交规范: 使用 Conventional Commits — feat: 新功能、fix: 修复、docs: 文档、perf: 性能、refactor: 重构。