实战中的写法总结

实战中的写法总结

#tech / dev / frontend #type / howto #status / growing

实战中的写法总结


总结版

  1. 组件间传 id,而非完整对象,子组件通过 store 获取对象,提升性能,确保响应式。

对象的创建编辑表单

  1. 给对象实现 clone(复制一个对象) 和 forCreate(创建一个初始化了基础信息(uuid等)的对象) 方法
  2. 给表单传入 对象 | null,再通过 watch 监听对象,如果对象为 null,则创建一个初始对象,否则就 clone 一个对象。
  3. 表单与本地对象双向绑定,编辑或创建提交时传递本地对象

修改当前 GoalDialog 使用类似 TemplateDialog 的方式,传入编辑对象的实例(编辑模式)或 null(创建模式),watch visible 和 传入的对象,使用对象的 clone 或 forCreate 方法初始化本地对象,表单与本地对象双向绑定,编辑或创建提交时传递本地对象

要同步变化的按钮表单

  1. 直接传入对象的 uuid
  2. 从 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返回数据但界面没有更新时,可能的问题:

  1. 数据转换问题:API数据格式与前端期望不匹配
  2. Store更新问题:数据虽然获取但未正确更新到Pinia store
  3. 响应式丢失:数据更新了但组件未监听到变化
  4. 组件渲染问题:使用了错误的数据源(如硬编码数据)

排查步骤:

// 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职责划分总结

  1. 聚合根 (Goal) - 核心业务逻辑 ✅ 拥有业务方法: createRecord(), updateRecord(), removeRecord() ✅ 封装业务规则: 数据验证、状态管理、不变性维护 ✅ 管理子实体: 控制 KeyResult、GoalRecord、GoalReview 的生命周期 ✅ 发布领域事件: 通知其他模块重要业务变更 ✅ 自包含逻辑: 单个聚合内的所有业务逻辑
  2. 领域服务 (GoalDomainService) - 跨聚合业务逻辑 ✅ 跨聚合协调: 处理多个 Goal 之间的关系 ✅ 复杂算法: 目标健康度计算、权重分配算法 ✅ 业务策略: 目标冲突检测、克隆策略 ✅ 无状态服务: 不持有数据,只有纯业务逻辑 ❌ 不做持久化: 不直接调用仓储或API
  3. 应用服务 (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 检查语法

创建于 2025/1/1 更新于 2026/5/27