关于jsDoc注释(api篇)
什么样的api封装更适合中国人的体质
在中国,我们有一个很有名的叫做“中国特色社会主义”的东西,这个东西是中国人自己发明的,所以它的特色就是适合中国人的体质。那么在js中,我们也可以发明一些适合中国人的体质的东西,比如说api的封装。
假如有一个api地址是 https://test.com/apis/user/getUserInfo?id=xxxx,那么我们可以把它封装成这样的形式:
首先你要有一个apis的文件夹,然后再新建一个user的文件。
普通版本
export default class User {
static async getUserInfo(id) {}
}2
3
但是这样做有一个问题,我们鼠标放上去的时候只会提示一个class User,并不知道这个User的类里面到底有哪些方法。
给类标注类名和方法名
/**
* @name Ca 授权API类
* @property {Function} getUserInfo - 获取用户信息
*/
export default class User {
static async getUserInfo(id) {}
}2
3
4
5
6
7
我们通过给这个类定义了一个 @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) {}
}2
3
4
5
6
7
8
9
10
11
12
13
这部分,我们扩展了两部分的内容,第一个。我们可以定义一个 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对象,包含用户姓名、年龄和电子邮件
* */2
3
那么我们可以通过上方的方法来定义这个 Object的内容。
不要做
不要返回 @returns Object 或者 @returns Promise<Object>
因为这样的话,这样是没有任何意义的。js会认为这是一个Any的类型
参数是固定值
/**
* @name getUserInfo 获取用户信息
* @param {Object} params 行参
* @param {('admin' | 'default' | 'blacklist')} params.userType 用户类型
* @returns {Promise<String> | Boolean} 用户名称 || false
* */2
3
4
5
6
如果参数是一个可选项。比如 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"
}
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
然后写一个 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);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
原理很简单,就是读取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;
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
总结
可能有人问,你这注释都不自己写的吗?这样的代码是没有灵魂的。巴拉巴拉。
我只能说,每个程序员最喜欢的东西,就是写了注释的代码,最讨厌的事情,就是写注释。所以,显然通过脚本生成注释,可以解决我们很多问题。
其实还有一些好处,就是我们通过工具生成的api。是比较好维护的,另外如果后台出了问题。文档写错了。或者文档没写好。这个锅,肯定不会是前端来背。哈哈哈哈。