关于jsDoc注释(api篇)
什么样的api封装更适合中国人的体质
在中国,我们有一个很有名的叫做“中国特色社会主义”的东西,这个东西是中国人自己发明的,所以它的特色就是适合中国人的体质。那么在js中,我们也可以发明一些适合中国人的体质的东西,比如说api的封装。
假如有一个api地址是https://test.com/apis/user/getUserInfo?id=xxxx
,那么我们可以把它封装成这样的形式:
首先你要有一个apis的文件夹,然后再新建一个user的文件。
普通版本
export default class User {
static async getUserInfo(id) {}
}
但是这样做有一个问题,我们鼠标放上去的时候只会提示一个class User,并不知道这个User的类里面到底有哪些方法。
给类标注类名和方法名
/**
* @name Ca 授权API类
* @property {Function} getUserInfo - 获取用户信息
*/
export default class User {
static async getUserInfo(id) {}
}
我们通过给这个类定义了一个@name
后边跟上方法名,后边跟上注释。首先知道了,这个类是干什么的,然后通过@property
定义类的属性,知道了这个类里面有哪些方法。
标记入参和返回值
/**
* @name Ca 授权API类
* @property {Function} getUserInfo - 获取用户信息
*/
export default class User {
/**
* @name getUserInfo 获取用户信息
* @param {Object} params 行参
* @param {Number} params.id 用户id
* @returns {Promise<String> | Boolean} 用户名称 || false
* */
static async getUserInfo(id) {}
}
这部分,我们扩展了两部分的内容,第一个。我们可以定义一个param
然后通过 params.id
的方式,展开这个参数的内容,这样我们就知道了这个参数的具体内容。第二个,我们可以通过@returns
来定义这个方法的返回值,比如说这个方法返回的是一个Promise
,那么我们就可以通过Promise<String>
来定义这个方法的返回值是一个Promise
,并且这个Promise
的返回值是一个String
类型的数据。
注意事项
returns 如果返回的是一个Object
,那么我们可以通过@returns {Object.xxx<string, number>}
来定义这个Object
的内容,这样我们就知道了这个Object
的内容是什么样的。
对象有多个属性
/**
* @returns {Promise<{name: string, age: number, email: string}>} 返回一个Promise对象,包含用户姓名、年龄和电子邮件
* */
那么我们可以通过上方的方法来定义这个Object
的内容。
不要做
不要返回 @returns Object
或者 @returns Promise<Object>
因为这样的话,这样是没有任何意义的。js会认为这是一个Any的类型
参数是固定值
/**
* @name getUserInfo 获取用户信息
* @param {Object} params 行参
* @param {('admin' | 'default' | 'blacklist')} params.userType 用户类型
* @returns {Promise<String> | Boolean} 用户名称 || false
* */
如果参数是一个可选项。比如userType
只能是admin
或者是 default
或者是 blacklist
。
那么我们可以通过@param {('admin' | 'default' | 'blacklist')} params.userType
来定义这个参数的类型。
注意事项
这样做的话,JsDoc会自动推断类型。且必须是基础类型。比如 @param (1|2|3) userId 用户Id
扩展
下方内容不适用所有人,不适用所有项目。
假如你有一个很靠谱的后端
众所周知除了NodeJs
。大部分的后台语言都是强类型语言,所以他们给的接口一般都是有类型的,那么在对接的时候,肯定要给我们一个文档。
然后类似swagger
的接口文档都是可以导出json
文件的。NodeJs
是可以读取json
文件的。既然可以读取,那我们的想法可以大胆一点。
通过NodeJs
生成需要调用的后台接口,我们要做的就是封装一个request.js
。然后新建一个目录
,开启我们的生成计划。
生成计划
我已经准备好了一个json
文件,这个后端的,可能和你的是不一样的。具体的方案还是要因地制宜
。
[
{
"desc": "登录接口",
"reqParams": [
{
"desc": "账号",
"name": "account",
"required": "必填",
"type": "String"
},
{
"desc": "密码",
"name": "password",
"required": "必填",
"type": "String 小写md5"
}
],
"requestTime": 0,
"requestType": "post,get",
"respParams": [],
"respText": "",
"title": "登录",
"url": "/api/account/login"
},
{
"desc": "",
"reqParams": [
{
"desc": "用户token",
"name": "token",
"required": "必填",
"type": "String"
}
],
"requestTime": 0,
"requestType": "get",
"respParams": [],
"respText": "",
"title": "退出",
"url": "/api/account/logout"
}
]
然后写一个NodeJs
的脚本进行处理
编写工具
const fs = require("fs");
const path = require("path");
const prettier = require("prettier");
// 读取api.json文件中的数据
const apiData = require("./api.json");
// 将字符串转换为小驼峰命名
function toCamelCase(str) {
return str.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""));
}
// 文件名命名格式化
const fileNameFormat = (str) => {
// 1. 小写字母
str = str.toLowerCase();
// 2. 单词之间用下划线连接
str = str.replace(/\s+/g, "_");
// 3. 不能有空格
str = str.replace(/\s+/g, "");
// 4. 不能有特殊字符
str = str.replace(/[^\w\u4e00-\u9fa5]/g, "");
// 5. 不能有中文
str = str.replace(/[\u4e00-\u9fa5]/g, "");
// 6. 不能以数字开头
str = str.replace(/^\d+/, "");
return str;
};
// 函数的方法
const toCamelCaseLowerCase = (str) => {
const camelCase = toCamelCase(str);
return camelCase[0].toLowerCase() + camelCase.slice(1);
};
// 创建apis目录(如果不存在)
const apisDir = "./apis";
if (!fs.existsSync(apisDir)) {
fs.mkdirSync(apisDir);
}
// 格式化参数
function formatReqParams(reqParams) {
const getStr = (param) => {
const typeOjb = {
int: "Number",
Long: "Number",
BigDecimal: "Number",
string: "String",
};
const pType = param.type;
const type =
(pType.includes("int") && typeOjb.int) ||
(pType.includes("Long") && typeOjb.Long) ||
(pType.includes("string") && typeOjb.string) ||
(pType.includes("BigDecimal") && typeOjb.BigDecimal) ||
"String";
// 通过正则去除空格,删除包含int long string 的文字
const desc = param.desc
.replace(/\s/g, "")
.replace(/(int|Long|string)/g, "");
// 动态生成参数名
const name =
param.required === "必填"
? "params." + param.name
: `[params.${param.name}]`;
const descRes = param.desc == desc ? ` - ${param.desc}` : param.desc + desc;
return `* @param {${type}} ${name} ${descRes} `;
};
const reqFast = "* @param {Object} params - 行参";
if (!reqParams.length) {
return reqFast;
}
return (
reqFast +
"\n" +
reqParams
.map((param) => {
return `${getStr(param)}`;
})
.join("\n")
);
}
// 生成方法注释
const methodComment = (arr) => {
if (!arr.length) {
return "*";
} else {
return arr
.map((item) => {
return `* @property {Function} ${item.fun} - ${item.title}`;
})
.join("\n");
}
};
// 循环分类
apiData.forEach(async (item) => {
const classArr = item.apiActions[0].url && item.apiActions[0].url.split("/");
const className = classArr && classArr[classArr.length - 2];
const categoryName = className[0].toUpperCase() + className.slice(1);
const fileName = toCamelCase(className);
const funList = [];
// 循环接口
const apiPromises = item.apiActions.map(async (api_action) => {
const actionTitle = api_action.title || "未定义";
const funText = api_action.url.split("/");
const funName = funText[funText.length - 1];
const translatedActionTitle = funName[0].toUpperCase() + funName.slice(1);
const formattedTitle = toCamelCase(translatedActionTitle);
// 生成 参数 注释
const jsDocComment = `
/**
* @name ${actionTitle}
${formatReqParams(api_action.reqParams)}
* @returns {Promise :Object} 返回值
* @exports ${categoryName}.${funName}
*/
`;
funList.push({
title: actionTitle,
fun: toCamelCaseLowerCase(formattedTitle),
});
// 生成方法代码
const methodCode = `
static async ${toCamelCaseLowerCase(formattedTitle)}(params) {
try {
const {data} = await request.${
api_action.requestType.includes("get") ? "get" : "post"
}("${api_action.url.replace("/api/", "")}", params);
return data;
} catch (error) {
console.error("[API] Error: class: ${categoryName} - static: ${formattedTitle} ", error);
throw error;
}
}
`;
// 将 JSDoc 注释和方法代码连接在一起
return jsDocComment + methodCode;
});
// 等待所有 API 动作的 Promise 完成
const apiMethodStrings = await Promise.all(apiPromises);
// 生成类代码
const categoryClassCode = `
import request from "../request";
/**
* @name ${categoryName} ${item.title} API 类
${methodComment(funList)}
* @module ${categoryName}
*/
export default class ${categoryName} {
${apiMethodStrings
.map((methodString) => {
return methodString
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
})
.join("\n\n")}
}
`;
const formattedClassCode = await prettier.format(categoryClassCode, {
parser: "babel",
singleQuote: true,
semi: true,
trailingComma: "all",
});
// 写入类代码到文件
const categoryFilePath = path.join(apisDir, `${fileNameFormat(fileName)}.js`);
fs.writeFileSync(categoryFilePath, formattedClassCode);
});
原理很简单,就是读取json。循环json。根据json生成一个模板字符串。然后使用prettier
这个库,进行格式化。最后写入文件。就大功告成了。
成果展示
import request from '../request';
/**
* 账户 API 类
* @property {Function} countryList - 国家列表
* @property {Function} sendEmailCode - 发送邮箱验证码
* @property {Function} sendSmsCode - 发送短信验证码
* @property {Function} captcha - 图片验证码
* @property {Function} register - 注册
* @property {Function} login - 登录
* @property {Function} logout - 退出
* @property {Function} checkLogin - 检查是否登录
* @property {Function} findPwdVerify - 重置密码验证
* @property {Function} sendResetPwdEmail - 发送重置密码邮箱验证码
* @property {Function} sendResetPwdSms - 发送重置密码短信验证码
* @module Account
*/
export default class Account {
/**
* @name countryList 国家列表
* @param {Object} params - 行参
* @returns {Promise :Object} 返回值
* @exports Account.countryList
*/
static async countryList(params) {
try {
const { data } = await request.get('account/countryList', params);
return data;
} catch (error) {
console.error(
'[API] Error: class: Account - static: CountryList ',
error,
);
throw error;
}
}
/**
* @name sendEmailCode 发送邮箱验证码
* @param {Object} params - 行参
* @param {String} params.email - 电子邮箱
* @returns {Promise :Object} 返回值
* @exports Account.sendEmailCode
*/
static async sendEmailCode(params) {
try {
const { data } = await request.get('account/sendEmailCode', params);
return data;
} catch (error) {
console.error(
'[API] Error: class: Account - static: SendEmailCode ',
error,
);
throw error;
}
}
/**
* @name sendSmsCode 发送短信验证码
* @param {Object} params - 行参
* @param {String} params.nationCode - 国籍
* @param {String} params.mobile - 手机号码
* @returns {Promise :Object} 返回值
* @exports Account.sendSmsCode
*/
static async sendSmsCode(params) {
try {
const { data } = await request.get('account/sendSmsCode', params);
return data;
} catch (error) {
console.error(
'[API] Error: class: Account - static: SendSmsCode ',
error,
);
throw error;
}
}
/**
* @name captcha 图片验证码
* @param {Object} params - 行参
* @returns {Promise :Object} 返回值
* @exports Account.captcha
*/
static async captcha(params) {
try {
const { data } = await request.get('account/captcha', params);
return data;
} catch (error) {
console.error('[API] Error: class: Account - static: Captcha ', error);
throw error;
}
}
/**
* @name register 注册
* @param {Object} params - 行参
* @param {String} params.rtype - 注册类型
* @param {String} params.nationCode - 国籍
* @param {String} [params.mobile] - 手机号码
* @param {String} [params.smsCode] - 短信验证码
* @param {String} [params.email] - 电子邮箱
* @param {String} [params.emailCode] - 邮箱验证码
* @param {String} params.userPwd - 账号密码
* @param {Number} [params.inviteUid] - 邀请人UID
* @returns {Promise :Object} 返回值
* @exports Account.register
*/
static async register(params) {
try {
const { data } = await request.get('account/register', params);
return data;
} catch (error) {
console.error('[API] Error: class: Account - static: Register ', error);
throw error;
}
}
/**
* @name login 登录
* @param {Object} params - 行参
* @param {String} params.account - 账号
* @param {String} params.password - 密码
* @returns {Promise :Object} 返回值
* @exports Account.login
*/
static async login(params) {
try {
const { data } = await request.get('account/login', params);
return data;
} catch (error) {
console.error('[API] Error: class: Account - static: Login ', error);
throw error;
}
}
/**
* @name logout 退出
* @param {Object} params - 行参
* @param {String} params.token - 用户token
* @returns {Promise :Object} 返回值
* @exports Account.logout
*/
static async logout(params) {
try {
const { data } = await request.get('account/logout', params);
return data;
} catch (error) {
console.error('[API] Error: class: Account - static: Logout ', error);
throw error;
}
}
/**
* @name checkLogin 检查是否登录
* @param {Object} params - 行参
* @param {String} params.token - 用户token
* @returns {Promise :Object} 返回值
* @exports Account.checkLogin
*/
static async checkLogin(params) {
try {
const { data } = await request.get('account/checkLogin', params);
return data;
} catch (error) {
console.error('[API] Error: class: Account - static: CheckLogin ', error);
throw error;
}
}
/**
* @name findPwdVerify 重置密码验证
* @param {Object} params - 行参
* @param {String} params.findType - 重置类型
* @param {String} params.operate - 操作
* @param {String} params.account - 账号
* @param {String} [params.smsCode] - 短信验证码
* @param {String} [params.emailCode] - 邮箱验证码
* @param {String} [params.clientTokenId] - 验证Token
* @param {String} [params.userPwd] - 登录密码
* @param {String} [params.rePwd] - 确认密码
* @returns {Promise :Object} 返回值
* @exports Account.findPwdVerify
*/
static async findPwdVerify(params) {
try {
const { data } = await request.get('account/findPwdVerify', params);
return data;
} catch (error) {
console.error(
'[API] Error: class: Account - static: FindPwdVerify ',
error,
);
throw error;
}
}
/**
* @name sendResetPwdEmail 发送重置密码邮箱验证码
* @param {Object} params - 行参
* @param {String} params.account - 电子邮箱
* @param {String} params.clientTokenId - 验证Token
* @returns {Promise :Object} 返回值
* @exports Account.sendResetPwdEmail
*/
static async sendResetPwdEmail(params) {
try {
const { data } = await request.get('account/sendResetPwdEmail', params);
return data;
} catch (error) {
console.error(
'[API] Error: class: Account - static: SendResetPwdEmail ',
error,
);
throw error;
}
}
/**
* @name sendResetPwdSms 发送重置密码短信验证码
* @param {Object} params - 行参
* @param {String} params.account - 手机
* @param {String} params.clientTokenId - 验证Token
* @returns {Promise :Object} 返回值
* @exports Account.sendResetPwdSms
*/
static async sendResetPwdSms(params) {
try {
const { data } = await request.get('account/sendResetPwdSms', params);
return data;
} catch (error) {
console.error(
'[API] Error: class: Account - static: SendResetPwdSms ',
error,
);
throw error;
}
}
}
总结
可能有人问,你这注释都不自己写的吗?这样的代码是没有灵魂的。巴拉巴拉。
我只能说,每个程序员最喜欢的东西,就是写了注释的代码,最讨厌的事情,就是写注释。所以,显然通过脚本生成注释,可以解决我们很多问题。
其实还有一些好处,就是我们通过工具生成的api。是比较好维护的,另外如果后台出了问题。文档写错了。或者文档没写好。这个锅,肯定不会是前端来背。哈哈哈哈。