技术决策记录 (Architecture Decision Records)
本文档记录 Manulife WeApp 项目中的重要技术决策及其背景,帮助团队理解为什么选择当前的技术方案。
📋 决策记录模板
每个决策记录包含以下信息:
- 决策日期: 何时做出此决策
- 决策者: 谁做出的决策
-
状态:
提案中|已采纳|已弃用|已替代 - 背景: 为什么需要这个决策
- 决策: 具体的技术选择
- 原因: 为什么选择这个方案
- 后果: 采用此决策的正面和负面影响
- 替代方案: 当时考虑的其他方案
🎯 已采纳的技术决策
1. UI 库选择:NutUI vs Vant
决策日期: 2025-12-01 决策者: 团队 状态: ✅ 已采纳
背景
项目需要为 Taro 小程序选择 UI 组件库,当时有两个主要选择:
- NutUI Taro(京东出品)
- Vant Taro(有赞出品)
决策
选择 NutUI Taro 作为主要 UI 组件库。
原因
- Taro 生态适配更好: NutUI 对 Taro 的支持更完善
- 组件丰富: 满足业务需求的组件更齐全
- 京东背书: 京东维护,社区活跃
- 文档完善: 中文文档详尽,便于团队上手
后果
正面影响:
- ✅ 快速搭建 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 基准
原因
- NutUI 生态: NutUI 组件基于 375px 设计
- 传统习惯: 750px 是移动端设计稿的传统标准
- 灵活适配: 两者并存,互不影响
实现方案
// 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、2 次可能还不稳定
- 避免过度抽象: 等待模式明确后再抽取
- 提升可维护性: 减少重复代码,统一修改点
成功案例
-
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
}
原因
- 用户体验好: 无需手动重新登录
- 防止并发: 单次刷新,避免多次请求
- 失败兜底: 刷新失败跳转认证页
后果
正面影响:
- ✅ 用户体验显著提升
- ✅ 减少登录次数
- ✅ 并发 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: '我的收藏'
}
])
原因
- 消除警告: 避免 "Component that was made a reactive object" 警告
- 性能优化: 避免不必要的深度代理
- 符合语义: 组件对象不需要响应式
后果
正面影响:
- ✅ 消除性能警告
- ✅ 页面初始化速度提升
- ✅ 内存占用减少
负面影响:
- ⚠️ 需要理解 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]
}
原因
- Taro 构建限制: 字符串路径无法被构建工具正确处理
- 资源处理: import 方式能让构建工具正确打包资源
- 类型安全: import 方式支持类型检查
后果
正面影响:
- ✅ 图标正常加载
- ✅ 构建产物优化
- ✅ 支持路径别名
负面影响:
- ⚠️ 需要手动 import 所有图标
- ⚠️ 动态路径需要额外处理
静态资源处理规则
| 资源类型 | 引用方式 |
|---|---|
| SVG 图标 |
import 导入 |
| 图片 |
import 或字符串路径 |
| 远程资源 | 字符串 URL |
相关文档
🔄 提案中的决策
8. 引入 TypeScript?(待讨论)
提案日期: 2026-02-01 状态: 🤔 提案中
背景
项目当前使用 JavaScript,考虑引入 TypeScript 提升代码质量。
提案
逐步迁移到 TypeScript:
- 新文件使用
.ts/.vue+<script lang="ts"> - 旧文件逐步迁移
- 定义类型定义文件(
types/)
原因
- 类型安全: 减少运行时错误
- IDE 支持: 更好的自动补全
- 可维护性: 类型即文档
疑虑
- 学习成本: 团队需要学习 TS
- 迁移成本: 现有代码需要改造
- 构建时间: TS 编译增加构建时间
替代方案
- JSDoc: 在 JS 中添加类型注释
- 保持现状: 继续使用纯 JS
📊 决策统计
按类别统计
- UI/样式: 3 个决策(NutUI、TailwindCSS、双设计宽度)
- 代码质量: 2 个决策(代码复用、响应式优化)
- 架构设计: 2 个决策(认证流程、静态资源)
按状态统计
- ✅ 已采纳: 7 个
- 🤔 提案中: 1 个
- ❌ 已弃用: 0 个
- 🔄 已替代: 0 个
📝 如何添加新决策
决策记录流程
- 识别需求: 发现需要技术决策的场景
- 讨论方案: 团队讨论可能的方案
- 记录决策: 在本文档中添加决策记录
- 定期回顾: 每季度回顾决策的有效性
决策模板
### N. 决策标题
**决策日期**: YYYY-MM-DD
**决策者**: 姓名/角色
**状态**: 状态标识
#### 背景
描述为什么需要这个决策...
#### 决策
具体的技术选择...
#### 原因
为什么选择这个方案...
#### 后果
**正面影响**: ...
**负面影响**: ...
#### 替代方案
当时考虑的其他方案...
#### 相关文档
- [相关文档链接](./xxx.md)
🔄 决策回顾机制
定期回顾
- 频率: 每季度一次
- 参与人: 技术负责人 + 核心开发
- 目的: 评估决策有效性,必要时调整
更新状态
当决策不再适用时,更新状态:
-
已采纳→已弃用: 决策不再使用 -
已采纳→已替代: 被新方案替代 - 记录替代方案和原因
📚 相关文档
最后更新: 2026-02-01 维护者: Claude Code 项目: Manulife WeApp