准备工作
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
- 使用方法
$wx.chooseWXPay()
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
微信支付
微信客户端内
/**
* 微信支付
*/
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
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¬ify_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×tamp=2021-02-02+14%3A11%3A40&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json">
<input type="submit" value="{"out_trade_no":"xxx_20201225094840485Bkhf","total_amount":"0.01","subject":"xxxx报名付款","timeout_express":"2m","product_code":"QUICK_WAP_PAY","body":"xxxx报名付款"}" 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(/"/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 检查是否支付成功
}