commit 1a431d18979d3c69bf7c98acc654199b5517d02f Author: wu_shao_wei Date: Wed Oct 16 20:55:48 2019 +0800 first diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..7c05067 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "vendors" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14ea590 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Windows +[Dd]esktop.ini +Thumbs.db +$RECYCLE.BIN/ + +# macOS +.DS_Store +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes + +# Node.js +node_modules/ 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..7b7a1c0 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +使用说明 +======== + +使用代码前,请先修改 `app.js` 里面的域名配置。 \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..c92fbcd --- /dev/null +++ b/app.js @@ -0,0 +1,9 @@ +//app.js +App({ + config: { + host: 'wsw1999.xyz' + }, + onLaunch () { + console.log('App.onLaunch()'); + } +}); \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 0000000..1246046 --- /dev/null +++ b/app.json @@ -0,0 +1,18 @@ +{ + "pages": [ + "pages/index/index", + "pages/config/config", + "pages/https/https", + "pages/session/session", + "pages/websocket/websocket", + "pages/game/game", + "pages/shouquan/shouquan" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#fff", + "navigationBarTitleText": " ", + "navigationBarTextStyle": "black" + }, + "sitemapLocation": "sitemap.json" +} \ No newline at end of file diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..d5b078b --- /dev/null +++ b/app.wxss @@ -0,0 +1,32 @@ +page, .lab { + height: 100%; +} +.lab { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 200rpx 0; + box-sizing: border-box; +} +.lab .status image { + width: 70px; + height: 70px; + border-radius: 100%; +} +.lab .hint { + text-align: center; + width: 80%; +} +.lab .interactive { + width: 80%; +} +.lab .interactive input { + border-bottom: 4rpx solid #EEE; +} +.lab .command { + height: 200rpx; +} +.lab .command button { + margin: 10px; +} \ No newline at end of file diff --git a/images/1.png b/images/1.png new file mode 100644 index 0000000..11b1459 Binary files /dev/null and b/images/1.png differ diff --git a/images/2.png b/images/2.png new file mode 100644 index 0000000..021b865 Binary files /dev/null and b/images/2.png differ diff --git a/images/3.png b/images/3.png new file mode 100644 index 0000000..99de040 Binary files /dev/null and b/images/3.png differ 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/emitter.js b/lib/emitter.js new file mode 100644 index 0000000..9beed1b --- /dev/null +++ b/lib/emitter.js @@ -0,0 +1,20 @@ + +module.exports = { + setup(target) { + let listeners = []; + + Object.assign(target, { + on(type, handle) { + if (typeof handle == 'function') { + listeners.push([type, handle]); + } + }, + emit(type, ...params) { + listeners.forEach(([listenType, handle]) => type == listenType && handle(...params)); + }, + removeAllListeners() { + listeners = []; + } + }); + } +} \ No newline at end of file diff --git a/lib/lab.js b/lib/lab.js new file mode 100644 index 0000000..2746212 --- /dev/null +++ b/lib/lab.js @@ -0,0 +1,19 @@ +function getFinishLabs() { + const sto = wx.getStorageSync('finishLabs'); + if (sto) { + return JSON.parse(sto); + } + return {}; +} + +function finish(id) { + const current = getFinishLabs(); + current[id] = 1; + wx.setStorageSync('finishLabs', JSON.stringify(current)); +} + +function clear() { + wx.setStorageSync('finishLabs', ''); +} + +module.exports = { getFinishLabs, finish, clear }; \ No newline at end of file diff --git a/lib/promisify.js b/lib/promisify.js new file mode 100644 index 0000000..cc269d4 --- /dev/null +++ b/lib/promisify.js @@ -0,0 +1,4 @@ +module.exports = (api) => + (options, ...params) => new Promise( + (resolve, reject) => api(Object.assign({}, options, { success: resolve, fail: reject }), ...params) + ); \ No newline at end of file diff --git a/lib/regenerator-runtime.js b/lib/regenerator-runtime.js new file mode 100644 index 0000000..eedd8cd --- /dev/null +++ b/lib/regenerator-runtime.js @@ -0,0 +1,713 @@ +!(function(global) { + "use strict"; + + var Op = Object.prototype; + var hasOwn = Op.hasOwnProperty; + var undefined; // More compressible than void 0. + var $Symbol = typeof Symbol === "function" ? Symbol : {}; + var iteratorSymbol = $Symbol.iterator || "@@iterator"; + var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; + + var inModule = typeof module === "object"; + var runtime = global.regeneratorRuntime; + if (runtime) { + if (inModule) { + // If regeneratorRuntime is defined globally and we're in a module, + // make the exports object identical to regeneratorRuntime. + module.exports = runtime; + } + // Don't bother evaluating the rest of this file if the runtime was + // already defined globally. + return; + } + + // Define the runtime globally (as expected by generated code) as either + // module.exports (if we're in a module) or a new, empty object. + runtime = global.regeneratorRuntime = inModule ? module.exports : {}; + + function wrap(innerFn, outerFn, self, tryLocsList) { + // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. + var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; + var generator = Object.create(protoGenerator.prototype); + var context = new Context(tryLocsList || []); + + // The ._invoke method unifies the implementations of the .next, + // .throw, and .return methods. + generator._invoke = makeInvokeMethod(innerFn, self, context); + + return generator; + } + runtime.wrap = wrap; + + // Try/catch helper to minimize deoptimizations. Returns a completion + // record like context.tryEntries[i].completion. This interface could + // have been (and was previously) designed to take a closure to be + // invoked without arguments, but in all the cases we care about we + // already have an existing method we want to call, so there's no need + // to create a new function object. We can even get away with assuming + // the method takes exactly one argument, since that happens to be true + // in every case, so we don't have to touch the arguments object. The + // only additional allocation required is the completion record, which + // has a stable shape and so hopefully should be cheap to allocate. + function tryCatch(fn, obj, arg) { + try { + return { type: "normal", arg: fn.call(obj, arg) }; + } catch (err) { + return { type: "throw", arg: err }; + } + } + + var GenStateSuspendedStart = "suspendedStart"; + var GenStateSuspendedYield = "suspendedYield"; + var GenStateExecuting = "executing"; + var GenStateCompleted = "completed"; + + // Returning this object from the innerFn has the same effect as + // breaking out of the dispatch switch statement. + var ContinueSentinel = {}; + + // Dummy constructor functions that we use as the .constructor and + // .constructor.prototype properties for functions that return Generator + // objects. For full spec compliance, you may wish to configure your + // minifier not to mangle the names of these two functions. + function Generator() {} + function GeneratorFunction() {} + function GeneratorFunctionPrototype() {} + + // This is a polyfill for %IteratorPrototype% for environments that + // don't natively support it. + var IteratorPrototype = {}; + IteratorPrototype[iteratorSymbol] = function () { + return this; + }; + + var getProto = Object.getPrototypeOf; + var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); + if (NativeIteratorPrototype && + NativeIteratorPrototype !== Op && + hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { + // This environment has a native %IteratorPrototype%; use it instead + // of the polyfill. + IteratorPrototype = NativeIteratorPrototype; + } + + var Gp = GeneratorFunctionPrototype.prototype = + Generator.prototype = Object.create(IteratorPrototype); + GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; + GeneratorFunctionPrototype.constructor = GeneratorFunction; + GeneratorFunctionPrototype[toStringTagSymbol] = + GeneratorFunction.displayName = "GeneratorFunction"; + + // Helper for defining the .next, .throw, and .return methods of the + // Iterator interface in terms of a single ._invoke method. + function defineIteratorMethods(prototype) { + ["next", "throw", "return"].forEach(function(method) { + prototype[method] = function(arg) { + return this._invoke(method, arg); + }; + }); + } + + runtime.isGeneratorFunction = function(genFun) { + var ctor = typeof genFun === "function" && genFun.constructor; + return ctor + ? ctor === GeneratorFunction || + // For the native GeneratorFunction constructor, the best we can + // do is to check its .name property. + (ctor.displayName || ctor.name) === "GeneratorFunction" + : false; + }; + + runtime.mark = function(genFun) { + if (Object.setPrototypeOf) { + Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); + } else { + genFun.__proto__ = GeneratorFunctionPrototype; + if (!(toStringTagSymbol in genFun)) { + genFun[toStringTagSymbol] = "GeneratorFunction"; + } + } + genFun.prototype = Object.create(Gp); + return genFun; + }; + + // Within the body of any async function, `await x` is transformed to + // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test + // `hasOwn.call(value, "__await")` to determine if the yielded value is + // meant to be awaited. + runtime.awrap = function(arg) { + return { __await: arg }; + }; + + function AsyncIterator(generator) { + function invoke(method, arg, resolve, reject) { + var record = tryCatch(generator[method], generator, arg); + if (record.type === "throw") { + reject(record.arg); + } else { + var result = record.arg; + var value = result.value; + if (value && + typeof value === "object" && + hasOwn.call(value, "__await")) { + return Promise.resolve(value.__await).then(function(value) { + invoke("next", value, resolve, reject); + }, function(err) { + invoke("throw", err, resolve, reject); + }); + } + + return Promise.resolve(value).then(function(unwrapped) { + // When a yielded Promise is resolved, its final value becomes + // the .value of the Promise<{value,done}> result for the + // current iteration. If the Promise is rejected, however, the + // result for this iteration will be rejected with the same + // reason. Note that rejections of yielded Promises are not + // thrown back into the generator function, as is the case + // when an awaited Promise is rejected. This difference in + // behavior between yield and await is important, because it + // allows the consumer to decide what to do with the yielded + // rejection (swallow it and continue, manually .throw it back + // into the generator, abandon iteration, whatever). With + // await, by contrast, there is no opportunity to examine the + // rejection reason outside the generator function, so the + // only option is to throw it from the await expression, and + // let the generator function handle the exception. + result.value = unwrapped; + resolve(result); + }, reject); + } + } + + if (typeof process === "object" && process.domain) { + invoke = process.domain.bind(invoke); + } + + var previousPromise; + + function enqueue(method, arg) { + function callInvokeWithMethodAndArg() { + return new Promise(function(resolve, reject) { + invoke(method, arg, resolve, reject); + }); + } + + return previousPromise = + // If enqueue has been called before, then we want to wait until + // all previous Promises have been resolved before calling invoke, + // so that results are always delivered in the correct order. If + // enqueue has not been called before, then it is important to + // call invoke immediately, without waiting on a callback to fire, + // so that the async generator function has the opportunity to do + // any necessary setup in a predictable way. This predictability + // is why the Promise constructor synchronously invokes its + // executor callback, and why async functions synchronously + // execute code before the first await. Since we implement simple + // async functions in terms of async generators, it is especially + // important to get this right, even though it requires care. + previousPromise ? previousPromise.then( + callInvokeWithMethodAndArg, + // Avoid propagating failures to Promises returned by later + // invocations of the iterator. + callInvokeWithMethodAndArg + ) : callInvokeWithMethodAndArg(); + } + + // Define the unified helper method that is used to implement .next, + // .throw, and .return (see defineIteratorMethods). + this._invoke = enqueue; + } + + defineIteratorMethods(AsyncIterator.prototype); + runtime.AsyncIterator = AsyncIterator; + + // Note that simple async functions are implemented on top of + // AsyncIterator objects; they just return a Promise for the value of + // the final result produced by the iterator. + runtime.async = function(innerFn, outerFn, self, tryLocsList) { + var iter = new AsyncIterator( + wrap(innerFn, outerFn, self, tryLocsList) + ); + + return runtime.isGeneratorFunction(outerFn) + ? iter // If outerFn is a generator, return the full iterator. + : iter.next().then(function(result) { + return result.done ? result.value : iter.next(); + }); + }; + + function makeInvokeMethod(innerFn, self, context) { + var state = GenStateSuspendedStart; + + return function invoke(method, arg) { + if (state === GenStateExecuting) { + throw new Error("Generator is already running"); + } + + if (state === GenStateCompleted) { + if (method === "throw") { + throw arg; + } + + // Be forgiving, per 25.3.3.3.3 of the spec: + // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume + return doneResult(); + } + + context.method = method; + context.arg = arg; + + while (true) { + var delegate = context.delegate; + if (delegate) { + var delegateResult = maybeInvokeDelegate(delegate, context); + if (delegateResult) { + if (delegateResult === ContinueSentinel) continue; + return delegateResult; + } + } + + if (context.method === "next") { + // Setting context._sent for legacy support of Babel's + // function.sent implementation. + context.sent = context._sent = context.arg; + + } else if (context.method === "throw") { + if (state === GenStateSuspendedStart) { + state = GenStateCompleted; + throw context.arg; + } + + context.dispatchException(context.arg); + + } else if (context.method === "return") { + context.abrupt("return", context.arg); + } + + state = GenStateExecuting; + + var record = tryCatch(innerFn, self, context); + if (record.type === "normal") { + // If an exception is thrown from innerFn, we leave state === + // GenStateExecuting and loop back for another invocation. + state = context.done + ? GenStateCompleted + : GenStateSuspendedYield; + + if (record.arg === ContinueSentinel) { + continue; + } + + return { + value: record.arg, + done: context.done + }; + + } else if (record.type === "throw") { + state = GenStateCompleted; + // Dispatch the exception by looping back around to the + // context.dispatchException(context.arg) call above. + context.method = "throw"; + context.arg = record.arg; + } + } + }; + } + + // Call delegate.iterator[context.method](context.arg) and handle the + // result, either by returning a { value, done } result from the + // delegate iterator, or by modifying context.method and context.arg, + // setting context.delegate to null, and returning the ContinueSentinel. + function maybeInvokeDelegate(delegate, context) { + var method = delegate.iterator[context.method]; + if (method === undefined) { + // A .throw or .return when the delegate iterator has no .throw + // method always terminates the yield* loop. + context.delegate = null; + + if (context.method === "throw") { + if (delegate.iterator.return) { + // If the delegate iterator has a return method, give it a + // chance to clean up. + context.method = "return"; + context.arg = undefined; + maybeInvokeDelegate(delegate, context); + + if (context.method === "throw") { + // If maybeInvokeDelegate(context) changed context.method from + // "return" to "throw", let that override the TypeError below. + return ContinueSentinel; + } + } + + context.method = "throw"; + context.arg = new TypeError( + "The iterator does not provide a 'throw' method"); + } + + return ContinueSentinel; + } + + var record = tryCatch(method, delegate.iterator, context.arg); + + if (record.type === "throw") { + context.method = "throw"; + context.arg = record.arg; + context.delegate = null; + return ContinueSentinel; + } + + var info = record.arg; + + if (! info) { + context.method = "throw"; + context.arg = new TypeError("iterator result is not an object"); + context.delegate = null; + return ContinueSentinel; + } + + if (info.done) { + // Assign the result of the finished delegate to the temporary + // variable specified by delegate.resultName (see delegateYield). + context[delegate.resultName] = info.value; + + // Resume execution at the desired location (see delegateYield). + context.next = delegate.nextLoc; + + // If context.method was "throw" but the delegate handled the + // exception, let the outer generator proceed normally. If + // context.method was "next", forget context.arg since it has been + // "consumed" by the delegate iterator. If context.method was + // "return", allow the original .return call to continue in the + // outer generator. + if (context.method !== "return") { + context.method = "next"; + context.arg = undefined; + } + + } else { + // Re-yield the result returned by the delegate method. + return info; + } + + // The delegate iterator is finished, so forget it and continue with + // the outer generator. + context.delegate = null; + return ContinueSentinel; + } + + // Define Generator.prototype.{next,throw,return} in terms of the + // unified ._invoke helper method. + defineIteratorMethods(Gp); + + Gp[toStringTagSymbol] = "Generator"; + + Gp.toString = function() { + return "[object Generator]"; + }; + + function pushTryEntry(locs) { + var entry = { tryLoc: locs[0] }; + + if (1 in locs) { + entry.catchLoc = locs[1]; + } + + if (2 in locs) { + entry.finallyLoc = locs[2]; + entry.afterLoc = locs[3]; + } + + this.tryEntries.push(entry); + } + + function resetTryEntry(entry) { + var record = entry.completion || {}; + record.type = "normal"; + delete record.arg; + entry.completion = record; + } + + function Context(tryLocsList) { + // The root entry object (effectively a try statement without a catch + // or a finally block) gives us a place to store values thrown from + // locations where there is no enclosing try statement. + this.tryEntries = [{ tryLoc: "root" }]; + tryLocsList.forEach(pushTryEntry, this); + this.reset(true); + } + + runtime.keys = function(object) { + var keys = []; + for (var key in object) { + keys.push(key); + } + keys.reverse(); + + // Rather than returning an object with a next method, we keep + // things simple and return the next function itself. + return function next() { + while (keys.length) { + var key = keys.pop(); + if (key in object) { + next.value = key; + next.done = false; + return next; + } + } + + // To avoid creating an additional object, we just hang the .value + // and .done properties off the next function object itself. This + // also ensures that the minifier will not anonymize the function. + next.done = true; + return next; + }; + }; + + function values(iterable) { + if (iterable) { + var iteratorMethod = iterable[iteratorSymbol]; + if (iteratorMethod) { + return iteratorMethod.call(iterable); + } + + if (typeof iterable.next === "function") { + return iterable; + } + + if (!isNaN(iterable.length)) { + var i = -1, next = function next() { + while (++i < iterable.length) { + if (hasOwn.call(iterable, i)) { + next.value = iterable[i]; + next.done = false; + return next; + } + } + + next.value = undefined; + next.done = true; + + return next; + }; + + return next.next = next; + } + } + + // Return an iterator with no values. + return { next: doneResult }; + } + runtime.values = values; + + function doneResult() { + return { value: undefined, done: true }; + } + + Context.prototype = { + constructor: Context, + + reset: function(skipTempReset) { + this.prev = 0; + this.next = 0; + // Resetting context._sent for legacy support of Babel's + // function.sent implementation. + this.sent = this._sent = undefined; + this.done = false; + this.delegate = null; + + this.method = "next"; + this.arg = undefined; + + this.tryEntries.forEach(resetTryEntry); + + if (!skipTempReset) { + for (var name in this) { + // Not sure about the optimal order of these conditions: + if (name.charAt(0) === "t" && + hasOwn.call(this, name) && + !isNaN(+name.slice(1))) { + this[name] = undefined; + } + } + } + }, + + stop: function() { + this.done = true; + + var rootEntry = this.tryEntries[0]; + var rootRecord = rootEntry.completion; + if (rootRecord.type === "throw") { + throw rootRecord.arg; + } + + return this.rval; + }, + + dispatchException: function(exception) { + if (this.done) { + throw exception; + } + + var context = this; + function handle(loc, caught) { + record.type = "throw"; + record.arg = exception; + context.next = loc; + + if (caught) { + // If the dispatched exception was caught by a catch block, + // then let that catch block handle the exception normally. + context.method = "next"; + context.arg = undefined; + } + + return !! caught; + } + + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + var record = entry.completion; + + if (entry.tryLoc === "root") { + // Exception thrown outside of any try block that could handle + // it, so set the completion value of the entire function to + // throw the exception. + return handle("end"); + } + + if (entry.tryLoc <= this.prev) { + var hasCatch = hasOwn.call(entry, "catchLoc"); + var hasFinally = hasOwn.call(entry, "finallyLoc"); + + if (hasCatch && hasFinally) { + if (this.prev < entry.catchLoc) { + return handle(entry.catchLoc, true); + } else if (this.prev < entry.finallyLoc) { + return handle(entry.finallyLoc); + } + + } else if (hasCatch) { + if (this.prev < entry.catchLoc) { + return handle(entry.catchLoc, true); + } + + } else if (hasFinally) { + if (this.prev < entry.finallyLoc) { + return handle(entry.finallyLoc); + } + + } else { + throw new Error("try statement without catch or finally"); + } + } + } + }, + + abrupt: function(type, arg) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.tryLoc <= this.prev && + hasOwn.call(entry, "finallyLoc") && + this.prev < entry.finallyLoc) { + var finallyEntry = entry; + break; + } + } + + if (finallyEntry && + (type === "break" || + type === "continue") && + finallyEntry.tryLoc <= arg && + arg <= finallyEntry.finallyLoc) { + // Ignore the finally entry if control is not jumping to a + // location outside the try/catch block. + finallyEntry = null; + } + + var record = finallyEntry ? finallyEntry.completion : {}; + record.type = type; + record.arg = arg; + + if (finallyEntry) { + this.method = "next"; + this.next = finallyEntry.finallyLoc; + return ContinueSentinel; + } + + return this.complete(record); + }, + + complete: function(record, afterLoc) { + if (record.type === "throw") { + throw record.arg; + } + + if (record.type === "break" || + record.type === "continue") { + this.next = record.arg; + } else if (record.type === "return") { + this.rval = this.arg = record.arg; + this.method = "return"; + this.next = "end"; + } else if (record.type === "normal" && afterLoc) { + this.next = afterLoc; + } + + return ContinueSentinel; + }, + + finish: function(finallyLoc) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.finallyLoc === finallyLoc) { + this.complete(entry.completion, entry.afterLoc); + resetTryEntry(entry); + return ContinueSentinel; + } + } + }, + + "catch": function(tryLoc) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.tryLoc === tryLoc) { + var record = entry.completion; + if (record.type === "throw") { + var thrown = record.arg; + resetTryEntry(entry); + } + return thrown; + } + } + + // The context.catch method must only be called with a location + // argument that corresponds to a known catch block. + throw new Error("illegal catch attempt"); + }, + + delegateYield: function(iterable, resultName, nextLoc) { + this.delegate = { + iterator: values(iterable), + resultName: resultName, + nextLoc: nextLoc + }; + + if (this.method === "next") { + // Deliberately forget the last sent value so that we don't + // accidentally pass it on to the delegate. + this.arg = undefined; + } + + return ContinueSentinel; + } + }; +})( + // Among the various tricks for obtaining a reference to the global + // object, this seems to be the most reliable technique that does not + // use indirect eval (which violates Content Security Policy). + typeof global === "object" ? global : + typeof window === "object" ? window : + typeof self === "object" ? self : this +); \ No newline at end of file diff --git a/lib/tunnel.js b/lib/tunnel.js new file mode 100644 index 0000000..c86804e --- /dev/null +++ b/lib/tunnel.js @@ -0,0 +1,44 @@ + +const Emitter = require('./emitter'); + +/** + * 基于小程序 WebSocket 接口封装信道 + */ +module.exports = class Tunnel { + constructor() { + Emitter.setup(this.emitter = {}); + } + + connect(url, header) { + + // 小程序 wx.connectSocket() API header 参数无效,把会话信息附加在 URL 上 + const query = Object.keys(header).map(key => `${key}=${encodeURIComponent(header[key])}`).join('&'); + const seperator = url.indexOf('?') > -1 ? '&' : '?'; + url = [url, query].join(seperator); + + return new Promise((resolve, reject) => { + wx.onSocketOpen(resolve); + wx.onSocketError(reject); + wx.onSocketMessage(packet => { + try { + const { message, data } = JSON.parse(packet.data); + this.emitter.emit(message, data); + } catch (e) { + console.log('Handle packet failed: ' + packet.data, e); + } + }); + wx.onSocketClose(() => this.emitter.emit('close')); + wx.connectSocket({ url, header }); + }); + } + + on(message, handle) { + this.emitter.on(message, handle); + } + + emit(message, data) { + wx.sendSocketMessage({ + data: JSON.stringify({ message, data }) + }); + } +} \ No newline at end of file diff --git a/pages/config/config.js b/pages/config/config.js new file mode 100644 index 0000000..6ebcd28 --- /dev/null +++ b/pages/config/config.js @@ -0,0 +1,24 @@ +const app = getApp(); +const config = app.config; +const lab = require('../../lib/lab'); + +const done = config.host != '<请配置访问域名>'; + + +Page({ + data: { + done, + status: done ? 'success' : 'waiting', + host: config.host, + hintLine1: done ? '域名已配置' : '请修改小程序源码 app.js', + hintLine2: done ? '小程序实验将使用下面域名进行' : '配置小程序使用的服务器域名' + }, + goBack() { + wx.navigateBack(); + }, + onShow() { + if (done) { + lab.finish('config'); + } + } +}); \ No newline at end of file diff --git a/pages/config/config.json b/pages/config/config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/pages/config/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pages/config/config.wxml b/pages/config/config.wxml new file mode 100644 index 0000000..9094a34 --- /dev/null +++ b/pages/config/config.wxml @@ -0,0 +1,13 @@ + + + + + + {{hintLine1}} + {{hintLine2}} + + + {{host}} + + + \ No newline at end of file diff --git a/pages/config/config.wxss b/pages/config/config.wxss new file mode 100644 index 0000000..93bf0aa --- /dev/null +++ b/pages/config/config.wxss @@ -0,0 +1 @@ +/* pages/config/config.wxss */ \ No newline at end of file diff --git a/pages/game/game.js b/pages/game/game.js new file mode 100644 index 0000000..5ea0b56 --- /dev/null +++ b/pages/game/game.js @@ -0,0 +1,253 @@ +"use strict"; + +require('../../lib/regenerator-runtime'); + +const regeneratorRuntime = global.regeneratorRuntime; + +// 引入 co 和 promisify 帮助我们进行异步处理 +const co = require('../../lib/co'); +const promisify = require('../../lib/promisify'); + +// 引入 Wafer 客户端 SDK 支持会话 +const wafer = require('../../vendors/wafer-client-sdk/index'); + +// 简单的小程序 WebSocket 信道封装 +const Tunnel = require('../../lib/tunnel'); + +// 登录接口转成返回 Promise 形式 +const login = promisify(wafer.login); + +// 获得小程序实例 +const app = getApp(); + +// 用于记录实验成功 +const lab = require('../../lib/lab'); + +// 设置会话登录地址 +wafer.setLoginUrl(`https://${app.config.host}/login`); + +// 文案 +const WIN_TEXTS = ['很棒', '秒杀', '赢了', 'Winner', '胜利', '不要大意', '无敌啊']; +const LOSE_TEXTS = ['失误', '卧槽', '不可能', 'Loser', '行不行啊', '加油', '大侠再来']; +const EQ_TEXTS = ['平局', '平分秋色', '对方学你', '照镜子', '半斤八两', '换一个', '一样的']; +const pickText = texts => texts[Math.floor(texts.length * Math.random())]; + +// 定义页面 +Page({ + data: { + // 是否已经和服务器连接 + connected: false, + + // 游戏是否进行中 + playing: false, + + // 当前需要展示的游戏信息 + gameInfo: "", + + // 开始游戏按钮文本 + startButtonText: "开始", + + //「我」的信息,包括昵称、头像、分数、选择 + myName: "", + myAvatar: null, + myScore: 0, + myStreak: 0, + myChoice: Math.floor(Math.random() * 10000) % 3 + 1, + + //「你」的信息 + youHere: false, + yourName: "", + yourAvatar: null, + yourScore: 0, + yourStreak: 0, + yourChoice: 1, + yourMove: 0, + + // 取得胜利的是谁 + win: null + }, + + // 页面显示后,开始连接 + onShow: function() { + this.begin(); + }, + + // 进行登录和链接,完成后开始启动游戏服务 + begin: co.wrap(function *() { + try { + this.setData({ gameInfo: "正在登陆" }); + yield login(); + + this.setData({ gameInfo: "正在连接"}); + yield this.connect(); + } catch (error) { + console.error('error on login or connect: ', error); + } + this.serve(); + }), + + // 链接到服务器后进行身份识别 + connect: co.wrap(function *() { + + const tunnel = this.tunnel = new Tunnel(); + try { + yield tunnel.connect(`wss://${app.config.host}/game`, wafer.buildSessionHeader()); + } catch (connectError) { + console.error({ connectError }); + this.setData({ gameInfo: "连接错误" }); + throw connectError; + } + tunnel.on('close', () => { + this.setData({ + connected: false, + gameInfo: "连接已中断" + }); + }); + this.setData({ + gameInfo: "准备", + connected: true, + gameState: 'connected' + }); + return new Promise((resolve, reject) => { + // 10 秒后超时 + const timeout = setTimeout(() => reject, 10000); + tunnel.on('id', ({ uname, uid, uavatar }) => { + this.uid = uid; + this.setData({ + myName: uname, + myAvatar: uavatar + }); + resolve(tunnel); + clearTimeout(timeout); + }); + }); + }), + + // 开始进行游戏服务 + serve: co.wrap(function *() { + const tunnel = this.tunnel; + + // 游戏开始,初始化对方信息,启动计时器 + tunnel.on('start', packet => { + const you = packet.players.filter(user => user.uid !== this.uid).pop(); + + this.setData({ + playing: false, + done: false, + finding: true, + gameInfo: '正在寻找玩伴...' + }); + setTimeout(() => { + this.setData({ + youHere: true, + yourName: you.uname, + yourAvatar: you.uavatar, + finding: false, + playing: true, + gameInfo: "准备" + }); + }, 10); + + let gameTime = packet.gameTime; + clearInterval(this.countdownId); + this.countdownId = setInterval(() => { + if (gameTime > 0) { + this.setData({ gameInfo: --gameTime }); + } else { + clearInterval(this.countdownId); + } + }, 1000); + + this.tunnel.emit('choice', { choice: this.data.myChoice }); + }); + + // 对方有动静的时候,触发提醒 + let movementTimer = 0; + const movementTimeout = 300; + tunnel.on('movement', packet => { + const lastMove = this.lastMove; + + this.setData({ yourMove: lastMove == 1 ? 2 : 1 }); + + clearTimeout(movementTimer); + movementTimer = setTimeout(() => { + this.lastMove = this.data.yourMove; + this.setData({ yourMove: 0 }); + }, 300); + }); + + // 服务器通知结果 + tunnel.on('result', packet => { + + // 清除计时器 + clearInterval(this.countdownId); + + // 双方结果 + const myResult = packet.result.find(x => x.uid == this.uid); + const yourResult = packet.result.find(x => x.uid != this.uid); + + // 本局结果 + let gameInfo, win = 'nobody'; + + if (myResult.roundScore == 0 && yourResult.roundScore == 0) { + gameInfo = pickText(EQ_TEXTS); + } + else if (myResult.roundScore > 0) { + gameInfo = pickText(WIN_TEXTS); + win = 'me'; + } + else { + gameInfo = pickText(LOSE_TEXTS); + win = 'you' + } + + // 更新到视图 + this.setData({ + gameInfo, + myScore: myResult.totalScore, + myStreak: myResult.winStreak, + yourChoice: yourResult.choice, + yourScore: yourResult.totalScore, + yourStreak: yourResult.winStreak, + gameState: 'finish', + win, + startButtonText: win == 'you' ? "不服" : "再来", + done: true + }); + + lab.finish('game'); + setTimeout(() => this.setData({ playing: false }), 1000); + }); + }), + + requestComputer() { + if (this.tunnel) { + this.tunnel.emit('requestComputer'); + } + }, + + // 点击开始游戏按钮,发送加入游戏请求 + startGame: co.wrap(function *() { + if (this.data.playing) return; + if (!this.data.connected) return; + + this.setData({ + playing: false, + done: false, + finding: true, + gameInfo: '正在寻找玩伴...' + }); + this.tunnel.emit('join'); + }), + + // 点击手势,更新选择是石头、剪刀还是布 + switchChoice(e) { + if (!this.data.playing) return; + let myChoice = this.data.myChoice + 1; + if (myChoice == 4) { + myChoice = 1; + } + this.setData({ myChoice }); + this.tunnel.emit('choice', { choice: myChoice }); + } +}); diff --git a/pages/game/game.json b/pages/game/game.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/pages/game/game.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pages/game/game.wxml b/pages/game/game.wxml new file mode 100644 index 0000000..8595afb --- /dev/null +++ b/pages/game/game.wxml @@ -0,0 +1,33 @@ + + + + + + + + {{myName}} + 得分 {{myScore}} + + + 连胜 {{myStreak}} + + + + + + + + + {{yourName}} + 得分 {{yourScore}} + + + 连胜 {{yourStreak}} + + + + {{gameInfo}} + + + + \ No newline at end of file diff --git a/pages/game/game.wxss b/pages/game/game.wxss new file mode 100644 index 0000000..9787c90 --- /dev/null +++ b/pages/game/game.wxss @@ -0,0 +1,191 @@ +.root { + overflow: hidden; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.my-side { + width: 1500rpx; + height: 1500rpx; + position: absolute; + bottom: -1125rpx; + left: -375rpx; + border-radius: 100%; + background: lightskyblue; + overflow: visible; +} + +.your-side { + width: 1500rpx; + height: 1500rpx; + position: absolute; + top: -1500rpx; + left: -375rpx; + border-radius: 100%; + background: lightpink; + overflow: visible; + transition: top ease 0.6s; +} + +.your-side.here { + top: -1125rpx; +} + +.avatar { + position: absolute; + width: 172rpx; + height: 172rpx; + border-radius: 100%; + border: 8rpx solid white; + background-color: #f0f0f0; +} + +.hand { + position: absolute; + width: 187rpx; + height: 187rpx; + border-radius: 100%; + overflow: visible; +} + +.score, .streak { + position: absolute; + color: #334567; + width: 225rpx; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.streak { + text-align: right; +} + +.my-side .avatar { + left: 656.25rpx; + top: -93.75rpx; + transition: top ease .2s; +} + +.my-side .hand { + left: 656.25rpx; + top: 150rpx; + transition: top ease .2s; +} + +.root.playing .my-side .hand:active { + transform: scale(1.3); +} + +.root.done .my-side .avatar { + top: 168.75rpx; +} + +.root.done .my-side .hand { + top: -112.5rpx; +} + +.my-side .score { + left: 412.5rpx; + bottom: 1162.5rpx; +} + +.my-side .streak { + right: 412.5rpx; + bottom: 1162.5rpx; +} + +.your-side .avatar { + left: 656.25rpx; + bottom: -93.75rpx; + transition: all ease .2s; + opacity: 0; +} + +.your-side.here .avatar { + opacity: 1; +} +.your-side.move-1 .avatar { + transform: rotate(15deg); +} +.your-side.move-2 .avatar { + transform: rotate(-15deg); +} + +.your-side .hand { + left: 656.25rpx; + bottom: 150rpx; + opacity: 0; + transition: bottom ease .2s; + transform: rotate(180deg); +} + +.root.done .your-side .avatar { + bottom: 168.75rpx; +} + +.root.done .your-side .hand { + bottom: -93.75rpx; + opacity: 1; +} + +.your-side .score { + left: 412.5rpx; + top: 1106.25rpx; +} + +.your-side .streak { + right: 412.5rpx; + top: 1106.25rpx; +} + + +.start-game, .request-computer { + position: absolute; + width: 200rpx; + height: 80rpx; + line-height: 80rpx; + margin-left: -100rpx; + bottom: 500rpx; + left: 50%; + display: none; +} + +.request-computer { + width: 300rpx; + margin-left: -150rpx; +} + +.root.finding .request-computer { + display: block; +} + +.root.connected .start-game { + display: block; +} + +.root.connected.playing .start-game, +.root.finding .start-game { + display: none; +} + +.game-info { + font-size: 75rpx; + position: absolute; + width: 750rpx; + height: 112.5rpx; + line-height: 112.5rpx; + margin-top: -56.25rpx; + bottom: 600rpx; + left: 0; + text-align: center; + opacity: 1; +} + +.root.connected.root.playing .game-info, +.root.connected.root.done .game-info { + opacity: 1; +} diff --git a/pages/https/https.js b/pages/https/https.js new file mode 100644 index 0000000..d51aa2e --- /dev/null +++ b/pages/https/https.js @@ -0,0 +1,54 @@ +const app = getApp(); +const config = app.config; +const lab = require('../../lib/lab'); + +Page({ + data: { + status: 'waiting', + url: 'https://' + config.host + '/hello', + requesting: false, + hintLine1: '完成服务器开发,', + hintLine2: '使得下面的地址可以访问' + }, + request() { + this.setData({ + requesting: true, + status: 'waiting', + hintLine1: '正在发送', + hintLine2: '...' + }); + wx.request({ + url: this.data.url, + method: 'GET', + success: (res) => { + if (+res.statusCode == 200) { + this.setData({ + status: 'success', + hintLine1: '服务器响应成功', + hintLine2: '返回:' + res.data + }); + lab.finish('https'); + } else { + this.setData({ + status: 'warn', + hintLine1: '响应错误', + hintLine2: '响应码:' + res.statusCode + }); + } + }, + fail: (res) => { + console.log(res); + this.setData({ + status: 'warn', + hintLine1: '请求失败', + hintLine2: res.errMsg + }); + }, + complete: () => { + this.setData({ + requesting: false + }); + } + }); + } +}); \ No newline at end of file diff --git a/pages/https/https.json b/pages/https/https.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/pages/https/https.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pages/https/https.wxml b/pages/https/https.wxml new file mode 100644 index 0000000..ce75548 --- /dev/null +++ b/pages/https/https.wxml @@ -0,0 +1,15 @@ + + + + + + {{hintLine1}} + {{hintLine2}} + + + {{url}} + + + + + \ No newline at end of file diff --git a/pages/https/https.wxss b/pages/https/https.wxss new file mode 100644 index 0000000..8b52cef --- /dev/null +++ b/pages/https/https.wxss @@ -0,0 +1 @@ +/* pages/https/https.wxss */ \ No newline at end of file diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..f1def29 --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,23 @@ +const lab = require('../../lib/lab'); + +Page({ + data: { + labs: [ + { id: 'config', title: '实验准备:配置请求域名' }, + { id: 'https', title: '实验一:HTTPS' }, + { id: 'session', title: '实验二:会话' }, + { id: 'websocket', title: '实验三:WebSocket' }, + { id: 'game', title: '实验四:剪刀石头布小游戏' } + ], + done: lab.getFinishLabs() + }, + + onShow() { + this.setData({ done: lab.getFinishLabs() }); + }, + + clear() { + lab.clear(); + this.setData({ done: lab.getFinishLabs() }); + } +}); \ No newline at end of file diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..69f4998 --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "腾讯云实验室" +} \ No newline at end of file diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..2d696b0 --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,11 @@ + + + + + {{lab.title}} + + + + + + \ No newline at end of file diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..a680707 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,41 @@ +/* pages/index/index.wxss */ +.index { + font-family: 'PingFang SC'; +} + +.nav { + display: flex; + flex-direction: column; + padding: 0 50rpx; +} + +.nav navigator { + padding: 30rpx 0; + border-bottom: 1rpx solid #EEE; + position: relative; +} + +.nav navigator:after { + content: '>'; + display: block; + position: absolute; + right: 3rpx; + top: 50%; + height: 40rpx; + line-height: 40rpx; + margin-top: -20rpx; + color: #999; +} + +.nav navigator icon, +.nav navigator text { + vertical-align: middle; +} + +.nav navigator icon { + margin-right: 25rpx; +} + +.clear { + margin: 100rpx 50rpx; +} \ No newline at end of file diff --git a/pages/session/session.js b/pages/session/session.js new file mode 100644 index 0000000..df218f7 --- /dev/null +++ b/pages/session/session.js @@ -0,0 +1,67 @@ +const app = getApp(); +const config = app.config; +const wafer = require('../../vendors/wafer-client-sdk/index'); +const lab = require('../../lib/lab'); + +wafer.setLoginUrl(`https://` + config.host + '/login'); + +Page({ + data: { + status: 'waiting', + url: 'https://' + config.host + '/me', + requesting: false, + hintLine1: '完成服务器开发,', + hintLine2: '让服务器可以识别小程序会话' + }, + request() { + this.setData({ + requesting: true, + status: 'waiting', + hintLine1: '正在发送', + hintLine2: '...' + }); + wafer.request({ + login: true, + url: this.data.url, + method: 'GET', + success: (res) => { + if (+res.statusCode == 200) { + if (res.data.openId) { + this.setData({ + status: 'success', + hintLine1: '成功获取会话', + hintLine2: res.data.nickName, + avatarUrl: res.data.avatarUrl + }); + lab.finish('session'); + } else { + this.setData({ + status: 'warn', + hintLine1: '会话获取失败', + hintLine2: '未获取到 openId' + }); + console.error('会话获取失败', res.data); + } + } else { + this.setData({ + status: 'warn', + hintLine1: '响应错误', + hintLine2: '响应码:' + res.statusCode + }); + } + }, + fail: (error) => { + this.setData({ + status: 'warn', + hintLine1: '获取失败', + hintLine2: error.message + }); + }, + complete: () => { + this.setData({ + requesting: false + }); + } + }); + } +}); \ No newline at end of file diff --git a/pages/session/session.json b/pages/session/session.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/pages/session/session.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pages/session/session.wxml b/pages/session/session.wxml new file mode 100644 index 0000000..f3a2b8c --- /dev/null +++ b/pages/session/session.wxml @@ -0,0 +1,16 @@ + + + + + + + {{hintLine1}} + {{hintLine2}} + + + {{url}} + + + + + \ No newline at end of file diff --git a/pages/session/session.wxss b/pages/session/session.wxss new file mode 100644 index 0000000..7aa605e --- /dev/null +++ b/pages/session/session.wxss @@ -0,0 +1 @@ +/* pages/session/session.wxss */ \ No newline at end of file diff --git a/pages/shouquan/shouquan.js b/pages/shouquan/shouquan.js new file mode 100644 index 0000000..485ea80 --- /dev/null +++ b/pages/shouquan/shouquan.js @@ -0,0 +1,77 @@ +// pages/shouquan.js +Page({ + + bindGetUserInfo: function (e) { + var that = this; + //此处授权得到userInfo + console.log(e.detail.userInfo); + //接下来写业务代码 + + //最后,记得返回刚才的页面 + wx.navigateBack({ + delta: 1 + }) + }, + /** + * 页面的初始数据 + */ + data: { + + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad: function (options) { + + }, + + /** + * 生命周期函数--监听页面初次渲染完成 + */ + onReady: function () { + + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow: function () { + + }, + + /** + * 生命周期函数--监听页面隐藏 + */ + onHide: function () { + + }, + + /** + * 生命周期函数--监听页面卸载 + */ + onUnload: function () { + + }, + + /** + * 页面相关事件处理函数--监听用户下拉动作 + */ + onPullDownRefresh: function () { + + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom: function () { + + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage: function () { + + } +}) \ No newline at end of file diff --git a/pages/shouquan/shouquan.json b/pages/shouquan/shouquan.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/pages/shouquan/shouquan.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/pages/shouquan/shouquan.wxml b/pages/shouquan/shouquan.wxml new file mode 100644 index 0000000..f2d6e00 --- /dev/null +++ b/pages/shouquan/shouquan.wxml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/pages/shouquan/shouquan.wxss b/pages/shouquan/shouquan.wxss new file mode 100644 index 0000000..e69de29 diff --git a/pages/websocket/websocket.js b/pages/websocket/websocket.js new file mode 100644 index 0000000..021915f --- /dev/null +++ b/pages/websocket/websocket.js @@ -0,0 +1,109 @@ +const app = getApp(); +const config = app.config; +const wafer = require('../../vendors/wafer-client-sdk/index'); +const lab = require('../../lib/lab'); + +Page({ + data: { + status: 'waiting', + url: 'wss://' + config.host + '/ws', + connecting: false, + hintLine1: '完成服务器开发,', + hintLine2: '让服务器支持 WebSocket 连接' + }, + + /** + * WebSocket 是否已经连接 + */ + socketOpen: false, + + /** + * 开始连接 WebSocket + */ + connect() { + this.setData({ + status: 'waiting', + connecting: true, + hintLine1: '正在连接', + hintLine2: '...' + }); + this.listen(); + wafer.setLoginUrl(`https://${config.host}/login`); + wafer.login({ + success: () => { + const header = wafer.buildSessionHeader(); + const query = Object.keys(header).map(key => `${key}=${encodeURIComponent(header[key])}`).join('&'); + wx.connectSocket({ + // 小程序 wx.connectSocket() API header 参数无效,把会话信息附加在 URL 上 + url: `${this.data.url}?${query}`, + header + }); + }, + fail: (err) => { + this.setData({ + status: 'warn', + connecting: false, + hintLine1: '登录失败', + hintLine2: err.message || err + }); + } + }); + }, + + /** + * 监听 WebSocket 事件 + */ + listen() { + wx.onSocketOpen(() => { + this.socketOpen = true; + this.setData({ + status: 'success', + connecting: false, + hintLine1: '连接成功', + hintLine2: '现在可以通过 WebSocket 发送接收消息了' + }); + console.info('WebSocket 已连接'); + }); + wx.onSocketMessage((message) => { + this.setData({ + hintLine2: message.data + }); + lab.finish('websocket'); + }); + wx.onSocketClose(() => { + this.setData({ + status: 'waiting', + hintLine1: 'WebSocket 已关闭' + }); + console.info('WebSocket 已关闭'); + }); + wx.onSocketError(() => { + setTimeout(() => { + this.setData({ + status: 'warn', + connecting: false, + hintLine1: '发生错误', + hintLine2: 'WebSocket 连接建立失败' + }); + }); + console.error('WebSocket 错误'); + }); + }, + + /** + * 发送一个包含当前时间信息的消息 + */ + send() { + wx.sendSocketMessage({ + data: new Date().toTimeString().split(' ').shift() + '.' + (new Date().getMilliseconds()) + }); + }, + + /** + * 关闭 WebSocket 连接 + */ + close() { + this.socketOpen = false; + wx.closeSocket(); + } +}); \ No newline at end of file diff --git a/pages/websocket/websocket.json b/pages/websocket/websocket.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/pages/websocket/websocket.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pages/websocket/websocket.wxml b/pages/websocket/websocket.wxml new file mode 100644 index 0000000..2c2af45 --- /dev/null +++ b/pages/websocket/websocket.wxml @@ -0,0 +1,17 @@ + + + + + + {{hintLine1}} + {{hintLine2}} + + + {{url}} + + + + + + + \ No newline at end of file diff --git a/pages/websocket/websocket.wxss b/pages/websocket/websocket.wxss new file mode 100644 index 0000000..36bb319 --- /dev/null +++ b/pages/websocket/websocket.wxss @@ -0,0 +1 @@ +/* pages/websocket/websocket.wxss */ \ No newline at end of file diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..a7461b2 --- /dev/null +++ b/project.config.json @@ -0,0 +1,51 @@ +{ + "description": "项目配置文件", + "packOptions": { + "ignore": [] + }, + "setting": { + "urlCheck": false, + "es6": true, + "postcss": true, + "minified": true, + "newFeature": true, + "coverView": true, + "autoAudits": false, + "checkInvalidKey": true, + "checkSiteMap": true, + "uploadWithSourceMap": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + } + }, + "compileType": "miniprogram", + "libVersion": "2.8.3", + "appid": "wx2a901996834ef5fb", + "projectname": "miniprogram-3", + "debugOptions": { + "hidedInDevtools": [] + }, + "isGameTourist": false, + "simulatorType": "wechat", + "simulatorPluginLibVersion": {}, + "condition": { + "search": { + "current": -1, + "list": [] + }, + "conversation": { + "current": -1, + "list": [] + }, + "game": { + "currentL": -1, + "list": [] + }, + "miniprogram": { + "current": -1, + "list": [] + } + } +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..ca02add --- /dev/null +++ b/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} \ No newline at end of file diff --git a/vendors/wafer-client-sdk/.bower.json b/vendors/wafer-client-sdk/.bower.json new file mode 100644 index 0000000..05d43a6 --- /dev/null +++ b/vendors/wafer-client-sdk/.bower.json @@ -0,0 +1,42 @@ +{ + "name": "wafer-client-sdk", + "description": "QCloud 微信小程序客户端 SDK", + "main": "index.js", + "authors": [ + "Tencent Cloud" + ], + "license": "MIT", + "keywords": [ + "qcloud", + "weapp", + "wechat", + "sdk", + "client", + "auth", + "websocket" + ], + "homepage": "https://github.com/tencentyun/wafer-client-sdk", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "typings.json", + "jsconfig.json", + "package.json", + ".npmignore", + ".travis.yml", + ".gitignore" + ], + "version": "0.8.4", + "_release": "0.8.4", + "_resolution": { + "type": "version", + "tag": "v0.8.4", + "commit": "3f605d1aa5bd0206bb9368586f4acb104f5f8ae5" + }, + "_source": "https://github.com/tencentyun/wafer-client-sdk.git", + "_target": "^0.8.3", + "_originalSource": "wafer-client-sdk" +} \ No newline at end of file diff --git a/vendors/wafer-client-sdk/LICENSE b/vendors/wafer-client-sdk/LICENSE new file mode 100644 index 0000000..7bb43b8 --- /dev/null +++ b/vendors/wafer-client-sdk/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/vendors/wafer-client-sdk/README.md b/vendors/wafer-client-sdk/README.md new file mode 100644 index 0000000..5c4f392 --- /dev/null +++ b/vendors/wafer-client-sdk/README.md @@ -0,0 +1,247 @@ +# 微信小程序客户端腾讯云增强 SDK + +[![Build Status](https://travis-ci.org/tencentyun/wafer-client-sdk.svg?branch=master)](https://travis-ci.org/tencentyun/wafer-client-sdk) +[![Coverage Status](https://coveralls.io/repos/github/tencentyun/wafer-client-sdk/badge.svg?branch=master)](https://coveralls.io/github/tencentyun/wafer-client-sdk?branch=master) +[![License](https://img.shields.io/github/license/tencentyun/wafer-client-sdk.svg)](LICENSE) + +本 项目是 [Wafer](https://github.com/tencentyun/wafer-solution) 的组成部分,为小程序客户端开发提供 SDK 支持会话服务和信道服务。 + +## SDK 获取与安装 + +解决方案[客户端 Demo](https://github.com/tencentyun/wafer-client-demo) 已经集成并使用最新版的 SDK,需要快速了解的可以从 Demo 开始。 + +如果需要单独开始,本 SDK 已经发布为 bower 模块,可以直接安装到小程序目录中。 + +```sh +npm install -g bower +bower install wafer-client-sdk +``` + +安装之后,就可以使用 `require` 引用 SDK 模块: + +```js +var qcloud = require('./bower_components/wafer-client-sdk/index.js'); +``` + +## 会话服务 + +[会话服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)让小程序拥有会话管理能力。 + +### 登录 + +登录可以在小程序和服务器之间建立会话,服务器由此可以获取到用户的标识和信息。 + +```js +var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js'); + +// 设置登录地址 +qcloud.setLoginUrl('https://199447.qcloud.la/login'); +qcloud.login({ + success: function (userInfo) { + console.log('登录成功', userInfo); + }, + fail: function (err) { + console.log('登录失败', err); + } +}); +``` +本 SDK 需要配合云端 SDK 才能提供完整会话服务。通过 [setLoginUrl](#setLoginUrl) 设置登录地址,云服务器在该地址上使用云端 SDK 处理登录请求。 + +> `setLoginUrl` 方法设置登录地址之后会一直有效,因此你可以在微信小程序启动时设置。 + +登录成功后,可以获取到当前微信用户的基本信息。 + +### 请求 + +如果希望小程序的网络请求包含会话,登录之后使用 [request](#request) 方法进行网络请求即可。 + +```js +qcloud.request({ + url: 'http://199447.qcloud.la/user', + success: function (response) { + console.log(response); + }, + fail: function (err) { + console.log(err); + } +}); +``` + +如果调用 `request` 之前还没有登录,则请求不会带有会话。`request` 方法也支持 `login` 参数支持在请求之前自动登录。 + +```js +// 使用 login 参数之前,需要设置登录地址 +qcloud.setLoginUrl('https://199447.qcloud.la/login'); +qcloud.request({ + login: true, + url: 'http://199447.qcloud.la/user', + success: function (response) { + console.log(response); + }, + fail: function (err) { + console.log(err); + } +}); +``` + +关于会话服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)。 + +## 信道服务 + +[信道服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)小程序支持利用腾讯云的信道资源使用 WebSocket 服务。 + +```js +// 创建信道,需要给定后台服务地址 +var tunnel = this.tunnel = new qcloud.Tunnel('https://199447.qcloud.la/tunnel'); + +// 监听信道内置消息,包括 connect/close/reconnecting/reconnect/error +tunnel.on('connect', () => console.log('WebSocket 信道已连接')); +tunnel.on('close', () => console.log('WebSocket 信道已断开')); +tunnel.on('reconnecting', () => console.log('WebSocket 信道正在重连...')); +tunnel.on('reconnect', () => console.log('WebSocket 信道重连成功')); +tunnel.on('error', error => console.error('信道发生错误:', error)); + +// 监听自定义消息(服务器进行推送) +tunnel.on('speak', speak => console.log('收到 speak 消息:', speak)); + +// 打开信道 +tunnel.open(); +// 发送消息 +tunnel.emit('speak', { word: "hello", who: { nickName: "techird" }}); +// 关闭信道 +tunnel.close(); +``` + +信道服务同样需要业务服务器配合云端 SDK 支持,构造信道实例的时候需要提供业务服务器提供的信道服务地址。通过监听信道消息以及自定义消息来通过信道实现业务。 + +关于信道使用的更完整实例,建议参考客户端 Demo 中的[三木聊天室应用源码](https://github.com/tencentyun/wafer-client-demo/blob/master/pages/chat/chat.js)。 + +关于信道服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)。 + +## API + + +### setLoginUrl +设置会话服务登录地址。 + +#### 语法 +```js +qcloud.setLoginUrl(loginUrl); +``` + +#### 参数 +|参数         |类型 |说明 +|-------------|---------------|-------------- +|loginUrl |string |会话服务登录地址 + +### login +登录,建立微信小程序会话。 + +#### 语法 +```js +qcloud.login(options); +``` + +#### 参数 +|参数         |类型 |说明 +|-------------|---------------|-------------- +|options     |PlainObject   |会话服务登录地址 +|options.success | () => void | 登录成功的回调 +|options.error | (error) => void | 登录失败的回调 + + +### request +进行带会话的请求。 + +#### 语法 +```js +qcloud.request(options); +``` + +#### 参数 +|参数         |类型 |说明 +|-------------|---------------|-------------- +|options     |PlainObject   | 会话服务登录地址 +|options.login | bool         | 是否自动登录以获取会话,默认为 false +|options.url   | string       | 必填,要请求的地址 +|options.header | PlainObject | 请求头设置,不允许设置 Referer +|options.method | string     | 请求的方法,默认为 GET +|options.success | (response) => void | 登录成功的回调。 +|options.error | (error) => void | 登录失败的回调 +|options.complete | () => void | 登录完成后回调,无论成功还是失败 + +### Tunnel + +表示一个信道。由于小程序的限制,同一时间只能有一个打开的信道。 + +#### constructor + +##### 语法 +```js +var tunnel = new Tunnel(tunnelUrl); +``` + +#### 参数 +|参数         |类型 |说明 +|-------------|---------------|-------------- +|tunnelUrl   |String   | 会话服务登录地址 + + +#### on +监听信道上的事件。信道上事件包括系统事件和服务器推送消息。 + +##### 语法 +```js +tunnel.on(type, listener); +``` + +##### 参数 +|参数         |类型 |说明 +|-------------|---------------|-------------- +|type       |string     | 监听的事件类型 +|listener     |(message?: any) => void | 监听器,具体类型的事件发生时调用监听器。如果是消息,则会有消息内容。 + +##### 事件 +|事件 |说明 +|-------------|------------------------------- +|connect |信道连接成功后回调 +|close |信道关闭后回调 +|reconnecting |信道发生重连时回调 +|reconnected |信道重连成功后回调 +|error |信道发生错误后回调 +|[message]   |信道服务器推送过来的消息类型,如果消息类型和上面内置的时间类型冲突,需要在监听的时候在消息类型前加 `@` +|\*           |监听所有事件和消息,监听器第一个参数接收到时间或消息类型  + +#### open +打开信道,建立连接。由于小程序的限制,同一时间只能有一个打开的信道。 + +##### 语法 +```js +tunnel.open(); +``` + +#### emit +向信道推送消息。 + +##### 语法 +```js +tunnel.emit(type, content); +``` + +##### 参数 +|参数         |类型 |说明 +|-------------|---------------|-------------- +|type       |string       | 要推送的消息的类型 +|content |any | 要推送的消息的内容 + +#### close +关闭信道 + +##### 语法 +```js +tunnel.close(); +``` + +## LICENSE + +[MIT](LICENSE) diff --git a/vendors/wafer-client-sdk/bower.json b/vendors/wafer-client-sdk/bower.json new file mode 100644 index 0000000..7019494 --- /dev/null +++ b/vendors/wafer-client-sdk/bower.json @@ -0,0 +1,32 @@ +{ + "name": "wafer-client-sdk", + "description": "QCloud 微信小程序客户端 SDK", + "main": "index.js", + "authors": [ + "Tencent Cloud" + ], + "license": "MIT", + "keywords": [ + "qcloud", + "weapp", + "wechat", + "sdk", + "client", + "auth", + "websocket" + ], + "homepage": "", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "typings.json", + "jsconfig.json", + "package.json", + ".npmignore", + ".travis.yml", + ".gitignore" + ] +} diff --git a/vendors/wafer-client-sdk/index.js b/vendors/wafer-client-sdk/index.js new file mode 100644 index 0000000..709a172 --- /dev/null +++ b/vendors/wafer-client-sdk/index.js @@ -0,0 +1,26 @@ +var constants = require('./lib/constants'); +var login = require('./lib/login'); +var Session = require('./lib/session'); +var request = require('./lib/request'); +var Tunnel = require('./lib/tunnel'); + +var exports = module.exports = { + login: login.login, + setLoginUrl: login.setLoginUrl, + LoginError: login.LoginError, + + clearSession: Session.clear, + + request: request.request, + buildSessionHeader: request.buildSessionHeader, + RequestError: request.RequestError, + + Tunnel: Tunnel, +}; + +// 导出错误类型码 +Object.keys(constants).forEach(function (key) { + if (key.indexOf('ERR_') === 0) { + exports[key] = constants[key]; + } +}); \ No newline at end of file diff --git a/vendors/wafer-client-sdk/lib/constants.js b/vendors/wafer-client-sdk/lib/constants.js new file mode 100644 index 0000000..fbede43 --- /dev/null +++ b/vendors/wafer-client-sdk/lib/constants.js @@ -0,0 +1,20 @@ +module.exports = { + WX_HEADER_CODE: 'X-WX-Code', + WX_HEADER_ENCRYPTED_DATA: 'X-WX-Encrypted-Data', + WX_HEADER_IV: 'X-WX-IV', + WX_HEADER_ID: 'X-WX-Id', + WX_HEADER_SKEY: 'X-WX-Skey', + + WX_SESSION_MAGIC_ID: 'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A', + + ERR_INVALID_PARAMS: 'ERR_INVALID_PARAMS', + + ERR_WX_LOGIN_FAILED: 'ERR_WX_LOGIN_FAILED', + ERR_WX_GET_USER_INFO: 'ERR_WX_GET_USER_INFO', + ERR_LOGIN_TIMEOUT: 'ERR_LOGIN_TIMEOUT', + ERR_LOGIN_FAILED: 'ERR_LOGIN_FAILED', + ERR_LOGIN_SESSION_NOT_RECEIVED: 'ERR_LOGIN_MISSING_SESSION', + + ERR_INVALID_SESSION: 'ERR_INVALID_SESSION', + ERR_CHECK_LOGIN_FAILED: 'ERR_CHECK_LOGIN_FAILED', +}; \ No newline at end of file diff --git a/vendors/wafer-client-sdk/lib/login.js b/vendors/wafer-client-sdk/lib/login.js new file mode 100644 index 0000000..3212d6a --- /dev/null +++ b/vendors/wafer-client-sdk/lib/login.js @@ -0,0 +1,175 @@ +var utils = require('./utils'); +var constants = require('./constants'); +var Session = require('./session'); + +/*** + * @class + * 表示登录过程中发生的异常 + */ +var LoginError = (function () { + function LoginError(type, message) { + Error.call(this, message); + this.type = type; + this.message = message; + } + + LoginError.prototype = new Error(); + LoginError.prototype.constructor = LoginError; + + return LoginError; +})(); + +/** + * 微信登录,获取 code 和 encryptData + */ +var getWxLoginResult = function getLoginCode(callback) { + wx.login({ + success: function (loginResult) { + + wx.getUserInfo({ + withCredentials: true, + success: function (res) { + //此处为获取微信信息后的业务方法 + + callback(null, { + code: loginResult.code, + encryptedData: userResult.encryptedData, + iv: userResult.iv, + userInfo: userResult.userInfo, + }); + }, + fail: function () { + //获取用户信息失败后。请跳转授权页面 + wx.showModal({ + title: '警告', + content: '尚未进行授权,请点击确定跳转到授权页面进行授权。', + success: function (res) { + if (res.confirm) { + console.log('用户点击确定') + wx.navigateTo({ + url: '../shouquan/shouquan', + }) + } + } + }) + }, + }); + }, + + fail: function (loginError) { + var error = new LoginError(constants.ERR_WX_LOGIN_FAILED, '微信登录失败,请检查网络状态'); + error.detail = loginError; + callback(error, null); + }, + }); +}; + +var noop = function noop() {}; +var defaultOptions = { + method: 'GET', + success: noop, + fail: noop, + loginUrl: null, +}; + +/** + * @method + * 进行服务器登录,以获得登录会话 + * + * @param {Object} options 登录配置 + * @param {string} options.loginUrl 登录使用的 URL,服务器应该在这个 URL 上处理登录请求 + * @param {string} [options.method] 请求使用的 HTTP 方法,默认为 "GET" + * @param {Function} options.success(userInfo) 登录成功后的回调函数,参数 userInfo 微信用户信息 + * @param {Function} options.fail(error) 登录失败后的回调函数,参数 error 错误信息 + */ +var login = function login(options) { + options = utils.extend({}, defaultOptions, options); + + if (!defaultOptions.loginUrl) { + options.fail(new LoginError(constants.ERR_INVALID_PARAMS, '登录错误:缺少登录地址,请通过 setLoginUrl() 方法设置登录地址')); + return; + } + + var doLogin = () => getWxLoginResult(function (wxLoginError, wxLoginResult) { + if (wxLoginError) { + options.fail(wxLoginError); + return; + } + + var userInfo = wxLoginResult.userInfo; + + // 构造请求头,包含 code、encryptedData 和 iv + var code = wxLoginResult.code; + var encryptedData = wxLoginResult.encryptedData; + var iv = wxLoginResult.iv; + var header = {}; + + header[constants.WX_HEADER_CODE] = code; + header[constants.WX_HEADER_ENCRYPTED_DATA] = encryptedData; + header[constants.WX_HEADER_IV] = iv; + + // 请求服务器登录地址,获得会话信息 + wx.request({ + url: options.loginUrl, + header: header, + method: options.method, + data: options.data, + + success: function (result) { + var data = result.data; + + // 成功地响应会话信息 + if (data && data[constants.WX_SESSION_MAGIC_ID]) { + if (data.session) { + data.session.userInfo = userInfo; + Session.set(data.session); + options.success(userInfo); + } else { + var errorMessage = '登录失败(' + data.error + '):' + (data.message || '未知错误'); + var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, errorMessage); + options.fail(noSessionError); + } + + // 没有正确响应会话信息 + } else { + var errorMessage = '登录请求没有包含会话响应,请确保服务器处理 `' + options.loginUrl + '` 的时候正确使用了 SDK 输出登录结果'; + var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, errorMessage); + options.fail(noSessionError); + } + }, + + // 响应错误 + fail: function (loginResponseError) { + var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常'); + options.fail(error); + }, + }); + }); + + var session = Session.get(); + if (session) { + wx.checkSession({ + success: function () { + options.success(session.userInfo); + }, + + fail: function () { + Session.clear(); + doLogin(); + }, + }); + } else { + doLogin(); + } +}; + +var setLoginUrl = function (loginUrl) { + defaultOptions.loginUrl = loginUrl; +}; + +module.exports = { + LoginError: LoginError, + login: login, + setLoginUrl: setLoginUrl, +}; + diff --git a/vendors/wafer-client-sdk/lib/request.js b/vendors/wafer-client-sdk/lib/request.js new file mode 100644 index 0000000..1797283 --- /dev/null +++ b/vendors/wafer-client-sdk/lib/request.js @@ -0,0 +1,125 @@ +var constants = require('./constants'); +var utils = require('./utils'); +var Session = require('./session'); +var loginLib = require('./login'); + +var noop = function noop() {}; + +var buildSessionHeader = function buildSessionHeader() { + var session = Session.get(); + var header = {}; + + if (session && session.id && session.skey) { + header[constants.WX_HEADER_ID] = session.id; + header[constants.WX_HEADER_SKEY] = session.skey; + } + + return header; +}; + +/*** + * @class + * 表示请求过程中发生的异常 + */ +var RequestError = (function () { + function RequestError(type, message) { + Error.call(this, message); + this.type = type; + this.message = message; + } + + RequestError.prototype = new Error(); + RequestError.prototype.constructor = RequestError; + + return RequestError; +})(); + +function request(options) { + if (typeof options !== 'object') { + var message = '请求传参应为 object 类型,但实际传了 ' + (typeof options) + ' 类型'; + throw new RequestError(constants.ERR_INVALID_PARAMS, message); + } + + var requireLogin = options.login; + var success = options.success || noop; + var fail = options.fail || noop; + var complete = options.complete || noop; + var originHeader = options.header || {}; + + // 成功回调 + var callSuccess = function () { + success.apply(null, arguments); + complete.apply(null, arguments); + }; + + // 失败回调 + var callFail = function (error) { + fail.call(null, error); + complete.call(null, error); + }; + + // 是否已经进行过重试 + var hasRetried = false; + + if (requireLogin) { + doRequestWithLogin(); + } else { + doRequest(); + } + + // 登录后再请求 + function doRequestWithLogin() { + loginLib.login({ success: doRequest, fail: callFail }); + } + + // 实际进行请求的方法 + function doRequest() { + var authHeader = buildSessionHeader(); + + wx.request(utils.extend({}, options, { + header: utils.extend({}, originHeader, authHeader), + + success: function (response) { + var data = response.data; + + // 如果响应的数据里面包含 SDK Magic ID,表示被服务端 SDK 处理过,此时一定包含登录态失败的信息 + if (data && data[constants.WX_SESSION_MAGIC_ID]) { + // 清除登录态 + Session.clear(); + + var error, message; + if (data.error === constants.ERR_INVALID_SESSION) { + // 如果是登录态无效,并且还没重试过,会尝试登录后刷新凭据重新请求 + if (!hasRetried) { + hasRetried = true; + doRequestWithLogin(); + return; + } + + message = '登录态已过期'; + error = new RequestError(data.error, message); + + } else { + message = '鉴权服务器检查登录态发生错误(' + (data.error || 'OTHER') + '):' + (data.message || '未知错误'); + error = new RequestError(constants.ERR_CHECK_LOGIN_FAILED, message); + } + + callFail(error); + return; + } + + callSuccess.apply(null, arguments); + }, + + fail: callFail, + complete: noop, + })); + }; + +}; + +module.exports = { + RequestError: RequestError, + request: request, + buildSessionHeader: buildSessionHeader +}; \ No newline at end of file diff --git a/vendors/wafer-client-sdk/lib/session.js b/vendors/wafer-client-sdk/lib/session.js new file mode 100644 index 0000000..eb37d82 --- /dev/null +++ b/vendors/wafer-client-sdk/lib/session.js @@ -0,0 +1,18 @@ +var constants = require('./constants'); +var SESSION_KEY = 'weapp_session_' + constants.WX_SESSION_MAGIC_ID; + +var Session = { + get: function () { + return wx.getStorageSync(SESSION_KEY) || null; + }, + + set: function (session) { + wx.setStorageSync(SESSION_KEY, session); + }, + + clear: function () { + wx.removeStorageSync(SESSION_KEY); + }, +}; + +module.exports = Session; \ No newline at end of file diff --git a/vendors/wafer-client-sdk/lib/tunnel.js b/vendors/wafer-client-sdk/lib/tunnel.js new file mode 100644 index 0000000..e94c7df --- /dev/null +++ b/vendors/wafer-client-sdk/lib/tunnel.js @@ -0,0 +1,528 @@ +var requestLib = require('./request'); +var wxTunnel = require('./wxTunnel'); + +/** + * 当前打开的信道,同一时间只能有一个信道打开 + */ +var currentTunnel = null; + +// 信道状态枚举 +var STATUS_CLOSED = Tunnel.STATUS_CLOSED = 'CLOSED'; +var STATUS_CONNECTING = Tunnel.STATUS_CONNECTING = 'CONNECTING'; +var STATUS_ACTIVE = Tunnel.STATUS_ACTIVE = 'ACTIVE'; +var STATUS_RECONNECTING = Tunnel.STATUS_RECONNECTING = 'RECONNECTING'; + +// 错误类型枚举 +var ERR_CONNECT_SERVICE = Tunnel.ERR_CONNECT_SERVICE = 1001; +var ERR_CONNECT_SOCKET = Tunnel.ERR_CONNECT_SOCKET = 1002; +var ERR_RECONNECT = Tunnel.ERR_RECONNECT = 2001; +var ERR_SOCKET_ERROR = Tunnel.ERR_SOCKET_ERROR = 3001; + +// 包类型枚举 +var PACKET_TYPE_MESSAGE = 'message'; +var PACKET_TYPE_PING = 'ping'; +var PACKET_TYPE_PONG = 'pong'; +var PACKET_TYPE_TIMEOUT = 'timeout'; +var PACKET_TYPE_CLOSE = 'close'; + +// 断线重连最多尝试 5 次 +var DEFAULT_MAX_RECONNECT_TRY_TIMES = 5; + +// 每次重连前,等待时间的增量值 +var DEFAULT_RECONNECT_TIME_INCREASE = 1000; + +function Tunnel(serviceUrl) { + if (currentTunnel && currentTunnel.status !== STATUS_CLOSED) { + throw new Error('当前有未关闭的信道,请先关闭之前的信道,再打开新信道'); + } + + currentTunnel = this; + + // 等确认微信小程序全面支持 ES6 就不用那么麻烦了 + var me = this; + + //========================================================================= + // 暴露实例状态以及方法 + //========================================================================= + this.serviceUrl = serviceUrl; + this.socketUrl = null; + this.status = null; + + this.open = openConnect; + this.on = registerEventHandler; + this.emit = emitMessagePacket; + this.close = close; + + this.isClosed = isClosed; + this.isConnecting = isConnecting; + this.isActive = isActive; + this.isReconnecting = isReconnecting; + + + //========================================================================= + // 信道状态处理,状态说明: + // closed - 已关闭 + // connecting - 首次连接 + // active - 当前信道已经在工作 + // reconnecting - 断线重连中 + //========================================================================= + function isClosed() { return me.status === STATUS_CLOSED; } + function isConnecting() { return me.status === STATUS_CONNECTING; } + function isActive() { return me.status === STATUS_ACTIVE; } + function isReconnecting() { return me.status === STATUS_RECONNECTING; } + + function setStatus(status) { + var lastStatus = me.status; + if (lastStatus !== status) { + me.status = status; + } + } + + // 初始为关闭状态 + setStatus(STATUS_CLOSED); + + + //========================================================================= + // 信道事件处理机制 + // 信道事件包括: + // connect - 连接已建立 + // close - 连接被关闭(包括主动关闭和被动关闭) + // reconnecting - 开始重连 + // reconnect - 重连成功 + // error - 发生错误,其中包括连接失败、重连失败、解包失败等等 + // [message] - 信道服务器发送过来的其它事件类型,如果事件类型和上面内置的事件类型冲突,将在事件类型前面添加前缀 `@` + //========================================================================= + var preservedEventTypes = 'connect,close,reconnecting,reconnect,error'.split(','); + var eventHandlers = []; + + /** + * 注册消息处理函数 + * @param {string} messageType 支持内置消息类型("connect"|"close"|"reconnecting"|"reconnect"|"error")以及业务消息类型 + */ + function registerEventHandler(eventType, eventHandler) { + if (typeof eventHandler === 'function') { + eventHandlers.push([eventType, eventHandler]); + } + } + + /** + * 派发事件,通知所有处理函数进行处理 + */ + function dispatchEvent(eventType, eventPayload) { + eventHandlers.forEach(function (handler) { + var handleType = handler[0]; + var handleFn = handler[1]; + + if (handleType === '*') { + handleFn(eventType, eventPayload); + } else if (handleType === eventType) { + handleFn(eventPayload); + } + }); + } + + /** + * 派发事件,事件类型和系统保留冲突的,事件名会自动加上 '@' 前缀 + */ + function dispatchEscapedEvent(eventType, eventPayload) { + if (preservedEventTypes.indexOf(eventType) > -1) { + eventType = '@' + eventType; + } + + dispatchEvent(eventType, eventPayload); + } + + + //========================================================================= + // 信道连接控制 + //========================================================================= + var isFirstConnection = true; + var isOpening = false; + + /** + * 连接信道服务器,获取 WebSocket 连接地址,获取地址成功后,开始进行 WebSocket 连接 + */ + function openConnect() { + if (isOpening) return; + isOpening = true; + + // 只有关闭状态才会重新进入准备中 + setStatus(isFirstConnection ? STATUS_CONNECTING : STATUS_RECONNECTING); + + requestLib.request({ + url: serviceUrl, + method: 'GET', + success: function (response) { + if (+response.statusCode === 200 && response.data && response.data.url) { + openSocket(me.socketUrl = response.data.url); + } else { + dispatchConnectServiceError(response); + } + }, + fail: dispatchConnectServiceError, + complete: () => isOpening = false, + }); + + function dispatchConnectServiceError(detail) { + if (isFirstConnection) { + setStatus(STATUS_CLOSED); + + dispatchEvent('error', { + code: ERR_CONNECT_SERVICE, + message: '连接信道服务失败,网络错误或者信道服务没有正确响应', + detail: detail || null, + }); + + } else { + startReconnect(detail); + } + } + } + + /** + * 打开 WebSocket 连接,打开后,注册微信的 Socket 处理方法 + */ + function openSocket(url) { + wxTunnel.listen({ + onOpen: handleSocketOpen, + onMessage: handleSocketMessage, + onClose: handleSocketClose, + onError: handleSocketError, + }); + + wx.connectSocket({ url: url }); + isFirstConnection = false; + } + + + //========================================================================= + // 处理消息通讯 + // + // packet - 数据包,序列化形式为 `${type}` 或者 `${type}:${content}` + // packet.type - 包类型,包括 message, ping, pong, close + // packet.content? - 当包类型为 message 的时候,会附带 message 数据 + // + // message - 消息体,会使用 JSON 序列化后作为 packet.content + // message.type - 消息类型,表示业务消息类型 + // message.content? - 消息实体,可以为任意类型,表示消息的附带数据,也可以为空 + // + // 数据包示例: + // - 'ping' 表示 Ping 数据包 + // - 'message:{"type":"speak","content":"hello"}' 表示一个打招呼的数据包 + //========================================================================= + + // 连接还没成功建立的时候,需要发送的包会先存放到队列里 + var queuedPackets = []; + + /** + * WebSocket 打开之后,更新状态,同时发送所有遗留的数据包 + */ + function handleSocketOpen() { + /* istanbul ignore else */ + if (isConnecting()) { + dispatchEvent('connect'); + + } + else if (isReconnecting()) { + dispatchEvent('reconnect'); + resetReconnectionContext(); + } + + setStatus(STATUS_ACTIVE); + emitQueuedPackets(); + nextPing(); + } + + /** + * 收到 WebSocket 数据包,交给处理函数 + */ + function handleSocketMessage(message) { + resolvePacket(message.data); + } + + /** + * 发送数据包,如果信道没有激活,将先存放队列 + */ + function emitPacket(packet) { + if (isActive()) { + sendPacket(packet); + } else { + queuedPackets.push(packet); + } + } + + /** + * 数据包推送到信道 + */ + function sendPacket(packet) { + var encodedPacket = [packet.type]; + + if (packet.content) { + encodedPacket.push(JSON.stringify(packet.content)); + } + + wx.sendSocketMessage({ + data: encodedPacket.join(':'), + fail: handleSocketError, + }); + } + + function emitQueuedPackets() { + queuedPackets.forEach(emitPacket); + + // empty queued packets + queuedPackets.length = 0; + } + + /** + * 发送消息包 + */ + function emitMessagePacket(messageType, messageContent) { + var packet = { + type: PACKET_TYPE_MESSAGE, + content: { + type: messageType, + content: messageContent, + }, + }; + + emitPacket(packet); + } + + /** + * 发送 Ping 包 + */ + function emitPingPacket() { + emitPacket({ type: PACKET_TYPE_PING }); + } + + /** + * 发送关闭包 + */ + function emitClosePacket() { + emitPacket({ type: PACKET_TYPE_CLOSE }); + } + + /** + * 解析并处理从信道接收到的包 + */ + function resolvePacket(raw) { + var packetParts = raw.split(':'); + var packetType = packetParts.shift(); + var packetContent = packetParts.join(':') || null; + var packet = { type: packetType }; + + if (packetContent) { + try { + packet.content = JSON.parse(packetContent); + } catch (e) {} + } + + switch (packet.type) { + case PACKET_TYPE_MESSAGE: + handleMessagePacket(packet); + break; + case PACKET_TYPE_PONG: + handlePongPacket(packet); + break; + case PACKET_TYPE_TIMEOUT: + handleTimeoutPacket(packet); + break; + case PACKET_TYPE_CLOSE: + handleClosePacket(packet); + break; + default: + handleUnknownPacket(packet); + break; + } + } + + /** + * 收到消息包,直接 dispatch 给处理函数 + */ + function handleMessagePacket(packet) { + var message = packet.content; + dispatchEscapedEvent(message.type, message.content); + } + + + //========================================================================= + // 心跳、断开与重连处理 + //========================================================================= + + /** + * Ping-Pong 心跳检测超时控制,这个值有两个作用: + * 1. 表示收到服务器的 Pong 相应之后,过多久再发下一次 Ping + * 2. 如果 Ping 发送之后,超过这个时间还没收到 Pong,断开与服务器的连接 + * 该值将在与信道服务器建立连接后被更新 + */ + let pingPongTimeout = 15000; + let pingTimer = 0; + let pongTimer = 0; + + /** + * 信道服务器返回 Ping-Pong 控制超时时间 + */ + function handleTimeoutPacket(packet) { + var timeout = packet.content * 1000; + /* istanbul ignore else */ + if (!isNaN(timeout)) { + pingPongTimeout = timeout; + ping(); + } + } + + /** + * 收到服务器 Pong 响应,定时发送下一个 Ping + */ + function handlePongPacket(packet) { + nextPing(); + } + + /** + * 发送下一个 Ping 包 + */ + function nextPing() { + clearTimeout(pingTimer); + clearTimeout(pongTimer); + pingTimer = setTimeout(ping, pingPongTimeout); + } + + /** + * 发送 Ping,等待 Pong + */ + function ping() { + /* istanbul ignore else */ + if (isActive()) { + emitPingPacket(); + + // 超时没有响应,关闭信道 + pongTimer = setTimeout(handlePongTimeout, pingPongTimeout); + } + } + + /** + * Pong 超时没有响应,信道可能已经不可用,需要断开重连 + */ + function handlePongTimeout() { + startReconnect('服务器已失去响应'); + } + + // 已经重连失败的次数 + var reconnectTryTimes = 0; + + // 最多允许失败次数 + var maxReconnectTryTimes = Tunnel.MAX_RECONNECT_TRY_TIMES || DEFAULT_MAX_RECONNECT_TRY_TIMES; + + // 重连前等待的时间 + var waitBeforeReconnect = 0; + + // 重连前等待时间增量 + var reconnectTimeIncrease = Tunnel.RECONNECT_TIME_INCREASE || DEFAULT_RECONNECT_TIME_INCREASE; + + var reconnectTimer = 0; + + function startReconnect(lastError) { + if (reconnectTryTimes >= maxReconnectTryTimes) { + close(); + + dispatchEvent('error', { + code: ERR_RECONNECT, + message: '重连失败', + detail: lastError, + }); + } + else { + wx.closeSocket(); + waitBeforeReconnect += reconnectTimeIncrease; + setStatus(STATUS_RECONNECTING); + reconnectTimer = setTimeout(doReconnect, waitBeforeReconnect); + } + + if (reconnectTryTimes === 0) { + dispatchEvent('reconnecting'); + } + + reconnectTryTimes += 1; + } + + function doReconnect() { + openConnect(); + } + + function resetReconnectionContext() { + reconnectTryTimes = 0; + waitBeforeReconnect = 0; + } + + /** + * 收到服务器的关闭请求 + */ + function handleClosePacket(packet) { + close(); + } + + function handleUnknownPacket(packet) { + // throw away + } + + var isClosing = false; + + /** + * 收到 WebSocket 断开的消息,处理断开逻辑 + */ + function handleSocketClose() { + /* istanbul ignore if */ + if (isClosing) return; + + /* istanbul ignore else */ + if (isActive()) { + // 意外断开的情况,进行重连 + startReconnect('链接已断开'); + } + } + + function close() { + isClosing = true; + closeSocket(); + setStatus(STATUS_CLOSED); + resetReconnectionContext(); + isFirstConnection = false; + clearTimeout(pingTimer); + clearTimeout(pongTimer); + clearTimeout(reconnectTimer); + dispatchEvent('close'); + isClosing = false; + } + + function closeSocket(emitClose) { + if (isActive() && emitClose !== false) { + emitClosePacket(); + } + + wx.closeSocket(); + } + + + //========================================================================= + // 错误处理 + //========================================================================= + + /** + * 错误处理 + */ + function handleSocketError(detail) { + switch (me.status) { + case Tunnel.STATUS_CONNECTING: + dispatchEvent('error', { + code: ERR_SOCKET_ERROR, + message: '连接信道失败,网络错误或者信道服务不可用', + detail: detail, + }); + break; + } + } + +} + +module.exports = Tunnel; \ No newline at end of file diff --git a/vendors/wafer-client-sdk/lib/utils.js b/vendors/wafer-client-sdk/lib/utils.js new file mode 100644 index 0000000..67fdcd4 --- /dev/null +++ b/vendors/wafer-client-sdk/lib/utils.js @@ -0,0 +1,18 @@ + +/** + * 拓展对象 + */ +exports.extend = function extend(target) { + var sources = Array.prototype.slice.call(arguments, 1); + + for (var i = 0; i < sources.length; i += 1) { + var source = sources[i]; + for (var key in source) { + if (source.hasOwnProperty(key)) { + target[key] = source[key]; + } + } + } + + return target; +}; \ No newline at end of file diff --git a/vendors/wafer-client-sdk/lib/wxTunnel.js b/vendors/wafer-client-sdk/lib/wxTunnel.js new file mode 100644 index 0000000..a1d32be --- /dev/null +++ b/vendors/wafer-client-sdk/lib/wxTunnel.js @@ -0,0 +1,32 @@ +/* istanbul ignore next */ +const noop = () => void(0); + +let onOpen, onClose, onMessage, onError; + +/* istanbul ignore next */ +function listen(listener) { + if (listener) { + onOpen = listener.onOpen; + onClose = listener.onClose; + onMessage = listener.onMessage; + onError = listener.onError; + } else { + onOpen = noop; + onClose = noop; + onMessage = noop; + onError = noop; + } +} + +/* istanbul ignore next */ +function bind() { + wx.onSocketOpen(result => onOpen(result)); + wx.onSocketClose(result => onClose(result)); + wx.onSocketMessage(result => onMessage(result)); + wx.onSocketError(error => onError(error)); +} + +listen(null); +bind(); + +module.exports = { listen }; \ No newline at end of file