UNPKG

co-wechat-api

Version:

微信公共平台Node库API,ES6版本

244 lines (220 loc) 8.07 kB
'use strict'; // 本文件用于wechat API,基础文件,主要用于Token的处理和mixin机制 const httpx = require('httpx'); const liburl = require('url'); const JSONbig = require('json-bigint'); const { replaceJSONCtlChars } = require('./util'); class AccessToken { constructor(accessToken, expireTime) { this.accessToken = accessToken; this.expireTime = expireTime; } /*! * 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比 * Examples: * ``` * token.isValid(); * ``` */ isValid() { return !!this.accessToken && Date.now() < this.expireTime; } } class API { /** * 根据 appid 和 appsecret 创建API的构造函数 * 如需跨进程跨机器进行操作Wechat API(依赖access token),access token需要进行全局维护 * 使用策略如下: * 1. 调用用户传入的获取 token 的异步方法,获得 token 之后使用 * 2. 使用appid/appsecret获取 token 。并调用用户传入的保存 token 方法保存 * Tips: * - 如果跨机器运行wechat模块,需要注意同步机器之间的系统时间。 * Examples: * ``` * var API = require('wechat-api'); * var api = new API('appid', 'secret'); * ``` * 以上即可满足单进程使用。 * 当多进程时,token 需要全局维护,以下为保存 token 的接口。 * ``` * var api = new API('appid', 'secret', async function () { * // 传入一个获取全局 token 的方法 * var txt = await fs.readFile('access_token.txt', 'utf8'); * return JSON.parse(txt); * }, async function (token) { * // 请将 token 存储到全局,跨进程、跨机器级别的全局,比如写到数据库、redis等 * // 这样才能在cluster模式及多机情况下使用,以下为写入到文件的示例 * await fs.writeFile('access_token.txt', JSON.stringify(token)); * }); * ``` * @param {String} appid 在公众平台上申请得到的appid * @param {String} appsecret 在公众平台上申请得到的app secret * @param {AsyncFunction} getToken 可选的。获取全局token对象的方法,多进程模式部署时需在意 * @param {AsyncFunction} saveToken 可选的。保存全局token对象的方法,多进程模式部署时需在意 */ constructor(appid, appsecret, getToken, saveToken, tokenFromCustom) { this.appid = appid; this.appsecret = appsecret; this.getToken = getToken || async function () { return this.store; }; this.saveToken = saveToken || async function (token) { this.store = token; if (process.env.NODE_ENV === 'production') { console.warn('Don\'t save token in memory, when cluster or multi-computer!'); } }; this.prefix = 'https://api.weixin.qq.com/cgi-bin/'; this.snsPrefix = 'https://api.weixin.qq.com/sns/'; this.mpPrefix = 'https://mp.weixin.qq.com/cgi-bin/'; this.fileServerPrefix = 'http://file.api.weixin.qq.com/cgi-bin/'; this.payPrefix = 'https://api.weixin.qq.com/pay/'; this.merchantPrefix = 'https://api.weixin.qq.com/merchant/'; this.customservicePrefix = 'https://api.weixin.qq.com/customservice/'; this.wxaPrefix = 'https://api.weixin.qq.com/wxa/'; this.defaults = {}; this.tokenFromCustom = tokenFromCustom; // set default js ticket handle this.registerTicketHandle(); } /** * 用于设置urllib的默认options * Examples: * ``` * api.setOpts({timeout: 15000}); * ``` * @param {Object} opts 默认选项 */ setOpts(opts) { this.defaults = opts; } /** * 设置urllib的hook */ async request(url, opts, retry) { if (typeof retry === 'undefined') { retry = 3; } var options = {}; Object.assign(options, this.defaults); opts || (opts = {}); var keys = Object.keys(opts); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (key !== 'headers') { options[key] = opts[key]; } else { if (opts.headers) { options.headers = options.headers || {}; Object.assign(options.headers, opts.headers); } } } var res = await httpx.request(url, options); if (res.statusCode < 200 || res.statusCode > 204) { var err = new Error(`url: ${url}, status code: ${res.statusCode}`); err.name = 'WeChatAPIError'; throw err; } var buffer = await httpx.read(res); var contentType = res.headers['content-type'] || ''; if (contentType.includes('application/json') || contentType.includes('text/plain')) { var data; var origin = buffer.toString(); try { data = JSONbig.parse(replaceJSONCtlChars(origin)); } catch (ex) { if (contentType.includes('text/plain')) { return origin; } let err = new Error('JSON.parse error. buffer is ' + origin); err.name = 'WeChatAPIError'; throw err; } if (data && data.errcode) { let err = new Error(data.errmsg); err.name = 'WeChatAPIError'; err.code = data.errcode; if ((err.code === 40001 || err.code === 42001) && retry > 0 && !this.tokenFromCustom) { // 销毁已过期的token await this.saveToken(null); let token = await this.getAccessToken(); let urlobj = liburl.parse(url, true); if (urlobj.query && urlobj.query.access_token) { urlobj.query.access_token = token.accessToken; delete urlobj.search; } return this.request(liburl.format(urlobj), opts, retry - 1); } throw err; } return data; } return buffer; } /*! * 根据创建API时传入的appid和appsecret获取access token * 进行后续所有API调用时,需要先获取access token * 详细请看:<http://mp.weixin.qq.com/wiki/index.php?title=获取access_token> * 应用开发者无需直接调用本API。 * Examples: * ``` * var token = await api.getAccessToken(); * ``` * - `err`, 获取access token出现异常时的异常对象 * - `result`, 成功时得到的响应结果 * Result: * ``` * {"access_token": "ACCESS_TOKEN","expires_in": 7200} * ``` */ async getAccessToken() { var url = this.prefix + 'token?grant_type=client_credential&appid=' + this.appid + '&secret=' + this.appsecret; var data = await this.request(url); // 过期时间,因网络延迟等,将实际过期时间提前10秒,以防止临界点 var expireTime = Date.now() + (data.expires_in - 10) * 1000; var token = new AccessToken(data.access_token, expireTime); await this.saveToken(token); return token; } /*! * 需要access token的接口调用如果采用preRequest进行封装后,就可以直接调用。 * 无需依赖 getAccessToken 为前置调用。 * 应用开发者无需直接调用此API。 * Examples: * ``` * await api.ensureAccessToken(); * ``` */ async ensureAccessToken() { // 调用用户传入的获取token的异步方法,获得token之后使用(并缓存它)。 var token = await this.getToken(); var accessToken; if (token && (accessToken = new AccessToken(token.accessToken, token.expireTime)).isValid()) { return accessToken; } else if (this.tokenFromCustom) { let err = new Error('accessToken Error'); err.name = 'WeChatAPIError'; err.code = 40001; throw err; } return this.getAccessToken(); } } /** * 用于支持对象合并。将对象合并到API.prototype上,使得能够支持扩展 * Examples: * ``` * // 媒体管理(上传、下载) * API.mixin(require('./lib/api_media')); * ``` * @param {Object} obj 要合并的对象 */ API.mixin = function (obj) { for (var key in obj) { if (API.prototype.hasOwnProperty(key)) { throw new Error('Don\'t allow override existed prototype method. method: '+ key); } API.prototype[key] = obj[key]; } }; API.AccessToken = AccessToken; module.exports = API;