AI时代的框架设计(三):文档即代码
返回文章列表

AI时代的框架设计(三):文档即代码

#文档的困境

前几天,一个用了我们内部工具库半年的同事问我:"那个 processData 函数,第二个参数传什么?"

我去看代码:

JavaScript
export function processData(data, options) {
  // 150行实现
}

没有注释,参数是 any 类型,函数名也不够清晰。我只能翻 Git 历史,找到当初的PR,才看懂 options 是配置对象。

后来我加了详细注释:

JavaScript
/**
 * 处理和规范化用户数据
 * 
 * @param {Object} data - 原始用户数据
 * @param {Object} options - 处理选项
 * @param {boolean} options.strict - 是否启用严格验证
 * @param {string[]} options.required - 必需字段列表
 * @returns {Object} 规范化后的数据
 * 
 * @example
 * processData(rawData, {
 *   strict: true,
 *   required: ['name', 'email']
 * })
 */
export function processData(data, options) {
  // 150行实现
}

现在不仅同事能看懂,AI也能准确地告诉别人怎么用。

这让我意识到:在AI时代,注释不是可有可无的装饰,而是框架能否被理解的关键。

#文档的层次

一个完善的框架需要多个层次的文档:

README.md           ← 第一印象:这是什么?
├── 快速开始        ← 5分钟上手
└── 核心概念        ← 理解设计思路

代码注释(JSDoc)     ← 日常使用:怎么调用?
├── 函数用途        ← 做什么
├── 参数说明        ← 传什么
├── 返回值          ← 得到什么
└── 示例代码        ← 怎么用

API文档            ← 深入参考:所有细节
├── 完整API列表    
├── 类型定义
└── 高级用法

示例代码           ← 实战学习:真实场景
├── 基础示例
├── 常见模式
└── 完整应用

不同的文档服务不同的目的,但对AI来说,代码注释是最重要的,因为它就在代码旁边,AI读取代码时会同时读到。

#JSDoc:AI的主要信息源

AI理解你的代码主要靠类型定义和JSDoc。好的JSDoc应该包含:

#最小必需信息

JavaScript
/**
 * [一句话描述:这个函数是做什么的]
 * 
 * @param {Type} paramName - [参数说明]
 * @returns {Type} [返回值说明]
 * 
 * @example
 * [最简单的用法]
 */

#完整的JSDoc模板

JavaScript
/**
 * [一句话概述]
 * 
 * [可选:详细说明,包括]
 * - 使用场景
 * - 注意事项
 * - 与其他函数的关系
 * 
 * @param {Type} paramName - [参数说明]
 * @param {Type} [optionalParam] - [可选参数说明]
 * @returns {Type} [返回值说明]
 * 
 * @throws {ErrorType} [什么情况下抛出错误]
 * 
 * @example
 * [基础用法]
 * ```javascript
 * const result = yourFunction(param);
 * ```
 * 
 * @example
 * [高级用法或边界情况]
 * ```javascript
 * const result = yourFunction(specialCase);
 * ```
 * 
 * @see {@link relatedFunction} [相关函数]
 */

#实战示例1:工具函数

JavaScript
/**
 * 深拷贝对象
 * 
 * 使用JSON序列化实现,不支持函数、Date、循环引用。
 * 选择这个实现是因为:
 * - 需要支持Node.js 16(structuredClone是17+)
 * - 我们的数据不包含特殊类型
 * - 性能测试显示比lodash快10倍
 * 
 * @param {Object} obj - 要拷贝的对象
 * @returns {Object} 深拷贝后的新对象
 * 
 * @throws {TypeError} 当对象包含循环引用时
 * 
 * @example
 * 基础用法:
 * ```javascript
 * const original = { user: { name: 'Alice' } };
 * const copy = deepClone(original);
 * copy.user.name = 'Bob';
 * console.log(original.user.name); // 仍然是 'Alice'
 * ```
 * 
 * @example
 * 不支持的情况:
 * ```javascript
 * // ❌ Date会变成字符串
 * deepClone({ date: new Date() }); 
 * 
 * // ❌ 函数会丢失
 * deepClone({ fn: () => {} });
 * 
 * // ✅ 应该用这些函数:
 * cloneWithDates({ date: new Date() });
 * ```
 * 
 * @see {@link cloneWithDates} 支持Date对象的拷贝
 * @see {@link safeClone} 支持循环引用的拷贝
 */
export function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

注释解释了:

  • 这个函数做什么(深拷贝)
  • 为什么这样实现(性能、兼容性)
  • 有什么限制(不支持Date、函数)
  • 什么时候会出错(循环引用)
  • 应该怎么用(示例)
  • 不能怎么用(反例)
  • 有什么替代方案(相关函数)

AI现在能准确回答所有关于这个函数的问题。

#实战示例2:业务函数

JavaScript
/**
 * 计算订单运费
 * 
 * 运费计算规则:
 * - 基础费用:5美元
 * - 重量费用:每公斤2美元
 * - 距离费用:每100公里0.5美元
 * - 快递附加费:总价的50%
 * 
 * @param {number} weight - 包裹重量(公斤)
 * @param {number} distance - 配送距离(公里)
 * @param {boolean} [express=false] - 是否快递
 * @returns {number} 运费(美元)
 * 
 * @throws {Error} 重量为负数或零时
 * @throws {Error} 距离为负数时
 * 
 * @example
 * 标准配送:
 * ```javascript
 * const cost = calculateShipping(2.5, 500);
 * // 计算:5 + (2.5 × 2) + (500 ÷ 100 × 0.5) = 12.5
 * ```
 * 
 * @example
 * 快递配送:
 * ```javascript
 * const cost = calculateShipping(2.5, 500, true);
 * // 计算:12.5 × 1.5 = 18.75(加收50%)
 * ```
 * 
 * @remarks
 * 定价策略文档:docs/business/shipping-policy.md
 * 最近修改:2024-12-15(快递附加费从40%提升到50%)
 * 
 * @see {@link estimateDeliveryTime} 计算配送时间
 * @see {@link validateAddress} 验证配送地址
 */
export function calculateShipping(weight, distance, express = false) {
  if (weight <= 0) {
    throw new Error('重量必须大于零');
  }
  if (distance < 0) {
    throw new Error('距离不能为负');
  }
  
  const baseRate = 5;
  const weightCost = weight * 2;
  const distanceCost = (distance / 100) * 0.5;
  const subtotal = baseRate + weightCost + distanceCost;
  
  return express ? subtotal * 1.5 : subtotal;
}

业务函数的注释要点:

  • 解释业务规则(不只是代码逻辑)
  • 说明单位(公斤、公里、美元)
  • 给出计算公式(让人能验证)
  • 链接到政策文档
  • 记录变更历史(方便追溯)

#实战示例3:扩展点

当你提供扩展机制时,文档要特别详细:

JavaScript
/**
 * 定义自定义类型
 * 
 * 这是创建自定义验证类型的主要方式。自定义类型可以用于
 * 验证特定格式的数据,比如邮箱、电话号码、身份证等。
 * 
 * @param {Object} definition - 类型定义
 * @param {string} definition.name - 类型名称(必须唯一)
 * @param {Function} definition.parse - 解析函数,将输入转换为目标类型
 * @param {Function} definition.validate - 验证函数,检查值是否合法
 * @param {*} [definition.default] - 默认值
 * @param {string} [definition.errorMessage] - 自定义错误信息
 * @returns {Type} 类型实例
 * 
 * @example
 * 简单的邮箱类型:
 * ```javascript
 * const EmailType = defineType({
 *   name: 'email',
 *   parse: (input) => String(input).toLowerCase().trim(),
 *   validate: (value) => /\S+@\S+\.\S+/.test(value),
 *   errorMessage: '请输入有效的邮箱地址'
 * });
 * 
 * // 使用
 * const schema = { email: EmailType };
 * validate(schema, { email: 'user@example.com' });
 * ```
 * 
 * @example
 * 带默认值的类型:
 * ```javascript
 * const TimestampType = defineType({
 *   name: 'timestamp',
 *   parse: (input) => new Date(input),
 *   validate: (value) => value instanceof Date,
 *   default: () => new Date()  // 函数形式,每次返回新值
 * });
 * ```
 * 
 * @example
 * 组合现有类型:
 * ```javascript
 * const PositiveNumber = defineType({
 *   name: 'positiveNumber',
 *   parse: (input) => {
 *     const num = Number(input);
 *     if (num <= 0) throw new Error('必须为正数');
 *     return num;
 *   },
 *   validate: (value) => typeof value === 'number' && value > 0
 * });
 * ```
 * 
 * @remarks
 * 常见模式:
 * - parse负责转换和清理(如trim、toLowerCase)
 * - validate负责检查合法性
 * - 如果parse失败应该抛出错误
 * - validate应该是纯函数,不应该修改值
 * 
 * @see {@link extendType} 扩展现有类型
 * @see {@link registerType} 全局注册类型
 */
export function defineType(definition) {
  // 实现...
}

扩展点的文档要点:

  • 多个示例覆盖不同场景
  • 说明常见模式(how、what、why)
  • 给出最佳实践
  • 指向相关函数

#什么时候需要详细注释

不是所有函数都需要长篇大论。根据复杂度调整:

#简单函数:最小注释

JavaScript
/**
 * 检查值是否为字符串
 * @param {*} value - 要检查的值
 * @returns {boolean} 是否为字符串
 */
export function isString(value) {
  return typeof value === 'string';
}

函数足够简单,一句话就够了。

#中等函数:标准注释

JavaScript
/**
 * 格式化日期为指定格式
 * 
 * @param {Date} date - 要格式化的日期
 * @param {'ISO'|'US'|'EU'} format - 目标格式
 * @returns {string} 格式化后的日期字符串
 * 
 * @example
 * formatDate(new Date('2025-01-03'), 'ISO')
 * // '2025-01-03'
 */
export function formatDate(date, format) {
  // 实现...
}

有一定复杂度,需要说明参数和示例。

#复杂函数:详细注释

JavaScript
/**
 * 验证并处理用户注册数据
 * 
 * 这个函数会:
 * 1. 验证必需字段
 * 2. 规范化数据格式(邮箱转小写、电话号码格式化等)
 * 3. 检查邮箱和用户名是否已被占用
 * 4. 生成密码哈希
 * 
 * @param {Object} data - 原始注册数据
 * @param {string} data.username - 用户名(3-20字符)
 * @param {string} data.email - 邮箱地址
 * @param {string} data.password - 密码(至少8字符)
 * @param {string} [data.phone] - 可选:电话号码
 * @returns {Promise<Object>} 处理后的用户对象
 * 
 * @throws {ValidationError} 数据验证失败
 * @throws {DuplicateError} 用户名或邮箱已存在
 * 
 * @example
 * 标准注册:
 * ```javascript
 * try {
 *   const user = await processRegistration({
 *     username: 'alice',
 *     email: 'alice@example.com',
 *     password: 'SecurePass123'
 *   });
 *   console.log('注册成功', user.id);
 * } catch (error) {
 *   if (error instanceof ValidationError) {
 *     console.error('验证失败', error.fields);
 *   }
 * }
 * ```
 * 
 * @remarks
 * 业务规则:
 * - 用户名只能包含字母、数字、下划线
 * - 邮箱会自动转为小写
 * - 密码会用bcrypt加密(强度10)
 * - 电话号码会格式化为E.164格式
 * 
 * @see {@link validateUsername} 用户名验证规则
 * @see {@link hashPassword} 密码加密实现
 */
export async function processRegistration(data) {
  // 实现...
}

复杂函数需要完整文档,包括流程、错误处理、业务规则。

#记录"为什么"而不只是"做什么"

代码本身能说明"做什么",注释应该解释"为什么"。

#❌ 无用的注释

JavaScript
// 创建一个新数组
const arr = [];

// 遍历items
for (const item of items) {
  // 把item添加到数组
  arr.push(item);
}

// 返回数组
return arr;

这些注释只是复述代码,毫无价值。

#✅ 有价值的注释

JavaScript
/**
 * 为什么要手动复制而不是用slice或spread?
 * 因为原数组可能有数百万条数据,我们需要:
 * 1. 边复制边进行数据转换
 * 2. 跳过无效的条目
 * 3. 在内存受限环境下运行
 * 
 * 性能测试(1M条数据):
 * - 手动循环 + 转换:1.2秒
 * - slice + map + filter:2.8秒
 */
const result = [];
for (const item of items) {
  // 跳过已删除的条目(API返回软删除数据)
  if (item.deleted) continue;
  
  // 转换为前端需要的格式
  result.push(transformItem(item));
}
return result;

注释解释了:

  • 为什么不用更简洁的方法
  • 性能考虑
  • 业务原因(软删除)

#内部库的特殊需求

对于公司内部的工具库,注释要包含更多业务上下文。

#记录变更历史

JavaScript
/**
 * 计算会员折扣价格
 * 
 * 变更历史:
 * - 2025-01-03: 修复国际订单的税率计算 (PR #234)
 * - 2024-12-15: 白金会员折扣从12%提升到15% (Ticket #SALES-456)
 * - 2024-11-20: 添加对企业会员的支持 (PR #189)
 * - 2024-10-01: 初始实现
 * 
 * @param {number} price - 原价
 * @param {string} membershipLevel - 会员等级
 * @returns {number} 折后价
 */
export function calculateMemberDiscount(price, membershipLevel) {
  // 实现...
}

#标注影响范围

JavaScript
/**
 * 格式化用户显示名称
 * 
 * ⚠️ 警告:这是一个高频使用的函数
 * 
 * 使用位置:
 * - 前端用户资料页 (ProfilePage.tsx)
 * - 邮件模板 (email-templates/*.tsx)
 * - PDF报表生成 (reports/user-report.ts)
 * - 管理后台用户列表 (admin/UserList.tsx)
 * - Webhook推送 (webhooks/user-events.ts)
 * 
 * 修改前需要:
 * 1. 通知前端团队(影响UI显示)
 * 2. 检查邮件模板(格式变化可能影响邮件)
 * 3. 测试PDF生成(可能影响排版)
 * 4. 通知Webhook接收方(可能影响对方系统)
 * 
 * @param {Object} user - 用户对象
 * @returns {string} 显示名称
 */
export function formatUserDisplayName(user) {
  return `${user.firstName} ${user.lastName}`;
}

#链接到业务文档

JavaScript
/**
 * 验证配送地址
 * 
 * 业务规则来源:
 * - 配送区域:docs/business/shipping-regions.md
 * - 地址格式:docs/business/address-format.md
 * - 禁运限制:docs/legal/shipping-restrictions.md
 * 
 * 相关票号:
 * - 初始需求:#SHIP-123
 * - PO Box限制:#POLICY-456
 * - 国际配送:#INTL-789
 * 
 * @param {Object} address - 配送地址
 * @returns {ValidationResult} 验证结果
 */
export function validateShippingAddress(address) {
  // 实现...
}

#应该发布哪些文档到npm

除了代码注释,还应该在npm包中包含一些文档文件。

#必须包含

your-framework/
├── README.md          ← 项目概述和快速开始
├── CHANGELOG.md       ← 版本变更历史
└── LICENSE            ← 开源协议

#推荐包含

your-framework/
├── docs/
│   ├── getting-started.md    ← 新手指南
│   ├── api/
│   │   ├── core.md          ← 核心API文档
│   │   ├── types.md         ← 类型系统文档
│   │   └── plugins.md       ← 插件系统文档
│   └── guides/
│       ├── custom-types.md   ← 自定义类型指南
│       └── best-practices.md ← 最佳实践
├── examples/
│   ├── basic/
│   │   ├── hello-world.js
│   │   └── validation.js
│   └── advanced/
│       └── custom-type.js
└── templates/
    └── custom-type.template.js

为什么要包含这些:

  • AI能读取本地文档
  • 用户离线时也能查看
  • 文档版本和代码版本完全匹配
  • 不依赖外部网站

#不应该包含

website/               ← 网站内容
  blog/
  tutorials/
CONTRIBUTING.md       ← 贡献指南
.github/              ← GitHub配置
docs/
  architecture.md     ← 架构设计文档
  development.md      ← 开发指南

这些只对开发者有用,用户不需要。

#package.json配置

JSON
{
  "files": [
    "dist",
    "docs",
    "examples",
    "templates",
    "README.md",
    "CHANGELOG.md",
    "LICENSE"
  ]
}

#示例代码的重要性

对AI来说,示例代码比文字描述更有价值

#基础示例

JavaScript
// examples/basic/hello-world.js
/**
 * 最简单的使用示例
 * 
 * 这个示例展示了框架的基本用法:
 * 1. 导入框架
 * 2. 创建应用
 * 3. 运行应用
 */

import { createApp } from 'your-framework';

const app = createApp({
  name: 'hello-world'
});

console.log('应用已创建:', app.name);

#实用示例

JavaScript
// examples/validation/user-schema.js
/**
 * 用户数据验证示例
 * 
 * 展示如何:
 * - 定义数据结构
 * - 使用内置类型
 * - 处理验证错误
 */

import { createSchema, StringType, NumberType, EmailType } from 'your-framework';

// 定义用户数据结构
const userSchema = createSchema({
  username: StringType.min(3).max(20),
  email: EmailType,
  age: NumberType.min(18).max(120)
});

// 验证数据
try {
  const user = userSchema.parse({
    username: 'alice',
    email: 'alice@example.com',
    age: 25
  });
  
  console.log('验证成功:', user);
} catch (error) {
  console.error('验证失败:', error.message);
}

#高级示例

JavaScript
// examples/advanced/custom-type.js
/**
 * 自定义类型完整示例
 * 
 * 这个示例展示如何创建一个自定义的邮政编码验证类型。
 * 包含:
 * - 类型定义
 * - 格式验证
 * - 错误处理
 * - 实际使用
 */

import { defineType, createSchema } from 'your-framework';

// 定义邮政编码类型
const PostalCodeType = defineType({
  name: 'postalCode',
  
  // 解析:去除空格和连字符
  parse(input) {
    return String(input).replace(/[\s-]/g, '');
  },
  
  // 验证:台湾邮政编码是3或5位数字
  validate(value) {
    return /^(\d{3}|\d{5})$/.test(value);
  },
  
  errorMessage: '请输入有效的邮政编码(3位或5位数字)'
});

// 使用自定义类型
const addressSchema = createSchema({
  city: StringType,
  postalCode: PostalCodeType
});

// 测试
const testCases = [
  { city: 'Taipei', postalCode: '10048' },    // ✓ 有效
  { city: 'Taipei', postalCode: '100' },      // ✓ 有效
  { city: 'Taipei', postalCode: '100-48' },   // ✓ 有效(会被规范化)
  { city: 'Taipei', postalCode: 'invalid' }   // ✗ 无效
];

testCases.forEach(data => {
  try {
    const result = addressSchema.parse(data);
    console.log('✓', data.postalCode, '→', result.postalCode);
  } catch (error) {
    console.log('✗', data.postalCode, '→', error.message);
  }
});

#模板文件

提供可直接复制的模板:

JavaScript
// templates/custom-type.template.js
/**
 * 自定义类型模板
 * 
 * 使用说明:
 * 1. 复制这个文件到你的项目
 * 2. 把 'MyType' 替换成你的类型名
 * 3. 实现 parse 和 validate 方法
 * 4. 添加测试和文档
 */

import { defineType } from 'your-framework';

export const MyType = defineType({
  // 类型的唯一标识符
  name: 'myType',
  
  /**
   * 解析输入值
   * 
   * 这个方法负责:
   * - 清理和规范化输入
   * - 转换为目标类型
   * - 抛出错误如果无法解析
   */
  parse(input) {
    // TODO: 实现解析逻辑
    // 示例:
    // if (typeof input !== 'string') {
    //   throw new Error('Expected string input');
    // }
    // return input.trim().toLowerCase();
    
    throw new Error('Not implemented');
  },
  
  /**
   * 验证值是否合法
   * 
   * 这个方法应该:
   * - 是纯函数(不修改输入)
   * - 返回 boolean
   * - 执行快速
   */
  validate(value) {
    // TODO: 实现验证逻辑
    // 示例:
    // return typeof value === 'string' && value.length > 0;
    
    return false;
  },
  
  // 可选:提供默认值
  default: undefined,
  
  // 可选:自定义错误信息
  errorMessage: 'Invalid value for MyType'
});

// 使用示例
// const schema = createSchema({
//   myField: MyType
// });

#自动化工具

#ESLint检查注释完整性

JavaScript
// .eslintrc.js
module.exports = {
  plugins: ['jsdoc'],
  rules: {
    // 要求所有导出函数都有JSDoc
    'jsdoc/require-jsdoc': ['error', {
      require: {
        FunctionDeclaration: true,
        MethodDefinition: true
      },
      contexts: [
        'ExportNamedDeclaration > FunctionDeclaration'
      ]
    }],
    
    // 要求描述
    'jsdoc/require-description': 'error',
    
    // 要求参数文档
    'jsdoc/require-param': 'error',
    'jsdoc/require-param-description': 'error',
    
    // 要求返回值文档
    'jsdoc/require-returns': 'error',
    'jsdoc/require-returns-description': 'error',
    
    // 建议添加示例(警告级别)
    'jsdoc/require-example': 'warn'
  }
};

#IDE代码片段

在VSCode中创建快捷片段:

JSON
// .vscode/snippets.code-snippets
{
  "JSDoc Function": {
    "prefix": "jsdocfn",
    "body": [
      "/**",
      " * ${1:函数描述}",
      " * ",
      " * @param {${2:type}} ${3:paramName} - ${4:参数说明}",
      " * @returns {${5:type}} ${6:返回值说明}",
      " * ",
      " * @example",
      " * ```javascript",
      " * ${7:// 使用示例}",
      " * ```",
      " */"
    ]
  }
}

现在输入 jsdocfn 就能快速插入JSDoc模板。

##小结

在AI时代,文档不再是"写完代码后补的东西",而是代码的一部分。 核心要点:

JSDoc是AI理解代码的主要来源 记录"为什么",不只是"做什么" 提供多个示例覆盖不同场景 内部库要包含业务上下文和变更历史 发布文档、示例、模板到npm 使用工具自动检查文档完整性

实用建议:

简单函数:最小注释(一句话 + 参数 + 返回值) 中等函数:标准注释(+ 示例) 复杂函数:详细注释(+ 流程 + 错误 + 业务规则) 扩展点:完整文档(+ 多示例 + 最佳实践)