Initial commit

This commit is contained in:
yomeeliu
2016-09-27 15:36:14 +08:00
commit 4d1f9a3e40
11 changed files with 476 additions and 0 deletions

24
LICENSE Normal file
View 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
View 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`

3
app.js Normal file
View File

@@ -0,0 +1,3 @@
App({
});

3
app.json Normal file
View File

@@ -0,0 +1,3 @@
{
"pages": ["pages/example/example"]
}

237
lib/co.js Normal file
View 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
View 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
View 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
View 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);
},
});
},
});

View File

@@ -0,0 +1,3 @@
<view>
<button type="primary" bindtap="doRequest">{{info}}</button>
</view>

View File

43
protocol.md Normal file
View 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`(首次请求有可能触发该异常)