评论

通过授权登录介绍小程序原生开发如何引入async/await、状态管理等工具

看完这些源码,你也可以自己在原生小程序里面实现 async/await,GraphQL,状态管理等特性。

登陆和授权是小程序开发会遇到的第一个问题,这里把相关业务逻辑、工具代码抽取出来,展示我们如何引入的一些包使得原生微信小程序内也可以使用 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 };
    },
  },
};
最后一次编辑于  2019-04-21  
点赞 23
收藏
评论

6 个评论

  • 2019-04-28

    等于你这个实现方法是没有mutation,直接action就更改state?

    2019-04-28
    赞同
    回复
  • 2019-04-28

    然后是没有实现mutation?还是得用action来提交mutation

    2019-04-28
    赞同
    回复
  • 2019-04-28

    dispatch的那段代码

    const res = this.actions[type](this, payload)

    res.then(update)

    const update = res => {      

    if (typeof res !== 'object') return this.setState(res)

    this.ctxs.map(ctx => ctx.setData(res))

    return res }


    等于action 方法必须返回一个对象?

    2019-04-28
    赞同
    回复
  • wills
    wills
    2019-04-23

    老哥 有git 地址吗

    2019-04-23
    赞同
    回复 1
    • 2019-04-23

      计划开源一个前后端的demo,有进展会在这里更新。

      2019-04-23
      回复
  • &|!🤗
    &|!🤗
    2019-04-22

    不过我奇怪的是,你们项目里写注释都是英文的咩

    2019-04-22
    赞同
    回复 6
    • 陈式坚
      陈式坚
      2019-04-22

      英文?

      2019-04-22
      回复
    • &|!🤗
      &|!🤗
      2019-04-22回复陈式坚

      没仔细看,中间的那几个都是引用别人的包,以为是自己写的,方法注释这些都是英文

      2019-04-22
      回复
    • 陈式坚
      陈式坚
      2019-04-22回复&|!🤗

      是的 基本都是引用别人的包而已

      2019-04-22
      回复
    • 2019-04-23
      regeneratorRuntime 是 Facebook 提供的,其他的包是 Github 上找的,然后有一些 modify。
      2019-04-23
      回复
    • lynn
      lynn
      2019-04-26

      不错

      2019-04-26
      回复
    查看更多(1)
  • &|!🤗
    &|!🤗
    2019-04-22

    最近正好想改一下用callback封装的请求以及登录态的那些东西,刷到了这个,是我需要的

    2019-04-22
    赞同
    回复
登录 后发表内容