通过授权登录介绍小程序原生开发如何引入async/await、状态管理等工具
登陆和授权是小程序开发会遇到的第一个问题,这里把相关业务逻辑、工具代码抽取出来,展示我们如何引入的一些包使得原生微信小程序内也可以使用 async/await、fetch、localStorage、状态管理、GraphQL 等等特性,希望对大家有所帮助。
前端
目录结构
[代码]├── app.js
├── app.json
├── app.wxss
├── common
│ └── api
│ └── index.js
├── config.js
├── pages
│ └── index
│ ├── api
│ │ └── index.js
│ ├── img
│ │ ├── btn.png
│ │ └── bg.jpg
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
├── project.config.json
├── store
│ ├── action.js
│ └── index.js
├── utils
│ └── index.js
└── vendor
├── event-emitter.js
├── fetch.js
├── fetchql.js
├── http.js
├── promisify.js
├── regenerator.js
├── storage.js
└── store.js
[代码]
业务代码
app.js
[代码]import store from './store/index'
const { loginInfo } = store.state
App({
store,
onLaunch() {
// 打开小程序即登陆,无需用户授权可获得 openID
if(!loginInfo) store.dispatch('login')
},
})
[代码]
store/index.js
[代码]import Store from '../vendor/store'
import localStorage from '../vendor/storage'
import actions from './action'
const loginInfo = localStorage.getItem('loginInfo')
export default new Store({
state: {
// 在全局状态中维护登陆信息
loginInfo,
},
actions,
})
[代码]
store/action.js
[代码]import regeneratorRuntime from '../vendor/regenerator';
import wx from '../vendor/promisify';
import localStorage from '../vendor/storage'
import api from '../common/api/index';
export default {
async login({ state }, payload) {
const { code } = await wx.loginAsync();
const { authSetting } = await wx.getSettingAsync()
// 如果用户曾授权,直接可以拿到 encryptedData
const { encryptedData, iv } = authSetting['scope.userInfo']
? await wx.getUserInfoAsync({ withCredentials: true })
: {};
// 如果用户未曾授权,也可以拿到 openID
const { token, userInfo } = await api.login({ code, encryptedData, iv });
// 为接口统一配置 Token
getApp().gql.requestObject.headers['Authorization'] = `Bearer ${token}`;
// 本地缓存登陆信息
localStorage.setItem('loginInfo', { token, userInfo } )
return { loginInfo: { token, userInfo } }
}
}
[代码]
common/api/index.js
[代码]import regeneratorRuntime from '../../vendor/regenerator.js'
export default {
/**
* 登录接口
* 如果只有 code,只返回 token,如果有 encryptedData, iv,同时返回用户的昵称和头像
* @param {*} param0
*/
async login({ code, encryptedData, iv }) {
const query = `query login($code: String!, $encryptedData: String, $iv: String){
login(code:$code, encryptedData:$encryptedData, iv:$iv, appid:$appid){
token
userInfo {
nickName
avatarUrl
}
}
}`
const {
login: { token, userInfo }
} = await getApp().query({ query, variables: { code, encryptedData, iv } })
return { token, userInfo }
},
}
[代码]
pages/index/index.js
[代码]import regeneratorRuntime from '../../vendor/regenerator.js'
const app = getApp()
Page({
data: {},
onLoad(options) {
// 将用户登录信息注入到当前页面的 data 中,并且当数据在全局范围内被更新时,都会自动刷新本页面
app.store.mapState(['loginInfo'], this)
},
async login({ detail: { errMsg } }) {
if (errMsg === 'getUserInfo:fail auth deny') return
app.store.dispatch('login')
// 继续处理业务
},
})
[代码]
pages/index/index.wxml
[代码]<view class="container">
<form report-submit="true" bindsubmit="saveFormId">
<button form-type="submit" open-type="getUserInfo" bindgetuserinfo="login">登录</button>
</form>
</view>
[代码]
工具代码
事件处理
vendor/event-emitter.js
[代码]const id_Identifier = '__id__';
function randomId() {
return Math.random().toString(36).substr(2, 16);
}
function findIndexById(id) {
return this.findIndex(item => item[id_Identifier] === id);
}
export default class EventEmitter {
constructor() {
this.events = {}
}
/**
* listen on a event
* @param event
* @param listener
*/
on(event, listener) {
let { events } = this;
let container = events[event] || [];
let id = randomId();
let index;
listener[id_Identifier] = id;
container.push(listener);
return () => {
index = findIndexById.call(container, id);
index >= 0 && container.splice(index, 1);
}
};
/**
* remove all listen of an event
* @param event
*/
off (event) {
this.events[event] = [];
};
/**
* clear all event listen
*/
clear () {
this.events = {};
};
/**
* listen on a event once, if it been trigger, it will cancel the listner
* @param event
* @param listener
*/
once (event, listener) {
let { events } = this;
let container = events[event] || [];
let id = randomId();
let index;
let callback = () => {
index = findIndexById.call(container, id);
index >= 0 && container.splice(index, 1);
listener.apply(this, arguments);
};
callback[id_Identifier] = id;
container.push(callback);
};
/**
* emit event
*/
emit () {
const { events } = this;
const argv = [].slice.call(arguments);
const event = argv.shift();
((events['*'] || []).concat(events[event] || [])).map(listener => self.emitting(event, argv, listener));
};
/**
* define emitting
* @param event
* @param dataArray
* @param listener
*/
emitting (event, dataArray, listener) {
listener.apply(this, dataArray);
};
}
[代码]
封装 wx.request() 接口
vendor/http.js
[代码]import EventEmitter from './event-emitter.js';
const DEFAULT_CONFIG = {
maxConcurrent: 10,
timeout: 0,
header: {},
dataType: 'json'
};
class Http extends EventEmitter {
constructor(config = DEFAULT_CONFIG) {
super();
this.config = config;
this.ctx = wx;
this.queue = [];
this.runningTask = 0;
this.maxConcurrent = DEFAULT_CONFIG.maxConcurrent;
this.maxConcurrent = config.maxConcurrent;
this.requestInterceptor = () => true;
this.responseInterceptor = () => true;
}
create(config = DEFAULT_CONFIG) {
return new Http(config);
}
next() {
const queue = this.queue;
if (!queue.length || this.runningTask >= this.maxConcurrent) return;
const entity = queue.shift();
const config = entity.config;
const { requestInterceptor, responseInterceptor } = this;
if (requestInterceptor.call(this, config) !== true) {
let response = {
data: null,
errMsg: `Request Interceptor: Request can\'t pass the Interceptor`,
statusCode: 0,
header: {}
};
entity.reject(response);
return;
}
this.emit('request', config);
this.runningTask = this.runningTask + 1;
let timer = null;
let aborted = false;
let finished = false;
const callBack = {
success: (res) => {
if (aborted) return;
finished = true;
timer && clearTimeout(timer);
entity.response = res;
this.emit('success', config, res);
responseInterceptor.call(this, config, res) !== true
? entity.reject(res)
: entity.resolve(res);
},
fail: (res) => {
if (aborted) return;
finished = true;
timer && clearTimeout(timer);
entity.response = res;
this.emit('fail', config, res);
responseInterceptor.call(this, config, res) !== true
? entity.reject(res)
: entity.resolve(res);
},
complete: () => {
if (aborted) return;
this.emit('complete', config, entity.response);
this.next();
this.runningTask = this.runningTask - 1;
}
};
const requestConfig = Object.assign(config, callBack);
const task = this.ctx.request(requestConfig);
if (this.config.timeout > 0) {
timer = setTimeout(() => {
if (!finished) {
aborted = true;
task && task.abort();
this.next();
}
}, this.config.timeout);
}
}
request(method, url, data, header, dataType = 'json') {
const config = {
method,
url,
data,
header: { ...header, ...this.config.header },
dataType: dataType || this.config.dataType
};
return new Promise((resolve, reject) => {
const entity = { config, resolve, reject, response: null };
this.queue.push(entity);
this.next();
});
}
head(url, data, header, dataType) {
return this.request('HEAD', url, data, header, dataType);
}
options(url, data, header, dataType) {
return this.request('OPTIONS', url, data, header, dataType);
}
get(url, data, header, dataType) {
return this.request('GET', url, data, header, dataType);
}
post(url, data, header, dataType) {
return this.request('POST', url, data, header, dataType);
}
put(url, data, header, dataType) {
return this.request('PUT', url, data, header, dataType);
}
['delete'](url, data, header, dataType) {
return this.request('DELETE', url, data, header, dataType);
}
trace(url, data, header, dataType) {
return this.request('TRACE', url, data, header, dataType);
}
connect(url, data, header, dataType) {
return this.request('CONNECT', url, data, header, dataType);
}
setRequestInterceptor(interceptor) {
this.requestInterceptor = interceptor;
return this;
}
setResponseInterceptor(interceptor) {
this.responseInterceptor = interceptor;
return this;
}
clean() {
this.queue = [];
}
}
export default new Http();
[代码]
兼容 fetch 标准
vendor/fetch.js
[代码]import http from './http';
const httpClient = http.create({
maxConcurrent: 10,
timeout: 0,
header: {},
dataType: 'json'
});
function generateResponse(res) {
let header = res.header || {};
let config = res.config || {};
return {
ok: ((res.statusCode / 200) | 0) === 1, // 200-299
status: res.statusCode,
statusText: res.errMsg,
url: config.url,
clone: () => generateResponse(res),
text: () =>
Promise.resolve(
typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
),
json: () => {
if (typeof res.data === 'object') return Promise.resolve(res.data);
let json = {};
try {
json = JSON.parse(res.data);
} catch (err) {
console.error(err);
}
return json;
},
blob: () => Promise.resolve(new Blob([res.data])),
headers: {
keys: () => Object.keys(header),
entries: () => {
let all = [];
for (let key in header) {
if (header.hasOwnProperty(key)) {
all.push([key, header[key]]);
}
}
return all;
},
get: n => header[n.toLowerCase()],
has: n => n.toLowerCase() in header
}
};
}
export default (typeof fetch === 'function'
? fetch.bind()
: function(url, options) {
options = options || {};
return httpClient
.request(options.method || 'get', url, options.body, options.headers)
.then(res => Promise.resolve(generateResponse(res)))
.catch(res => Promise.reject(generateResponse(res)));
});
[代码]
GraphQL客户端
vendor/fetchql.js
[代码]import fetch from './fetch';
// https://github.com/gucheen/fetchql
/** Class to realize fetch interceptors */
class FetchInterceptor {
constructor() {
this.interceptors = [];
/* global fetch */
this.fetch = (...args) => this.interceptorWrapper(fetch, ...args);
}
/**
* add new interceptors
* @param {(Object|Object[])} interceptors
*/
addInterceptors(interceptors) {
const removeIndex = [];
if (Array.isArray(interceptors)) {
interceptors.map((interceptor) => {
removeIndex.push(this.interceptors.length);
return this.interceptors.push(interceptor);
});
} else if (interceptors instanceof Object) {
removeIndex.push(this.interceptors.length);
this.interceptors.push(interceptors);
}
this.updateInterceptors();
return () => this.removeInterceptors(removeIndex);
}
/**
* remove interceptors by indexes
* @param {number[]} indexes
*/
removeInterceptors(indexes) {
if (Array.isArray(indexes)) {
indexes.map(index => this.interceptors.splice(index, 1));
this.updateInterceptors();
}
}
/**
* @private
*/
updateInterceptors() {
this.reversedInterceptors = this.interceptors
.reduce((array, interceptor) => [interceptor].concat(array), []);
}
/**
* remove all interceptors
*/
clearInterceptors() {
this.interceptors = [];
this.updateInterceptors();
}
/**
* @private
*/
interceptorWrapper(fetch, ...args) {
let promise = Promise.resolve(args);
this.reversedInterceptors.forEach(({ request, requestError }) => {
if (request || requestError) {
promise = promise.then(() => request(...args), requestError);
}
});
promise = promise.then(() => fetch(...args));
this.reversedInterceptors.forEach(({ response, responseError }) => {
if (response || responseError) {
promise = promise.then(response, responseError);
}
});
return promise;
}
}
/**
* GraphQL client with fetch api.
* @extends FetchInterceptor
*/
class FetchQL extends FetchInterceptor {
/**
* Create a FetchQL instance.
* @param {Object} options
* @param {String} options.url - the server address of GraphQL
* @param {(Object|Object[])=} options.interceptors
* @param {{}=} options.headers - request headers
* @param {FetchQL~requestQueueChanged=} options.onStart - callback function of a new request queue
* @param {FetchQL~requestQueueChanged=} options.onEnd - callback function of request queue finished
* @param {Boolean=} options.omitEmptyVariables - remove null props(null or '') from the variables
* @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api)
*/
constructor({
url,
interceptors,
headers,
onStart,
onEnd,
omitEmptyVariables = false,
requestOptions = {},
}) {
super();
this.requestObject = Object.assign(
{},
{
method: 'POST',
headers: Object.assign({}, {
Accept: 'application/json',
'Content-Type': 'application/json',
}, headers),
credentials: 'same-origin',
},
requestOptions,
);
this.url = url;
this.omitEmptyVariables = omitEmptyVariables;
// marker for request queue
this.requestQueueLength = 0;
// using for caching enums' type
this.EnumMap = {};
this.callbacks = {
onStart,
onEnd,
};
this.addInterceptors(interceptors);
}
/**
* operate a query
* @param {Object} options
* @param {String} options.operationName
* @param {String} options.query
* @param {Object=} options.variables
* @param {Object=} options.opts - addition options(will not be passed to server)
* @param {Boolean=} options.opts.omitEmptyVariables - remove null props(null or '') from the variables
* @param {Object=} options.requestOptions - addition options to fetch request(refer to fetch api)
* @returns {Promise}
* @memberOf FetchQL
*/
query({ operationName, query, variables, opts = {}, requestOptions = {}, }) {
const options = Object.assign({}, this.requestObject, requestOptions);
let vars;
if (this.omitEmptyVariables || opts.omitEmptyVariables) {
vars = this.doOmitEmptyVariables(variables);
} else {
vars = variables;
}
const body = {
operationName,
query,
variables: vars,
};
options.body = JSON.stringify(body);
this.onStart();
return this.fetch(this.url, options)
.then((res) => {
if (res.ok) {
return res.json();
}
// return an custom error stack if request error
return {
errors: [{
message: res.statusText,
stack: res,
}],
};
})
.then(({ data, errors }) => (
new Promise((resolve, reject) => {
this.onEnd();
// if data in response is 'null'
if (!data) {
return reject(errors || [{}]);
}
// if all properties of data is 'null'
const allDataKeyEmpty = Object.keys(data).every(key => !data[key]);
if (allDataKeyEmpty) {
return reject(errors);
}
return resolve({ data, errors });
})
));
}
/**
* get current server address
* @returns {String}
* @memberOf FetchQL
*/
getUrl() {
return this.url;
}
/**
* setting a new server address
* @param {String} url
* @memberOf FetchQL
*/
setUrl(url) {
this.url = url;
}
/**
* get information of enum type
* @param {String[]} EnumNameList - array of enums' name
* @returns {Promise}
* @memberOf FetchQL
*/
getEnumTypes(EnumNameList) {
const fullData = {};
// check cache status
const unCachedEnumList = EnumNameList.filter((element) => {
if (this.EnumMap[element]) {
// enum has been cached
fullData[element] = this.EnumMap[element];
return false;
}
return true;
});
// immediately return the data if all enums have been cached
if (!unCachedEnumList.length) {
return new Promise((resolve) => {
resolve({ data: fullData });
});
}
// build query string for uncached enums
const EnumTypeQuery = unCachedEnumList.map(type => (
`${type}: __type(name: "${type}") {
...EnumFragment
}`
));
const query = `
query {
${EnumTypeQuery.join('\n')}
}
fragment EnumFragment on __Type {
kind
description
enumValues {
name
description
}
}`;
const options = Object.assign({}, this.requestObject);
options.body = JSON.stringify({ query });
this.onStart();
return this.fetch(this.url, options)
.then((res) => {
if (res.ok) {
return res.json();
}
// return an custom error stack if request error
return {
errors: [{
message: res.statusText,
stack: res,
}],
};
})
.then(({ data, errors }) => (
new Promise((resolve, reject) => {
this.onEnd();
// if data in response is 'null' and have any errors
if (!data) {
return reject(errors || [{ message: 'Do not get any data.' }]);
}
// if all properties of data is 'null'
const allDataKeyEmpty = Object.keys(data).every(key => !data[key]);
if (allDataKeyEmpty && errors && errors.length) {
return reject(errors);
}
// merge enums' data
const passData = Object.assign(fullData, data);
// cache new enums' data
Object.keys(data).map((key) => {
this.EnumMap[key] = data[key];
return key;
});
return resolve({ data: passData, errors });
})
));
}
/**
* calling on a request starting
* if the request belong to a new queue, call the 'onStart' method
*/
onStart() {
this.requestQueueLength++;
if (this.requestQueueLength > 1 || !this.callbacks.onStart) {
return;
}
this.callbacks.onStart(this.requestQueueLength);
}
/**
* calling on a request ending
* if current queue finished, calling the 'onEnd' method
*/
onEnd() {
this.requestQueueLength--;
if (this.requestQueueLength || !this.callbacks.onEnd) {
return;
}
this.callbacks.onEnd(this.requestQueueLength);
}
/**
* Callback of requests queue changes.(e.g. new queue or queue finished)
* @callback FetchQL~requestQueueChanged
* @param {number} queueLength - length of current request queue
*/
/**
* remove empty props(null or '') from object
* @param {Object} input
* @returns {Object}
* @memberOf FetchQL
* @private
*/
doOmitEmptyVariables(input) {
const nonEmptyObj = {};
Object.keys(input).map(key => {
const value = input[key];
if ((typeof value === 'string' && value.length === 0) || value === null || value === undefined) {
return key;
} else if (value instanceof Object) {
nonEmptyObj[key] = this.doOmitEmptyVariables(value);
} else {
nonEmptyObj[key] = value;
}
return key;
});
return nonEmptyObj;
}
}
export default FetchQL;
[代码]
将wx的异步接口封装成Promise
vendor/promisify.js
[代码]function promisify(wx) {
let wxx = { ...wx };
for (let attr in wxx) {
if (!wxx.hasOwnProperty(attr) || typeof wxx[attr] != 'function') continue;
// skip over the sync method
if (/sync$/i.test(attr)) continue;
wxx[attr + 'Async'] = function asyncFunction(argv = {}) {
return new Promise(function (resolve, reject) {
wxx[attr].call(wxx, {
...argv,
...{ success: res => resolve(res), fail: err => reject(err) }
});
});
};
}
return wxx;
}
export default promisify(typeof wx === 'object' ? wx : {});
[代码]
localstorage
vendor/storage.js
[代码]class Storage {
constructor(wx) {
this.wx = wx;
}
static get timestamp() {
return new Date() / 1000;
}
static __isExpired(entity) {
if (!entity) return true;
return Storage.timestamp - (entity.timestamp + entity.expiration) >= 0;
}
static get __info() {
let info = {};
try {
info = this.wx.getStorageInfoSync() || info;
} catch (err) {
console.error(err);
}
return info;
}
setItem(key, value, expiration) {
const entity = {
timestamp: Storage.timestamp,
expiration,
key,
value
};
this.wx.setStorageSync(key, JSON.stringify(entity));
return this;
}
getItem(key) {
let entity;
try {
entity = this.wx.getStorageSync(key);
if (entity) {
entity = JSON.parse(entity);
} else {
return null;
}
} catch (err) {
console.error(err);
return null;
}
// 没有设置过期时间, 则直接返回值
if (!entity.expiration) return entity.value;
// 已过期
if (Storage.__isExpired(entity)) {
this.remove(key);
return null;
} else {
return entity.value;
}
}
removeItem(key) {
try {
this.wx.removeStorageSync(key);
} catch (err) {
console.error(err);
}
return this;
}
clear() {
try {
this.wx.clearStorageSync();
} catch (err) {
console.error(err);
}
return this;
}
get info() {
let info = {};
try {
info = this.wx.getStorageInfoSync();
} catch (err) {
console.error(err);
}
return info || {};
}
get length() {
return (this.info.keys || []).length;
}
}
export default new Storage(wx);
[代码]
状态管理
vendor/store.js
[代码]module.exports = class Store {
constructor({ state, actions }) {
this.state = state || {}
this.actions = actions || {}
this.ctxs = []
}
// 派发action, 统一返回promise action可以直接返回state
dispatch(type, payload) {
const update = res => {
if (typeof res !== 'object') return
this.setState(res)
this.ctxs.map(ctx => ctx.setData(res))
return res
}
if (typeof this.actions[type] !== 'function') return
const res = this.actions[type](this, payload)
return res.constructor.toString().match(/function\s*([^(]*)/)[1] === 'Promise'
? res.then(update)
: new Promise(resolve => resolve(update(res)))
}
// 修改state的方法
setState(data) {
this.state = { ...this.state, ...data }
}
// 根据keys获取state
getState(keys) {
return keys.reduce((acc, key) => ({ ...acc, ...{ [key]: this.state[key] } }), {})
}
// 映射state到实例中,可在onload或onshow中调用
mapState(keys, ctx) {
if (!ctx || typeof ctx.setData !== 'function') return
ctx.setData(this.getState(keys))
this.ctxs.push(ctx)
}
}
[代码]
兼容 async/await
vendor/regenerator.js
[代码]/**
* Copyright (c) 2014-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var regeneratorRuntime = (function (exports) {
"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 asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator";
var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag";
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;
}
exports.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);
};
});
}
exports.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;
};
exports.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.
exports.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.
result.value = unwrapped;
resolve(result);
}, function(error) {
// If a rejected Promise was yielded, throw the rejection back
// into the async generator function so it can be handled there.
return invoke("throw", error, resolve, reject);
});
}
}
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);
AsyncIterator.prototype[asyncIteratorSymbol] = function () {
return this;
};
exports.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.
exports.async = function(innerFn, outerFn, self, tryLocsList) {
var iter = new AsyncIterator(
wrap(innerFn, outerFn, self, tryLocsList)
);
return exports.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") {
// Note: ["return"] must be used for ES3 parsing compatibility.
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";
// A Generator should always return itself as the iterator object when the
// @@iterator function is called on it. Some browsers' implementations of the
// iterator prototype chain incorrectly implement this, causing the Generator
// object to not be returned from this call. This ensures that doesn't happen.
// See https://github.com/facebook/regenerator/issues/274 for more details.
Gp[iteratorSymbol] = function() {
return this;
};
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);
}
exports.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 };
}
exports.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;
}
};
// Regardless of whether this script is executing as a CommonJS module
// or not, return the runtime object so that we can declare the variable
// regeneratorRuntime in the outer scope, which allows this module to be
// injected easily by `bin/regenerator --include-runtime script.js`.
return exports;
}(
// If this script is executing as a CommonJS module, use module.exports
// as the regeneratorRuntime namespace. Otherwise create a new empty
// object. Either way, the resulting object will be used to initialize
// the regeneratorRuntime variable at the top of this file.
typeof module === "object" ? module.exports : {}
));
[代码]
后端
[代码]const typeDefs = gql`
# schema 下面是根类型,约定是 RootQuery 和 RootMutation
schema {
query: Query
}
# 定义具体的 Query 的结构
type Query {
# 登陆接口
login(code: String!, encryptedData: String, iv: String): Login
}
type Login {
token: String!
userInfo: UserInfo
}
type UserInfo {
nickName: String
gender: String
avatarUrl: String
}
`;
const resolvers = {
Query: {
async login(parent, {
code, encryptedData, iv
}) {
const { sessionKey, openId, unionId } = await wxService.code2Session(code);
const userInfo = encryptedData && iv
? wxService.decryptData(sessionKey, encryptedData, iv)
: { openId, unionId };
if (userInfo.nickName) {
userService.createOrUpdateWxUser(userInfo);
}
const token = await userService.generateJwtToken(userInfo);
return { token, userInfo };
},
},
};
[代码]