小程序開發另類小技巧 --用戶受權篇

getUserInfo較爲特殊,不包含在本文範圍內,主要針對須要受權的功能性api,例如:wx.startRecordwx.saveImageToPhotosAlbum, wx.getLocationjavascript

倉庫地址:github.com/jinxuanzhen…html

背景

小程序內若是要調用部分接口須要用戶進行受權,例如獲取地理位置信息,收穫地址,錄音等等,可是小程序對於這些須要受權的接口並非特別友好,最明顯的有兩點:java

  • 若是用戶已拒絕受權,則不會出現彈窗,而是直接進入接口 fail 回調
  • 沒有統一的錯誤信息提示,例如錯誤碼

通常狀況而言,每次受權時都應該激活彈窗進行提示,是否進行受權,例如:git

image.png

而小程序內只有第一次進行受權時纔會主動激活彈窗(微信提供的),其餘狀況下都會直接走fail回調,微信文檔也在句末添加了一句請開發者兼容用戶拒絕受權的場景

這種未作兼容的狀況下若是用戶想要使用錄音功能,第一次點擊拒絕受權,那麼以後不管如何也
沒法再次開啓錄音權限**,很明顯不符合咱們的預期。github

因此咱們須要一個能夠進行二次受權的解決方案web

常見處理方法

官方demo

下面這段代碼是微信官方提供的受權代碼, 能夠看到也並無兼容拒絕過受權的場景查詢是否受權(即沒法再次調起受權)小程序

// 能夠經過 wx.getSetting 先查詢一下用戶是否受權了 "scope.record" 這個 scope
wx.getSetting({
  success(res) {
    if (!res.authSetting['scope.record']) {
      wx.authorize({
        scope: 'scope.record',
        success () {
          // 用戶已經贊成小程序使用錄音功能,後續調用 wx.startRecord 接口不會彈窗詢問
          wx.startRecord()
        }
      })
    }
  }
})
複製代碼

通常處理方式

那麼正常狀況下咱們該怎麼作呢?以地理位置信息受權爲例:api

wx.getLocation({
   success(res) { 
      console.log('success', res);
   },
   fail(err) {
      // 檢查是不是由於未受權引發的錯誤
      wx.getSetting({
         success (res) {               
            // 當未受權時直接調用modal窗進行提示
            !res.authSetting['scope.userLocation'] && wx.showModal({
               content: '您暫未開啓權限,是否開啓',
               confirmColor: '#72bd4a',
               success: res => {              
                  // 用戶確認受權後,進入設置列表
                  if (res.confirm) {
                     wx.openSetting({
                        success(res){
                           // 查看設置結果
                           console.log(!!res.authSetting['scope.userLocation'] ? '設置成功' : '設置失敗');
                        },
                     });
                  }
               }
            });
         }
      });
   }
});
複製代碼

上面代碼,有些同窗可能會對在fail回調裏直接使用wx.getSetting有些疑問,這裏主要是由於緩存

  • 微信返回的錯誤信息沒有一個統一code
  • errMsg又在不一樣平臺有不一樣的表現
  • 從埋點數據得出結論,調用這些api接口出錯率基本集中在未受權的狀態下

這裏爲了方便就直接調用權限檢查了 ,也能夠稍微封裝一下,方便擴展和複用,變成:微信

bindGetLocation(e) {
        let that = this;
        wx.getLocation({
            success(res) {
                console.log('success', res);
            },
            fail(err) {
                that.__authorization('scope.userLocation');
            }
        });
    },
    bindGetAddress(e) {
        let that = this;
        wx.chooseAddress({
            success(res) {
                console.log('success', res);
            },
            fail(err) {
                that.__authorization('scope.address');
            }
        });
    },
    __authorization(scope) {
		  	/** 爲了節省行數,不細寫了,能夠參考上面的fail回調,大體替換了下變量res.authSetting[scope] **/ 
    }
複製代碼

看上去好像沒有什麼問題,fail裏只引入了一行代碼,

這裏若是隻針對較少頁面的話我認爲已經夠用了,畢竟**‘如非必要,勿增實體’,可是對於小打卡這個小程序來講可能涉及到的頁面,須要調用的場景偏多**,我並不但願每次都人工去調用這些方法,畢竟人總會犯錯

梳理目標

上文已經提到了背景和常見的處理方法,那麼梳理一下咱們的目標,咱們究竟是爲了解決什麼問題?列了下大體爲下面三點:

  • 兼容用戶拒絕受權的場景,即提供二次受權
  • 解決多場景,多頁面調用沒有統一規範的問題
  • 在底層解決,業務層不須要關心二次受權的問題

擴展wx[funcName]方法

爲了節省認知成本和減小出錯機率,我但願他是這個api默認攜帶的功能,也就是說因未受權出現錯誤時自動調起是否開啓受權的彈窗

爲了實現這個功能,咱們可能須要對wx的原生api進行一層包裝了(關於頁面的包裝能夠看:如何基於微信原生構建應用級小程序底層架構

爲wx.getLocation添加本身的方法

這裏須要注意的一點是直接使用常見的裝飾模式是會出現報錯,由於wx這個對象在設置屬性時沒有設置set方法,這裏須要單獨處理一下

// 直接裝飾,會報錯 Cannot set property getLocation of #<Object> which has only a getter 
let $getLocation = wx.getLocation;
wx.getLocation = function (obj) {
    $getLocation(obj);	
};

// 須要作一些小處理
wx = {...wx};										// 對wx對象從新賦值
let $getLocation = wx.getLocation;
wx.getLocation = function (obj) {					
    console.log('調用了wx.getLocation');
    $getLocation(obj);	
};

// 再次調用時會在控制檯打印出 '調用了wx.getLocation' 字樣
wx.getLocation()
複製代碼

劫持fail方法

第一步咱們已經控制了wx.getLocation這個api,接下來就是對於fail方法的劫持,由於咱們須要在fail里加入咱們本身的受權邏輯

// 方法劫持
wx.getLocation = function (obj) {
    let originFail = obj.fail;

    obj.fail = async function (errMsg) {
        // 0 => 已受權 1 => 拒絕受權 2 => 受權成功
        let authState = await authorization('scope.userLocation');
        
        // 已受權報錯說明並非權限問題引發,因此繼續拋出錯誤
        // 拒絕受權,走已有邏輯,繼續排除錯誤
        authState !== 2 && originFail(errMsg);
    };
    $getLocation(obj);
};

// 定義檢查受權方法
function authorization(scope) {
    return new Promise((resolve, reject) => {
        wx.getSetting({
            success (res) {
                !res.authSetting[scope]
                    ? wx.showModal({
                        content: '您暫未開啓權限,是否開啓',
                        confirmColor: '#72bd4a',
                        success: res => {
                            if (res.confirm) {
                                wx.openSetting({
                                    success(res){
                                        !!res.authSetting[scope] ? resolve(2) : resolve(1)
                                    },
                                });
                            }else {
                                resolve(1);
                            }
                        }
                    })
                    : resolve(0);
            }
        })
    });
}

// 業務代碼中的調用
  bindGetLocation(e) {
        let that = this;
        wx.getLocation({
            type: 'wgs84',
            success(res) {
                console.log('success', res);
            },
            fail(err) {
                console.warn('fail', err);
            }
        });
  }

複製代碼

能夠看到如今已實現的功能已經達到了咱們最開始的預期,即因受權報錯做爲了wx.getLocation默認攜帶的功能,咱們在業務代碼裏不再須要處理任何再次受權的邏輯

也意味着wx.getLocation這個api不論在任何頁面,組件,出現頻次如何,**咱們都不須要關心它的受權邏輯(**效果原本想貼gif圖的,後面發現有圖點大,具體效果去git倉庫跑一下demo吧)

讓咱們再優化一波

上面所述大體是整個原理的一個思路,可是應用到實際項目中還須要考慮到總體的擴展性和維護成本,那麼就讓咱們再來優化一波

代碼包結構:
本質上只要在app.js這個啓動文件內,引用./x-wxx/index文件對原有的wx對象進行覆蓋便可

image.png

**簡單的代碼邏輯: **

// 大體流程:

//app.js
wx = require('./x-wxx/index');						// 入口處引入文件

// x-wxx/index 
const apiExtend = require('./lib/api-extend');
module.exports = (function (wxx) {				    // 對原有方法進行擴展
    wxx = {...wxx};
    for (let key in wxx) {
        !!apiExtend[key] && (()=> {

            // 緩存原有函數
            let originFunc = wxx[key];

            // 裝飾擴展的函數
            wxx[key] = (...args) => apiExtend[key](...args, originFunc);
        })();
    }
    return wxx;
})(wx);

// lib/api-extend
const Func = require('./Func');
(function (exports) {								// 須要擴展的api(相似於config)
    // 獲取權限
    exports.authorize = function (opts, done) {
        // 當調用爲"確認受權方法時"直接執行,避免死循環
        if (opts.$callee === 'isCheckAuthApiSetting') {
            console.log('optsopts', opts);
            done(opts);
            return;
        }
        Func.isCheckAuthApiSetting(opts.scope, () => done(opts));
    };

    // 選擇地址
    exports.chooseAddress = function (opts, done) {
        Func.isCheckAuthApiSetting('scope.address', () => done(opts));
    };

    // 獲取位置信息
    exports.getLocation = function (opts, done) {
        Func.isCheckAuthApiSetting('scope.userLocation', () => done(opts));
    };

    // 保存到相冊
    exports.saveImageToPhotosAlbum = function (opts, done) {
        Func.isCheckAuthApiSetting('scope.writePhotosAlbum', () => done(opts));
    }

    // ...more
})(module.exports);
複製代碼

更多的玩法

能夠看到咱們不管後續擴展任何的微信api,都只須要在lib/api-extend.js 配置便可,這裏不只僅侷限於受權,也能夠作一些日誌,傳參的調整,例如:

// 讀取本地緩存(同步)
exports.getStorageSync = (key, done) => {
        let storage = null;
        try {
            storage = done(key);
        } catch (e) {
            wx.$logger.error('getStorageSync', {msg: e.type});
        }
        return storage;
};
複製代碼

這樣是否是很方便呢,至於Func.isCheckAuthApiSetting這個方法具體實現,爲了節省文章行數請自行去git倉庫裏查看

關於音頻受權

錄音受權略爲特殊,以wx.getRecorderManager爲例,它並不能直接調起錄音受權,因此並不能直接用上述的這種方法,不過咱們能夠曲線救國,達到相似的效果,還記得咱們對於wx.authorize的包裝麼,本質上咱們是能夠直接使用它來進行受權的,好比將它用在咱們已經封裝好的錄音管理器的start方法進行校驗

wx.authorize({
   scope: 'scope.record'
});
複製代碼

實際上,爲方便統一管理,Func.isCheckAuthApiSetting方法其實都是使用wx.authorize來實現受權的

exports.isCheckAuthApiSetting = async function(type, cb) {

        // 簡單的類型校驗
        if(!type && typeof type !== 'string') return;

        // 聲明
        let err, result;

        // 獲取本地配置項
        [err, result] = await to(getSetting());         // 這裏能夠作一層緩存,檢查緩存的狀態,若是已受權能夠沒必要再次走下面的流程,直接return出去便可
        if (err) {
            return cb('fail');													
        }

        // 當受權成功時,直接執行
        if (result.authSetting[type]) {
            return cb('success');
        }

        // 調用獲取權限
        [err, result] = await to(authorize({scope: type, $callee: 'isCheckAuthApiSetting'}));
        if (!err) {
            return cb('success');
        }
}
複製代碼

關於用戶受權

用戶受權極爲特殊,由於微信將wx.getUserInfo升級了一版,沒有辦法直接喚起了,詳見《公告》
因此須要單獨處理,關於這裏會拆出單獨的一篇文章來寫一些有趣的玩法

總結

最後稍微總結下,經過上述的方案,咱們解決了最開始目標的同時,也爲wx這個對象上的方法提供了統一的裝飾接口(lib/api-extend文件),便於後續其餘行爲的操做好比埋點,日誌,參數校驗

仍是那麼一句話吧,小程序無論和web開發有多少不一樣,本質上都是在js
境上進行開發的,
但願小程序的社區環境更加活躍,帶來更多有趣的東西

相關文章
相關標籤/搜索