From 4d1f9a3e401a5a22b723c4c559101258a05b767b Mon Sep 17 00:00:00 2001 From: yomeeliu Date: Tue, 27 Sep 2016 15:36:14 +0800 Subject: [PATCH] Initial commit --- LICENSE | 24 ++++ README.md | 25 ++++ app.js | 3 + app.json | 3 + lib/co.js | 237 +++++++++++++++++++++++++++++++++++++ lib/promisify.js | 7 ++ lib/session-request.js | 105 ++++++++++++++++ pages/example/example.js | 26 ++++ pages/example/example.wxml | 3 + pages/example/example.wxss | 0 protocol.md | 43 +++++++ 11 files changed, 476 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.js create mode 100644 app.json create mode 100644 lib/co.js create mode 100644 lib/promisify.js create mode 100644 lib/session-request.js create mode 100644 pages/example/example.js create mode 100644 pages/example/example.wxml create mode 100644 pages/example/example.wxss create mode 100644 protocol.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7bb43b8 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..61a7eb3 --- /dev/null +++ b/README.md @@ -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`。 \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..8236164 --- /dev/null +++ b/app.js @@ -0,0 +1,3 @@ +App({ + +}); \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 0000000..1a62841 --- /dev/null +++ b/app.json @@ -0,0 +1,3 @@ +{ + "pages": ["pages/example/example"] +} \ No newline at end of file diff --git a/lib/co.js b/lib/co.js new file mode 100644 index 0000000..87ba8ba --- /dev/null +++ b/lib/co.js @@ -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; +} diff --git a/lib/promisify.js b/lib/promisify.js new file mode 100644 index 0000000..48df43f --- /dev/null +++ b/lib/promisify.js @@ -0,0 +1,7 @@ +module.exports = api => { + return (options, ...params) => { + return new Promise((resolve, reject) => { + api(Object.assign({}, options, { success: resolve, fail: reject }), ...params); + }); + }; +}; \ No newline at end of file diff --git a/lib/session-request.js b/lib/session-request.js new file mode 100644 index 0000000..4aba892 --- /dev/null +++ b/lib/session-request.js @@ -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; \ No newline at end of file diff --git a/pages/example/example.js b/pages/example/example.js new file mode 100644 index 0000000..8a13784 --- /dev/null +++ b/pages/example/example.js @@ -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); + }, + }); + }, +}); \ No newline at end of file diff --git a/pages/example/example.wxml b/pages/example/example.wxml new file mode 100644 index 0000000..bd61113 --- /dev/null +++ b/pages/example/example.wxml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/pages/example/example.wxss b/pages/example/example.wxss new file mode 100644 index 0000000..e69de29 diff --git a/protocol.md b/protocol.md new file mode 100644 index 0000000..171d06c --- /dev/null +++ b/protocol.md @@ -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`(首次请求有可能触发该异常)