UNPKG

10.8 kBJavaScriptView Raw
1'use strict';
2
3// 本文件用于wechat API,基础文件,主要用于Token的处理和mixin机制
4var urllib = require('urllib');
5var util = require('./util');
6var extend = require('util')._extend;
7var wrapper = util.wrapper;
8
9var AccessToken = function (accessToken, expireTime) {
10 if (!(this instanceof AccessToken)) {
11 return new AccessToken(accessToken, expireTime);
12 }
13 this.accessToken = accessToken;
14 this.expireTime = expireTime;
15};
16
17/*!
18 * 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比
19 *
20 * Examples:
21 * ```
22 * token.isValid();
23 * ```
24 */
25AccessToken.prototype.isValid = function () {
26 return !!this.accessToken && (new Date().getTime()) < this.expireTime;
27};
28
29/**
30 * 根据appid和appsecret创建API的构造函数
31 * 如需跨进程跨机器进行操作Wechat API(依赖access token),access token需要进行全局维护
32 * 使用策略如下:
33 *
34 * 1. 调用用户传入的获取token的异步方法,获得token之后使用
35 * 2. 使用appid/appsecret获取token。并调用用户传入的保存token方法保存
36 *
37 * Tips:
38 *
39 * - 如果跨机器运行wechat模块,需要注意同步机器之间的系统时间。
40 *
41 * Examples:
42 * ```
43 * var API = require('wechat-api');
44 * var api = new API('appid', 'secret');
45 * ```
46 * 以上即可满足单进程使用。
47 * 当多进程时,token需要全局维护,以下为保存token的接口。
48 * ```
49 * var api = new API('appid', 'secret', function (callback) {
50 * // 传入一个获取全局token的方法
51 * fs.readFile('access_token.txt', 'utf8', function (err, txt) {
52 * if (err) {return callback(err);}
53 * callback(null, JSON.parse(txt));
54 * });
55 * }, function (token, callback) {
56 * // 请将token存储到全局,跨进程、跨机器级别的全局,比如写到数据库、redis等
57 * // 这样才能在cluster模式及多机情况下使用,以下为写入到文件的示例
58 * fs.writeFile('access_token.txt', JSON.stringify(token), callback);
59 * });
60 * ```
61 * @param {String} appid 在公众平台上申请得到的appid
62 * @param {String} appsecret 在公众平台上申请得到的app secret
63 * @param {Function} getToken 可选的。获取全局token对象的方法,多进程模式部署时需在意
64 * @param {Function} saveToken 可选的。保存全局token对象的方法,多进程模式部署时需在意
65 */
66var API = function (appid, appsecret, getToken, saveToken) {
67 this.appid = appid;
68 this.appsecret = appsecret;
69 this.getToken = getToken || function (callback) {
70 callback(null, this.store);
71 };
72 this.saveToken = saveToken || function (token, callback) {
73 this.store = token;
74 if (process.env.NODE_ENV === 'production') {
75 console.warn('Don\'t save token in memory, when cluster or multi-computer!');
76 }
77 callback(null);
78 };
79 this.endpoint = 'https://api.weixin.qq.com';
80 this.mpPrefix = 'https://mp.weixin.qq.com/cgi-bin/';
81 this.fileServerPrefix = 'http://file.api.weixin.qq.com/cgi-bin/';
82 this.defaults = {};
83 // set default js ticket handle
84 this.registerTicketHandle();
85};
86
87/**
88 * 用于设置接入点
89 *
90 * - 通用域名(api.weixin.qq.com),使用该域名将访问官方指定就近的接入点;
91 * - 上海域名(sh.api.weixin.qq.com),使用该域名将访问上海的接入点;
92 * - 深圳域名(sz.api.weixin.qq.com),使用该域名将访问深圳的接入点;
93 * - 香港域名(hk.api.weixin.qq.com),使用该域名将访问香港的接入点。
94 *
95 * Examples:
96 * ```
97 * api.setEndpoint('api.weixin.qq.com');
98 * ```
99 * @param {String} domain 域名,默认为api.weixin.qq.com
100 */
101API.prototype.setEndpoint = function (domain) {
102 this.endpoint = 'https://' + domain;
103};
104
105/**
106 * 用于设置urllib的默认options
107 *
108 * Examples:
109 * ```
110 * api.setOpts({timeout: 15000});
111 * ```
112 * @param {Object} opts 默认选项
113 */
114API.prototype.setOpts = function (opts) {
115 this.defaults = opts;
116};
117
118/**
119 * 设置urllib的hook
120 *
121 * Examples:
122 * ```
123 * api.setHook(function (options) {
124 * // options
125 * });
126 * ```
127 * @param {Function} beforeRequest 需要封装的方法
128 */
129API.prototype.request = function (url, opts, callback) {
130 var options = {};
131 extend(options, this.defaults);
132 if (typeof opts === 'function') {
133 callback = opts;
134 opts = {};
135 }
136 for (var key in opts) {
137 if (key !== 'headers') {
138 options[key] = opts[key];
139 } else {
140 if (opts.headers) {
141 options.headers = options.headers || {};
142 extend(options.headers, opts.headers);
143 }
144 }
145 }
146 urllib.request(url, options, callback);
147};
148
149/*!
150 * 根据创建API时传入的appid和appsecret获取access token
151 * 进行后续所有API调用时,需要先获取access token
152 * 详细请看:<http://mp.weixin.qq.com/wiki/11/0e4b294685f817b95cbed85ba5e82b8f.html>
153 *
154 * 应用开发者无需直接调用本API。
155 *
156 * Examples:
157 * ```
158 * api.getAccessToken(callback);
159 * ```
160 * Callback:
161 *
162 * - `err`, 获取access token出现异常时的异常对象
163 * - `result`, 成功时得到的响应结果
164 *
165 * Result:
166 * ```
167 * {"access_token": "ACCESS_TOKEN","expires_in": 7200}
168 * ```
169 * @param {Function} callback 回调函数
170 */
171API.prototype.getAccessToken = function (callback) {
172 var that = this;
173 var url = this.endpoint + '/cgi-bin/token?grant_type=client_credential&appid=' + this.appid + '&secret=' + this.appsecret;
174 this.request(url, {dataType: 'json'}, wrapper(function (err, data) {
175 if (err) {
176 return callback(err);
177 }
178 // 过期时间,因网络延迟等,将实际过期时间提前10秒,以防止临界点
179 var expireTime = (new Date().getTime()) + (data.expires_in - 10) * 1000;
180 var token = AccessToken(data.access_token, expireTime);
181 that.saveToken(token, function (err) {
182 if (err) {
183 return callback(err);
184 }
185 callback(err, token);
186 });
187 }));
188 return this;
189};
190
191/*!
192 * 需要access token的接口调用如果采用preRequest进行封装后,就可以直接调用。
193 * 无需依赖getAccessToken为前置调用。
194 * 应用开发者无需直接调用此API。
195 *
196 * Examples:
197 * ```
198 * api.preRequest(method, arguments);
199 * ```
200 * @param {Function} method 需要封装的方法
201 * @param {Array} args 方法需要的参数
202 */
203API.prototype.preRequest = function (method, args, retryed) {
204 var that = this;
205 var callback = args[args.length - 1];
206 // 调用用户传入的获取token的异步方法,获得token之后使用(并缓存它)。
207 that.getToken(function (err, token) {
208 if (err) {
209 return callback(err);
210 }
211 var accessToken;
212 // 有token并且token有效直接调用
213 if (token && (accessToken = AccessToken(token.accessToken, token.expireTime)).isValid()) {
214 // 暂时保存token
215 that.token = accessToken;
216 if (!retryed) {
217 var retryHandle = function (err, data, res) {
218 // 40001 重试
219 if (data && data.errcode && data.errcode === 40001) {
220 return that.preRequest(method, args, true);
221 }
222 callback(err, data, res);
223 };
224 // 替换callback
225 var newargs = Array.prototype.slice.call(args, 0, -1);
226 newargs.push(retryHandle);
227 method.apply(that, newargs);
228 } else {
229 method.apply(that, args);
230 }
231 } else {
232 // 使用appid/appsecret获取token
233 that.getAccessToken(function (err, token) {
234 // 如遇错误,通过回调函数传出
235 if (err) {
236 return callback(err);
237 }
238 // 暂时保存token
239 that.token = token;
240 method.apply(that, args);
241 });
242 }
243 });
244};
245
246/**
247 * 获取最新的token
248 *
249 * Examples:
250 * ```
251 * api.getLatestToken(callback);
252 * ```
253 * Callback:
254 *
255 * - `err`, 获取access token出现异常时的异常对象
256 * - `token`, 获取的token
257 *
258 * @param {Function} method 需要封装的方法
259 * @param {Array} args 方法需要的参数
260 */
261API.prototype.getLatestToken = function (callback) {
262 var that = this;
263 // 调用用户传入的获取token的异步方法,获得token之后使用(并缓存它)。
264 that.getToken(function (err, token) {
265 if (err) {
266 return callback(err);
267 }
268 var accessToken;
269 // 有token并且token有效直接调用
270 if (token && (accessToken = AccessToken(token.accessToken, token.expireTime)).isValid()) {
271 return callback(null, accessToken);
272 }
273 // 使用appid/appsecret获取token
274 that.getAccessToken(callback);
275 });
276};
277
278/**
279 * 用于支持对象合并。将对象合并到API.prototype上,使得能够支持扩展
280 * Examples:
281 * ```
282 * // 媒体管理(上传、下载)
283 * API.mixin(require('./lib/api_media'));
284 * ```
285 * @param {Object} obj 要合并的对象
286 */
287API.mixin = function (obj) {
288 for (var key in obj) {
289 if (API.prototype.hasOwnProperty(key)) {
290 throw new Error('Don\'t allow override existed prototype method. method: '+ key);
291 }
292 API.prototype[key] = obj[key];
293 }
294};
295
296/**
297 * 用于扩展API.
298 * 作用是:当微信的官方 API 添加新功能,而wechat-api没有来得及升级时,用这个方法快速添加此功能。
299 * 当 api 升级后应该用 API 内提供的方法。
300 * Examples:
301 * ```
302 * // 为 API 添加一个 createQr 方法。
303 * API.ext("createQr", "https://api.weixin.qq.com/card/qrcode/create");
304 * ```
305 * Usage:
306 * ```
307 * // 调用这个 createQr 方法。
308 * api.createQr({'card_id': ’dkjeuDfsfeu3242w3dnjlq23i'}, callback);
309 * ```
310 * @param {String} functionName 用户调用的方法名
311 * @param {String} apiUrl 此 API 的 url 地址
312 * @param {Bool} override 如果填写 true 则覆盖原来 api 已有的方法, false 或不传,则抛错。
313 */
314API.patch = function (functionName, apiUrl, override) {
315 if (typeof apiUrl !== 'string') {
316 throw new Error('The second argument expect a type of string as the request url of wechat');
317 }
318
319 if (typeof functionName !== 'string') {
320 throw new Error('The first argument expect a type of string as the name of new function');
321 }
322
323 if (API.prototype[functionName] || API.prototype['_' + functionName] ) {
324 if (override !== true) {
325 throw new Error('wechat-api already has a prototype named ['+ functionName + '], use "true" as third param to override it or change your new function name.');
326 } else {
327 console.warn('wechat-api already has a prototype named ['+ functionName + '], will override the orignal one.');
328 }
329 }
330
331 util.make(API.prototype, functionName, function (info, callback) {
332 var hasMark = apiUrl.indexOf('?') >= 0;
333 var url = apiUrl + (hasMark ? '&access_token=': '?access_token=') + this.token.accessToken;
334 this.request(url, util.postJSON(info), wrapper(callback));
335 });
336};
337
338API.AccessToken = AccessToken;
339
340module.exports = API;