smart_class
wu_shao_wei 5 years ago
commit 1a431d1897

@ -0,0 +1,3 @@
{
"directory": "vendors"
}

14
.gitignore vendored

@ -0,0 +1,14 @@
# Windows
[Dd]esktop.ini
Thumbs.db
$RECYCLE.BIN/
# macOS
.DS_Store
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
# Node.js
node_modules/

@ -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.

@ -0,0 +1,4 @@
使用说明
========
使用代码前,请先修改 `app.js` 里面的域名配置。

@ -0,0 +1,9 @@
//app.js
App({
config: {
host: 'wsw1999.xyz'
},
onLaunch () {
console.log('App.onLaunch()');
}
});

@ -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"
}

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

@ -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;
}

@ -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 = [];
}
});
}
}

@ -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 };

@ -0,0 +1,4 @@
module.exports = (api) =>
(options, ...params) => new Promise(
(resolve, reject) => api(Object.assign({}, options, { success: resolve, fail: reject }), ...params)
);

@ -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
);

@ -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 })
});
}
}

@ -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');
}
}
});

@ -0,0 +1,13 @@
<view class="lab https">
<view class="status">
<icon type="{{status}}" size="70" />
</view>
<view class="hint">
<view class="line">{{hintLine1}}</view>
<view class="line">{{hintLine2}}</view>
</view>
<view class="hint">
<view class="line">{{host}}</view>
</view>
<view class="command"></view>
</view>

@ -0,0 +1 @@
/* pages/config/config.wxss */

@ -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 });
}
});

@ -0,0 +1,33 @@
<view class="root {{connected && 'connected'}} {{finding && 'finding'}} {{playing && 'playing'}} {{done && 'done'}}">
<view class="my-side">
<image class="avatar" src="{{myAvatar}}" mode="aspectFill" />
<image class="hand {{win == 'me' && 'win'}}" src="../../images/{{myChoice}}.png" bindtap="switchChoice"></image>
<text class="score">
{{myName}}
得分 {{myScore}}
</text>
<text class="streak">
连胜 {{myStreak}}
</text>
</view>
<view class="your-side {{youHere && 'here'}} {{yourMove && 'move-' + yourMove}}">
<image class="avatar" src="{{yourAvatar}}" mode="aspectFill" />
<image class="hand {{win == 'you' && 'win'}}" src="../../images/{{yourChoice}}.png"></image>
<text class="score">
{{yourName}}
得分 {{yourScore}}
</text>
<text class="streak">
连胜 {{yourStreak}}
</text>
</view>
<view class="game-info">{{gameInfo}}</view>
<button class="start-game" type="primary" bindtap="startGame">{{startButtonText}}</button>
<button class="request-computer" bindtap="requestComputer">跟机器人玩</button>
</view>

@ -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;
}

@ -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
});
}
});
}
});

@ -0,0 +1,15 @@
<view class="lab https">
<view class="status">
<icon type="{{status}}" size="70" />
</view>
<view class="hint">
<view class="line">{{hintLine1}}</view>
<view class="line">{{hintLine2}}</view>
</view>
<view class="hint">
<view class="line">{{url}}</view>
</view>
<view class="command">
<button wx:if="{{status != 'success'}}" type="primary" bindtap="request" disabled="{{requesting}}">发送请求</button>
</view>
</view>

@ -0,0 +1 @@
/* pages/https/https.wxss */

@ -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() });
}
});

@ -0,0 +1,3 @@
{
"navigationBarTitleText": "腾讯云实验室"
}

@ -0,0 +1,11 @@
<view class="index">
<view class="nav">
<navigator wx:for="{{labs}}" wx:key="{{lab.id}}" wx:for-item="lab" url="/pages/{{lab.id}}/{{lab.id}}">
<icon type="{{ done[lab.id] ? 'success' : 'waiting_circle' }}" size="24"></icon>
<text>{{lab.title}}</text>
</navigator>
</view>
<view class="clear">
<button bindtap="clear">清空任务状态</button>
</view>
</view>

@ -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;
}

@ -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
});
}
});
}
});

@ -0,0 +1,16 @@
<view class="lab https">
<view class="status">
<icon wx:if="{{status != 'success'}}" type="{{status}}" size="70" />
<image wx:if="{{status == 'success'}}" src="{{avatarUrl}}" mode="aspectFill" />
</view>
<view class="hint">
<view class="line">{{hintLine1}}</view>
<view class="line">{{hintLine2}}</view>
</view>
<view class="hint">
<view class="line">{{url}}</view>
</view>
<view class="command">
<button wx:if="{{status != 'success'}}" type="primary" bindtap="request" disabled="{{requesting}}">获取会话</button>
</view>
</view>

@ -0,0 +1 @@
/* pages/session/session.wxss */

@ -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 () {
}
})

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

@ -0,0 +1,5 @@
<view class="lab https">
<view class="command">
<button open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo">授权登录</button>
</view>
</view>

@ -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();
}
});

@ -0,0 +1,17 @@
<view class="lab https">
<view class="status">
<icon type="{{status}}" size="70" />
</view>
<view class="hint">
<view class="line">{{hintLine1}}</view>
<view class="line">{{hintLine2}}</view>
</view>
<view class="hint">
<view class="line">{{url}}</view>
</view>
<view class="command">
<button type="primary" bindtap="connect" disabled="{{connecting || status == 'success'}}">连接</button>
<button bindtap="send" disabled="{{ status != 'success' }}">发送消息</button>
<button bindtap="close" disabled="{{ status != 'success' }}">断开</button>
</view>
</view>

@ -0,0 +1 @@
/* pages/websocket/websocket.wxss */

@ -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": []
}
}
}

@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}

@ -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"
}

@ -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.

@ -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 | 登录成功的回调。<ul><li>`response.statusCode`:请求返回的状态码</li><li>`response.data`:请求返回的数据</li></ul>
|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)

@ -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"
]
}

@ -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];
}
});

@ -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',
};

@ -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,
};

@ -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
};

@ -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;

@ -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;

@ -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;
};

@ -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 };
Loading…
Cancel
Save