如何封装通用的SNS开放平台第三方用户信息获取

规律是创造与发现的,记忆是规律的推导与提取

背景

服务端同学忽然找到我说做微信公众号推送总是失败,是否是前端代码的问题,我赶紧看了看代码,发现代码封装的挺好啊,如何会出问题。

已有的平台兼容了许多SNS平台的第三方用户信息获取,包括微信、QQ、微博和钉钉。由于很久之前做过服务端,而且做过SNS平台,所以对OAuth2.0这块比较了解,于是在两年前和我们服务端提出把这块的三方授权抽象成一个接口,我们只需要根据不同的type传相应的参数,服务端统一返回通用的用户信息,比如nickname、avatar、openid、unionid等等。这套接口和前端的库已经正常运行两年了,为啥现在会出问题?

仔细一看原来是公众号平台变了,是有个同学在现有的平台接入了新的公众号导致的。

由于当时做埋点方案的时候,专门为公众平台做了静默授权和手动授权,以获取相应的设备标识,所以就做了用户信息的缓存。当时想不同平台是不同的端,因此就只做了一个缓存KEY,现在用户在已使用当前公众号的前提下,又使用了另一个公众号,导致第二个公众号读取了第一个公众号的缓存,结果就GG了,思考了一个晚上,第二天赶紧来coding,思路就是为不同的公众平台设置各自的缓存。

流程图

在此总结一下整体流程,以下为流程图:

流程图.png

需求的结果

基于流程图来展示各个阶段代码(基于vue2),我们的终极需求就是在页面中只需要调用一个函数即可获取第三方授权信息:

this.GetUnionAuthUserInfo((userinfo) => {
     console.log(userinfo.nickname)
     console.log(userinfo.avatar)
     console.log(userinfo.openid)
     console.log(userinfo.unionid)
 })
复制代码

监听函数

实际上这个就是我们的监听函数,具体实现如下:

const GetUnionAuthUserInfo = function (callback) {
  // 这里读取缓存KEY
  const from = this.$route.params.from !== undefined ? +this.$route.params.from : 1
  const unionAuthCacheKey = GetUnionAuthCacheKey(from)
  if (callback && typeof callback == 'function') {
    const cacheUserInfo = window[unionAuthCacheKey] || (window.localStorage.getItem(unionAuthCacheKey) && JSON.parse(window.localStorage.getItem(unionAuthCacheKey)))
    // 如果读取到缓存 则直接返回
    if (cacheUserInfo && cacheUserInfo.openid !== '') {
      callback(cacheUserInfo)
    } else {
      try {
         // 否则设置一个全局监听器,等待异步返回
        Object.defineProperty(window, unionAuthCacheKey, {
          set: function (cacheUserInfo) {
            callback(cacheUserInfo)
          },
          configurable: true
        })
      } catch (err) {
        console.log(err)
      }
    }
  } else {
    console.log('参数需要传入回调函数')
  }
}
复制代码

以上我们看到代码中访问了vue实例this,因此这个方法会挂载到全局实例上,以方便我们在模板文件中调用

缓存key

其中,GetUnionAuthCacheKey 这个方法实际上就是为了判断来源,具体如下

// 获取三方授权的缓存KEY
const GetUnionAuthCacheKey = (from = 1) => {
  const wxmap = ['wxc1111111','wxc22222']
  const qqmap = ['qqxxxx','qq2222']
  const weibomap = ['wb4214','wb44324']
  const ddmap = ['dd432432','dd123231']
  const appId = {
    wx: wxmap[from],
    qq: qqmap[from],
    weibo: weibomap[from],
    dd: ddmap[from]
  }
  const app = GetEnv()
  let appid = appId[app] ? appId[app] : appId.wx
  return `auth_${appid}`
}


// 获取浏览器userAgent
const ua = window.navigator.userAgent.toLowerCase()
const isWx = ua.indexOf('micromessenger') != -1
const isQQ = ua.indexOf('qq') != -1
const isWeibo = ua.indexOf('weibo') != -1
const isDD = ua.indexOf('dingtalk') != -1
const app = isDD ? 'dd' : isWx ? 'wx' : isQQ ? 'qq' : isWeibo ? 'weibo' : 'web'
/**
 * @author DFine
 * @date 2020/1/9
 * @description 获取浏览器环境
 * @return 'wx' 'qq' 'weibo' 'app' 'web' 'dd'
 */
const GetEnv = () => {
  return app
}
复制代码

初始化

我们再看初始化的时机,开始也是检测缓存,如果缓存读取不到再去主动调用接口:


// 缓存微信 如果微信授权则在首页进行一次授权即可 后续页面读取缓存
function MonitorCacheWxInfo() {
  // 获取缓存key
  const unionAuthCacheKey = this.GetUnionAuthCacheKey(this.$route.params.from !== undefined ? +this.$route.params.from : 1)
  const cacheWxInfo = window.localStorage.getItem(unionAuthCacheKey) && JSON.parse(window.localStorage.getItem(unionAuthCacheKey))
  if (cacheWxInfo && cacheWxInfo.openid !== '') {
    try {
      if (Object.keys(cacheWxInfo).length === 0) {
        window.localStorage.removeItem(unionAuthCacheKey)
        MonitorCacheWxInfo.call(gloabVue, null)
      } else {
        // 触发setter
        window[unionAuthCacheKey] = {
          ...cacheWxInfo
        }
        // 这里是埋点信息处理,就不展开讲了
        MonitorSetWxInfo.call(gloabVue, cacheWxInfo)
      }
    } catch (err) {
      window.localStorage.removeItem(unionAuthCacheKey)
      MonitorCacheWxInfo.call(gloabVue, null)
    }
  }
  // 检测如果非静默授权是否缓存了unionid
  if (cacheWxInfo && (!window[unionAuthCacheKey] || window[unionAuthCacheKey].unionid === '') && !!this.$route.query.get_wx_info) {
    MonitorUnionAuth.call(gloabVue, null)
  } else if (cacheWxInfo) {
    return
  }
  MonitorUnionAuth.call(gloabVue, null)
}
复制代码

仔细看这里函数也调用了vue实例,不过这个方法只需要在main.js调用即可,因此我们只需通过call函数直接传入vue的实例,并不需要挂载到全局实例上。

服务端请求

那么上面方法如果没有读取到缓存,则走接口:


// 通过三方授权获取用户的微信信息
function MonitorUnionAuth() {
  const from = this.$route.params.from !== undefined ? +this.$route.params.from : 1
  const unionAuthCacheKey = this.GetUnionAuthCacheKey(from)
  this.UnionAuth(window.location.href.split('#')[0],
    from,
    !this.$route.query.get_wx_info).then( // 如果从uri中查询到get_wx_info则需要用户授权 否则静默授权
      result => {
        const data = result.data
        // 缓存全局微信用户信息
        window.localStorage.setItem(unionAuthCacheKey, JSON.stringify({
          ...data
        }))
        // 触发setter
        window[unionAuthCacheKey] = {
          ...data
        }
        // 埋点处理 略
        MonitorSetWxInfo.call(gloabVue, {
          ...data
        })
      }
    )
}
复制代码

这个方法中我们调用了一个挂载到全局vue实例上的方法UnionAuth,这个方法就是获取code同时请求服务端接口的方法:

/**
 * @author DFine
 * @date 2020/1/9
 * @description 统一三方登录获取账号基本信息
 * @example
 * this.UnionAuth(redirectUri,1).then(result => { console.log(result.data) })
 * result.data = {
 *   nickname
 *   openid
 *   headimgurl
 *   unionid
 * }
 * @return promise
 * */
const UnionAuth = (redirectUri, from = 1, isDefault = false) => {
  const app = commonJs.GetEnv()
  let [domUri, uri = ''] = redirectUri.split('?')
  uri = uri.replace(/&/gi, '%26')
  redirectUri = uri == '' ? domUri : [domUri, uri].join('?')
  if (commonJs.GetQueryString('code') == null || commonJs.GetQueryString('code') === '') {
    window.location.href = GetCodeUri(app, app == 'qq' ? domUri : redirectUri, isDefault, from)
  } else {
    const code = commonJs.GetQueryString('code')
    let authRequest = null
    switch (app) {
      case 'wx':
        authRequest = commonapi.UnionAuthUserInfo({
          code,
          type: 1,
          from,
          isDefault
        })
        break
      case 'qq':
        authRequest = commonapi.UnionAuthUserInfo({
          code,
          type: 3,
          redirectUri: domUri,
          uri: uri,
          from,
          isDefault
        })
        break
      case 'weibo':
        authRequest = commonapi.UnionAuthUserInfo({
          code,
          type: 2,
          redirectUri,
          from,
          isDefault
        })
        break
      case 'dd':
        authRequest = commonapi.UnionAuthUserInfo({
          code,
          type: 4,
          from,
          appid: 'xxxx', 
          isDefault
        })
        break
      default:
        authRequest = commonapi.UnionAuthUserInfo({
          code,
          type: 1,
          from,
          isDefault
        })
        break
    }
    return authRequest
  }
}


// 公众号授权code获取
const GetCodeUri = (app, redirectUri, isDefault = false, from = 1) => {
 const wxmap = ['wxc1111111','wxc22222']
  const qqmap = ['qqxxxx','qq2222']
  const weibomap = ['wb4214','wb44324']
  const ddmap = ['dd432432','dd123231']
  const appId = {
    wx: wxmap[from],
    qq: qqmap[from],
    weibo: weibomap[from],
    dd: ddmap[from]
  }
  if (!appId[app]) {
    return
  }
  const codeUrl = {
    wx: `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId[app]}&redirect_uri=${redirectUri}&response_type=code&scope=${isDefault ? 'snsapi_base' : 'snsapi_userinfo'}&state=1#wechat_redirect`,
    qq: `https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=${appId[app]}&redirect_uri=${redirectUri}&state=1&scope=get_user_info`,
    weibo: `https://api.weibo.com/oauth2/authorize?client_id=${appId[app]}&redirect_uri=${redirectUri}&response_type=code`,
    dd: `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${appId[app]}&response_type=code&scope=snsapi_auth&state=STATE&redirect_uri=${redirectUri}`
  }
  return codeUrl[app]
}
复制代码

总结

以上具体的接口代码就不贴了,大家可以按自己的想法去写,由于现在的SNS平台第三方授权基本都是基于OAuth2.0来实现,即使不同也大同小异,因此才给了我们统一封装的机会。

所以协议这个概念在互联网和计算机中是很重要的,大家在coding、阅读和学习的过程中,甚至工作的过程中,引入协议的思想,对事情的推进,标准的形成都是很有作用的。

最后基于这一流程,大家可以封装为vue3的compositionapi,或者react hook,快去try一下吧!

猜你喜欢

转载自juejin.im/post/7075630609812422693