Skip to content
扫码开始移动端阅读

什么样的api封装更适合中国人的体质

在中国,我们有一个很有名的叫做“中国特色社会主义”的东西,这个东西是中国人自己发明的,所以它的特色就是适合中国人的体质。那么在js中,我们也可以发明一些适合中国人的体质的东西,比如说api的封装。

假如有一个api地址是https://test.com/apis/user/getUserInfo?id=xxxx,那么我们可以把它封装成这样的形式:

首先你要有一个apis的文件夹,然后再新建一个user的文件。

普通版本

js
export default class User {
  static async getUserInfo(id) {}
}

但是这样做有一个问题,我们鼠标放上去的时候只会提示一个class User,并不知道这个User的类里面到底有哪些方法。

给类标注类名和方法名

js
/**
 * @name Ca 授权API类
 * @property {Function} getUserInfo - 获取用户信息
 */
export default class User {
  static async getUserInfo(id) {}
}

我们通过给这个类定义了一个@name后边跟上方法名,后边跟上注释。首先知道了,这个类是干什么的,然后通过@property定义类的属性,知道了这个类里面有哪些方法。

标记入参和返回值

js
/**
 * @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的内容是什么样的。

对象有多个属性

js
/**
 * @returns {Promise<{name: string, age: number, email: string}>} 返回一个Promise对象,包含用户姓名、年龄和电子邮件
 * */

那么我们可以通过上方的方法来定义这个Object的内容。

不要做

不要返回 @returns Object 或者 @returns Promise<Object>
因为这样的话,这样是没有任何意义的。js会认为这是一个Any的类型

参数是固定值

js
/**
 * @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文件,这个后端的,可能和你的是不一样的。具体的方案还是要因地制宜

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的脚本进行处理

编写工具

js
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这个库,进行格式化。最后写入文件。就大功告成了。

成果展示

js
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。是比较好维护的,另外如果后台出了问题。文档写错了。或者文档没写好。这个锅,肯定不会是前端来背。哈哈哈哈。