实战中的写法总结
实战中的写法总结
实战中的写法总结
总结版
- 组件间传 id,而非完整对象,子组件通过 store 获取对象,提升性能,确保响应式。
对象的创建编辑表单
- 给对象实现 clone(复制一个对象) 和 forCreate(创建一个初始化了基础信息(uuid等)的对象) 方法
- 给表单传入 对象 | null,再通过 watch 监听对象,如果对象为 null,则创建一个初始对象,否则就 clone 一个对象。
- 表单与本地对象双向绑定,编辑或创建提交时传递本地对象
修改当前 GoalDialog 使用类似 TemplateDialog 的方式,传入编辑对象的实例(编辑模式)或 null(创建模式),watch visible 和 传入的对象,使用对象的 clone 或 forCreate 方法初始化本地对象,表单与本地对象双向绑定,编辑或创建提交时传递本地对象
要同步变化的按钮表单
- 直接传入对象的 uuid
- 从 store 中获取响应式对象
对象的数据相关方法
toDTO(): 将对象实例转为对象接口数据static fromDTO(dto: T): 将对象接口数据转为对象实例clone(): 创建对象的副本forCreate(): 创建对象实例,用于创建模式isxxx(): 判断对象实例是否为xxx对象实例ensurexxx(): 确保对象实例为xxx对象实例ensurexxxNeverNull(): 确保对象实例为xxx对象实例,并且不为null
对象属性有些是创建是传入的,有些是对象内部初始化的比如生命周期等(不能在创建时传入,uuid 在forCreate中不需要传入),所以 clone() 等从已有对象创建对象实例时,需要通过constructor 创建实例后再利用 setter 方法设置内部属性。
类型断定相关语法糖写法整理
1. 可选链(Optional Chaining)?.
用于安全地访问对象的嵌套属性或方法,避免因中间属性为 null/undefined 而报错。
const arr = systemGroup?.templates;
等价于:
const arr = systemGroup ? systemGroup.templates : undefined;
2. 空值合并运算符(Nullish Coalescing)??
用于为 null 或 undefined 的值提供默认值。
const arr = systemGroup?.templates ?? [];
等价于:
const arr = (systemGroup && systemGroup.templates) != null ? systemGroup.templates : [];
3. 非空运算符(Non-null Assertion)!
用于告诉 TypeScript 忽略 null 或 undefined 的值。
3. 展开运算符(Spread Operator)...
用于数组或对象的展开、合并。
const allTemplates = [...(systemGroup?.templates ?? []), ...otherTemplates];
函数的参数
函数的参数应该采用对象形式:
- 不需要记住参数顺序,调用时只需写明属性名。
- 可选参数可以省略,不会因顺序错乱导致 bug。
- 代码更易读、易维护,扩展性更好。
修改对象的表单
给表单传入 要修改的对象(编辑) || null(创建),直接使用 forCreate(编辑) 或者 clone(创建) 方法来生成一个对象,然后让表单与这个对象双向绑定来修改或创建(每一个属性利用 computed 的 get set 方法来处理),成功后调用相应服务和传入对象。
Electron 的进程间数据传输遇到问题(无法被克隆)
[AccountIpcClient] 发送账号注册请求 Proxy(Object) {username: '123', firstName: '12', lastName: '3', sex: 1}
accountIpcClient.ts:9 📝 [AccountIpcClient] 序列化后的注册数据 {username: '123', firstName: '12', lastName: '3', sex: 1}
注意 vue 中的对象数据很有可能是被 代理 的,需要转为普通对象(可以使用 JSON 方法)
Ts 在类中使用 pinia 状态的延迟加载
使用 get 方法延迟获取 pinia 状态,否则会报未在 pinia 初始化前就访问 pinia 状态的错。
ts 是编译时的,所以无法在运行时检查 pinia 状态的访问。
private get store() {
return useReminderStore();
}
类的懒加载
延迟获取依赖,避免因为静态编译时加载未准备好的依赖。
class p {
private _goalStore: ReturnType<typeof useGoalStore> | null = null;
/**
* 延迟获取 goalStore,确保 Pinia 已经初始化
*/
private get goalStore() {
if (!this._goalStore) {
this._goalStore = useGoalStore();
}
return this._goalStore;
}
async addGoal(goal: IGoal): Promise<void> {
try {
await this.goalStore.addGoal(goal);
console.log(`✅ [StateRepo] 添加目标到状态: ${goal.id}`);
} catch (error) {
console.error(`❌ [StateRepo] 添加目标失败: ${goal.id}`, error);
throw error;
}
}
isAvailable(): boolean {
try {
// 简单检查 store 是否可用
return this.goalStore !== null && this.goalStore !== undefined;
} catch (error) {
console.error("❌ [StateRepo] 状态仓库不可用", error);
return false;
}
}
}
类的工厂函数创建实例(异步)+单例+依赖注入(切换不同的创建工厂)
class AuthenticationLogoutService {
private static instance: AuthenticationLogoutService;
private authCredentialRepository: IAuthCredentialRepository;
constructor(authCredentialRepository: IAuthCredentialRepository) {
this.authCredentialRepository = authCredentialRepository;
}
static async createInstance(): Promise<AuthenticationLogoutService> {
const authenticationContainer = await AuthenticationContainer.getInstance();
const authCredentialRepository = authenticationContainer.getAuthCredentialRepository();
return new AuthenticationLogoutService(authCredentialRepository);
}
static async getInstance(): Promise<AuthenticationLogoutService> {
if (!AuthenticationLogoutService.instance) {
AuthenticationLogoutService.instance = await this.createInstance();
}
return AuthenticationLogoutService.instance;
}
// 你的 logout 方法等...
}
const logoutService = await AuthenticationLogoutService.createInstance();
领域服务类
领域服务类,用于处理业务逻辑,封装了领域模型,并定义了领域模型之间的关系。
领域服务类不直接导入基础设施层(仓库),应该通过应用服务层注入仓库。
对象属性的表单
Vue 的响应式系统对 class 实例的 getter/setter 支持有限,直接用 class 实例属性做 v-model,只有在 class 内部用 reactive/ref 包裹,或者用 computed 包装,才能保证视图和数据同步。
const enableMode = computed({
get: () => props.templateGroup?.enableMode || 'group',
set: (val) => {
if (localTemplateGroup.value) {
localTemplateGroup.value.enableMode = val;
}
}
});
对象类型变化
TS 中的对象貌似类型会莫名其妙的变化,导致报类型的匹配的错误,需要使用 ensure 函数来确保类型
<!-- TemplateDialog -->
<template-dialog
:model-value="templateDialog.show"
:template="templateDialog.template" // 这里会报错,要改为下面的参数
// ReminderTemplate.ensureReminderTemplate(templateDialog.template)
@update:modelValue="templateDialog.show = $event"
@submit="handleTemplateDialogSubmit"
@create-template="handleCreateReminderTemplate"
@update-template="handleUpdateReminderTemplate"
/>
const templateDialog = ref({
show: false,
template: null as ReminderTemplate | null
});
类型定义
后端中一般对于存在的但没有值的字段都是存 null 值,返回的也是 null 值,
对此,在 ts 中的类型定义中,是给这类字段定义为 ? 可选字段吗?,不过可选字段不是和 undefined 相关的吗,一个非可选字段的值为 null 会报错吗
Vue 组件中的方法模式
是直接在子组件中调用业务方法 还是子组件传递事件和数据到父组件,让父组件调用方法
推荐:子组件传递事件,父组件调用业务方法
- 保持组件职责单一,子组件专注UI展示
- 父组件统一管理业务逻辑和数据流
- 便于测试和维护,降低组件间耦合
数据同步问题排查
当API返回数据但界面没有更新时,可能的问题:
- 数据转换问题:API数据格式与前端期望不匹配
- Store更新问题:数据虽然获取但未正确更新到Pinia store
- 响应式丢失:数据更新了但组件未监听到变化
- 组件渲染问题:使用了错误的数据源(如硬编码数据)
排查步骤:
// 1. 检查API数据是否正确转换
console.log('API response:', response.data);
// 2. 检查Store是否更新
console.log('Store state after update:', store.goalDirs);
// 3. 检查组件是否使用正确的数据源
console.log('Component computed data:', computedGoalDirs.value);
下载链接创建
浏览器的 download 属性仅在同源资源或使用 blob: URL 时可靠。对于跨域或服务器返回可 inline 打开的资源(如直接访问的视频/PDF),浏览器通常会在新标签页打开而不是下载。 解决办法:先以二进制(blob)方式下载文件(需后端允许 CORS),用 URL.createObjectURL 生成 blob: 地址并触发下载;若请求失败再回退到直接打开链接。
async function downloadSingleFile(file: any) {
if (!file || !file.fileUrl) return;
const filename = file.fileName || 'download';
const sourceUrl = formatImgUrl(file.fileUrl);
try {
const resp = await axios.get(sourceUrl, { responseType: 'blob' });
const blob = new Blob([resp.data], { type: resp.data.type || 'application/octet-stream' });
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = filename;
// 不设置 target,避免浏览器在新标签打开(download 会触发保存)
document.body.appendChild(link);
link.click();
// 清理
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
}, 1000);
} catch (err) {
// 如果因为 CORS 或其他原因不能以 blob 方式获取,则回退到在新标签打开(用户可手动另存)
window.open(sourceUrl, '_blank');
}
}
DDD 实践相关
领域服务 和 应用层服务
以 Goal 领域为例,有 聚合根 Goal,实体 GoalRecord、GoalReview、Keyresult。
领域服务中应该有什么,我目前的理解是 把 Goal 领域内的一些业务逻辑放在领域服务中,比如 addGoalRecord(在Goal 中添加 Record), 然后 goal 实体中的 addGoalRecord 来调用这个领域服务。
应用层服务则是 跨领域的一些 应用层面的服务,
我的理解对吗,正确的应该是什么样的
📋 正确的DDD职责划分总结
- 聚合根 (Goal) - 核心业务逻辑 ✅ 拥有业务方法: createRecord(), updateRecord(), removeRecord() ✅ 封装业务规则: 数据验证、状态管理、不变性维护 ✅ 管理子实体: 控制 KeyResult、GoalRecord、GoalReview 的生命周期 ✅ 发布领域事件: 通知其他模块重要业务变更 ✅ 自包含逻辑: 单个聚合内的所有业务逻辑
- 领域服务 (GoalDomainService) - 跨聚合业务逻辑 ✅ 跨聚合协调: 处理多个 Goal 之间的关系 ✅ 复杂算法: 目标健康度计算、权重分配算法 ✅ 业务策略: 目标冲突检测、克隆策略 ✅ 无状态服务: 不持有数据,只有纯业务逻辑 ❌ 不做持久化: 不直接调用仓储或API
- 应用服务 (GoalWebApplicationService) - 用例协调 ✅ 用例编排: 协调完整的业务流程 ✅ 跨领域协调: 调用多个领域的服务 ✅ 技术关注点: API调用、缓存管理、错误处理 ✅ 事务管理: 确保数据一致性 ✅ 基础设施集成: 持久化、通知、日志等
contracts
事件总线中的各种事件类型的管理,如果在各个模块中直接定义某个事件类型,其他模块导入了此事件类型,此时耦合了。
模块只依赖共享契约,不依赖其他业务模块(就只是把事件类型定义放在共享文件夹中了吧)
聚合根内业务
在 服务方法中先获取对象,参数等,并确保正常,内部方法则利用验证的数据来操作,确保内部不会发生错误
/**
* 添加记录
*/
addRecord(record: Record): void {
const { keyResultUuid } = record;
const keyResult = this._keyResults.find((kr) => kr.uuid === keyResultUuid);
if (keyResult) {
this.increaseKeyResultProgressByKR(keyResult, record.value);
this._records.push(record);
this._lifecycle.updatedAt = new Date();
this._version++;
}
}
仓库实现
实现 mapRowToKeyResult(数据库数据转换成对象)和 mapKeyResultToRow(对象转换成数据库数据)
{ “uuid”: “3271617c-4f41-4a73-96c9-535081ba091d”, “goalUuid”: “05bd7999-f0fe-4746-a263-f3eab8d41e47”, “title”: "", “type”: “custom”, “reviewDate”: “2025-07-27T15:01:44.646Z”, “content”: { “achievements”: “2”, “challenges”: "", “learnings”: "", “nextSteps”: “3” }, “snapshot”: { “snapshotDate”: “2025-07-27T15:01:44.646Z”, “overallProgress”: 0, “weightedProgress”: 0, “completedKeyResults”: 0, “totalKeyResults”: 0, “keyResultsSnapshot”: [ { “uuid”: “ca8fee17-6fe3-4dc8-b799-7ed8d70fb10e”, “name”: “22222”, “progress”: 0, “currentValue”: 0, “targetValue”: 10 } ] }, “rating”: { “progressSatisfaction”: 0, “executionEfficiency”: 0, “goalReasonableness”: 0 }, “lifecycle”: { “createdAt”: “2025-07-27T15:01:44.647Z”, “updatedAt”: “2025-07-27T15:01:44.647Z” } }
事件总线
发布、订阅、转发
事件总线系统是一个监听,一个发送事件,总线来转发;
auth 模块 登录时向 account 模块 发送请求获取完整 account 信息;
account模块 再向 auth 模块发送请求来返回完整信息。
这样是类似 electron 中的 send 和 on,单向的。
我是不是可以实现:
auth 模块 发送请求,同时监听返回的响应;
account 模块 接收到请求后,直接返回响应;
类似 electron 中的 invoke 和 handle
直接使用 nodejs 内置的 eventEmitter 或者 mitt
我如果用 nodejs 内置的 eventEmitter 实现事件类并让 eventBus 继承的话,是不是会让 前端 client的 domain 也引入 nodejs 的模块导致报错? 我是不是应该使用 mitt 这种广泛支持的或者有什么其他方法
还得用 mitt
pwsh 命令
pnpm tsc --noEmit 2>&1 | Select-Object -First 50 检查语法