惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
雷峰网
雷峰网
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Spread Privacy
Spread Privacy
H
Hacker News: Front Page
PCI Perspectives
PCI Perspectives
Webroot Blog
Webroot Blog
罗磊的独立博客
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Last Week in AI
Last Week in AI
美团技术团队
Help Net Security
Help Net Security
The Hacker News
The Hacker News
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
J
Java Code Geeks
The Register - Security
The Register - Security
IT之家
IT之家
WordPress大学
WordPress大学
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Help Net Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
NISL@THU
NISL@THU
爱范儿
爱范儿
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
Vulnerabilities – Threatpost
B
Blog
T
Tenable Blog
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
S
Security Affairs
小众软件
小众软件
Hacker News: Ask HN
Hacker News: Ask HN
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
W
WeLiveSecurity
A
Arctic Wolf
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
支持外部链接跳转的 Vue Router 扩展实现 - 元视角
飞鸿踏雪 · 2022-07-13 · via 元视角

众所周知,Vue RouterVue 中重要的插件之一,特别是在当下流行的 单页面应用/SPA 中,这种感觉会越来越明显。此时,路由的作用就是根据 URL 来决定要显示什么内容。诚然,页面这个概念在工程/模块中依然存在,可当你开始关注最终发布的产物时,你会发现本质上它只有一个页面。无论你选择 hash 或者是 history 模式的路由,它都像是在同一张纸上反复写写画画,让你看起来觉得它有很多个不同的页面。回顾早期的前端项目,它往往会有多个不同的页面组成,我们是通过一个个的超链接来实现不同页面间的跳转。如今,这一切都已一去不复返,我们只能在单页面应用的世界里继续披荆斩棘。当然,绝大多数的普通用户无法感知到这种程度的变化,在他们的眼中,那依然不过是普通的一个超链接。那么,当一个项目中充斥着各种各样的超链接的时候,这个问题就值得我们单独拿出来讲一讲。所以,今天这篇博客的主题是路由和外部链接。请注意,这是一组相对通用的概念,不受限于任何一个前端框架,我们只是选择了使用 Vue 来进行说明。

问题现状

我们的项目存在着大量的超链接以及导航菜单,在 UI 设计阶段,通常不会有人关心,一个链接到底是内部链接还是外部链接。与此同时,由于 HTML 这门标记语言的极大灵活性,实现一个导航链接的方式有 N 多种,可以是一个 a 标签,可以是一个 div 标签,甚至可以是一个 span 标签。虽然 Vue Router 里提供了 router-link 组件,可在实际的项目中,需要综合考虑团队风格和第三方 UI 库的因素,甚至有时候,再没有设计规范的情况下,可能大家连 router-link 组件都不愿意用或者说压根就没机会用。

这样就造成一个非常尴尬的局面,当你需要为页面编写业务代码的时候,你不得不在各种各样的超链接上浪费时间,只要不是通过 a 标签实现的,你都必须处理它点击的事件,更不必说,你还要区分这个链接是一个内部链接还是一个外部链接,原因是 Vue Router 不支持外部链接,你不得不通过 window.location 或者 window.open() 的这样的方式来实现“曲线救国”,试想,如果每一个都这么折腾一遍,你还会觉得有趣吗?

而在我们的项目里,实际上它还需要从网页端唤起应用,这样便又涉及到了 URL Schemes 这个话题。除了 AndroidiOS 这个平台上的差异,单单就 Windows 而言,其基于注册表的方案对协议提供者的约束并不强,如果团队内对此没有任何规范的话,你将面对各种千奇百怪的参数传递方式。听到这里,你是不是感觉头都大了一圈?如果因为某种原因,它还需要你每次都传递一个令牌过去,你告诉我,你准备如何让这一切的混乱与不堪重新归于宁静呢?

学如逆水行舟,不进则退 学如逆水行舟,不进则退

改进思路

OK,现在假设,我们制止这场混乱的方式,是强迫大家都去使用 router-link 这个组件,虽然它最终渲染出来就是一个 a 标签。相信参加工作以后,大家都会有这样一种感觉,那就是工作中 99.9% 的事情,都是在最好和最坏中间选一个过渡状态,然后不断地为之投入精力或者叫做填坑,甚至有很多东西,从来都不是为了让一件事情变得更好而存在。作为这个地球上脆弱而渺小的个体,时间、生命、爱,每一样东西都像缓缓从指尖滑落的沙子,我们实在是太喜欢这种可以掌控点什么的感觉了。所以,如果一件事情没法从道理或者科学上讲通的话,那就用制度或者规范来作为武器,在一个连国家都可以宣布破产的年代,大概,话语权比是非对错更重要。因此,在博主的博客里,在这小小的一方天地里,不妨假设我有这种话语权,可以强迫大家都使用 router-link 这个组件。我们讲,Vue Router 不支持外部链接,一个非常直观的理由是,当我们写出下面的代码时,它会完全辜负我们的期望:

<router-link to="https://blog.yuanpei.me">Go</router-link>

显然,我们期望它可以跳转到 https://blog.yuanpei.me 这个地址,可你只要亲自试一下,就会知道这是你的一厢情愿。因为,此时浏览器地址栏中的地址会显示为:

http://localhost:8080/#/https://https://blog.yuanpei.me

当然,我们的用户不会操心这种事情,正如他们从来不会去刻意地分辨,这是一个内部链接还是一个外部链接。这里讲一下博主的思路,博主打算在 router-link 的基础上再做一层封装,内部链接通常是以/ 来开头的,基于这个特点,我们可以区分出这是一个内部链接还是一个外部链接。针对内部链接,我们继续使用 router-link 组件;针对外部链接,我们直接使用 a 标签即可。此时,对应的 Vue 模板定义如下:

<template>
  <a v-if="isExternal" :href="formatedUrl" :target="target">
    <slot></slot>
  </a>
  <router-link v-else v-bind="originProps">
    <slot></slot>
  </router-link>
</template>

在这里,我们对外暴露了 totarget 两个属性,前者允许我们传入一个字符串或者对象,后者可以控制这个链接的打开方式,是在当前窗口还是一个新窗口中打开:

export default {
  name: "MyRouterLink",
  props: {
    to: {
      type: [Object, String],
      default: () => { 
        path: '/'
      },
      required: true,
    },
    target: {
      type: String,
      default: () => '',
    },
  },
  // ...
}

还记得我们是怎么区分内部链接和外部链接的吗?只需要判断传入的 URL 是否以 / 开头。在这里,我们需要对 to 的类型进行判断:

computed: {
    isExternal() {
      if (typeof(this.to) === 'object') {
        return this.to.path && this.to.path[0] !== '/'
      }

      if (typeof(this.to) === 'string') {
        return this.to && this.to[0] !== '/'
      }
      
      return false
    },
}

当然,在某些情况下,这个 URL 允许使用者传入查询参数(QueryString)。这里,我们用 formatedUrl 这个计算属性来统一进行处理:

computed: {
    formatedUrl() {
      let url = "";
      if (typeof(this.to) === 'object') {
        url = this.to.path
      } else if (typeof(this.to) === 'string') {
        url = this.to
      }

      let queryArray = [];
      if (this.to.query) {
        for (let key in this.to.query) {
          const value = encodeURIComponent(this.to.query[key]);
          queryArray.push(`${key}=${value}`);
        }
      }

      if (queryArray.length == 0) {
        return url;
      }

      if (url.indexOf("?") != -1) {
        url = `${url}${queryArray.join("&")}`;
      } else {
        url = `${url}?${queryArray.join("&")}`;
      }

      return url;
    },
}

最后,需要特别说明的是 originProps 这个计算属性,虽然我们封装了 router-link 这个组件,但我们希望这个新组件是兼容 router-link 本身自带的属性的。此时,我们可以采用下面的方式来处理,具体可以参考官方文档:vm.$attrs

computed: {
    originProps() {
      return { ...this.$props, ...this.$attrs };
    },
}

现在,万事具备,我们来试用一下这个新的组件,看看效果如何:

<header>
    <my-router-link to="/home">首页</my-router-link>
    <my-router-link to="/message">消息</my-router-link>
    <my-router-link to="https://blog.yuanpei.me" target="_blank">博客</my-router-link>
    <my-router-link
       :to="{ path: 'https://www.baidu.com/s', query: { wd: '天气' } }"
       target="_blank"
    >
       百度
    </my-router-link>
    <my-router-link
       :to="{
          path: 'tencent://',
          query: { uin: '875974254', site: 'Vue', menu: 'yes' },
        }"
    >
       QQ
    </my-router-link>
</header>

我们可以注意到,现在它可以同时支持内部链接和外部链接,并且我们可以传递一个对象来更好地控制 URL 的细节,当然,它还可以从桌面唤起 QQ 应用,只要协议提供方采用类似的传参方式,那么,这个方案其实可以做到一劳永逸的。完整的代码我已上传到 Github,方便大家可以做进一步的探索。

从网页端唤起应用 从网页端唤起应用

话题延伸

坦白讲,在我写这篇文章的时候,我一直在思考一个问题,即:如何给所有出站的超链接携带令牌信息?这个想法其实是在解决别人产生的问题,譬如,从子系统 A 跳转到子系统 B 的过程中,为了实现所谓的“免登录”,大佬们提议直接把令牌信息附加到 URL上传递过去,先不说令牌信息刷新和过期的问题,就单单是令牌信息附加到 URL上这一项,看起来都是非常愚蠢的做法,众所周知,浏览器对针对 GET 请求时的 URL 长度存在限制,你这不是直愣愣地往人家枪口上撞吗?放着 CASKeycloak 这种成熟的方案不用,非要用这种掩耳盗铃式的半桶水方案?也许,人类还真就喜欢做这样的事情,毕竟这样可以制造出问题和麻烦,让别人有事可做。听我说,谢谢你,因为有你…吐槽归吐槽,一开始我是写了一个自定义指令来做这个事情:

Vue.directive('attach-query-string',function (el, binding) {
  if (el.tagName === 'A') {
    const token = resolveToken()
    const userId = resolveUserId()
    const posting = resolvePostings()[0] || ''
    if (el.href.indexOf('?') != -1){
      el.href = `${el.href}&token=${token}&userId=${userId}&deviceType=${DeviceType.RCT}&posting=${posting}`
    } else {
      el.href = `${el.href}?token=${token}&userId=${userId}&deviceType=${DeviceType.RCT}&posting=${posting}`
    }
  }
})

注意到,这个指令只对 a 标签有效,所以,那些花里胡哨、奇形怪状的超链接依然是个令人头疼的问题,我们先忽略它们就好:

<a 
    :href="item.link" 
    :target="item.openNewTab ? '_blank' : '_self'" 
    v-attach-query-string
>
    {{ item.name }}
</a>

这个指令表示,它将会在 mountedupdated 的时候触发相应的逻辑,对于大多数的超链接而言,其 src 只会初始化一次,所以,这个方案基本上可行的,唯一的难点在于,并不是所有人都会如你期望的那样使用 a 标签。当然,我内心深处永远相信 jQuery 一把梭,所以,通常尝试过 querySelectorAll() ,但我始终觉得这样子显得有点丑陋,说好的不再操作 DOM 了呢?如果按照我们现在的思路,其实可以在组件内部统一处理,下面是一个简单的实现:

  computed: {
    formatedUrl() {
      let url = "";
      if (typeof(this.to) === 'object') {
        url = this.to.path
      } else if (typeof(this.to) === 'string') {
        url = this.to
      }

      let queryArray = [];

      // 统一追加参数
      const token = resolveToken()
      const userId = resolveUserId()
      const posting = resolvePostings()[0] || ''
      queryArray.push(`token=${token}`)
      queryArray.push(`userId=${userId}`)
      queryArray.push(`posting=${posting}`)
      
      // 处理组件传入的参数
      if (this.to.query) {
        for (let key in this.to.query) {
          const value = encodeURIComponent(this.to.query[key]);
          queryArray.push(`${key}=${value}`);
        }
      }

      if (queryArray.length == 0) {
        return url;
      }

      if (url.indexOf("?") != -1) {
        url = `${url}${queryArray.join("&")}`;
      } else {
        url = `${url}?${queryArray.join("&")}`;
      }

      return url;
    },
  },

从本质上讲,这两种方案做得事情是完全相同的,无非是拥有了新知识或者技能以后,再去重新审视过去的种种选择,人虽然始终没有办法打破自身的历史局限性,可是能从新知识或者技能中不断丰富自我的认知,这又属实是种颇具幸福感的事情,因为,从这一刻起,你已经告别了昨天的自己,真正做到了“且将新火试新茶”。回过头来再次审视这个问题的时候,你会觉得哪一种更好呢?欢迎大家在评论区留下你的答案。

本文小结

本文介绍了一种针对 Vue Router 进行扩展的思路,主要是为了解决 router-link 不支持外部链接跳转的问题。关注这个问题的契机,则是来源于项目中大量存在着的超链接和导航菜单。其中,除了指向站内的内部链接,还有指向站外的外部链接,而这些外部链接中,又牵扯到从网页端唤醒应用的问题,所以,我们需要一种相对统一的机制来处理这些内部细节,因此,就有了今天的这篇博客。除此以外,因为一部分人的愚蠢决定,我们必须要在所有出站的 URL 上附加令牌信息,针对这个问题,博主先是尝试了自定义指令的做法,然后又在现在的方案上做了一点处理,这使得我们能把精力放在真正重要的地方。从整体上而言,如果在设计 UI 前,就定好这样一种规范,所有人都使用这个统一的组件,这个问题处理起来会稍微简单一点,可惜,从人类让一群人一起编程的那一刻起,这种人与人间的磨合和牵制就会一直存在,正所谓“有人的地方就有江湖”,身处江湖的人,多少会有点身不由己的磕磕绊绊,本文完!