MateChat 是一个支持多框架(Vue 3 和 Angular)的 AI 组件库,采用 monorepo 架构进行统一管理,旨在提供跨框架一致的组件体验和开发效率。
MateChat/
├── docs/ # 文档站点
│ ├── components/ # Vue 组件文档
│ ├── components-ng/ # Angular 组件文档
│ ├── theme-default/ # 文档主题
│ └── use-guide/ # 使用指南
├── packages/ # 组件库源码
│ ├── components/ # Vue 3 组件库
│ │ ├── Attachment/ # 附件组件
│ │ ├── Bubble/ # 气泡组件
│ │ ├── FileList/ # 文件列表组件
│ │ ├── Header/ # 头部组件
│ │ ├── Input/ # 输入框组件
│ │ └── ... # 其他组件
│ └── components-ng/ # Angular 组件库
│ └── projects/
│ ├── components-ng/ # Angular 组件源码
│ │ └── src
│ │ ├── Base/ # 基础组件
│ │ ├── Bubble/ # 气泡组件
│ │ ├── Input/ # 输入框组件
│ │ ├── ...
│ │ └── components-common/ # 跨框架公共逻辑
│ └── demo-app/ # Angular 示例应用
├── CONTRIBUTING.md # 贡献指南
├── CONTRIBUTING-DEV.md # 开发者文档
└── package.json # 项目根配置
| 依赖项 | 版本要求 | 说明 |
|---|---|---|
| Node.js | >= 22.12.0 | 核心运行环境 |
| npm | >= 10.9.0 | 包管理工具 |
| Angular CLI | >= 20.3.9 | 仅 Angular 开发需要 |
# 安装项目依赖
pnpm install每个 Vue 组件需遵循以下目录结构:
ComponentName/ # 组件名称(大驼峰)
├── components/ # 子组件目录(可选)
├── index.ts # 组件入口文件
├── ComponentName.vue # 主组件文件(大驼峰)
├── componentName-types.ts # 类型定义文件(小驼峰 + types)
├── componentName.scss # 样式文件(小驼峰)
└── *.ts # 辅助文件(如常量、工具函数等)
# 在 packages/components 目录下创建新组件
cd packages/components
mkdir NewComponent- 创建
NewComponent.vue主组件文件,实现组件模板、逻辑和样式引用 - 创建
index.ts导出组件 - 创建
componentName-types.ts定义组件类型 - 创建
componentName.scss编写组件样式
在 packages/components/index.ts 中导出新组件:
export { default as NewComponent } from "./NewComponent";# 启动 Vue 文档站点,支持热更新
npm run docs:dev每个 Angular 组件需遵循以下目录结构:
ComponentName/ # 组件名称(大驼峰)
├── index.ts # 组件入口文件
├── componentName.component.ts # 组件类文件(小驼峰 + component)
├── componentName.component.html # 模板文件(小驼峰 + component)
├── componentName.component.scss # 样式文件(小驼峰 + component)
├── componentName.component.spec.ts # 测试文件(小驼峰 + component + spec)
├── componentName.module.ts # 组件模块文件(小驼峰 + module)
└── componentName-types.ts # 类型定义文件(可选,小驼峰 + types)
# 运行 Angular 示例应用,支持热更新,适用于组件开发初期
pnpm run demo:ng
# 启动文档站点,用于预览组件最终效果
pnpm run docs:dev# 进入 Angular 组件源码目录
cd packages/components-ng/projects/components-ng/src
mkdir NewComponent- 创建组件类文件
componentName.component.ts - 创建模板文件
componentName.component.html - 创建样式文件
componentName.component.scss - 创建模块文件
componentName.module.ts - 编写测试文件
componentName.component.spec.ts
在 packages/components-ng/projects/components-ng/src/public-api.ts 中导出新组件:
export * from "./NewComponent";为实现跨框架组件的高效复用,MateChat 设计了 @matechat/common 公共包,位于 packages/components-ng/projects/components-ng/src/components-common/ 目录下,包含跨框架复用的公共逻辑、样式和类型定义。
- 公共样式需保持框架无关性,避免使用
ng-deep、v-deep、:host等框架特定语法 - 若需框架特定样式,需单独创建
vue.scss或angular.scss进行适配 - 公共样式应具备通用性,避免包含组件特定的业务样式
Vue 中使用公共样式:
@import "@matechat/common/Bubble/common/bubble.scss";Angular 中使用公共样式:
@import "../components-common/Bubble/common/bubble.scss";MateChat 跨框架组件采用「核心层 + 适配层」的分层设计模式:
- Foundation 层(核心层):包含组件核心业务逻辑,如 UI 行为触发后的计算、分支判断等,不直接操作 DOM,所有 DOM 操作委托给 Adapter 层执行
- Adapter 层(适配层):定义 Foundation 层所需的接口,负责组件 DOM 结构声明和所有 DOM 操作/更新逻辑,使用框架 API 实现状态管理和事件处理
Angular 组件继承 BaseComponent 基类,并传入对应的 Foundation 泛型约束,同时实现 Adapter 接口:
@Component({
selector: 'mc-bubble',
standalone: true,
imports: [CommonModule, BubbleLoadingComponent, AvatarComponent, TranslatePipe],
templateUrl: './bubble.component.html',
styleUrls: ['./bubble.component.scss']
})
export class BubbleComponent extends BaseComponent<BubbleFoundation> {
// 组件输入属性
@Input() content: string = '';
@Input() loading: boolean = false;
@Input() align: BubbleAlign = 'left';
@Input() avatarPosition: AvatarPosition = 'side';
@Input() variant: BubbleVariant = 'filled';
@Input() avatarConfig?: BubbleAvatar;
// 内容投影模板引用
@ContentChild('avatar') avatarTemplate: TemplateRef<any> | null = null;
@ContentChild('top') topTemplate: TemplateRef<any> | null = null;
@ContentChild('loadingTpl') loadingTplTemplate: TemplateRef<any> | null = null;
@ContentChild('bottom') bottomTemplate: TemplateRef<any> | null = null;
// 内容投影检测
@ContentChildren('ng-content') contentChildren: QueryList<any>;
get ngContentProjected(): boolean {
return this.contentChildren && this.contentChildren.length > 0;
}
constructor() { super(); }
ngOnInit() {
this.foundation = new BubbleFoundation(this.adapter);
this.foundation.init();
}
override get adapter(): BubbleAdapter {
return {
...super.adapter,
getProps: () => ({
content: this.content,
loading: this.loading,
align: this.align,
avatarPosition: this.avatarPosition,
variant: this.variant,
avatarConfig: this.avatarConfig,
}),
};
}
// 计算属性
get bubbleClasses(): string {
return this.foundation.getBubbleClasses();
}
get isEmptyAvatar(): boolean {
return this.foundation.getIsEmptyAvatar(this.avatarConfig);
}
}Vue中使用BubbleFoundation公共逻辑示例: 新建useBubbleFoundation.ts文件, 在vue组件中直接使用即可。
import { computed } from 'vue';
import { useFoundation, useDefaultAdapter } from '@matechat/common/Base/useFoundation';
import { BubbleFoundation, BubbleAdapter } from '@matechat/common/Bubble/foundation';
import { BubbleAvatar } from './bubble-types';
export interface UseBubbleFoundationOptions {
props: any;
}
export function useBubbleFoundation({ props }: UseBubbleFoundationOptions) {
// 创建适配器,将Vue组件的props和状态传递给Foundation
const defaultAdapter = useDefaultAdapter();
const adapter: BubbleAdapter = {
...defaultAdapter,
getProps: () => props,
getProp: (key: string) => props[key],
getStates: () => ({}),
getState: (key: string) => undefined,
setState: (key: string, value: any) => {
// Implementation of setState if needed
},
};
// 使用useFoundation创建BubbleFoundation实例
const { foundation } = useFoundation<BubbleFoundation>({
adapter,
foundationClass: BubbleFoundation,
});
// 计算气泡类名
const bubbleClasses = computed(() => {
return foundation.getBubbleClasses();
});
// 计算是否为空头像
const isEmptyAvatar = computed(() => {
return foundation.getIsEmptyAvatar(props.avatarConfig);
});
return {
foundation,
bubbleClasses,
isEmptyAvatar,
};
}公共逻辑包括 interface、type、enum、工具函数等 API 定义,统一存放在 @matechat/common 包中对应的组件目录下。
// 引入公共工具函数
import MdParserUtils from "../components-common/MarkdownCard/common/parser";
// 使用公共工具函数
MdParserUtils.clearElementChildren(existingElement);在 docs/components/{component-name}/ 目录下创建以下文件:
api.md- 组件 API 文档,包括属性、事件、插槽等demo.md- 组件示例代码,展示组件的各种用法
遵循 VitePress 文档格式,使用 Markdown 编写,支持代码高亮和组件演示。
在 docs/components-ng/{component-name}/ 目录下创建以下文件:
api.md- 组件 API 文档demo.md- 组件示例代码
- 在
packages/components-ng/projects/demo-app/src/app/demo/目录下创建 Demo 目录,命名规则为{ComponentName}Demo - 在 Demo 目录下创建具体演示组件,如
markdown-basic(基础用法) - 开发调试阶段,可在
demo-app/src/app/app.html中引入该 Demo 组件,运行npm run demo:ng进行热更新调试
根据 demo-prompt.md 提示词生成 Demo 组件,并注册为 WebComponent,以便在文档中使用。
在 demo.md 中使用注册好的 WebComponent:
### 基本用法
基本用法只需传入 content 属性即可。
<mc-ng-markdown-basic></mc-ng-markdown-basic># 启动文档站点
npm run docs:dev# 构建 Vue 组件库
npm run build:lib# 构建 Angular 组件库
pnpm run build:ng| 问题描述 | 解决方案 |
|---|---|
| Vue 和 Angular 组件结构不一致 | 根据 Angular 组件规范调整子组件结构 |
Vue 特殊指令(如 v-if、v-for) |
使用 Angular 对应语法替换(如 *ngIf、*ngFor) |
| Vue Watch 函数 | 使用 Angular ngOnChanges 生命周期钩子监听输入属性变化 |
| 样式语法差异 | 提取公共样式到 @matechat/common,并为各框架创建适配样式 |
- DOM 结构迁移:将 Vue render 函数转换为 Angular 模板语法,包括 DOM 结构、属性绑定和事件绑定
- 样式与工具迁移:提取 Vue 组件的公共样式和工具函数到
@matechat/common,Angular 组件直接引入使用 - 组件状态迁移:将 Vue 的
ref/reactive状态替换为 Angular 的类属性 - 生命周期迁移:将 Vue 生命周期钩子转换为 Angular 对应生命周期
- API 定义迁移:将 Vue 组件的 API 定义提取到
@matechat/common,Angular 组件保持一致的 API 定义
- 遵循组件单一职责原则,每个组件只负责一个功能
- 优先使用公共样式和逻辑,减少跨框架重复代码
- 保持组件 API 跨框架一致性,降低用户学习成本
- 为组件编写完整的文档和测试用例
- 遵循各框架的最佳实践和编码规范
- 定期更新依赖,确保使用最新稳定版本
- Fork 仓库并创建特性分支
- 开发组件或修复 bug
- 编写测试用例和文档
- 运行构建和测试命令确保代码质量
- 提交 PR 并描述变更内容
- 等待代码 review 和合并
通过遵循本开发指南,您可以高效地参与 MateChat 组件库的开发,确保跨框架组件的一致性和质量。