decisions.md 11.8 KB

技术决策记录 (Architecture Decision Records)

本文档记录 Manulife WeApp 项目中的重要技术决策及其背景,帮助团队理解为什么选择当前的技术方案。

📋 决策记录模板

每个决策记录包含以下信息:

  • 决策日期: 何时做出此决策
  • 决策者: 谁做出的决策
  • 状态: 提案中 | 已采纳 | 已弃用 | 已替代
  • 背景: 为什么需要这个决策
  • 决策: 具体的技术选择
  • 原因: 为什么选择这个方案
  • 后果: 采用此决策的正面和负面影响
  • 替代方案: 当时考虑的其他方案

🎯 已采纳的技术决策

1. UI 库选择:NutUI vs Vant

决策日期: 2025-12-01 决策者: 团队 状态: ✅ 已采纳

背景

项目需要为 Taro 小程序选择 UI 组件库,当时有两个主要选择:

  • NutUI Taro(京东出品)
  • Vant Taro(有赞出品)

决策

选择 NutUI Taro 作为主要 UI 组件库。

原因

  1. Taro 生态适配更好: NutUI 对 Taro 的支持更完善
  2. 组件丰富: 满足业务需求的组件更齐全
  3. 京东背书: 京东维护,社区活跃
  4. 文档完善: 中文文档详尽,便于团队上手

后果

正面影响:

  • ✅ 快速搭建 UI,提升开发效率
  • ✅ 组件样式统一,视觉一致性好
  • ✅ 自动导入配置,无需手动 import

负面影响:

  • ⚠️ 部分组件有样式覆盖限制(如 textarea)
  • ⚠️ 嵌套弹窗在真机有层级冲突问题

替代方案

当时未采纳:Vant Taro

  • Vant 在 Web 端更流行,但 Taro 版本相对 NutUI 较弱

相关文档


2. 样式策略:TailwindCSS + Less 混合使用

决策日期: 2025-12-05 决策者: 团队 状态: ✅ 已采纳

背景

项目需要一套高效的样式解决方案,既要快速开发,又要灵活定制。

决策

采用 TailwindCSS (80%) + Less (20%) 混合策略。

原因

TailwindCSS 优势:

  • 原子化 CSS,开发速度快
  • 响应式设计简单(sm:md:lg:
  • 避免命名冲突
  • 文件体积小(按需生成)

Less 补充:

  • 组件特定样式(需要 scoped
  • 深度选择器(:deep()
  • 复杂动画和伪元素

后果

正面影响:

  • ✅ 开发效率提升 40%+
  • ✅ 样式一致性更好
  • ✅ 减少 CSS 文件数量

负面影响:

  • ⚠️ 需要学习 TailwindCSS 类名
  • ⚠️ 初期有学习曲线

使用指南

<!-- TailwindCSS: 80% 场景 -->
<div class="flex items-center p-4 bg-white rounded-lg">
  <h1 class="text-xl font-bold">标题</h1>
</div>

<!-- Less: 20% 场景 -->
<style lang="less" scoped>
.custom-card {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

  :deep(.nut-button) {
    background-color: rgba(255, 255, 255, 0.2);
  }
}
</style>

相关文档


3. 双设计宽度系统:375px (NutUI) + 750px (自定义)

决策日期: 2025-12-10 决策者: 团队 状态: ✅ 已采纳

背景

NutUI 组件库基于 375px 设计稿,但项目自定义页面使用 750px 设计稿。

决策

实现 双设计宽度系统

  • NutUI 组件 → 375px 基准
  • 自定义页面 → 750px 基准

原因

  1. NutUI 生态: NutUI 组件基于 375px 设计
  2. 传统习惯: 750px 是移动端设计稿的传统标准
  3. 灵活适配: 两者并存,互不影响

实现方案

// config/index.js
const designWidth = {
  750: [375, 375],  // NutUI 组件
  750: [750, 750]   // 其他所有页面
}

后果

正面影响:

  • ✅ NutUI 组件尺寸正确
  • ✅ 自定义页面符合传统标准

负面影响:

  • ⚠️ 需要明确区分 NutUI 组件和自定义元素
  • ⚠️ 新手可能混淆

最佳实践

<!-- NutUI 组件: 使用 375px 设计稿 -->
<nut-button :custom-style="{ fontSize: '14px', padding: '8px 16px' }">
  按钮
</nut-button>

<!-- 自定义元素: 使用 750px 设计稿 -->
<view class="custom-box" style="width: 750px; height: 200px">
  内容
</view>

相关文档


4. 代码复用原则:"第 3 次出现原则"

决策日期: 2026-01-15 决策者: Claude Code 状态: ✅ 已采纳

背景

项目中出现大量重复代码,需要制定抽取标准。

决策

采用 "第 3 次出现原则"(Rule of Three)

  • 代码出现 1-2 次:直接实现
  • 代码出现 3 次:必须抽取为 Composable 或组件

原因

  1. 避免过早抽取: 第 1、2 次可能还不稳定
  2. 避免过度抽象: 等待模式明确后再抽取
  3. 提升可维护性: 减少重复代码,统一修改点

成功案例

  • useSectionList - 减少约 60 行重复代码
  • useFileOperation - 减少约 290 行重复代码
  • useListItemClick - 统一列表点击逻辑

后果

正面影响:

  • ✅ 代码重复显著减少
  • ✅ 可维护性大幅提升
  • ✅ 修改点统一,降低 bug 率

负面影响:

  • ⚠️ 需要主动识别重复代码
  • ⚠️ 前期可能略有过度设计

触发条件

  • ✅ 代码重复 ≥ 3 次 → 必须抽取
  • ✅ v-for 模板超过 5 行 → 提取列表项组件
  • ✅ 函数超过 50 行 → 拆分函数

相关文档


5. 认证流程:静默刷新 + Promise 单例模式

决策日期: 2026-01-20 决策者: 团队 状态: ✅ 已采纳

背景

项目需要处理会话过期(401)问题,提供无感知的用户体验。

决策

实现 静默刷新 + Promise 单例模式

  • API 返回 401 时自动刷新会话
  • 使用 Promise 单例防止并发刷新
  • 刷新失败后跳转到认证页

核心逻辑

// src/utils/authRedirect.js

let auth_promise = null  // Promise 单例

async function refreshSession() {
  // 如果已有刷新任务,返回现有 Promise
  if (auth_promise) {
    return auth_promise
  }

  // 创建新的刷新任务
  auth_promise = doRefresh()
    .finally(() => {
      auth_promise = null  // 清理单例
    })

  return auth_promise
}

原因

  1. 用户体验好: 无需手动重新登录
  2. 防止并发: 单次刷新,避免多次请求
  3. 失败兜底: 刷新失败跳转认证页

后果

正面影响:

  • ✅ 用户体验显著提升
  • ✅ 减少登录次数
  • ✅ 并发 401 请求共享刷新结果

负面影响:

  • ⚠️ 增加代码复杂度
  • ⚠️ 需要完善的错误处理

相关文档


6. 响应式优化:shallowRef + markRaw 处理组件对象

决策日期: 2026-01-25 决策者: Claude Code 状态: ✅ 已采纳

背景

Vue 3 对包含组件对象的响应式数据进行深度代理,导致性能警告。

决策

使用 shallowRef + markRaw 处理组件对象响应式。

核心代码

import { shallowRef, markRaw } from 'vue'

// ✅ GOOD - 浅层响应式
const menuItems = shallowRef([
  {
    icon: markRaw(IconFont),  // 标记为原始对象
    name: 'heart',
    title: '我的收藏'
  }
])

原因

  1. 消除警告: 避免 "Component that was made a reactive object" 警告
  2. 性能优化: 避免不必要的深度代理
  3. 符合语义: 组件对象不需要响应式

后果

正面影响:

  • ✅ 消除性能警告
  • ✅ 页面初始化速度提升
  • ✅ 内存占用减少

负面影响:

  • ⚠️ 需要理解 Vue 3 响应式原理
  • ⚠️ 不正确使用可能导致视图不更新

相关文档


7. 静态资源加载:SVG 图标使用 import 导入

决策日期: 2026-01-28 决策者: Claude Code 状态: ✅ 已采纳

背景

使用字符串路径加载 SVG 图标导致 500 错误。

决策

SVG 图标必须使用 ES6 import 导入,不能使用字符串路径。

核心代码

// ❌ BAD - 字符串路径导致 500 错误
export const getDocumentIcon = (type) => {
  const icons = {
    pdf: '/assets/images/icon/doc/doc.svg',  // ❌ 500 错误
  }
  return icons[type]
}

// ✅ GOOD - 使用 import 导入
import pdfIcon from '@/assets/images/icon/doc/pdf.svg'

export const getDocumentIcon = (type) => {
  const icons = {
    pdf: pdfIcon,  // ✅ 正确
  }
  return icons[type]
}

原因

  1. Taro 构建限制: 字符串路径无法被构建工具正确处理
  2. 资源处理: import 方式能让构建工具正确打包资源
  3. 类型安全: import 方式支持类型检查

后果

正面影响:

  • ✅ 图标正常加载
  • ✅ 构建产物优化
  • ✅ 支持路径别名

负面影响:

  • ⚠️ 需要手动 import 所有图标
  • ⚠️ 动态路径需要额外处理

静态资源处理规则

资源类型 引用方式
SVG 图标 import 导入
图片 import 或字符串路径
远程资源 字符串 URL

相关文档


🔄 提案中的决策

8. 引入 TypeScript?(待讨论)

提案日期: 2026-02-01 状态: 🤔 提案中

背景

项目当前使用 JavaScript,考虑引入 TypeScript 提升代码质量。

提案

逐步迁移到 TypeScript

  1. 新文件使用 .ts / .vue + <script lang="ts">
  2. 旧文件逐步迁移
  3. 定义类型定义文件(types/

原因

  1. 类型安全: 减少运行时错误
  2. IDE 支持: 更好的自动补全
  3. 可维护性: 类型即文档

疑虑

  1. 学习成本: 团队需要学习 TS
  2. 迁移成本: 现有代码需要改造
  3. 构建时间: TS 编译增加构建时间

替代方案

  • JSDoc: 在 JS 中添加类型注释
  • 保持现状: 继续使用纯 JS

📊 决策统计

按类别统计

  • UI/样式: 3 个决策(NutUI、TailwindCSS、双设计宽度)
  • 代码质量: 2 个决策(代码复用、响应式优化)
  • 架构设计: 2 个决策(认证流程、静态资源)

按状态统计

  • 已采纳: 7 个
  • 🤔 提案中: 1 个
  • 已弃用: 0 个
  • 🔄 已替代: 0 个

📝 如何添加新决策

决策记录流程

  1. 识别需求: 发现需要技术决策的场景
  2. 讨论方案: 团队讨论可能的方案
  3. 记录决策: 在本文档中添加决策记录
  4. 定期回顾: 每季度回顾决策的有效性

决策模板

### N. 决策标题

**决策日期**: YYYY-MM-DD
**决策者**: 姓名/角色
**状态**: 状态标识

#### 背景
描述为什么需要这个决策...

#### 决策
具体的技术选择...

#### 原因
为什么选择这个方案...

#### 后果
**正面影响**: ...
**负面影响**: ...

#### 替代方案
当时考虑的其他方案...

#### 相关文档
- [相关文档链接](./xxx.md)

🔄 决策回顾机制

定期回顾

  • 频率: 每季度一次
  • 参与人: 技术负责人 + 核心开发
  • 目的: 评估决策有效性,必要时调整

更新状态

当决策不再适用时,更新状态:

  • 已采纳已弃用: 决策不再使用
  • 已采纳已替代: 被新方案替代
  • 记录替代方案和原因

📚 相关文档


最后更新: 2026-02-01 维护者: Claude Code 项目: Manulife WeApp