小程式的填坑小技巧之網路請求改造

dduke發表於2018-05-02

前言

小程式在內測的時候就已經開始玩了,不過最開始的時候覺得,這sx東西東西怎麼這麼坑的樣子,網路請求居然不是返回Promise而是用Callback的方式, 傳值居然不能把值寫在方法裡只能用dataset,在這個全面元件化的大環境下居然不支援元件化...

其實最開始主要是書寫時習慣的問題,秉承著我又不做小程式開發,就先忍著你的態度放任不管了。然而天有不測風雲,最近因為業務的需求不得不做小程式相關的開發,我就倔脾氣果斷就不能忍了。果斷的把不爽的地方改成能按我喜歡的方式來走,其中還遇到了一些其他的坑,一個個慢慢填,並把這些記錄下來整理成了文章。

網路請求

網路請求小程式提供了wx.request,這個是我最想吐槽的點, 仔細看一下 api,這貨不就是n年前的$.ajax嗎,好古老啊。

// 官方例子
wx.request({
  url: 'test.php', //僅為示例,並非真實的介面地址
  data: {
     x: '' ,
     y: ''
  },
  header: {
      'content-type': 'application/json' // 預設值
  },
  success: function(res) {
    console.log(res.data)
  }
})
複製程式碼

現在還是隻有一個請求就已經感覺寫的很長了,如果一個頁面需要多個請求呢,如果請求的順序還有要求呢該怎麼辦,各種巢狀又臭又長,如果要求所以請求都完成之後再顯示介面呢 瞬間懵逼。

這個時候我弱弱的看了一眼小程式的JS版本的支援,歐耶,比較良心的支援ES6。也就是說我們可以將其改成Promise是可能的。接下來看我如何改造。

/* utils/api.js  自定義網路請求 */
const baseURL = 'https://yourapi.com' // 自己後臺API地址
const http = ({ url = '', params = {}, ...other} = {}) => {
  wx.showLoading({
    title: '載入中...'
  })
  let time = Date.now()
  console.log(`開始:${time}`)
  return new Promise((resolve, reject) => {
    wx.request({
      url: getUrl(url),
      data: params,
      header: getHeader(),
      ...other,
      complete: (res) => {
        wx.hideLoading()
        console.log(`耗時:${Date.now() - time}`)
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data)
        } else {
          reject(res)
        }
      }
    })
  })
}
const getUrl = url => {
  if (url.indexOf('://') == -1) {
    url = baseURL + url
  }
  return url
}
const getHeader = () => {
  try {
    var token = wx.getStorageSync('token')
    if (token) {
      return { 'token': token }
    }
    return {}
  } catch (e) {
    return {}
  }
}
module.exports = {
  baseURL,
  get (url, params = {}) {
    return http({
      url,
      params
    })
  },
  post(url, params = {}) {
    return http({
      url,
      params,
      method: 'post'
    })
  },
  put(url, params = {}) {
    return http({
      url,
      params,
      method: 'put'
    })
  },
  // 這裡不能使用 delete, delete為關鍵欄位
  myDelete(url, params = {}) {
    return http({
      url,
      params,
      method: 'delete'
    })
  }
}
複製程式碼

接了下來我們就可以正常的時候用了,寫一下簡單的例子吧

const api = require('../../utils/api.js')

// 單個請求
api.get('list').then(res => {
  console.log(res)
}).catch(e => {
  console.log(e)
})

// 一個頁面多個請求
Promise.all([
  api.get('list'),
  api.get(`detail/${id}`)
]).then(result => {
  console.log(result)
}).catch(e => {
  console.log(e)
})
複製程式碼

如果習慣了 fetch 以及 axios 的朋友應該都會比較喜歡這種寫法。 在做網路請求的時候還遇到一個問題,登入授權的問題。

登入問題

做一個應用,肯定避免不了登入操作。使用者的個人資訊啊,相關的收藏列表等功能都需要使用者登入之後才能操作。一般我們使用token做標識。然後又會涉及到token不存在,使用者第一次登入,token過期後,重新登入等問題。比較常規的操作是直接跳轉到登入頁面。

然後坑就出現了,小程式並沒有登入介面,使用的是 wx.loginwx.login 會獲取到一個 code,拿著該 code 去請求我們的後臺會最後返回一個token到小程式這邊,儲存這個值為 token 每次請求的時候帶上這個值。(詳情可以檢視小程式登入。)

然而僅僅這樣就夠了嗎? 很顯然並不是。一般還需要把使用者的資訊帶上比如使用者微信暱稱,微信頭像等,這時候就需要使用 wx.getUserInfo,這裡涉及到一個使用者授權的問題,留一個坑接下來再解決。帶上使用者資訊就夠了嘛? too young too simple!我們的專案不可能只有小程式,相應的微信公眾平臺可能還有相應的App,我們需要把賬號系統打通,讓使用者在我們的專案中的賬戶是同一個。這就需要用到微信開放平臺提供的 UnionID

ps.基於小程式在微信中的易傳播性, 為了鼓勵使用者去傳播分享一般還會提供邀請獎勵機制。但是微信這邊又會對誘導分享進行和諧處理。視情況慎用。(本文會在例子上加上該功能)

看到這,是不是覺得頭都大了,就一個小小的登入功能坑這麼多。 年輕的我瑟瑟發抖~~~。慢慢開始填吧。先上登入程式碼

/* utils/api.js  自定義網路請求 */
...
function login() {
  return new Promise((resolve, reject) => {
    // 先呼叫 wx.login 獲取到 code
    wx.login({
      success: res => {
        // 再呼叫 wx.getUserInfo 獲取到使用者的一些資訊 (一些基本資訊,以及生成UnionID 所用到的資訊 比如 rawData, signature, encryptedData, iv)
        wx.getUserInfo({
          // 若獲取不到使用者資訊 (最大可能是使用者授權不允許,也有可能是網路請求失敗,但該情況很少)
          fail: (e) => {
            reject(e)
          },
          success: ({ rawData, signature, encryptedData, iv }) => {
            let param = {
              code: res.code,
              rawData,
              signature,
              encryptedData,
              iv
            }
            // 若有邀請ID
            try {
              let invite = wx.getStorageSync('invite')
              if (invite) {
                param.invite = invite
              }
            } catch (e) {
            }
            // 登入操作
            http({
              url: 'login',
              params: param,
              method: 'post'
            }).then(res => {
              // 該為我們後端的邏輯 若code > 0為登入成功,其他情況皆為異常 (視自身情況而定)
              if (res.code > 0) {
                // 儲存使用者資訊
                 wx.setStorage({
                  key: 'userinfo',
                  data: res.data
                })
                wx.setStorage({
                  key: "token",
                  data: res.message,
                  success: () => {
                    resolve(res)
                  }
                })
              } else {
                reject(res)
              }
            }).catch(error => reject(error))
          }
        })
      }
    })
  })
}
...
複製程式碼

授權問題

根據上面的程式碼,可以很清楚的看到,若使用者在登入的時候不允許小程式獲取他的使用者資訊之後才能繼續。若使用者在這個時候點拒絕了呢, 會怎麼樣? 一片空白!~~ What’s the fuck! 怎麼什麼都沒有!垃圾破小程式~~ 冷靜點的使用者也會百臉懵逼狀。我是誰?我該怎麼辦? 也許你會覺得,使用者點允許就好了啊,怎麼會這麼笨,這種使用者肯定不會多之類的話。我在我們小程式中加了統計大約有 20%的使用者點了拒絕, 如果後續我們沒有做任何引導的話,這 20% 的使用者就會永遠失去。這個後果我們完全不能接受。

經過我們的小組研究與討論,給出了一下的一套方案。

小程式的填坑小技巧之網路請求改造

具體程式碼可以如下表示,用到了 wx.openSetting 來跳轉到設定授權介面。

/* index.js */
// 若有使用者資訊存在則繼續
Page({
  onLoad () {
    wx.getStorage({
      key: 'userinfo',
      success: (res) => {
        this.setUserinfo(res)
      },
      fail: (res) => {
        api.login().then((res) => {
          this.setUserinfo(res)
        }).catch(e => {
          if (e.errMsg && e.errMsg === 'getUserInfo:fail auth deny') {
            this.setData({
              isauth: false
            })
          }
        })
      }
    })
  },
  toSetting() {
    wx.openSetting({
      success: (res) => {
        this.setData({
          isauth: res.authSetting['scope.userInfo']
        })
        if (res.authSetting['scope.userInfo']) {
          api.login().then((res) => {
            this.setUserinfo(res)
          })
        }
      }
    })
  }
})
// setUserinfo 就是對使用者資訊做一下處理 不具體展開了

/* index.wxml */
<view class="unauth" wx:if="{{!isauth}}">
   <image class="unauth-img" src="../../images/auth.png"></image> 
  <text class="unauth-text">檢查到您沒開啟授權</text>
  <button class="color-button unauth-button" bindtap="toSetting">去設定</button>
</view>
<view class="container" wx:else>
...
</view>
複製程式碼

token 失效問題

登入獲取到的 token 是有時效的,失效過了會怎麼樣 ? 如果後臺小夥伴嚴格按照 REST API 規範設計介面 API 的話,他會給我們返回一個錯了 http code 為 401。(常見的Http Code以及相關程式碼的意義本文就不做展開了,不瞭解的小夥伴可以自行 google 百度一下。)401 之後我們就需要對該Code進行相應的處理。可以如下這麼寫

api.get('list').then(res => {
  /* do something */
}).catch(e => {
  if (res.statusCode === 401) {
    api.login().then(() => {
      api.get('list').then(res => {
        /* do something */
      })
    })
  }
})
複製程式碼

看起來沒什麼問題,也完成需求了。但是會發現這有很大的問題。

  1. 每個請求都需要加 401 的判斷,專案大起來 這塊的程式碼量是非常恐怖的
  2. 介面返回的之後的處理 /* do something */ 也是重複的 (當然把這整塊內容都提取出來,這裡就呼叫也行。不過還是想把呼叫這邊也省略掉 ^-^ )

屢一下我們要實現的目標。

  1. 需要在每個請求後面都加一個 401 的判斷
  2. 若未授權 則進行重新登入
  3. 重新登入之後繼續前一個請求
  4. 將該請求結果返回到第一個請求的結果裡去(實現無感知重新登入獲取資訊)

這個體現出把自己封裝一個網路請求的好處, 我們可以直接改寫 api.js 中的 http 方法裡對 error 的處理就好。上程式碼:

const http = ({ url = '', params = {}, ...other} = {}) => {
  wx.showLoading({
    title: '載入中...'
  })
  let time = Date.now()
  console.log(`開始:${time}`)
  return new Promise((resolve, reject) => {
    wx.request({
      url: getUrl(url),
      data: params,
      header: getHeader(),
      ...other,
      complete: (res) => {
        wx.hideLoading()
        console.log(`耗時:${Date.now() - time}`)
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data)
        } else if (res.statusCode === 401) {
          // 401 為鑑權失敗 很大可能是token過期
          // 重新登入 並且重複請求
          login().then(res => {
            http({ url, params, ...other }).then(res => {
              resolve(res)
            })
          })
        } else {
          reject(res)
        }
      }
    })
  })
}
複製程式碼

小結

網路請求這塊,算目前開發專案中必不可少的一塊。但是例如 小程式,vue, react, weex 等其實都有一套自己的或者自己推薦的一套API以及相應的寫法。沒一個都按照他推薦的來寫,其實挺蛋疼的,用著很不爽。把他們的API封裝一下,暴露出來統一的API, 給自己用或者尤其是給自己團隊的小夥伴用就比較方便,少了很多重複學習成本,並且因為統一的API帶來的統一的格式也是很大的一個好處。

說到小程式要弄清楚的東西不少,有些坑我還在摸索怎麼處理。比如小程式的元件化,全域性變數的使用(什麼值可以放在app.js裡),html標籤的轉換等,後續弄透了我會再出來獻醜的。

相關文章