架构概览

Kupola 是一个零运行时依赖的声明式 UI 组件系统。核心由 5 个引擎模块协作驱动。

kupolaBootstrap() DOMContentLoaded / setTimeout(0)
kupolaData Proxy 双向绑定 data-bind
kupolaTheme 暗/亮主题切换 data-theme
ComponentInit MutationObserver 自动发现 data-dropdown

五大核心引擎

模块文件职责行数
Initializerjs/initializer.jsMutationObserver 组件自动发现 + 生命周期管理~200
DataBindjs/data-bind.jsProxy 响应式绑定 + EventBus + Store + ref()~1360
KupolaCorejs/kupola-core.js组件注册表 + Bootstrap 编排 + Mixin~130
GlobalEventsjs/global-events.js统一事件委托 + 作用域清理~280
Themejs/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 个子系统。

子系统职责关键数据结构
KupolaDataBindProxy 双向绑定 + 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.cssCSS 自定义属性(颜色/字体/间距)
主题theme-dark.css / theme-light.css暗/亮主题变量覆盖
品牌brand-themes.css11 种品牌色变量
组件components.css / components-ext.cssds-* 组件样式
表格table.cssKupolaTable 专用样式
状态states.cssis-active / is-disabled 等状态类
响应式responsive.css断点适配
动画animations.css过渡 + 关键帧
无障碍accessibility.cssWCAG AA 焦点/对比度
工具utilities.css.text-sm, .flex, .gap-* 等
CSS 命名约定: 所有组件类以 ds- 前缀,BEM 风格:ds-card__titleds-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 JSJS 变量/函数 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: 重构。