微信h5授权、JSSDK,支付宝支付

494 阅读3分钟

准备工作

utils url

/**
 * 获取所有 url 参数
 * @example
 * params().id
 * @param {string?} url
 * @returns {object} url上的所有参数,返回一个对象
 */
export function params(url = location.href) {
  const reg = /([^?=&#]*)=([^?=&#]*)/g
  const map = {}
  let m = []

  url = decodeURIComponent(url)
  while ((m = reg.exec(url))) {
    const [, key, value] = m
    if (value === 'true') value = true
    else if (value === 'false') value = false
    else if (value === 'null') value = null
    else if (value === 'undefined') value = undefined
    map[key] = value
  }

  return map
}

/**
 * 获取单个url参数
 * @param {string} key url参数名
 * @param {string?} url
 * @returns {string} 返回url参数值
 */
export function param(key, url = location.href) {
  return RegExp(`[?&]${key}=([^?=&#]*)|$`, 'g').exec(url)[0] || undefined
}

/**
 * 设置参数返回新的url
 *
 * @example
 * setParams('url?a=1&b=2#/Page?c=3&d=4', {a:2,e:5})
 * @example
 * setParams({a:2,e:5})
 * @param {string?} url
 * @param {object} params 设置的参数
 * @returns {string} new url
 */
export function setParams(url, params) {
  if (!params) {
    params = url
    url = location.href
  }

  for (var key in params) {
    var value = params[key]

    // remove key=xxx
    url = url
      .replace(RegExp(`([?&])${key}=[^?=&#]*`, 'g'), '$1') // [?&]key=xxx => [?&]
      .replace(/([?&])[?&]+/, '$1') // [?&]& => [?&]

    // add key=value
    // url => url?e=1
    // url url?c=2 => url?c=2&e=1
    // url#  url?e=1#
    if (value !== undefined) {
      const [urlSub, hash] = url.split('#')

      url = urlSub + `${urlSub.match(/[?]/) ? '&' : '?'}${key}=${value}`
      url += hash ? `#${hash}` : ''
    }
  }
  return url
}

/**
 * 修改当前url
 * @param {object} params
 */
export function replaceUrl(params) {
  var url = setParams(params)
  history.replaceState('', '', url)
}

/**
 * 相对地址转绝对地址
 * @param {string} path
 */
export function getFullPath(path) {
  var a = document.createElement('a')
  a.href = path
  return a.href
}

/**
 * 返回新的url并设置参数
 * @param {string} path
 * @param {object} params
 */
function Url(path = location.href, params = {}) {
  if (typeof path == 'object') {
    params = path
    path = location.href
  }

  var url = getFullPath(path)
  return setParams(url, params)
}

export { params as getParams, param as getParam }
export default Url

code.html

  • 部署在与微信测试环境域名下
  • 因为微信 local ip 不能作为 redirect_uri 链接,所以本地开发的时候,需要有个中转页
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>code</title>
    <style>
      html {
        word-break: break-all;
      }
      button {
        display: block;
        width: max-content;
        margin: 2em;
      }
    </style>
  </head>

  <body>
    <div id="out"></div>
    <button onclick="go">back</button>

    <script>
      const params = new URLSearchParams(location.search)
      var ps = new URLSearchParams(params.get('redirect_uri'))
      ps.append('code', params.get('code'))

      var url = ps.toString()

      out.innerHTML = url
      function go() {
        location.replace(url)
      }
      setTimeout(go, 2000)
    </script>
  </body>
</html>

微信授权

微信网页授权文档

  • 使用方法 const loginInfo = await wxAuth.getUserInfo()
/**
 * 微信网页授权文档
 * https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
 */

import { Url, param, replaceUrl } from './url'

// TODO
// config 配置
const unicode = 'xxx'
const config = {
  appid: '', // 微信appid
}
const getLoginApi = async code => {
  const { data } = await fetch('xxx', { code })
  return data
}

// storage key
const OPEN_ID_KEY = `wx:openId:${unicode}`
const UNION_ID_KEY = `wx:unionId:${unicode}`
const USER_INFO_KEY = `wx:userInfo:${unicode}`

/**
 * 返回微信授权地址
 * @param {object|boolean} options true=>{scope:'snsapi_userinfo'}
 */
function getAuthUrl(options = {}) {
  // 需要用户手动同意
  if (options === true) {
    options = { scope: 'snsapi_userinfo' }
  }

  // redirect_uri
  let redirect_uri = options.redirect_uri || location.href
  redirect_uri = Url(redirect_uri, {
    code: undefined,
    state: undefined,
  })
  // TODO
  // 本地开发 & 微信开发者工具
  if (process.env.NODE_ENV === 'development') {
    // 需要配置安全域名 local ip 不能作为跳转链接
    // xxx => 部署在测试环境的重定向文件(仅仅用于测试)
    redirect_uri = `xxx/code.html?redirect_uri=${encodeURIComponent(redirect_uri)}`
  }

  // 参数顺序不可调换,特别是 微信pc版 appid 须放第一位
  const authUrl = `
       https://open.weixin.qq.com/connect/oauth2/authorize
       ?appid=${config.appid}
       &redirect_uri=${encodeURIComponent(redirect_uri)}
       &response_type=code
       &scope=${options.scope || 'snsapi_base' || 'snsapi_userinfo'}
       &state=${options.state || 'STATE'}
       #wechat_redirect
     `.replace(/\s/g, '')

  return authUrl
}

/**
 * A(url) -> B(url+code) => B(url+code)
 */
function getCode(options) {
  const code = param('code')

  // 1: !code jump
  if (!code) {
    // jump
    const authUrl = getAuthUrl(options)
    location.replace(authUrl)

    // 用户拒绝授权,点击页面会再次调起授权
    // reject? click jump again
    // document: window 微信并非点所有元素都触发
    document.addEventListener('click', e => {
      location.replace(authUrl)
    })
    document.body.style.pointerEvents = 'none'

    return new Promise(r => {}) // never resolve
  }

  // 2: code
  // wechatdevtools remove code
  if (/wechatdevtools/i.test(navigator.userAgent)) {
    replaceUrl({ code: undefined, state: undefined })
  }
  return code
}

/**
 *
 * @param {boolean} bool true? {unionId,openId}: {openId}
 */
async function fetchUserInfo(bool) {
  // !wx env
  if (!/MicroMessenger/i.test(navigator.userAgent)) {
    console.warn('!wx fetchUserInfo')
    // test env
    if (/^https?:..(1|wx)/.test(location.href)) {
      return {
        openId: 'test',
        unionId: 'test',
      }
    }
    // default
    return {
      openId: '',
      unionId: '',
    }
  }

  // fetch
  return new Promise(async rs => {
    // clear
    localStorage.setItem(OPEN_ID_KEY, '')
    sessionStorage.setItem(OPEN_ID_KEY, '')

    const code = await getCode(bool)
    const data = await getLoginApi(code)

    // save
    localStorage.setItem(OPEN_ID_KEY, data.openId)
    sessionStorage.setItem(OPEN_ID_KEY, data.openId)
    if (bool) {
      localStorage.setItem(UNION_ID_KEY, data.unionId)
      sessionStorage.setItem(UNION_ID_KEY, data.unionId)
      localStorage.setItem(USER_INFO_KEY, data)
      sessionStorage.setItem(USER_INFO_KEY, data)
    }

    rs(data)
  })
}

async function getOpenId() {
  const openId = localStorage.getItem(OPEN_ID_KEY)
  if (openId) {
    return openId
  }

  return new Promise(rs => {
    // delay for getUnionId
    setTimeout(async () => {
      const userInfo = await fetchUserInfo()
      rs(userInfo.openId)
    }, 1)
  })
}

async function getUnionId() {
  const unionId = localStorage.getItem(UNION_ID_KEY)
  if (unionId) {
    return unionId
  }
  const userInfo = await fetchUserInfo(true)
  return userInfo.unionId
}

// TODO 用户修改微信头像,头像链接会失效
async function getUserInfo() {
  let userInfo = localStorage.getItem(USER_INFO_KEY)
  if (userInfo) {
    return userInfo
  }
  userInfo = await fetchUserInfo(true)
  return userInfo
}

export { getAuthUrl, getCode, getOpenId, getUnionId, getUserInfo }
export default getAuthUrl

微信 JSDK

JS-SDK说明文档

  • 使用方法 $wx.chooseWXPay()

image.png

import { param } from './url'

var wx = window.wx
var $wx = {}

// script
if (wx) {
  set$wx()
} else {
  var res = 'http://res.wx.qq.com/open/js/jweixin-1.6.0.js'
  var res2 = 'http://res2.wx.qq.com/open/js/jweixin-1.6.0.js'

  var script = document.createElement('script')
  script.src = res
  script.onerror = e => (script.src = res2)
  script.onload = set$wx
  document.body.appendChild(script)
}

async function config(href = location.href) {
  // console.log('config', href)
  // console.log('firstLocationHref', config.firstLocationHref)

  // cache
  // 可能某些机型(iphone7p) hash 变化也要重新 config,所以要带hash判断
  if (href == config.lastHref) {
    console.warn('config cache', href)
    return true
  }

  // TODO
  // config data
  var { returnObject: data } = await fetch('xxx', {
    method: 'post',
    pageUrl: href.split('#')[0],
  })

  return new Promise(rs => {
    // console.info('config Promise')
    setTimeout(() => {
      // 这个setTimeout应该可以去掉,试试phone7plus是否正常
      wx.config({
        ...data,
        debug: param('debug'),
        jsApiList: [
          'closeWindow',

          'updateAppMessageShareData',
          'updateTimelineShareData',
          'onMenuShareTimeline',
          'onMenuShareAppMessage',

          'hideOptionMenu',
          'showOptionMenu',
          'hideMenuItems',
          'showMenuItems',
          'hideAllNonBaseMenuItem',
          'showAllNonBaseMenuItem',

          'chooseImage',
          'uploadImage',
          'downloadImage',
        ],
      })
      wx.ready(function() {
        console.info('config wx.ready')
        rs()

        // cache
        config.lastHref = href
      })
      wx.error(async function(res) {
        if (!config.isAgain) {
          return
        }
        config.isAgain = true

        // ios try again
        console.warn('config error try again')
        await config(config.firstLocationHref)
        rs()
      })
    }, 500)
  })
}
config.firstLocationHref = location.href // ios: history.replaceState
$wx.$config = config

// function: await config()
function set$wx() {
  for (let method in wx) {
    var value = wx[method]

    if (typeof value === 'function') {
      $wx[method] = async function() {
        await config()

        if (method in pageshowCallMap) {
          pageshowCallMap[method] += location.href + ' | '
        }

        return wx[method].apply(this, arguments)
      }
    }
  }
}

// fix: hideAllNonBaseMenuItem,... 跳转之后返回失效
var pageshowCallMap = {
  hideAllNonBaseMenuItem: 'urls | ',
}
addEventListener('pageshow', e => {
  for (var method in pageshowCallMap) {
    var urls = pageshowCallMap[method]
    if (urls.indexOf(location.href) >= 0) {
      console.warn('pageshow $wx', method)
      wx[method]()
    }
  }
})

export { $wx, config }
export default $wx

微信支付

image.png

微信客户端内

/**
 * 微信支付
 */
async function pay() {
  // validate form => get wx pay info
  const form = {}
  const { data } = await fetch('xxx', form)

  // fix
  data.timestamp = data.timeStamp

  // wx.chooseWXPay cb
  const cb = res => {
    console.info('[chooseWXPay cb]', res)
    // TODO
    if (res.errMsg === 'chooseWXPay:ok') {
      // success
      // api checkPayRes
      this.paySuccess()
    } else {
      // pay error
    }
  }

  wx.chooseWXPay({
    ...data,
    success: cb,
    cancel: cb,
    fail: cb,
  })
}

h5

image.png

import { setParams, param } from 'utils/url'

const H5_PAY_PID = "ibh5"; // h5(非微信环境)支付标识符
const H5_PAY_QUERY_CODE_KEY = "h5_pay_query_code";

mounted() {
  this.checkH5PayRedirectUrl()
}

async function h5Pay() {
  // TODO
  // validate form => get wx h5 pay url
  const form = {};
  const { data } = await fetch("/h5pay", form);

  // 跳转到微信支付的页面
  const currUrl = window.setParams(location.href, {
    [H5_PAY_PID]: 1,
  });

  // 记录订单号 - 跳转回来查询订单是否支付成功
  sessionStorage.setItem(H5_PAY_QUERY_CODE_KEY, data.outTradeNo);
  location.href = `${data.mwebUrl}&redirect_url=${encodeURIComponent(
    currUrl
  )}`;
}

/**
 * 检查 h5 支付重定向链接
 */
function checkH5PayRedirectUrl() {
  const hasParamH5PayPID = param(H5_PAY_PID);
  const platformCode = sessionStorage.getItem(H5_PAY_QUERY_CODE_KEY);

  // TODO
  // 反馈 ui
  if (!this.isWx && hasParamH5PayPID && platformCode) {
    this.isShowH5PayFeedback = true;
  }
}

/**
 * 检查 h5 支付是否成功
 */
function checkH5PayResult() {
  const platformCode = sessionStorage.getItem(H5_PAY_QUERY_CODE_KEY);

  // TODO
  // api 检查是否支付成功

  // if pay error
  // 删除订单信息
  // sessionStorage.setItem(H5_PAY_QUERY_CODE_KEY, undefined);
}

支付宝h5支付接入

formString 示例

// 响应为表单格式,可嵌入页面,具体以返回的结果为准
<form name="submit_form" method="post" action="https://openapi.alipay.com/gateway.do?charset=UTF-8&method=alipay.trade.wap.pay&sign=k0w1DePFqNMQWyGBwOaEsZEJuaIEQufjoPLtwYBYgiX%2FRSkBFY38VuhrNumXpoPY9KgLKtm4nwWz4DEQpGXOOLaqRZg4nDOGOyCmwHmVSV5qWKDgWMiW%2BLC2f9Buil%2BEUdE8CFnWhM8uWBZLGUiCrAJA14hTjVt4BiEyiPrtrMZu0o6%2FXsBu%2Fi6y4xPR%2BvJ3KWU8gQe82dIQbowLYVBuebUMc79Iavr7XlhQEFf%2F7WQcWgdmo2pnF4tu0CieUS7Jb0FfCwV%2F8UyrqFXzmCzCdI2P5FlMIMJ4zQp%2BTBYsoTVK6tg12stpJQGa2u3%2BzZy1r0KNzxcGLHL%2BwWRTx%2FCU%2Fg%3D%3D&notify_url=http%3A%2F%2F114.55.81.185%2Fopendevtools%2Fnotify%2Fdo%2Fbf70dcb4-13c9-4458-a547-3a5a1e8ead04&version=1.0&app_id=2014100900013222&sign_type=RSA&timestamp=2021-02-02+14%3A11%3A40&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json">
<input type="submit" value="{&quot;out_trade_no&quot;:&quot;xxx_20201225094840485Bkhf&quot;,&quot;total_amount&quot;:&quot;0.01&quot;,&quot;subject&quot;:&quot;xxxx报名付款&quot;,&quot;timeout_express&quot;:&quot;2m&quot;,&quot;product_code&quot;:&quot;QUICK_WAP_PAY&quot;,&quot;body&quot;:&quot;xxxx报名付款&quot;}" style="display:none" >
</form>
<script>document.forms[0].submit();</script>

调用 _AP.pay() 实现支付

import {} from '@/../public/ap.js'

const ALI_PAY_ORDER_NO_KEY = 'ali_pay_key'

mounted() {
  // reload / android back
  const alipayOrderNo = sessionStorage.getItem(ALI_PAY_ORDER_NO_KEY)
  if (alipayOrderNo) {
    this.checkPayResult(alipayOrderNo)
    sessionStorage.setItem(ALI_PAY_ORDER_NO_KEY, '')
  }
}

async function payByAlipay(data) {
  // TODO
  // validate form => get ali h5 pay info
  const form = {};
  const { data: { formString, orderNo } } = await fetch("/aliPay", form);

  // reload / android back
  // check pay result
  sessionStorage.setItem(ALI_PAY_ORDER_NO_KEY, orderNo)

  // splice url
  const action = formString.match(/action="(.*?)"/)?.[1]
  const biz_content = formString
    .match(/([{].*?[}])/)?.[1]
    ?.replace(/&quot;/g, '"')
  const url = `${action}&biz_content=${encodeURIComponent(biz_content)}`
  // console.log(url)

  const cb = e => {
    if (document.visibilityState === 'visible') {
      this.checkPayResult(orderNo)
      
      // TODO 验证 ios 是否可以多次触发 cb
      // remove listener
      // removeEventListener('pageshow', cb)
      // document.removeEventListener('visibilitychange', cb)
    }
  }
  // ios cb
  addEventListener('pageshow', cb)
  document.addEventListener('visibilitychange', cb)
  // 引入 pay.js 调用 _AP.pay()
  window._AP.pay(url)
}

function checkPayResult(orderNo) {
  // TODO
  // api 检查是否支付成功
}