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

推荐订阅源

NISL@THU
NISL@THU
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threatpost
Cloudbric
Cloudbric
H
Heimdal Security Blog
P
Privacy International News Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Tor Project blog
A
Arctic Wolf
W
WeLiveSecurity
SecWiki News
SecWiki News
S
Security Affairs
Schneier on Security
Schneier on Security
PCI Perspectives
PCI Perspectives
Simon Willison's Weblog
Simon Willison's Weblog
K
Kaspersky official blog
P
Privacy & Cybersecurity Law Blog
AWS News Blog
AWS News Blog
T
The Exploit Database - CXSecurity.com
V2EX - 技术
V2EX - 技术
AI
AI
Google DeepMind News
Google DeepMind News
Stack Overflow Blog
Stack Overflow Blog
博客园 - 司徒正美
有赞技术团队
有赞技术团队
C
Cybersecurity and Infrastructure Security Agency CISA
腾讯CDC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 聂微东
H
Hacker News: Front Page
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
The Hacker News
The Hacker News
阮一峰的网络日志
阮一峰的网络日志
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
月光博客
月光博客
博客园 - 【当耐特】
Recorded Future
Recorded Future
O
OpenAI News
Hacker News: Ask HN
Hacker News: Ask HN
Scott Helme
Scott Helme
N
News and Events Feed by Topic
Help Net Security
Help Net Security
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Y
Y Combinator Blog
Martin Fowler
Martin Fowler
小众软件
小众软件

元视角

.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 前,就定好这样一种规范,所有人都使用这个统一的组件,这个问题处理起来会稍微简单一点,可惜,从人类让一群人一起编程的那一刻起,这种人与人间的磨合和牵制就会一直存在,正所谓“有人的地方就有江湖”,身处江湖的人,多少会有点身不由己的磕磕绊绊,本文完!