多渠道统一二维码

100 阅读3分钟

前言

如今的互联网环境下,营销活动中针对APP、小程序、H5等不同渠道的页面,希望提供一个统一的二维码链接,并支持动态参数透传。比如:

https://aaa.com/forwards?scene=activity&activityId=123

一、搭建配置平台

以cms为例,手动搭建一个cms配置平台,提供场景scene、渠道channel、渠道url、页面类型contentType、扩展参数extendParams。

【cms配置示例.png

二、渠道跳转支持

以小程序为例,微信扫码后,需要拉起小程序并打开映射的原生页面或H5页面,同时将透传参数拼接到指定链接上。

1、小程序管理后台配置映射

我们需要在小程序后台配置普通二维码域名映射,保证用户使用微信扫一扫后,能拉起微信小程序。

【a小程序普通二维码.png

2、渠道参数处理

小程序创建中间页 /pages/common/middle-page/middle-page;

中间页处理固定参数,比如source=transferActivity;


onLoad(options){
    // 获取普通二维码链接,当扫普通二维码进入时 options.q 才有值
    const scanUrl = decodeURIComponent(options.q)
    console.log('scanUrl========', scanUrl)
    if (scanUrl) {
        this.handleScanUrlFormQRCode(scanUrl)
    }
}

async handleScanUrlFormQRCode(scanUrl){
    // 通用规则二维码
    const forwardReg = new RegExp('forwards')
    
    // 判断是通用二维码链接 https://aaa.com/forwards?scene=activity
    if (forwardReg.test(scanUrl)) {
        // 获取二维码url场景参数
        const scanUrlParams = getUrlParams(scanUrl)
        // 根据scene查询cms配置平台的配置信息
        const [forwardsInfo = {}] = await queryCmsForwardsInfo(scanUrlParams.scene)
        const { url = '', contentType = '', extendParams = '' } = (forwardsInfo.routes && forwardsInfo.routes[0]) || {}
        
        // cms配置的扩展参数
        const extObj = JSON.parse(extendParams)
        // 判断是否跳转webview容器页面
        const isWebViewType = contentType === 'webview'
        // 接口请求回来的url,拼接在二维码携带的参数透传给h5
        const link = `${url}${url.indexOf('?') > -1 ? '&' : '?'}${scanUrl.split('?')[1] || ''}`
        // cms配置的渠道页面url的参数
        const {dest = '', source = '', type = ''} = getUrlParams(url)

        // 去掉扫码链接上的scene,除scene外的参数都需透传给原生页
        delete scanUrlParams.scene
        // 整合cms配置的扩展参数和扫码链接上的参数
        const destExtObj = Object.assign({}, extObj, scanUrlParams)

        // cms配置路径:/pages/common/middle-page/middle-page?source=transferActivity
        if (source === 'transferActivity') {
            // 这里的href代表的是cms配置的目标h5页面的链接
            const { href: extHref, ...other } = destExtObj || {}
            // 将所有参数拼接到目标链接上
            const nextHref = genUrl(extHref, destExtObj)
            // 跳转webview容器页,打开目标h5
            const destUrl = `/pages/common/webview/webview?href=${nextHref}`
            wx.navigateTo({
                url: destUrl
            })
        }
    }
}
/**
 * 对 url 查询参数进行增/删/改,生成新的 url
 * @param { string } url 需要处理的 url
 * @param { Query } [params] 需要处理的参数对象
 * @param { <T extends Query, K extends keyof T>(value: T[K]; key: K; params: T) => boolean } [filter] 参数筛选器;返回 true 保留该参数; 返回 false 删除该参数
 * @returns { string } 新的 url
 */
export const genUrl = (url, params, filter) => {
    const genPairs = (key, val) => {
        const name = encodeURIComponent(String(key))
        const value = encodeURIComponent(String(val))
        return `${name}=${value}`
    }
    /** @type {typeof filter} */
    const defaultFilter = val => val !== null && val !== undefined
    const includes = typeof filter === 'function' ? filter : defaultFilter
    const href = String(url).trim()
    const prevParams = getUrlParams(href)
    const nextParams = Object.assign({}, prevParams, params || {})
    const keys = Object.keys(nextParams).filter(key => includes(nextParams[key], key, nextParams))
    const qs = keys.map(key => genPairs(key, nextParams[key])).join('&')
    const index = href.indexOf('?')
    const search = index < 0 ? '' : href.slice(index + 1)
    let base = ''
    let hash = ''
    if (search) {
        const i = search.indexOf('#')
        hash = i < 0 ? '' : search.slice(i)
        base = href.slice(0, index)
    } else {
        const i = href.indexOf('#')
        hash = i < 0 ? '' : href.slice(i)
        base = i < 0 ? href : href.slice(0, i)
    }
    return [base, qs ? `?${qs}` : '', hash].join('')
}
/**
 * 获取URL中的所有查询参数
 * @param {string} url
 * @param {boolean} [autoDecode=true]
 *    - 解析参数项时,是否进行一次 decodeURIComponent,默认为 true
 *    - 只有你明确知道传 false 的场景时才需要传 false,否则请保留默认行为
 * @returns {Record<string,string>}
 */
export const getUrlParams = (url, autoDecode = true) => {
    const params = Object.create(null)
    if (typeof url !== 'string') {
        return params
    }
    const href = url.trim()
    const index = href.indexOf('?')
    if (index < 0) {
        return params
    }
    const search = href.slice(index + 1)
    const qs = search.split('#')[0] || ''
    const decode = value => {
        if (!autoDecode) {
            return value
        }
        return decodeURIComponent(String(value).replace(/\+/g, '%20'))
    }
    const reducer = (acc, item) => {
        if (!item.length) {
            return acc
        }
        const i = item.indexOf('=')
        const name = decode(i === -1 ? item : item.slice(0, i))
        const value = decode(i === -1 ? '' : item.slice(i + 1))
        return Object.assign(acc, { [name]: value })
    }
    // eslint-disable-next-line prettier/prettier
    return qs.split('&').filter(Boolean).reduce(reducer, params)
}

三、配置示例

扫码后打开如下地址

https://bbb.com?scene=wxappActivity&paramString=channel%3Dwxapp%26id=777

这个扫码后跳转的目标地址带了两个参数,场景scene和复合参数paramString

注意:这里paramString是跳转链接希望从渠道端(如微信小程序)带过去的参数,也可以是任意其他参数,比如

https://aaa.com/forwards?scene=activity&activityId=123

cms配置平台配置

scene: activity

channel: wxapp

url: /pages/common/middle-page/middle-page?source=transferActivity

contentType: native

extendParams: {"href":"https://bbb.com?scene=wxappActivity"}

对外提供扫码链接

https://aaa.com/forwards?scene=activity&paramString=channel%3Dwxapp%26id=777

需要注意的是,流程中涉及3个链接:扫码链接渠道页面链接跳转的目标链接