背景
SPA单页面应用实际是只有一个HTML文件,路由的切换都是通过JS动态切换组件的显示与隐藏,MPA应用拥有多个HTML文件。
Vue-Router和React-Router的原理类似,本文通过React-Router举例
路由模式分类
- history模式
- hash模式
前置知识history
History提供了浏览器会话历史的管理能力。通过history对象,开发者可以:
- 通过使用
go,back和forward在会话历史中导航,目标会话历史条目如果是通过传统方式跳转的,如直接修改window.location.href则会刷新页面。如果是通过pushState和replaceState修改的则不会修改刷新页面。
- 通过使用
pushState和replaceState添加和替换当前的会话历史,不会刷新页面。但是新url和旧url必须是同源的。
pushState和replaceState是HTML5新特性。
| API | 定义 |
|---|---|
| history.pushState(data, title [, url]) | pushState主要用于往历史记录堆栈顶部添加一条记录 。各参数解析如下:①data 会在onpopstate事件触发时作为参数传递过去;②title 为页面标题,当前所有浏览器都会忽略此参数;③url为页面地址,可选,缺少时表示为当前页地址 |
| history.replaceState(data, title [, url]) | 更改当前的历史记录,参数同上; 上面的pushState是添加,这个更改 |
| history.state | 用于存储以上方法的data数据,不同浏览器的读写权限不一样 |
| window.onpopstate | 修改当前会话历史条目时候,都会触发这个回调函数 |
注意⚠️的是 :用history.pushState() 或者 history.replaceState() 不会触发popstate事件。(区分出监听路由和改变路由的区别)
hash模式原理
一句话总结:监听url中hash的变化,来做页面组件的显示与隐藏。特点是在url中有#美观程度低。
关于hash需要知道的三点是:
- url中的hash变化不会导致页面重新加载。
- url中的hash变化会被window.history记录下来,当使用浏览器的页面的前进 和后退功能,是可以看到url中hash的变化的。
- 当通过url向服务端请求资源的时候,url中hash是不会传递给服务端的。hash路由模式是纯前端实现的路由跳转。
改变路由
通过window.location.hash可以获取到url的hash值,对应着项目的页面路径。
监听路由
通过hashchange事件监听url的hash的变化,也就是项目页面路径的变化。
javascript
window.addEventListener("hashchange",callback)
history模式原理
一句话总结:利用HTML5推出的pushState和replaceState改变url路径,而不会触发页面的刷新,通过监听popState事件来实现项目的页面组件切换。
改变路由
1.pushState:向会话历史中压入一个新的会话历史记录,相当于入栈。
2.replaceState:更改当前的会话记录历史 ,相当于更新栈顶元素。
监听路由
popState事件 这个名字起的不好,他是一个监听事件,当会话历史记录发生变化 就会回调出发(前进和后退都会触发),单看名字好像是一个可以主动调用的方法,极具迷惑性。
javascript
window.addEventListener("popstate",callback);
改变路由和监听路由的作用
- 改变路由 ,开发者通过React-Router提供的组件或者API主动的进行页面切换时使用,强调的是开发者主动。
- 监听路由 ,当用户使用手动修改浏览器地址栏中
URL,手动点击浏览器的前进后退按钮的方式改变URL的时候,项目需要监听到这些URL路径改变的操作,从而做出响应,强调的是外部变化,被动响应 。
React-Router基础的路由结构
xml
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Redirect from="/old" to="/new" />
<Route path="/new" component={NewPage} />
<Route component={NotFound} />
</Switch>
</Router>
Router组件
javascript
// 在 Router 组件内部:
componentDidMount() {
// 1. 监听 history 变化
this.unlisten = props.history.listen(location => {
// 2. URL 变了!更新状态
this.setState({ location: newLocation });
});
}
Router组件的主要作用:
- 根据选择的路由模式,添加改变路由的监听事件 ,
hash采用hashchange,history模式采用popState事件。
- 将变化后的
location通过Context传递给子组件
总结 :监听路径的变化,然后将变化的location传递给子组件,相当于做了一层【事件委托】,避免子组件都要进行监听。
Switch组件
ini
// Switch 内部逻辑:
// 1. 从 Router 获取最新的 location
const location = context.location; // { pathname: '/about' }
// 2. 按顺序检查每个子组件
React.Children.forEach(children, child => {
// 检查顺序:
// 1. <Route exact path="/" /> ❌ 不匹配(不是 exact /)
// 2. <Route path="/about" /> ✅ 匹配!停止检查
// 3. <Redirect from="/old" to="/new" /> 不检查
// 4. <Route path="/new" /> 不检查
// 5. <Route component={NotFound} />不检查
});
Switch组件的主要作用是:
- 所有的
Route组件都要直接放在Switch组件的children中
- 遍历所有的
Route组件,根据从Router组件接收到的新location信息,和Route组件配置的path进行匹配,找到当前URL对应的Route组件
总结 :根据当前的URL找到匹配的Route组件。
Route组件
php
// Route 内部逻辑:
// 1. 从 Switch 收到 computedMatch(匹配信息)
const match = this.props.computedMatch;
// 2. 匹配成功,准备渲染
if (match) {
// 根据配置决定渲染方式:
if (this.props.component) {
// 使用 component 方式:创建 About 组件
return React.createElement(About, {
history: context.history,
location: context.location,
match: match // { path: '/about', url: '/about', isExact: true }
});
}
// 如果是 render 或 children 方式,也类似
}
Route组件的作用:
根据配置在组件上的参数来决定最终需要渲染的组件。
ini
// 场景1:只想在匹配时显示组件
<Route path="/home" component={Home} />
// 场景2:匹配时要显示,但需要传额外参数
<Route
path="/user/:id"
render={(props) => <User userId={extraId} {...props} />}
/>
组件结构作用分层

代码层面解析路由跳转
背景示例:
xml
// 应用结构
<Router>
<Switch>
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Router>
初始状态,显示Home页面
URL:/home
用户点击链接切换到About
javascript
<Link to="/about">About</Link> // 也可以使用redirect和useNavigate
// 点击后调用 history.push('/about')
详细步骤分析
第1步:history.push 改变 URL
php
// history.push 内部简化代码:
function push(path) {
// 1. 改变浏览器 URL(不刷新页面)
window.history.pushState({}, '', path);
// 2. 创建新的 location 对象
const location = createLocation(path);
// 3. ✅ 关键:调用 setState
setState({
location: location,
action: 'PUSH'
});
}
第2步:setState 的内部操作
javascript
// setState 函数内部:
function setState(nextState) {
// 1. 更新 history 内部的状态
Object.assign(history, nextState);
// 2. ✅ 核心:通知所有监听器
listeners.forEach(listener => {
// 每个 listener 都是一个回调函数
listener(history.location, history.action);
});
}
第3步:Router 监听器被调用
kotlin
// 在 Router 组件构造函数中:
this.unlisten = props.history.listen(location => {
// ✅ 当 setState 通知监听器时,这个函数被调用
this.setState({ location: location });
});
第4步:Router 的 setState 触发重新渲染
scala
// React 内部:当调用 this.setState 时
class Router extends React.Component {
this.setState({ location: newLocation }, () => {
// setState 完成后,React 会自动调用 render 方法
this.render();
});
render() {
// ✅ Router 重新渲染,使用新的 location
return (
<RouterContext.Provider value={{
history: this.props.history,
location: this.state.location, // ✅ 这里是新的 location
match: /* ... */
}}>
{this.props.children}
</RouterContext.Provider>
);
}
}
第5步:Context 值变化触发子组件更新
ini
// Switch 组件内部:
<RouterContext.Consumer>
{(context) => {
// ✅ context.location 现在是新的 location
const location = context.location; // { pathname: '/about' }
// Switch 重新计算匹配
let match = null;
React.Children.forEach(this.props.children, child => {
if (match == null) {
// 检查每个 Route 是否匹配
const path = child.props.path;
if (path === '/about') {
match = true; // ✅ 匹配!
}
}
});
// 渲染匹配的 Route
if (match) {
return React.cloneElement(foundChild, {
location,
computedMatch: match
});
}
}}
</RouterContext.Consumer>
第6步:Route 组件渲染新页面
kotlin
// Route 组件收到新的 match 后:
render() {
if (this.props.computedMatch) {
// ✅ 匹配成功,渲染对应的组件
return React.createElement(this.props.component, {
history: context.history,
location: context.location,
match: this.props.computedMatch
});
}
return null; // 不匹配的 Route 不渲染
}
JavaScript
****从 setState 到 DOM 更新的完整链条
markdown
// 完整的调用链条:
history.setState()
↓
调用 Router 的监听函数 (listener)
↓
Router.setState({ location })
↓
React 调度 Router 重新渲染
↓
Router.render() 调用
↓
Context.Provider value 更新
↓
Switch (Consumer) 检测到变化
↓
Switch.render() 重新计算匹配
↓
匹配的 Route 重新渲染
↓
Route 创建/更新组件实例
↓
组件的 render 方法调用
↓
React 更新 Virtual DOM
↓
ReactDOM 更新实际 DOM
↓
页面显示新内容
一句话总结:setState 通过改变状态,触发 React 的重新渲染机制,配合 Context 将变化传播到整个组件树,最终实现页面的无缝切换。

为什么History模式需要服务端的支持,而Hash模式不需要
在页面刷新的时候,相当于使用浏览器地址栏中的URL发送了一次GET请求,请求项目的HTML文件。Hash模式下,路由跳转是通过Hash值变化来实现的,而在请求的时候Hash是不会传递给服务端的,所以使用www.baidu.com#/123向服务端请求资源和www.baidu.com 是等价的。所以能被nginx配置的静态资源代理拦截并正常匹配返回HTML文件
而History模式,则是通过改变URL的路径来实现路由跳转的,使用www.baidu.com/abc和使用www.baidu.com/123 向服务端请求资源是完全不同的。此时nginx配置的静态资源代理拦截到这个请求后会去服务器找/abc和/123路径中的资源,但是肯定是找不到的,所以会返回404给浏览器,要解决这个404的问题就在nginx加一个404找不到,重定向到默认HTML文件的配置。
本文参考: