Initial commit
This commit is contained in:
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
LICENSE - "MIT License"
|
||||
|
||||
Copyright (c) 2016 by Tencent Cloud
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
25
README.md
Normal file
25
README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
微信小程序会话管理 - 客户端
|
||||
=============================
|
||||
|
||||
微信的网络请求接口 `wx.request()` 没有携带 Cookies,这让传统基于 Cookies 实现的会话管理不再适用。为了让处理微信小程序的服务能够识别会话,我们推出了 `weapp-session`。
|
||||
|
||||
`weapp-session` 使用自定义 Header 来传递微信小程序内用户信息,在服务内可以直接获取用户在微信的身份。
|
||||
|
||||
本客户端需要配合[服务器代码](https://github.com/CFETeam/weapp-session)使用。
|
||||
|
||||
客户端的使用比较简单,提供了一个和 `wx.request` 参数一样的方法:
|
||||
|
||||
```js
|
||||
const request = require('./lib/session-request.js');
|
||||
|
||||
request({
|
||||
url: 'https://www.mydomain.com/myapi',
|
||||
success(data) {
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
具体使用可以参照 `pages/example/example.js` 的代码。
|
||||
|
||||
> 要使用本客户端,需要至少引用 `lib` 目录下的 `co.js`、`promisify.js` 以及 `session-request.js`。
|
||||
237
lib/co.js
Normal file
237
lib/co.js
Normal file
@@ -0,0 +1,237 @@
|
||||
|
||||
/**
|
||||
* slice() reference.
|
||||
*/
|
||||
|
||||
var slice = Array.prototype.slice;
|
||||
|
||||
/**
|
||||
* Expose `co`.
|
||||
*/
|
||||
|
||||
module.exports = co['default'] = co.co = co;
|
||||
|
||||
/**
|
||||
* Wrap the given generator `fn` into a
|
||||
* function that returns a promise.
|
||||
* This is a separate function so that
|
||||
* every `co()` call doesn't create a new,
|
||||
* unnecessary closure.
|
||||
*
|
||||
* @param {GeneratorFunction} fn
|
||||
* @return {Function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
co.wrap = function (fn) {
|
||||
createPromise.__generatorFunction__ = fn;
|
||||
return createPromise;
|
||||
function createPromise() {
|
||||
return co.call(this, fn.apply(this, arguments));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute the generator function or a generator
|
||||
* and return a promise.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* @return {Promise}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function co(gen) {
|
||||
var ctx = this;
|
||||
var args = slice.call(arguments, 1)
|
||||
|
||||
// we wrap everything in a promise to avoid promise chaining,
|
||||
// which leads to memory leak errors.
|
||||
// see https://github.com/tj/co/issues/180
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (typeof gen === 'function') gen = gen.apply(ctx, args);
|
||||
if (!gen || typeof gen.next !== 'function') return resolve(gen);
|
||||
|
||||
onFulfilled();
|
||||
|
||||
/**
|
||||
* @param {Mixed} res
|
||||
* @return {Promise}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function onFulfilled(res) {
|
||||
var ret;
|
||||
try {
|
||||
ret = gen.next(res);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
next(ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Error} err
|
||||
* @return {Promise}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function onRejected(err) {
|
||||
var ret;
|
||||
try {
|
||||
ret = gen.throw(err);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
next(ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next value in the generator,
|
||||
* return a promise.
|
||||
*
|
||||
* @param {Object} ret
|
||||
* @return {Promise}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function next(ret) {
|
||||
if (ret.done) return resolve(ret.value);
|
||||
var value = toPromise.call(ctx, ret.value);
|
||||
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
|
||||
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
|
||||
+ 'but the following object was passed: "' + String(ret.value) + '"'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a `yield`ed value into a promise.
|
||||
*
|
||||
* @param {Mixed} obj
|
||||
* @return {Promise}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function toPromise(obj) {
|
||||
if (!obj) return obj;
|
||||
if (isPromise(obj)) return obj;
|
||||
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
|
||||
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
|
||||
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
|
||||
if (isObject(obj)) return objectToPromise.call(this, obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a thunk to a promise.
|
||||
*
|
||||
* @param {Function}
|
||||
* @return {Promise}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function thunkToPromise(fn) {
|
||||
var ctx = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
fn.call(ctx, function (err, res) {
|
||||
if (err) return reject(err);
|
||||
if (arguments.length > 2) res = slice.call(arguments, 1);
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of "yieldables" to a promise.
|
||||
* Uses `Promise.all()` internally.
|
||||
*
|
||||
* @param {Array} obj
|
||||
* @return {Promise}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function arrayToPromise(obj) {
|
||||
return Promise.all(obj.map(toPromise, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an object of "yieldables" to a promise.
|
||||
* Uses `Promise.all()` internally.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Promise}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function objectToPromise(obj){
|
||||
var results = new obj.constructor();
|
||||
var keys = Object.keys(obj);
|
||||
var promises = [];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
var promise = toPromise.call(this, obj[key]);
|
||||
if (promise && isPromise(promise)) defer(promise, key);
|
||||
else results[key] = obj[key];
|
||||
}
|
||||
return Promise.all(promises).then(function () {
|
||||
return results;
|
||||
});
|
||||
|
||||
function defer(promise, key) {
|
||||
// predefine the key in the result
|
||||
results[key] = undefined;
|
||||
promises.push(promise.then(function (res) {
|
||||
results[key] = res;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `obj` is a promise.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Boolean}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function isPromise(obj) {
|
||||
return 'function' == typeof obj.then;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `obj` is a generator.
|
||||
*
|
||||
* @param {Mixed} obj
|
||||
* @return {Boolean}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function isGenerator(obj) {
|
||||
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `obj` is a generator function.
|
||||
*
|
||||
* @param {Mixed} obj
|
||||
* @return {Boolean}
|
||||
* @api private
|
||||
*/
|
||||
function isGeneratorFunction(obj) {
|
||||
var constructor = obj.constructor;
|
||||
if (!constructor) return false;
|
||||
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
|
||||
return isGenerator(constructor.prototype);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for plain object.
|
||||
*
|
||||
* @param {Mixed} val
|
||||
* @return {Boolean}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function isObject(val) {
|
||||
return Object == val.constructor;
|
||||
}
|
||||
7
lib/promisify.js
Normal file
7
lib/promisify.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = api => {
|
||||
return (options, ...params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
api(Object.assign({}, options, { success: resolve, fail: reject }), ...params);
|
||||
});
|
||||
};
|
||||
};
|
||||
105
lib/session-request.js
Normal file
105
lib/session-request.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const co = require('./co.js');
|
||||
const promisify = require('./promisify.js');
|
||||
|
||||
const headers = {
|
||||
WX_CODE: 'X-WX-Code',
|
||||
WX_RAW_DATA: 'X-WX-RawData',
|
||||
WX_SIGNATURE: 'X-WX-Signature',
|
||||
};
|
||||
|
||||
const errors = {
|
||||
ERR_SESSION_EXPIRED: 'ERR_SESSION_EXPIRED',
|
||||
ERR_SESSION_KEY_EXCHANGE_FAILED: 'ERR_SESSION_KEY_EXCHANGE_FAILED',
|
||||
ERR_UNTRUSTED_RAW_DATA: 'ERR_UNTRUSTED_RAW_DATA',
|
||||
};
|
||||
|
||||
const SESSION_MAGIC_ID = 'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A';
|
||||
const MAX_RETRY_TIMES = 3;
|
||||
|
||||
const login = promisify(wx.login);
|
||||
const getUserInfo = promisify(wx.getUserInfo);
|
||||
|
||||
// 用户当前的 code 凭据
|
||||
let currentCode = null;
|
||||
|
||||
let pendingHeader = null;
|
||||
|
||||
/**
|
||||
* 生成 header 信息
|
||||
*/
|
||||
const buildHeader = co.wrap(function *() {
|
||||
if (currentCode) {
|
||||
return { [headers.WX_CODE]: currentCode };
|
||||
} else {
|
||||
return pendingHeader = pendingHeader || co(function *() {
|
||||
const { code } = yield login();
|
||||
const { rawData, signature } = yield getUserInfo();
|
||||
|
||||
currentCode = code;
|
||||
pendingHeader = null;
|
||||
|
||||
return {
|
||||
[headers.WX_CODE]: currentCode,
|
||||
[headers.WX_RAW_DATA]: encodeURIComponent(rawData),
|
||||
[headers.WX_SIGNATURE]: signature,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 带会话管理的网络请求,参数配置和 wx.request 一致
|
||||
*/
|
||||
function requestWithSession(options = {}) {
|
||||
let tryTimes = 0;
|
||||
|
||||
const wrapRequest = co.wrap(function * () {
|
||||
const { success, fail, complete } = options;
|
||||
|
||||
const callSuccess = (...params) => {
|
||||
success && success(...params);
|
||||
complete && complete(...params);
|
||||
};
|
||||
|
||||
const callFail = error => {
|
||||
fail && fail(error);
|
||||
complete && complete(error);
|
||||
throw error;
|
||||
};
|
||||
|
||||
if (tryTimes++ > MAX_RETRY_TIMES) {
|
||||
return callFail(new Error('请求失败次数过多'));
|
||||
}
|
||||
|
||||
return wx.request(Object.assign({}, options, {
|
||||
header: Object.assign({}, options.headers, yield buildHeader()),
|
||||
|
||||
success({ data }) {
|
||||
if (SESSION_MAGIC_ID in data) {
|
||||
const error = data.error;
|
||||
|
||||
switch (data.reason) {
|
||||
case errors.ERR_SESSION_EXPIRED:
|
||||
case errors.ERR_SESSION_KEY_EXCHANGE_FAILED:
|
||||
currentCode = null;
|
||||
return wrapRequest();
|
||||
|
||||
case errors.ERR_UNTRUSTED_RAW_DATA:
|
||||
default:
|
||||
return callFail(error);
|
||||
}
|
||||
}
|
||||
|
||||
callSuccess(...arguments);
|
||||
},
|
||||
|
||||
fail: callFail,
|
||||
|
||||
complete: () => void(0),
|
||||
}));
|
||||
});
|
||||
|
||||
return wrapRequest();
|
||||
}
|
||||
|
||||
module.exports = requestWithSession;
|
||||
26
pages/example/example.js
Normal file
26
pages/example/example.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const request = require('../../lib/session-request.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
info: '点击按钮请求',
|
||||
},
|
||||
|
||||
doRequest() {
|
||||
request({
|
||||
url: 'https://www.qcloud.la/applet/session',
|
||||
method: 'GET',
|
||||
|
||||
success(data) {
|
||||
console.log('success', data);
|
||||
},
|
||||
|
||||
fail(error) {
|
||||
console.log('error', error);
|
||||
},
|
||||
|
||||
complete(what) {
|
||||
console.log('complete', what);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
3
pages/example/example.wxml
Normal file
3
pages/example/example.wxml
Normal file
@@ -0,0 +1,3 @@
|
||||
<view>
|
||||
<button type="primary" bindtap="doRequest">{{info}}</button>
|
||||
</view>
|
||||
0
pages/example/example.wxss
Normal file
0
pages/example/example.wxss
Normal file
43
protocol.md
Normal file
43
protocol.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# SDK 开发协议
|
||||
|
||||
1、首次发起请求时携带请求头,其中`X-WX-Code`是调用`wx.login`接口时获取的`code`值,`X-WX-RawData`和`X-WX-Signature`分别是调用`wx.getUserInfo`接口时获取的`rawData`和`signature`。
|
||||
|
||||
```js
|
||||
header: {
|
||||
'X-WX-Code': 'code',
|
||||
'X-WX-RawData': 'rawData',
|
||||
'X-WX-Signature': 'signature',
|
||||
}
|
||||
```
|
||||
|
||||
2、后续请求只需携带首次请求时保存的`code`作为请求。
|
||||
|
||||
```js
|
||||
header: {
|
||||
'X-WX-Code': 'code',
|
||||
}
|
||||
```
|
||||
|
||||
3、异常处理
|
||||
|
||||
异常错误均以`json`格式返回数据,数据格式如下:
|
||||
|
||||
```
|
||||
{
|
||||
'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A': 1,
|
||||
'reason'?: 'xxx',
|
||||
'error': {
|
||||
'name': 'xxx',
|
||||
'message': 'xxx',
|
||||
'detail'?: 'xxx',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
其中`F2C224D4-2BCE-4C64-AF9F-A6D872000D1A`是约定的唯一标识 ID,`reason`标记异常的原因,`error`包含错误的详细信息。
|
||||
|
||||
目前定义的`reason`有:
|
||||
|
||||
- `ERR_SESSION_EXPIRED`: 在`session`中未找到`code`对应的`wxUserInfo`,这里需要生成新的`code`和`signature`更新`session`(非首次请求触发有可能触发该异常)
|
||||
- `ERR_SESSION_KEY_EXCHANGE_FAILED`: `code`换取`session_key`失败(首次请求有可能触发该异常)
|
||||
- `ERR_UNTRUSTED_RAW_DATA`: 不可信的`rawData`,有可能是伪造的`rawData`或`signature`(首次请求有可能触发该异常)
|
||||
Reference in New Issue
Block a user