从前看《流浪地球》要去 A 影院,看《奥本海默》要跑到 B 影院,换一部电影就得换一栋楼——这是多页应用时代。如今城市里建起了“影院综合体”:一楼是总服务台,楼上几十个影厅,无论你看哪部片,都从同一扇大门进。观众在大楼里从 2 号厅挪到 5 号厅,不需要把大楼拆了重建。这个“换厅但不重建”的机制,就是前端路由。 放映员(Router)手里有一张“路由总览图”,清清楚楚写着每个路径对应哪个影厅;有的厅在门口贴一张“手写票根”(Hash 模式),有的靠“电子票务系统”(History 模式)来管理进出记录;VIP 厅里还套着包间(嵌套路由);每个门口站着检票员(路由守卫)查验身份。你只要学会调度这一栋“影院”的资源,就能让用户以为自己走进了一家全新的影院,而你的前端应用只是轻轻拨动了一个切换开关。

岗位关联

对于 AI 从业者而言,前端路由是驱动智能交互界面的“神经切换中枢”:无论是 AI 助手多页面任务流、大模型应用的流式会话页,还是 AI 平台的权限面板,都依赖路由来完成视图编排与状态衔接。掌握它,你就能把“AI 能力”顺畅地“装”进用户的每一步操作里。

目录
  1. 可视化演示
  2. 路由基本认知
  3. Hash 模式
  4. History 模式
  5. 嵌套路由
  6. 路由守卫
  7. 优化策略
  8. 常见陷阱
  9. 框架选型
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Hash与History两种路由模式的工作原理、嵌套路由树结构以及导航守卫的执行流程,直观理解前端路由的核心机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Hash路由与History路由对比 Hash路由通过URL片段(#)变化触发路由,兼容性好无需服务器配置 History路由使用pushState/replaceState API,URL更美观但需服务器支持 Hash: url#/path | History: pushState(state,url), server_fallback

路由基本认知

建影院综合体之前,开发商得先想明白:一栋楼里那么多影厅,观众凭什么在楼里就能找到想看的电影?答案是“总览图”。大厅墙上贴着一张图:2 号厅放什么片、5 号厅放什么片,一目了然。这张总览图就是路由表(Router Table),而观众从大门走到对应影厅的过程,就是一次路由跳转。

在多页应用时代,每个页面都是一栋独立影院。用户点一个链接,浏览器就要拆掉旧楼、盖一栋新楼——整页刷新。SPA(单页应用)不一样:整个应用只有一栋楼(一个 HTML 入口),用户切换页面时,放映员只需要查总览图,把观众引导到另一个影厅,楼体原封不动。这个“换厅但不重建”的角色,就是前端路由。

技术上的翻译是:路由表把 URL 路径和组件建立起映射关系,当地址栏的路径变化时,路由去表里查一下该渲染哪个组件,然后把它放进页面上的“渲染出口”(如 Vue 的 <router-view>)。以 Vue Router 为例,路由表长这样:{ path: '/home', component: Home }。地址变了,组件就换了,但浏览器没有整页刷新。

理解到这里,你已经看清了影院综合体的骨架:一栋楼、一张图、若干影厅。接下来要解决的问题是:观众手里的“票”怎么标记厅号?Hash 和 History 就是两种截然不同的“票根”。

View=RouterTable(Path)\displaystyle \text{View} = \text{RouterTable}(\text{Path})
这句话的意思是:当前路径走进路由表,查出对应的视图组件,渲染到屏幕上。路由表就是影院的“总览图”,视图就是观众最终坐进的那个影厅。
  1. RouterTable路由表是一张把路径映射到组件的“总览图”。
    RouterTable:Path→View\text{RouterTable}: \text{Path} \rightarrow \text{View}
  2. Path地址栏里的路径,相当于观众报出的“厅号”。
    Path=location.pathname 或 hash 片段\text{Path} = \text{location.pathname 或 hash 片段}
  3. View匹配到的组件,是最终渲染在页面上的内容。
    View=RouterTable(Path)\text{View} = \text{RouterTable}(\text{Path})

Hash 模式

最早的影院为了方便熟客,发明了一种“手写票根”:在影厅门口贴一张便利贴,上面写着当前场次。观众不看门牌号,只看便利贴上的字。Hash 模式就是这张便利贴——URL 里 # 后面的内容(如 /#/home)就是便利贴上的字。

这个方案妙在:便利贴不会被总台收走。浏览器向服务器发请求时,# 后面的片段(hash)根本不会出现在 HTTP 包里,服务器看到的永远只有 # 之前的部分。所以无论便利贴上写什么,都不会惊动后端;刷新页面时,浏览器也永远能靠这张便利贴找到原来的厅。

前端实现只需要盯住两个东西:一是 location.hash,它随时能读出当前的路径;二是 hashchange 事件,只要 hash 一变,路由就重新查表渲染。很多框架(比如 Vue Router 的默认模式)正是基于这个机制:简单、稳定、不需要服务器做任何额外配置。

手写票根虽然灵活,但有个短板:便利贴贴在门口,外人看门面觉得不够“正规”,搜索引擎也难读到里面的内容。于是更讲究的影院换用了电子票务系统——这就是 History 模式。

path=hash[1:]\displaystyle \text{path} = \text{hash}[1:]
hash 以 # 开头,去掉第一个字符后剩下的就是路由路径。比如 #/movie/1 去掉 # 得到 /movie/1。
  1. hashURL 中 # 及其后面的字符串,是挂在影厅门口的便利贴。
    hash=location.hash\text{hash} = \text{location.hash}
  2. [1]:从第二个字符开始取,即扔掉开头的 # 号。
    hash[1:]=去掉 # 后的路径\text{hash}[1:] = \text{去掉 \# 后的路径}
  3. path真正的路由路径,供路由表匹配。
    path=hash[1:]\text{path} = \text{hash}[1:]

History 模式

正规影院售票用的是电子票务系统:每一张票都记在系统里,观众可以前进、后退、翻看历史记录。History 模式就是让浏览器用 History API 来管理页面状态——pushState 和 replaceState 可以把地址栏改写成任意路径,而不触发整页刷新。

它的工作流像这样:用户点击“去购物车厅”,路由调用 history.pushState({}, '', '/cart'),地址栏立刻变成 /cart;接着路由内部完成组件切换。而当用户点击浏览器后退按钮时,系统触发 popstate 事件,路由监听到后,再根据当前地址渲染对应的影厅。整套流程下来,观众的“足迹”被完整记录,体验就像在真正的电子票务系统里走来走去。

不过电子票务系统有个代价:它在总部(服务器)留痕。用户直接访问 https://site.com/cart 时,浏览器会真的向服务器发起一次 /cart 请求——如果服务器没有配置“所有路径都回退到入口页”,就会回一个 404。这就是 History 路由部署时最需要留意的地方。

Hash 是贴便利贴,History 是开电子票,各有各的江湖地位。但无论哪种模式,影院里都可能出现更复杂的结构:VIP 厅里套着包间,包间里还有休息室。这就是嵌套路由要解决的场景。

URLnew=pushState(URLold),popstate→render(URL)\displaystyle \text{URL}_{\text{new}} = \text{pushState}(\text{URL}_{\text{old}}), \quad \text{popstate} \rightarrow \text{render}(\text{URL})
pushState 负责“改地址但不刷新”,popstate 负责“听到前进/后退就重新渲染”。前者是售票员开票,后者是检票系统响铃提醒。
  1. pushState把浏览器的地址栏改成一个新路径,不触发刷新。
    URLnew=pushState(URLold)\text{URL}_{\text{new}} = \text{pushState}(\text{URL}_{\text{old}})
  2. popstate用户点击前进或后退时,浏览器触发的事件。
    popstate→路由重新解析地址\text{popstate} \rightarrow \text{路由重新解析地址}
  3. render根据新 URL 渲染对应的组件视图。
    render(URL)=RouterTable(URL)\text{render}(\text{URL}) = \text{RouterTable}(\text{URL})

嵌套路由

你有没有见过这样的影院:一个 VIP 厅,进门是休息区,休息区里又分了两个小包间?前端应用也常见这种“厅中有厅”的结构。比如用户中心页面 /user,进去之后左侧是资料卡,右侧可能是 profile(个人资料)或 orders(订单记录)。这两个子页面共享同一个父布局,只是小包间里的内容在变。

嵌套路由的实现思路是:路由表不再是一张扁平的大总览图,而是“总图套分图”。父路由用 children 字段挂子路由,子路由的组件渲染在父组件预留的第二个出口里(同一个 <router-view> 可以嵌套使用)。比如父路由 /user 对应 UserLayout 组件,子路由 profile 对应 UserProfile,访问 /user/profile 时,外层是布局、内层是资料卡。

关键规则在于子路径的书写方式:子路由的 path 通常不以 / 开头,而是写相对路径。因为真正访问的完整路径是由父路径和子路径拼接出来的——就像影院里的包间号,一定是“VIP 厅 + 包间编号”合在一起才有意义。

厅中有厅意味着管理更复杂,也意味着安全更重要。VIP 厅不是谁都能进的——门口必须有人把守,这就是路由守卫。

fullPath=parentPath+’/’+childPath\displaystyle \text{fullPath} = \text{parentPath} + \text{'/'} + \text{childPath}
父路径搭骨架,子路径填内容。/user 加 profile 拼成 /user/profile,完整地定位到“用户中心的个人资料包间”。
  1. parentPath父路由的路径,像是上位厅号。
    parentPath=′/user′\text{parentPath} = '/user'
  2. childPath子路由的相对路径,只要写厅内小包间的编号。
    childPath=′profile′\text{childPath} = 'profile'
  3. fullPath拼接后的完整路径,浏览器地址栏里看到的最终地址。
    fullPath=′/user′+′/′+′profile′=′/user/profile′\text{fullPath} = '/user' + '/' + 'profile' = '/user/profile'

路由守卫

影院综合体开张后,总得有人管秩序。有些影厅是 VIP 厅,只有持特定会员卡的人能进;有些厅放的是限级片,未成年人不能入场。路由守卫就是站在每个厅门口的检票员,专门负责“查票、放行、劝返”。

在 Vue Router 中,守卫分为三类。全局守卫(beforeEach)是每场放映前都要做的例行检查——观众从任何厅换到任何厅,都得过他这一关。独享守卫(beforeEnter)只在进入某一个特定路由时生效,相当于只守一个厅的专属保安。组件内守卫(beforeRouteEnter / beforeRouteLeave)则是厅内服务员自己留个心眼:进来前确认座位,离开前提醒观众别落东西。

守卫的核心工作流程是一个“放行决策”:检票员拿到目标路径 to 和当前路径 from,检查条件,然后调用 next() 或 next('/login')。前者是挥手放行,后者是礼貌地把你引去另一个厅。权限拦截的逻辑就藏在这里:比如后台管理的路由设置一个全局守卫,发现没有登录凭证时,一律重定向到登录页。

检票员保证了安全,但影院那么大,如果所有影厅的片子都同时准备好,灯光、空调、人力都吃不消。聪明的影院会算一笔账:热门厅提前升温,冷门厅等观众进门再开设备——这就是路由的性能优化。

allow=⋀g∈Guardsg(to,from)\displaystyle \text{allow} = \bigwedge_{g \in \text{Guards}} g(\text{to}, \text{from})
这表示所有守卫都要点头,观众才能进场。只要有一个检票员说“不许进”,这次跳转就会被拦住或改道。
  1. Guards当前路由链路上的所有守卫集合,是一排检票员。
    Guards={g1,g2,…,gn}\text{Guards} = \{g_1, g_2, \dots, g_n\}
  2. g(to, from)每个守卫对“要去哪”和“从哪来”做一次判断。
    g(to,from)=true 或 false\text{g}(\text{to}, \text{from}) = \text{true 或 false}
  3. 逻辑与所有守卫都放行,最终才放行。
    ⋀g∈Guardsg(to,from)=true\bigwedge_{g \in \text{Guards}} g(\text{to}, \text{from}) = \text{true}

优化策略

影院综合体有一百个影厅,如果每天早上把所有厅的空调、放映机、灯光全部打开,电费高得惊人。更聪明的做法是:观众走进哪一区,再开哪一区的设备。前端也一样,如果把所有页面的代码都打进同一个 JS 文件,首屏加载会非常慢——这就是“全厅预热”的代价。

优化武器是代码分割(Code Splitting)。借助动态 import() 语法,路由可以按需加载:访问 /movie 时才去下载电影详情页的代码块(chunk),而不是一开始全下载。Vue Router 和 React Router 都支持这种写法——把组件定义从“静态导入”改成“动态导入”,打包工具就会自动把每个路由对应的代码拆成独立文件。

更进一步,还能做预加载(Prefetch):当用户把鼠标悬停在某个导航链接上时,浏览器抢先下载对应路由的 chunk。这就像影院引导员远远看到观众朝走廊走来,就先按开那个厅的灯。首屏只加载必要代码,后续页面按需就位,整个应用的启动速度和切换手感都会显著提升。

设备和电力优化好了,影院就能高效运转。但运营久了,总会遇到一些让人哭笑不得的翻车现场:观众拿着票根直接跑到影厅,却发现系统不认这张票;或者散场后观众走错了门,找不到自己的座位。这些坑,就是常见路由陷阱。

T首屏=T(chunkentry),bundle=chunkentry+∑i=1nchunki\displaystyle T_{\text{首屏}} = T(\text{chunk}_{\text{entry}}), \quad \text{bundle} = \text{chunk}_{\text{entry}} + \sum_{i=1}^{n}\text{chunk}_{i}
用户第一眼看到的只是入口 chunk 的渲染时间;剩下的 chunk 在用户后续进入对应影厅时才陆续加载。总包被切成了“入口 + 一堆按需加载的小包”。
  1. chunk_entry入口代码块,是影院的门厅,首屏只渲染它。
    chunkentry=应用启动必需的代码\text{chunk}_{\text{entry}} = \text{应用启动必需的代码}
  2. chunk_i按路由拆分的代码块,每一个对应一个影厅。
    chunki=第 i 个路由页面的代码\text{chunk}_{i} = \text{第 i 个路由页面的代码}
  3. T(首屏)首屏时间只取决于入口块,而不是整栋楼的所有代码。
    T首屏=T(chunkentry)T_{\text{首屏}} = T(\text{chunk}_{\text{entry}})

常见陷阱

影院开业三个月,投诉电话不断。第一种投诉:观众在楼下看到 https://site.com/movie/7 的广告,直接输进地址栏访问,影厅却回了一句“查无此厅”——刷新 404。这正是 History 模式的经典坑:浏览器真的请求了 /movie/7,服务器没有这个物理文件,也没配置回退规则,于是返回 404。解法是让服务器把所有路径都 rewrite 到入口页 index.html,把“找厅”这件事交给前端路由。

第二种投诉:观众从首页逛到电影详情页,看完想返回,一按后退键,页面数据回来了,但页面滚动的位置停在半山腰——滚动位置丢失。多页时代浏览器天然记住了每个页面的滚动条位置,SPA 里必须手动管理:路由切换时记录 scrollTop,切回来时恢复。

第三种投诉更隐蔽:两张票重号了。路由表里同时定义了 /movie/:id 和 /movie/new,当访问 /movie/new 时,new 会被当成 id 匹配到详情页。路由表的匹配顺序和参数命名稍不注意,就会“抢厅”。此外,路由传参时也要小心:用 query 传对象时刷新可能丢数据,重要参数应拼在路径里而不是塞在内存中。

趟过这些坑之后,影院经营者开始思考:我到底该用哪套票务系统?是自己开发一套,还是用成熟的第三方系统?这就是框架选型。

Resp(p)={index.htmlp∈Rewrite404otherwise\displaystyle \text{Resp}(p) = \begin{cases} \text{index.html} & p \in \text{Rewrite} \\ 404 & \text{otherwise} \end{cases}
服务器对任意路径的处理规则:命中了回退白名单,就返回入口页面;没命中,才真的给 404。这是 History 模式部署时的“保命条款”。
  1. Resp(p)服务器对某个路径 p 的响应结果。
    Resp(p)=服务器返回的内容\text{Resp}(p) = \text{服务器返回的内容}
  2. Rewrite回退规则集合,相当于“允许哪些路径走前端路由”。
    p∈Rewrite⇒返回 index.htmlp \in \text{Rewrite} \Rightarrow \text{返回 index.html}
  3. otherwise没有进入回退规则的路径才返回 404。
    Resp(p)=404(p∉Rewrite)\text{Resp}(p) = 404 \quad (p \notin \text{Rewrite})

框架选型

不同影院集团有各自的运营体系,但本质都是“换厅不重建”。前端框架们也是如此:Vue Router、React Router、Angular Router 都能实现路由,但风格和生态各异。

Vue Router 是 Vue 官配,API 简洁,支持嵌套路由、守卫、懒加载一条龙,文档详尽,最适合快速搭起一座标准化的“影院连锁”。React Router 以组件化著称,路由即组件,灵活性极高,适合大团队按需定制复杂的“展厅动线”。Angular Router 和 Angular 深度绑定,提供依赖注入、路由快照等企业级能力,适合大型“演艺中心”级别的复杂项目。Svelte 的路由则走轻量路线,没有太多历史包袱,适合追求极致性能的“迷你影院”。

选型时不能只看名气,要算一笔综合账:团队熟悉哪个框架(学习成本)、项目是否需要深度嵌套和守卫(复杂度)、SEO 要求高不高(模式选择)、有没有后端配合改配置(History 回退)。把这些维度加权求和,才能得出适合自己项目的答案。

从一张总览图到一整套选型方法论,前端路由的本质始终没变:让用户在同一个应用里流畅地“换厅”,而开发者只需要设计好路线图、安排好检票员、控制好每一场的设备开销。影院建得再大,规则清晰,观众就不会迷路。

Score(S)=∑k=1nwk×fitk(S)\displaystyle \text{Score}(S) = \sum_{k=1}^{n} w_{k} \times \text{fit}_{k}(S)
对每个候选方案 S,把“团队成本、功能匹配、部署难度”等维度分别打分,乘以权重后求和。得分最高者,就是最适合你这家影院的票务系统。
  1. S一个候选的框架方案,比如 Vue Router。
    S∈{Vue Router,React Router,Angular Router}S \in \{\text{Vue Router}, \text{React Router}, \text{Angular Router}\}
  2. fit_k(S)方案 S 在第 k 个维度上的契合度打分。
    fitk(S)∈[0,1]\text{fit}_{k}(S) \in [0, 1]
  3. w_k第 k 个维度的权重,反映这个维度对项目有多重要。
    ∑k=1nwk=1\sum_{k=1}^{n} w_{k} = 1
  4. Score(S)加权求和后的总分,用于横向对比。
    Score(S)=∑k=1nwk×fitk(S)\text{Score}(S) = \sum_{k=1}^{n} w_{k} \times \text{fit}_{k}(S)

AI 应用场景

场景一:AI 智能检票员——动态权限路由

阶段:用户画像与智能决策

解决的问题:传统权限路由是写死的“黑白名单”,用户角色一变,路由表就得发版更新。AI 根据用户历史行为、会员等级、风险标签实时计算访问权限,路由守卫配合 AI 接口动态放行或重定向。

具体案例:某 AI 创作平台的“高级模型”页面,普通用户访问时被守卫拦下并跳转到升级页;当 AI 判定该用户近期活跃度达标,自动发放 7 天体验券,下一次路由跳转时 AI 接口返回放行,用户无需手动刷新就看到了新页面。

场景二:AI 引导员——智能路由预取

阶段:行为预测与资源调度

解决的问题:路由懒加载虽然省了首屏流量,但用户点击后才加载仍会有短暂白屏。AI 通过用户点击序列预测下一个最可能访问的页面,提前触发动态 import,让“预加载”跑在用户手指落下之前。

具体案例:视频网站的“猜你喜欢”推荐位,AI 预测用户大概率会点第三个视频,于是提前 import() 该视频详情页的 chunk;用户真正点击时,页面几乎瞬时渲染,白屏时间从 300ms 降到 30ms。

场景三:AI 观众动线分析——路由事件流

阶段:数据采集与行为建模

解决的问题:传统埋点只能看到“页面停留时长”,但 SPA 内部的路由切换才是用户真实动线。把路由切换、守卫拦截、参数变化作为事件流接入 AI 分析平台,可以还原用户在“影院”里的完整路径。

具体案例:电商平台发现大量用户在 /cart 到 /checkout 之间被守卫重定向回 /login,AI 分析拦截率后推断出登录流程过长,产品团队据此把免密登录前置,转化率提升 12%。

Python 演示

python74 行
import refrom dataclasses import dataclassfrom typing import Callable, List, Optional  @dataclassclass Route:    path: str    handler: Callable  class Router:    def __init__(self):        self._routes_list: List[Route] = []        self._before_guard = None     def route(self, path: str):        def decorator(func):            self._routes_list.append(Route(path, func))            return func        return decorator     def guard(self, fn):        self._before_guard = fn     def match(self, path: str) -> Optional[Callable]:        for route in self._routes_list:            pattern = route.path.replace("<id>", r"(?P<id>\d+)")            if re.fullmatch(pattern, path):                return route.handler        return None     def navigate(self, path: str):        if self._before_guard and not self._before_guard(path):            print(f"守卫拦截: {path}, 重定向到 /login")            return None        handler = self.match(path)        if handler is None:            print(f"404: 未找到 {path}")            return None        return handler()  router = Router()current_user = {"login": False}  @router.guarddef login_required(path):    if path == "/cart":        return current_user["login"]    return True  @router.route("/")def home():    return "首页视图"  @router.route("/movie/<id>")def movie_detail(id):    return f"电影详情视图: {id}"  @router.route("/cart")def cart():    return "购物车视图"  print(router.navigate("/"))print(router.navigate("/movie/12"))print(router.navigate("/cart"))current_user["login"] = Trueprint(router.navigate("/cart"))
目的:用一个迷你 Python 路由器模拟前端路由的核心机制——路由表注册、路径参数匹配和全局守卫拦截,帮助你直观理解 Hash/History 路由背后的“查表-守卫-渲染”三步曲。
  1. self._routes_list: List[Route] = []路由表用列表存储,每个路由条目包含路径和对应的处理函数。
  2. def route(self, path: str)装饰器方法,相当于 Vue Router 里声明一个路由,把 handler 挂到路由表上。
  3. pattern = route.path.replace("<id>", r"(?P<id>\d+)")把路径参数 <id> 转成正则命名组,实现 /movie/12 这种动态路由。
  4. if re.fullmatch(pattern, path)对当前 URL 逐个匹配路由表,命中即进入渲染流程。
  5. def navigate(self, path: str)路由跳转入口,先过守卫,再查表,最后调用组件。
  6. @router.guard` / `def login_required一个简易全局守卫:访问购物车必须登录,否则重定向。
  7. print(router.navigate("/movie/12"))模拟一次路由跳转,输出渲染结果。

常见误区

误区很多初学者觉得 Hash 模式地址栏带个 `#` 很丑,History 模式干干净净,所以 History 一定更高级,换掉就完事了,不需要考虑别的。

正解两者最大的差异不在颜值,而在“是否惊动服务器”。Hash 的 # 后面内容不会发给服务器,刷新页面时前端自己就能恢复状态;History 的路径会真正请求服务器,如果服务器没有配置回退到 index.html,用户直接访问子路径就会 404。手写票根不需要总部备案,电子票必须总部承认。

为何常见:本地开发时两种模式都能跑,因为开发服务器默认做了回退;一旦部署到生产环境,服务器配置差异立刻暴露,初学者往往没经历这一步。

误区有人以为 `history.pushState` 就像按下遥控器换台,地址一变,Vue/React 就会自动感应并切换页面。

正解pushState 本身不触发任何事件,它只是默默改了地址栏。前端路由必须主动调用自己的 router.push(),在方法内部完成“改地址 + 触发渲染”两个动作;监听 popstate 只是为了捕捉浏览器自带的前进/后退键。就像电子票务改了座位号,放映员不知道,得有人喊他一声。

为何常见:原生 History API 和框架路由器的概念混在一起,误以为浏览器会替框架“包办一切”。

误区写嵌套路由时,习惯性地在父组件里 `import` 子组件,再写进父组件的 `<template>`,结果发现子路由怎么都渲染不出来。

正解嵌套路由的子组件不需要父组件手动引入,而是由子路由配置决定。父组件只需要留一个 <router-view> 出口,路由匹配到子路径时,框架会把子组件自动“塞”进这个出口。包间服务员由总台统一派,不用父厅自己招人。

为何常见:习惯了组件嵌套的思维,把“组件层级”和“路由层级”混为一谈,其实两者一个是物理结构,一个是调度逻辑。

误区有人写了个全局登录守卫,测试跳转时机正常,然后刷新页面直接进了后台,以为刷新能“绕过”守卫。

正解刷新页面时,应用会重新加载并解析当前 URL,路由初始化阶段同样会执行全局守卫。如果刷新后直接进入受保护页面,通常是因为守卫里读不到登录状态(比如 token 存在内存中),等于检票员手里没有花名册,不是他没上班。

为何常见:把“刷新”误当成浏览器的独立行为,忽略了 SPA 中刷新本质上是“重新启动一次路由系统”。

面试题

Q1. Hash 路由中 # 后面的内容在 HTTP 请求中会被如何处理?

单选题 · easy

  • A.作为查询参数发送
  • B.完整发送给服务器
  • C.不会发送给服务器← 正确答案
  • D.以 Cookie 方式发送

答案:C。# 后的 hash 片段只在浏览器端可见,服务器收到的请求中不包含它。这就像影厅门口的便利贴只服务于观众自己,不会交到总台手上。A 是 query 的行为,B 与事实相反,D 与 Cookie 机制无关。

Q2. Vue Router 的 hash 模式下,URL 形态通常是?

单选题 · easy

  • A.https://a.com/#/home← 正确答案
  • B.https://a.com/home
  • C.https://a.com?home
  • D.https://a.com:home

答案:A。hash 模式在路径后加 # 再写路由路径。B 是 History 模式的结果,C 是 query 写法,D 是端口写法,都不符合 hash 路由形态。

Q3. History 路由中,点击浏览器后退按钮会触发哪个事件?

单选题 · mid

  • A.hashchange 事件
  • B.popstate 事件← 正确答案
  • C.pushState 事件
  • D.historychange 事件

答案:B。浏览器后退/前进时会触发 popstate 事件,路由监听它来重新渲染。hashchange 属于 hash 模式;pushState 是主动调用而非被动触发;historychange 并不是标准事件。

Q4. 嵌套路由中,子路由的 path 通常如何书写?

单选题 · mid

  • A.以“/”开头表示绝对路径
  • B.以“子”开头表示子组件路径
  • C.不以“/”开头由父路径拼接← 正确答案
  • D.以“#”开头表示 hash 哈希

答案:C。子路由写相对路径,最终 URL 由父路径和子路径拼接,如父 /user 加子 profile 得到 /user/profile。以 / 开头会变成独立顶级路径,# 是 hash 标记,B 的“子”字没有实际语义。

Q5. 全局前置守卫中,调用 next() 的作用是?

单选题 · easy

  • A.停止本次路由跳转
  • B.放行到目标路由← 正确答案
  • C.重新加载当前页面
  • D.清空当前路由表

答案:B。next() 表示检票员放行,让观众顺利进入目标路由。不调用 next 或调用 next(false) 是拦截;next('/login') 是重定向。A、C、D 都不是 next() 的默认行为。

Q6. 以下哪些场景适合使用路由懒加载?

多选题 · mid

  • A.应用首屏体积过大← 正确答案
  • B.低频访问的二级页面← 正确答案
  • C.需要每秒刷新的数据页
  • D.实时推送的聊天组件

答案:AB。懒加载适合“不着急展示”的代码。首屏体积大时按路由拆包能显著提速;低频二级页面等用户访问时再加载最划算。C、D 属于高频核心功能,拆出去反而增加每次切换的等待成本。

Q7. History 模式部署时,服务器对未匹配路径应如何配置?

单选题 · hard

  • A.返回 404 页面提示刷新
  • B.重定向到入口页面← 正确答案
  • C.重定向到服务器首页
  • D.重定向到登录首页

答案:B。SPA 只有唯一入口,服务器应把所有路径 rewrite 到 index.html,让前端路由接管解析。返回 404 会让用户刷新后白屏;重定向到服务器首页或登录页都会丢失原始路径,导致路由无法恢复现场。

Q8. 下列关于路由守卫的说法正确的有?

多选题 · hard

  • A.全局守卫每次跳转会执行← 正确答案
  • B.独享守卫只对单路由生效← 正确答案
  • C.组件内守卫可监听离开路由← 正确答案
  • D.守卫不能进行路由重定向操作

答案:ABC。全局守卫像总检票口,每次跳转都会经历;独享守卫只守某个特定路由;组件内 beforeRouteLeave 可以监听离开,还能弹窗提示未保存内容。D 错误,守卫通过 next('/login') 恰恰是最常见的重定向手段。

Q9. 嵌套路由在渲染时,呈现父子层级结构的关键是?

单选题 · hard

  • A.父子组件嵌套渲染← 正确答案
  • B.路由表写在同一行
  • C.使用多个路由器实例
  • D.关闭应用路由懒加载

答案:A。嵌套路由的本质是在父组件里再放一个渲染出口,子路由的组件渲染在出口中,形成视觉和结构上的嵌套。路由表写在同一行只是写法,多个路由器实例会导致状态割裂,懒加载与层级结构无关。

Q10. 要设计一套适合大型应用的 History 路由方案,需要综合考虑?

多选题 · extreme

  • A.服务器回退规则配置← 正确答案
  • B.登录权限与路由守卫← 正确答案
  • C.路由级代码分包加载← 正确答案
  • D.浏览器是否支持 Flash

答案:ABC。大型应用选 History 模式后,服务器要配置回退避免刷新 404;权限系统需要守卫配合防止越权;代码量大还必须做路由级分包保证首屏速度。D 与路由无关——现代前端框架早已和 Flash 无关,属于常识性干扰项。