面试题 Interview Question
实现一个简单的前端路由器,使得当 URL 哈希改变时,根据路由表执行对应的渲染函数。
Implement a simple client-side router that listens to URL hash changes and executes the corresponding rendering function based on a routing table.
要求 Requirements
-
路由初始化:创建一个
Router
函数,用于初始化路由表(一个 key-value 对象),键名为路由路径(不含#
),值为对应的回调函数(渲染视图或加载内容)。
Router Initialization: Create aRouter
function to initialize a routing table—an object where each key is a route path (without the#
) and each value is a callback function (which renders the view or loads content). -
哈希监听:使用
window.onhashchange
事件监听 URL 哈希(location.hash
)的变化,提取新的哈希片段并根据路由表执行对应的处理器函数。
Hash Listening: Use thewindow.onhashchange
event to listen for URL hash (location.hash
) changes, extract the new hash fragment, and invoke the corresponding handler function from the routing table. -
初始路由处理:在路由初始化时,也需调用一次路由处理函数以处理页面首次加载(如用户直接访问
index.html#about
的情况)。
Initial Route Handling: Upon router initialization, immediately invoke the route handler once to handle the initial load (e.g., when a user directly visitsindex.html#about
). -
404 处理:当哈希不匹配任何路由表中的路径时,执行一个默认的 404 渲染函数。
404 Handling: If the hash does not match any key in the routing table, execute a default 404-rendering function. -
纯函数式或类封装:不使用第三方库(如 React Router 等),可以使用纯函数式风格,不要依赖
window
和location
以外的全局状态。
Pure FP or Class Encapsulation: Do not use any third-party libraries (e.g., React Router). You may implement in a pure functional style, but avoid relying on global state rather thanwindow
andlocation
. -
动态路由支持(可选加分) :路由表的键可以是正则表达式或支持参数(如
product/:id
),需要能提取动态参数并传递给处理器函数。(此情形比较复杂,限于篇幅另起文章探讨)
Dynamic Route Support (Bonus) : Allow routing table keys to be regex patterns or parameterized routes (e.g.,product/:id
), and extract dynamic parameters to pass to the handler function.
参考答案 Reference Solution
// 路由表(键为无 # 前缀的路径,值为对应渲染函数)
const routes = {
'': () => {
document.getElementById('app').innerHTML = '<h1>Home Page</h1>';
},
'about': () => {
document.getElementById('app').innerHTML = '<h1>About Us</h1>';
},
'contact': () => {
document.getElementById('app').innerHTML = '<h1>Contact Us</h1>';
},
// ...可拓展更多静态路由
};
// 默认 404 渲染函数
const notFound = () => {
document.getElementById('app').innerHTML = '<h1>404 - Page Not Found</h1>';
};
// 路由处理函数:提取当前 hash,并执行匹配的处理器
function handleRoute() {
// 获取 hash 部分并去除前导 #
const hash = location.hash.slice(1) || '';
// 如果路由存在则执行,否则 404
(routes[hash] || notFound)();
}
// 初始化路由:绑定 hashchange 事件并处理初始路由
function initRouter() {
window.addEventListener('hashchange', handleRoute); // :contentReference[oaicite:10]{index=10}
handleRoute(); // 处理首次加载时的路由
}
// 调用初始化,启动路由器
initRouter();
- 解释 Explanation:
-
定义一个
routes
对象作为路由表,键 为无#
的路径(如''
、about
)、值 为渲染函数(更改 DOM 内容)
Define aroutes
object as the routing table, where the keys are paths without the#
(such as''
andabout
), and the values are rendering functions (which modify the DOM content). -
handleRoute
函数通过location.hash.slice(1)
获取当前哈希,如果为空则使用''
,再根据是否在routes
表中找到对应的函数来执行或调用notFound
ThehandleRoute
function gets the current hash usinglocation.hash.slice(1)
. If it's empty, it defaults to''
. Then, it checks whether a corresponding function exists in theroutes
table to execute it; otherwise, it callsnotFound
. -
initRouter
绑定hashchange
事件:一旦 URL 发生哈希变化,浏览器会触发此事件,执行handleRoute
,从而动态加载内容
TheinitRouter
function binds thehashchange
event: whenever the URL hash changes, the browser triggers this event, executeshandleRoute
, and dynamically loads the content. -
在绑定完成后立即调用
handleRoute()
以处理页面首次加载时的路由(用户直接访问包含哈希的 URL)
TheinitRouter
function binds thehashchange
event: whenever the URL hash changes, the browser triggers this event, causinghandleRoute
to run and dynamically load the corresponding content. -
不使用任何框架,依赖原生
window
、location
及addEventListener
等 API,满足纯函数式 / 轻量级路由需求
Without using any frameworks, it relies on native APIs likewindow
,location
, andaddEventListener
to fulfill a purely functional and lightweight routing solution.
示例 Example
以下示例展示一个具有导航链接、内容区域和默认 404 处理的最小化 HTML + JS 结构。
The following example demonstrates a minimal HTML + JS structure with navigation links, a content area, and default 404 handling.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple SPA Router</title>
</head>
<body>
<!-- 导航栏 / Navigation Bar -->
<nav>
<a href="#/">Home</a>
<a href="#/about">About</a>
<a href="#/contact">Contact</a>
<a href="#/nonexistent">Invalid</a> <!-- 测试 404 / Test 404 -->
</nav>
<!-- 内容区域 / Content Area -->
<div id="app"></div>
<!-- 路由脚本 / Router Script -->
<script>
const routes = {
'': () => {
document.getElementById('app').innerHTML = '<h1>Home Page</h1>';
},
'about': () => {
document.getElementById('app').innerHTML = '<h1>About Us</h1>';
},
'contact': () => {
document.getElementById('app').innerHTML = '<h1>Contact Us</h1>';
}
};
const notFound = () => {
document.getElementById('app').innerHTML = '<h1>404 - Page Not Found</h1>';
};
function handleRoute() {
const hash = location.hash.replace(/^#/?/, '') || ''; // 移除 "#/" 前缀
(routes[hash] || notFound)();
}
function initRouter() {
window.addEventListener('hashchange', handleRoute); //
handleRoute(); // 初始加载 / Initial load
}
initRouter();
</script>
</body>
</html>
- 说明 Explanation:
-
<a href="#/">Home</a>
等导航链接将 URL 哈希更改为#/
、#/about
、#/contact
等,由浏览器触发hashchange
事件
Navigation links like<a href="#/">Home</a>
change the URL hash to#/
,#/about
,#/contact
, etc., which triggers thehashchange
event in the browser. -
handleRoute
中通过正则replace(/^#/?/, '')
去除#
或#/
前缀,得到纯路径字符串用作路由表的键
InhandleRoute
, the regular expressionreplace(/^#/?/, '')
is used to remove the#
or#/
prefix, resulting in a clean path string to be used as the key for the routing table. -
对于无匹配的哈希(如
#/nonexistent
),调用notFound()
渲染 404 页面
For unmatched hashes (such as#/nonexistent
), thenotFound()
function is called to render the 404 page. -
initRouter
在页面加载时立即执行一次handleRoute()
,确保首次访问 URL(带哈希或不带)时正确渲染
initRouter
immediately callshandleRoute()
once when the page loads to ensure the correct rendering on the initial visit, whether the URL contains a hash or not.
面试考察点 Interview Focus
-
前端路由基础:理解 SPA 路由原理,包括如何使用
location.hash
及hashchange
事件来管理浏览器历史记录和视图切换
Client-Side Routing Fundamentals: Understand the principles of SPA routing, including how to uselocation.hash
and thehashchange
event to manage browser history and view transitions. -
事件监听:熟练使用
window.addEventListener('hashchange', handler)
或window.onhashchange = handler
,并理解两者区别及兼容性
Event Listener Usage: Be proficient in usingwindow.addEventListener('hashchange', handler)
orwindow.onhashchange = handler
, and understand the differences and compatibility between the two approaches. -
路由表映射:能够设计一个灵活的路由表,将路径映射到渲染函数或组件,处理静态路由和(可选)动态路由(正则或参数化)
Routing Table Mapping: Be able to design a flexible routing table that maps paths to rendering functions or components, handling both static routes and (optionally) dynamic routes (using regular expressions or parameterization). -
404 处理:在没有匹配路径时提供默认的 404 渲染方案,保持用户体验完整性
404 Handling: Provide a default 404 rendering solution when no matching path is found, ensuring a consistent and complete user experience. -
首次加载:在
hashchange
绑定后,还必须手动调用一次路由处理函数,以支持用户直接通过 URL 访问含哈希的页面
Initial Load Handling: After binding thehashchange
event, you must also manually call the routing handler once to support users accessing a hash-based URL directly. -
代码结构与可扩展性:使用纯函数式设计以提高可读性、可维护性,同时支持动态添加路由、动态视图加载或懒加载等
Code Structure & Extensibility: Use a purely functional design to improve readability and maintainability, while also supporting dynamic route addition, dynamic view rendering, or lazy loading. -
性能与用户体验:理解为什么在高频切换时要避免重复重绘 DOM,以及如何在触发路由更改时只重新渲染必要部分,以提高 SPA 性能
Performance & UX: Understand why it's important to avoid redundant DOM re-rendering during frequent route changes, and how to re-render only the necessary parts when a route changes to improve SPA performance. -
浏览器历史管理:了解
hash
机制与 HTML5 History API(pushState
/popState
)的区别与适用场景,为后续实现基于 History 的伪路径路由打基础
Browser History Management: Understand the differences between thehash
mechanism and the HTML5 History API (pushState
/popState
), along with their appropriate use cases, to lay the groundwork for implementing History-based pseudo-path routing later on.