拦截页面a标签跳转

390 阅读2分钟

0. 背景

当你想对页面跳转逻辑添加参数,或者想动态替换域名,那么拦截页面a标签跳转,是个不错的选择!

网上有挺多实现,为啥我还写一遍?因为有些边界场景,是需要踩坑踩出来的!

1. 实现

// 不携带refer
const NO_REFER = {
    REFERRER_POLICY: ['no-referrer'],
    REL: ['noreferrer', 'noopener']
};
// 监听 click 事件,处理页面跳转相关逻辑
// 若在同一个元素上按下并松开鼠标左键,会依次触发 mousedown、mouseup、click,前一个事件执行完毕才会执行下一个事件
document.body.addEventListener('click', (e) => {

    //【2】获取 a 标签,最多 3 层
    const target = e.target || e.srcElement;
    const parentNode = target.parentNode;
    const grandParentNode = parentNode && parentNode.parentNode;

    let actualANode = null; // 实际a标签
    let url = ''; // 跳转链接
    // 判断是否 a 标签
    if (target.nodeName.toLocaleLowerCase() === 'a' && target.getAttribute('href')) {
        actualANode = target;
    } else if (parentNode && parentNode.nodeName.toLocaleLowerCase() === 'a' && parentNode.getAttribute('href')) {
        // a 标签含子标签
        actualANode = parentNode;
    } else if (grandParentNode && grandParentNode.nodeName.toLocaleLowerCase() === 'a' && grandParentNode.getAttribute('href')) {
        // a 标签含子子标签
        actualANode = grandParentNode;
    }

    // 不处理
    if (!actualANode){
        return;
    }

    url = actualANode.getAttribute('href');

    // 只处理 http:// 或 https:// 或 /xxx
    if (/^https?:\/\//.exec(url) || /^\//.test(url)) {
        // 适配如 vue router-link形式
        // e.returnValue 为IE8 已废弃,目前打印为true
        if ( e.defaultPrevented) {
            return;
        }
        // 禁止跳转行为
        if (e.preventDefault) {
            e.preventDefault();
        } else {
            e.returnValue = true;
        }

        // 定制化处理链接跳转逻辑
        // url = handleUrl(url);

        //【3】处理完 a 标签的内容,重新触发跳转,根据原来 a 标签页 target 来判断是否需要新窗口打开
        if (isNoRefer(actualANode)) {
            // 不携带 refer
            window.open(url, actualANode.getAttribute('target') || '_self', NO_REFER.REL.join(','));
        } else if (actualANode.getAttribute('target') === '_blank') {
            window.open(url);
        } else {
            window.location.href = url;
        }
    }
});

/**
 * 判断是否设置了不携带 refer
 * @param  {object} el 元素
 * @return {boolean} boolean
 */
function isNoRefer (el) {
    if (!el) {
        return false;
    }

    let referrerpolicy = false;
    let rel = false;

    const elReferrerpolicy = el.getAttribute('referrerpolicy');
    const elRel = el.getAttribute('rel');

    if (elReferrerpolicy) {
        const referArr = elReferrerpolicy.split(' ');
        referrerpolicy = !!(referArr.find(refer => NO_REFER.REFERRER_POLICY.includes(refer)));
    }

    if (elRel) {
        const relArr = elRel.split(' ');
        rel = !!(relArr.find(refer => NO_REFER.REL.includes(refer)));
    }

    return referrerpolicy || rel;
}

2. 注意点

2.1 框架内置路由组件

如vue 的router-link, 如果未设置 target="__blank" 属性,其是局部刷新的形式;这时,内部实现是会调用 e.preventDefault();

如果此时代码实现,未判断 e.defaultPrevented 是否为true,则会导致页面刷新;对于使用了store状态的,可能就会丢失;

2.2 a标签原生属性

有些链接,是配置了noreferrer 属性,需要特殊处理