#文档的困境
前几天,一个用了我们内部工具库半年的同事问我:"那个 processData 函数,第二个参数传什么?"
我去看代码:
export function processData(data, options) {
// 150行实现
}
没有注释,参数是 any 类型,函数名也不够清晰。我只能翻 Git 历史,找到当初的PR,才看懂 options 是配置对象。
后来我加了详细注释:
/**
* 处理和规范化用户数据
*
* @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应该包含:
#最小必需信息
/**
* [一句话描述:这个函数是做什么的]
*
* @param {Type} paramName - [参数说明]
* @returns {Type} [返回值说明]
*
* @example
* [最简单的用法]
*/
#完整的JSDoc模板
/**
* [一句话概述]
*
* [可选:详细说明,包括]
* - 使用场景
* - 注意事项
* - 与其他函数的关系
*
* @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:工具函数
/**
* 深拷贝对象
*
* 使用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:业务函数
/**
* 计算订单运费
*
* 运费计算规则:
* - 基础费用: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:扩展点
当你提供扩展机制时,文档要特别详细:
/**
* 定义自定义类型
*
* 这是创建自定义验证类型的主要方式。自定义类型可以用于
* 验证特定格式的数据,比如邮箱、电话号码、身份证等。
*
* @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)
- 给出最佳实践
- 指向相关函数
#什么时候需要详细注释
不是所有函数都需要长篇大论。根据复杂度调整:
#简单函数:最小注释
/**
* 检查值是否为字符串
* @param {*} value - 要检查的值
* @returns {boolean} 是否为字符串
*/
export function isString(value) {
return typeof value === 'string';
}
函数足够简单,一句话就够了。
#中等函数:标准注释
/**
* 格式化日期为指定格式
*
* @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) {
// 实现...
}
有一定复杂度,需要说明参数和示例。
#复杂函数:详细注释
/**
* 验证并处理用户注册数据
*
* 这个函数会:
* 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) {
// 实现...
}
复杂函数需要完整文档,包括流程、错误处理、业务规则。
#记录"为什么"而不只是"做什么"
代码本身能说明"做什么",注释应该解释"为什么"。
#❌ 无用的注释
// 创建一个新数组
const arr = [];
// 遍历items
for (const item of items) {
// 把item添加到数组
arr.push(item);
}
// 返回数组
return arr;
这些注释只是复述代码,毫无价值。
#✅ 有价值的注释
/**
* 为什么要手动复制而不是用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;
注释解释了:
- 为什么不用更简洁的方法
- 性能考虑
- 业务原因(软删除)
#内部库的特殊需求
对于公司内部的工具库,注释要包含更多业务上下文。
#记录变更历史
/**
* 计算会员折扣价格
*
* 变更历史:
* - 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) {
// 实现...
}
#标注影响范围
/**
* 格式化用户显示名称
*
* ⚠️ 警告:这是一个高频使用的函数
*
* 使用位置:
* - 前端用户资料页 (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}`;
}
#链接到业务文档
/**
* 验证配送地址
*
* 业务规则来源:
* - 配送区域: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配置
{
"files": [
"dist",
"docs",
"examples",
"templates",
"README.md",
"CHANGELOG.md",
"LICENSE"
]
}
#示例代码的重要性
对AI来说,示例代码比文字描述更有价值。
#基础示例
// examples/basic/hello-world.js
/**
* 最简单的使用示例
*
* 这个示例展示了框架的基本用法:
* 1. 导入框架
* 2. 创建应用
* 3. 运行应用
*/
import { createApp } from 'your-framework';
const app = createApp({
name: 'hello-world'
});
console.log('应用已创建:', app.name);
#实用示例
// 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);
}
#高级示例
// 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);
}
});
#模板文件
提供可直接复制的模板:
// 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检查注释完整性
// .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中创建快捷片段:
// .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 使用工具自动检查文档完整性
实用建议:
简单函数:最小注释(一句话 + 参数 + 返回值) 中等函数:标准注释(+ 示例) 复杂函数:详细注释(+ 流程 + 错误 + 业务规则) 扩展点:完整文档(+ 多示例 + 最佳实践)