小程序登录案例

本文讲述的是自微信官方在 wx.getUserInfo API更新后,微信小程序该如何实现登录,以及在登录与用户授权逻辑方面遇到的种种矛盾做出的一些可行性分析,文中出现的源码都可在以下链接中Clone,仅供大家交流、参考。

本文主要讲解的是小程序前端代码,但是Clone过来的源码包含前端与后台代码,并且只须简单几步安装即可在本地环境中运行起来。

Github:https://github.com/wuliang9524/mini_app_login
Gitee:https://gitee.com/wuliang924/demo_miniapp_login

常见问题

  • Q:在小程序代码中遇到异步嵌套的问题,代码能读性非常差

本文中采用ES6中的Promise对象解决异步嵌套问题,若有不了解者,建议先Google了解一番后再回头查阅本文。

  • Q:小程序Page.onLoad之后,App.onLaunch才返回用户登录态信息

这是由于在App.onLaunch中,获取用户登录态信息请求后台接口是异步执行而导致的,我们只需要在Page.onLoad中定义一个App的回调函数即可,但是如果每一个需要先验证登录的Page都要定义这么一个函数则实在不理智,本文后面也会提到封装一个公共方法。

  • 更多经典常见问题等待您的留言...

小程序前端

  1. 首先先对微信的API进行Promise对象的 “改造”,由于微信API的格式大都一致,在 /utils/ 文件夹中新建一个 wxapi.js 文件

    // /utils/wxapi.js
    
    const wxapi = {
      /**
       * 对微信Api Promise化的公共函数
       */
      wxapi: (wxApiName, obj) => {
        return new Promise((resolve, reject) => {
          wx[wxApiName]({
            ...obj,     //注意这里涉及的语法
            success: (res) => {
              resolve(res);
            },
            fail: (res) => {
              reject(res);
            }
          });
        });
      },
    
      /**
       * 以下是微信Api Promise化的特殊案例
       */
      wxsetData: (pageObj, obj) => {
        if(pageObj && obj){
          return new Promise((resolve, reject) => {
            pageObj.setData(obj, resolve(obj));
          });
        }
      },
    }
    
    module.exports = wxapi;
    
  2. 接着我们在 app.js 中封装一个 exeLogin() 方法,该方法主要做以下几件事情:

    • 调用 wx.login 获取到 code;
    • code 请求后台接口,后台接口返回自定义登录态信息(本文中包括登录态 token 及用户的基本信息);
    • 调用 wx.setStorage 缓存登录态信息

    注意代码中 exeLogin() 方法返回的是一个Promise对象,以及在 app.js 文件的开头,载入了上一步的 wxapi.js 中定义的方法。

    // app.js
    
    import {
      wxapi,
      wxsetData
    } from './utils/wxapi.js';
    
    /**
    * [exeLogin 执行登录流程]
    * @param  {[string]} loginKey  自定义登录态信息缓存的key
    * @param  {[string]} timeout   调用wx.login的超时时间
    * @return {[Promise]}          返回一个Promise对象
    */
    exeLogin: function(loginKey, timeout = 3000) {
        var _this = this;
        return new Promise((resolve, reject) => {
          wxapi('login', {
            'timeout': timeout
          }).then(function(res) {
            return wxapi('request', {
              'method': 'POST',
              'url': _this.gData.api.request + '/api/User/third',
              'header': {
                'Content-type': 'application/x-www-form-urlencoded',
              },
              'data': {
                'code': res.code,
                'platform': 'miniwechat',
              }
            })
          }).then(function(res) {
            //当服务器内部错误500(或者其它目前我未知的情况)时,wx.request还是会执行success回调,所以这里还增加一层服务器返回的状态码的判断
            if (res.statusCode === 200 && res.data.code === 1) {
              //获取到自定义登录态信息后存入缓存,由于我们无需在意缓存是否成功(前面代码有相应的处理逻辑),所以这里设置缓存可以由它异步执行即可
              wxapi('setStorage', {
                'key': loginKey,
                'data': res.data.data.userinfo
              });
              //userinfo里面包含有用户昵称、头像、性别等信息,以及自定义登录态的token
              resolve(res.data.data.userinfo);
            } else {
              return Promise.reject({
                'errMsg': (res.data.msg ? 'ServerApi error:' + res.data.msg : 'Fail to network request!') + ' Please feedback to manager and close the miniprogram manually.'
              });
            }
          }).catch(function(error) {
            reject(error);
          });
        });
    },
    
  3. OK,接着我们先继续看下去。在 app.js 中再定义一个方法 getLoginInfo(),主要做以下几件事情:

    • 调用 wx.checkSession() 验证当前的登录是否有效;
    • 若无效,则调用上一步的 exeLogin() ,执行登录并缓存登录态信息;
    • 若有效,则调用 wx.getStorage() 读取缓存;
    • 当然,调用读取缓存时我们还要判断是否成功,若是失败或读取到信息与预期的不符,也直接执行 exeLogin()

    由于同样是在 app.js 中,开头的导入 wxapi.js 这一段省略了,能理解文中代码里出现的 wxapi() 的含义即可,之后若没有特殊说明, wxapi() 的含义都一样。

    // app.js
    
    /**
    * [getLoginInfo 获得自定义登录态信息]
    * @param  {[string]]} loginKey [缓存的key值]
    * @return {[Promise]}          返回一个Promise对象
    */
    getLoginInfo: function(loginKey = 'loginInfo') {
        var _this = this;
        return new Promise((resolve, reject) => {
          wxapi('checkSession').then(function() {
            //登录态有效,从缓存中读取
            return wxapi('getStorage', {
              'key': loginKey
            }).then(function(res) {
              //获取loginKey缓存成功
              if (res.data) {
                //缓存获取成功,并且值有效
                return Promise.resolve(res.data);
              } else {
                //缓存获取成功,但值无效,重新登录
                return _this.exeLogin(loginKey, 3000);
              }
            }, function() {
              //获取loginKey缓存失败,重新登录
              return _this.exeLogin(loginKey, 3000);
            });
          }, function() {
            //登录态失效,重新调用登录
            return _this.exeLogin(loginKey, 3000);
          }).then(function(res) {
            resolve(res);
          }).catch(function(error) {
            reject(error);
          });
        });
    },
    
  4. 前面的这些,都是为接下来在 app.onLaunch() 中做准备。说说小程序注册时 onlaunch() 主要做些什么吧:

    • 调用上一步 getLoginInfo(),然后只需要对 resolve()reject() 做对应的逻辑即可;
    • resolve() 里面再调用 wx.getSetting() 获取到相关的授权列表,与登录态信息一并赋值给 app.gData

    代码中有这么一段 (_this.loginedCb && typeof(_this.loginedCb) === 'function') && _this.loginedCb();
    若无法理解可以先忽略,后面会重点说这就是为了解决文章开头 常见问题 第二个问题的解决方案。

    // app.js
    
    onLaunch: function() {
        var _this = this;
        
        // 获取登录态信息
        this.getLoginInfo().then(function(res) {
          if ((typeof res !== 'undefined') && res.token) {
            //获取用户全部的授权信息
            wxapi('getSetting').then(function(setting) {
              _this.gData.logined = true;
              _this.gData.userinfo = res;
              _this.gData.authsetting = setting.authSetting;
        
              //执行页面定义的回调方法
              (_this.loginedCb && typeof(_this.loginedCb) === 'function') && _this.loginedCb();
            }, function(error) {
              return Promise.reject(error);
            });
          } else {
            return Promise.reject({
              errMsg: 'LoginInfo miss token!',
            });
          }
        }).catch(function(error) {
          wx.showModal({
            title: 'Error',
            content: error.errMsg,
          });
          return false;
        });
    },
    
  5. 到此,小程序一进来开始的登录流程基本完成,在开始涉及页面 Page 相关的逻辑之前,我们先针对上述的代码提一个问题:

    上述代码中自始至终都没有提到 请求用户允许授权、获取用户昵称、头像等基本信息这一点;你甚至会发现,没有用户基本数据,在 exeLogin() 中我们登录请求后台接口只有一个 code 有效参数的情况下,后台怎么完用户注册的逻辑呢?

    说说我对这个问题的理解,也是本文创作的动力。

    首先明确一点,在逻辑设计上的确就只要 wx.login() 返回的 code 传给后台, 就能完成后台注册新用户、返回后台自定义登录态信息等等。

    实现上也不难,只需要后台API通过前端传过来的 code以及小程序 appid && secret 开发者管理的重要秘钥,即可在后台调用微信小程序服务端接口 code2Session,接口返回的 openid、unionid、session_key 就足够后台解决 用户唯一性 的问题了。

    至于用户注册需要的基本数据,先又系统随机生成。而真正要关心的是,设计一套自己的后台API token机制,机制里关联上当前注册的用户返回自定义登录态信息给前端。下一次请求业务接口时参数中带上自定义登录态信息即可验证登录,这样就已经完成了小程序登录流程。

    至于用户信息完善,则就涉及到小程序授权。我们只需要在某些需要用户授权的 Page 页面里,检验用户是否授权,若没有授权,统一跳转到 /pages/auth/auth 页面完成授权并请求后台更新用户信息的接口,而这个接口的前提是验证用户登录。

  6. 带上对上面答案的认知,我们开始说小程序页面 Page 方面的逻辑。分以下几点:

    • 由于在 App.onLaunch() 中,用户登录态信息是异步的方式请求后台接口的,接口返回登录态信息并赋值给全局变量 app.gData 前,很大可能小程序页面已经执行完了 onLoad() 方法,这样直接对我们页面里后面的写逻辑造成了致命的错误(页面中获取到的登录态信息是错误的)。

    • 还有就是用户授权方面的问题。对于那些业务逻辑要求必须有用户基本信息的页面,我们得在页面初始化时验证用户授权状态(在登录的时候我们为这一步做过准备),若未曾询问过或者用户拒绝授权,我们同意跳转到 /pages/auth/auth 页面进行用户授权步骤,同意后返回上一页并做相应的更新。

    我们很容易会想到,上面的这两点都是多页面中调用到的,必然会考虑到灵活封装好,之后每个页面调用即可。

    app.js 中先预定义全局控制字段,包括登录控制字段 logined, 授权列表 authsetting,以及用户信息(包含token,就是登录态信息):

    // app.js
    
    'gData': {
        'logined': false, //用户是否登录
        'authsetting': null, //用户授权结果
        'userinfo': null, //用户信息(包含自定义登录态token)
    },
    

    针对上面的第一点,我们在 app.js 下封装 pageGetLoginInfo() 方法,该方法主要做的事情有一下几点:

    • 判断登录控制字段 app.gData.logined,若已经登录——控制字段值为 true,直接把全局控制字段赋值给页面的控制字段;

    • 若全局登录状态控制字段值为 false,则我们完全可认为是由于异步请求后台的原因导致的全局登录控制字段未赋值(因为上文提到登录失败都可以认为是系统的一个Bug)。所以若为 false, 则在 app 对象中定义一个新函数 loginedCb(),供 app.onLaunch() 中异步获取到登录态信息后回调(在本文第四点有特意提过)。而 loginedCb() 方法要做的也是把全局控制字段赋值给页面的控制字段;

    代码中出现的 wxsetData() 方法是在 /utils/wxapi.js 定义的,这里我们导入进来

    // app.js
    
    import {
      wxsetData
    } from './utils/wxapi.js';
    
    /**
    * 获取小程序注册时返回的自定义登录态信息(小程序页面中调用)
    * 主要是解决pageObj.onLoad 之后app.onLaunch()才返回数据的问题
    */
    pageGetLoginInfo: function(pageObj) {
        var _this = this;
        return new Promise((resolve, reject) => {
          // console.log(_this.gData.logined);
          if (_this.gData.logined == true) {
            wxsetData(pageObj, {
              'logined': _this.gData.logined,
              'authsetting': _this.gData.authsetting,
              'userinfo': _this.gData.userinfo
            }).then(function(data) {
              //执行pageObj.onShow的回调方法
              (pageObj.authorizedCb && typeof(pageObj.authorizedCb) === 'function') && pageObj.authorizedCb(data);
              resolve(data);
            });
        
          } else {
            /**
             * 小程序注册时,登录并发起网络请求,请求可能会在 pageObj.onLoad 之后才返回数据
             * 这里加入loginedCb回调函数来预防,回调方法会在接收到请求后台返回的数据后执行,详看app.onLaunch()
             */
            _this.loginedCb = () => {
              wxsetData(pageObj, {
                'logined': _this.gData.logined,
                'authsetting': _this.gData.authsetting,
                'userinfo': _this.gData.userinfo
              }).then(function(data) {
                //执行pageObj.onShow的回调方法
                (pageObj.authorizedCb && typeof(pageObj.authorizedCb) === 'function') && pageObj.authorizedCb(data);
                resolve(data);
              });
            }
          }
        });
    },
    

    然后我们再封装一个 pageOnLoadInit() 方法,也简单说说方法的逻辑:

    • 调用上一步 pageGetLoginInfo() 方法,保证页面拿到有效准确的登录态信息;
    • 验证登录,同时通过参数来决定当前页面初始化时是否需要校验用户授权;
    • 若用户没有授权,则从当前页面跳转到 /pages/auth/auth 页面,auth 页面就是一个授权按钮,用户点击后弹窗提示用户确认授权(小程序官方已修改只能通过点击按钮弹窗用户授权);

    代码中若涉及到授权方面的我们放在后面讨论:

    // app.js
    
    /**
    * 封装小程序页面的公共方法
    * 在小程序页面onLoad里调用
    * @param {Object}  pageObj   小程序页面对象Page
    * @param {Boolean} needAuth  是否检验用户授权(scope.userInfo)
    * @return {Object}           返回Promise对象,resolve方法执行验证登录成功后且不检验授权(特指scope.userInfo)的回调函数,reject方法是验证登录失败后的回调
    */
    pageOnLoadInit: function(pageObj, needAuth = false) {
        var _this = this;
        return new Promise((resolve, reject) => {
          _this.pageGetLoginInfo(pageObj).then(function(res) {
            // console.log(_this.gData.logined);
            if (res.logined === true) {
              //登录成功、无需授权
              resolve(res);
        
              if (needAuth) {
                if (res.authsetting['scope.userInfo'] === false || typeof res.authsetting['scope.userInfo'] === 'undefined') {
                  common.navigateTo('/pages/auth/auth');
                }
              }
        
            } else {
              reject({
                'errMsg': 'Fail to login.Please feedback to manager.'
              });
            }
          });
        });
    },
    

    现在问题基本解决了,剩下的就是在每个小程序页面中调用,只校验登录的逻辑在 Page.onLoad() 里面执行,下面以代码写在小程序页面 /pages/mine/index/index.js 中为例:

    // /pages/mine/index/index.js
    
    const app = getApp();
    
    /**
    * 生命周期函数--监听页面加载
    */
    onLoad: function(options) {
        var _this = this;
        
        app.pageOnLoadInit(this).then(function(res) {
          //这里写验证登录成功后且无需验证授权 需要执行的逻辑
          //若还需验证授权成功才执行的逻辑需写在onShow方法里面,并且这里pageOnLoadInit()第二个参数要为 true
        
        }, function(error) {
          //登录失败
          wx.showModal({
            title: 'Error',
            content: error.errMsg ? error.errMsg : 'Fail to login.Please feedback to manager.',
          })
          return false;
        });
    },
    

    到此,针对第一点——页面登录已经完成。


    针对第二点用户授权的。先看看在 app.js 中封装的 exeAuth() 方法,该方法就是统一授权与后台接口的交互

      /**
       * [exeAuth 执行用户授权流程]
       * @param  {[string]} loginKey  自定义登录态信息缓存的key
       * @param  {[Object]} data      wx.getUserInfo接口返回的数据结构一致
       * @return {[Promise]}          返回一个Promise对象
       */
      exeAuth: function(loginKey, data) {
        var _this = this;
    
        return new Promise((resolve, reject) => {
          wxapi('request', {
            'method': 'POST',
            'url': _this.gData.api.request + '/api/User/thirdauth',
            'header': {
              'Content-type': 'application/x-www-form-urlencoded',
            },
            'data': {
              'platform': 'miniwechat',
              'token': _this.gData.userinfo.token,
              'encryptedData': data.encryptedData,
              'iv': data.iv,
            }
          }).then(function(res) {
            //当服务器内部错误500(或者其它目前我未知的情况)时,wx.request还是会执行success回调,所以这里还增加一层服务器返回的状态码的判断
            if (res.statusCode === 200 && res.data.code === 1) {
              //更新app.gData中的数据
              _this.gData.authsetting['scope.userInfo'] = true;
              _this.gData.userinfo = res.data.data.userinfo;
    
              //更新自定义登录态的缓存数据,防止再次进入小程序时读取到旧的缓存数据,这里让它异步执行即可,
              //倘若异步执行的结果失败,直接清除自定义登录态缓存,再次进入小程序时系统会自动重新登录生成新的
              wxapi('setStorage', {
                'key': loginKey,
                'data': res.data.data.userinfo
              }).catch(function(error) {
                console.warn(error.errMsg);
                wxapi('removeStorage', {
                  'key': loginKey
                });
              });
    
              resolve(res.data.data.userinfo);
            } else {
              return Promise.reject({
                'errMsg': res.data.msg ? 'ServerApi error:' + res.data.msg : 'Fail to network request!'
              });
            }
          }).catch(function(error) {
            reject(error);
          });
        });
      },
    

    要调用上述授权方法的地方必不可少的就是 /pages/auth/auth 统一授权页面了,对于其它可能用到的地方我们之后也可直接调用,我们来看看 /pages/auth/authbindGetUserinfo() type = getUserInfo 按钮的回调函数:

    /**
    * getUserinfo回调函数
    */
    bindGetUserinfo: function(e) {
        var data = e.detail;
        if (data.errMsg === "getUserInfo:ok") {
          app.exeAuth('loginInfo', data).then(function(res) {
            var pages = getCurrentPages();
            var prevPage = pages[pages.length - 2]; //上一个页面
        
            prevPage.setData({
              'userinfo': res,
              'authsetting.scope\\.userInfo': true  这里请注意反斜杠转义,'scope.userInfo'被看做一个完整的键名
            }, function() {
              wx.navigateBack({
                delta: 1
              });
            });
        
          }).catch(function(error) {
            console.error(error);
            wx.showModal({
              title: 'Error',
              content: error.errMsg,
            })
          });
        } else {
          wx.showModal({
            title: 'Warning',
            content: 'Please permit to authorize.',
            showCancel: false
          })
          return false;
        }
    },
    

    最后像上面登录一样,在 app.js 里封装一个 pageOnShowInit() 供需要授权的页面调用:

    /**
    * 封装小程序页面的公共方法
    * 在小程序页面onShow里调用
    * @param {Object}  pageObj   小程序页面对象Page
    * @return {Object}           返回Promise对象,resolve方法执行验证授权(特指scope.userInfo)成功后的回调函数,reject方法是验证授权失败后的回调
    */
    pageOnShowInit: function(pageObj) {
        var _this = this;
        return new Promise((resolve, reject) => {
          /**
           * 这里通过pageObj.data.authsetting == (null || undefined)
           * 来区分pageObj.onLoad方法中是否已经执行完成设置页面授权列表(pageObj.data.authsetting)的方法,
           * 
           * 因为如果已经执行完成设置页面授权列表(pageObj.data.authsetting)的方法,并且获取到的授权列表为空的话,会把pageObj.data.authsetting赋值为
           * 空对象 pageObj.data.authsetting = {} ,所以pageObj.data.authsetting倘若要初始化时,请务必初始化为 null ,不能初始化为 {},切记!
           */
          if (pageObj.data.authsetting === null || typeof pageObj.data.authsetting === 'undefined') {
            /**
             * pageObj.onLoad是异步获取用户授权信息的,很大可能会在 pageObj.onShow 之后才返回数据
             * 这里加入authorizedCb回调函数预防,回调方法会在pageObj.onLoad拿到用户授权状态列表后调用,详看app.pageOnLoadInit()
             */
            pageObj.authorizedCb = (res) => {
              if (res.authsetting['scope.userInfo'] === true) {
                //授权成功执行resolve
                resolve();
              } else {
                reject();
              }
            }
          } else {
            if (res.authsetting['scope.userInfo'] === true) {
              //授权成功执行resolve
              resolve();
            } else {
              reject();
            }
          }
        });
    },
    

    由于授权多数情况下是从授权页面跳转回来的,所以这个方法设计在小程序页面的 Page.onShow() 中调用,具体调用这里不贴代码了,类似校验登录一样。

PHP后端

由于文章篇幅原因,文中涉及的登录和授权两个后台接口这里不贴源码,有兴趣了解可以到文章开头的项目地址,项目的安装文档也有具体说明。若有疑问,可以联系本人。

结语

文章可能缺乏一定的条理性,望谅解~

纯属原创,转载请注明出处,谢谢~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • "use strict";function _classCallCheck(e,t){if(!(e instanc...
    久些阅读 2,027评论 0 2
  • 在ES6当中添加了很多新的API其中很值得一提的当然少不了Promise,因为Promise的出现,很轻松的就给开...
    嘿_那个谁阅读 3,662评论 2 3
  • # Ajax标签(空格分隔): 笔记整理---[TOC]### 从输入网址开始:- 在学习ajax之前,你应该先了...
    V8阅读 250评论 1 0
  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,888评论 1 4
  • /** * 拦截器(请求之前调用) * @fn: 请求函数 */ function wxPromisify(fn)...
    孤单的小草阅读 783评论 0 0