前端路由
约 5011 字大约 17 分钟
2025-03-15
一、简介
路由的概念来源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系。在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI View(视图)之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。在单页应用中,页面不会因为路径的改变而重新加载,而是通过前端路由
来动态更新视图
。这种机制依赖于浏览器的历史 API(History API)
和事件监听
,使得用户在切换页面时,不会触发页面的重新加载,从而实现快速无刷新切换。
1、核心
核心组成部分主要分为以下几部分:
- 路由器(Router):即路由表,负责管理路由和视图的匹配,路由状态信息管理。
- 路由(Route):定义了路径与处理函数的映射关系。
- 视图(View):与路由对应的页面内容。
2、如何实现?
要实现前端路由,需要解决两个核心问题:
- 如何改变 URL 却不引起页面刷新?
- 如何检测 URL 变化了?
二、工作原理
前端路由的工作原理主要依赖于浏览器提供的 History API 和事件监听机制。当用户在应用中导航到不同的 URL 时,前端路由会拦截这些导航事件,并根据定义的路由规则来更新页面内容,而不是重新加载整个页面。
1、匹配机制
前端路由的核心是路由匹配机制,它负责将 URL 路径与路由规则进行匹配,然后根据匹配结果执行相应的处理函数。这个过程通常包括以下几个步骤:
- 解析当前 URL,获取路径信息。
- 将路径信息与路由规则进行匹配。
- 如果找到匹配的路由规则,执行对应 handler
- 更新页面视图。
2、事件监听
前端路由通过监听浏览器的事件来实现路由的切换。以下是几个主要的事件:
- History.popstate(): 当用户点击浏览器的前进、后退按钮或某个按钮、链接来触发 URL 的变化时,会触发
popstate
事件。- History 路由监听这个事件来更新页面视图。
- window.load: 当用户通过特定的 URL 打来网页或者刷新网页时,会触发 load 事件。
- Hash 路由一般会根据路由规则找到对应的页面来更新视图
- History 路由下,服务器一般会根据路由返回对应的归属页面,对应页面内的 History 路由根据路由规则找到对应的视图加载页面
- window.hashchange: 当页面点击链接、按钮或者浏览器的前进、后退按钮时。
- Hash 路由一般会根据路由规则找到对应的页面来更新视图
三、三种常见路由模式
路由模式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Hash | |||
History (Html 5) | |||
Memory (Abstract) |
下面分别对 Hash,History(HTML5), Abstract(Memory) 三种实现方式分别介绍
1、Hash 哈希模式
核心通过hashchange
事件来监听url
中的hash
,根据 hash 查找路由表,拿到相应页面渲染到当前页面
特点:
- 不依赖服务器:所有工作在前端完成,不依赖服务器配合
- URL 中带有
#
:有点不美观 - 兼容性好:支持所有浏览器,尤其一些老的浏览器,最低为 IE8
- 对 SEO 不友好:因为搜索引擎对无法识别#后边的内容,影响页面排名
2、History(HTML5)模式
history 模式核心依赖 History Api
,该 Api 提供了丰富的 router 相关属性。
特点:
- 兼容性有限制:依赖
HTML5
提供的history api
,所以意味着很多老浏览器不支持,最低支持 IE10 - URL 不含
#
: 看起来美观,同时意味着一些限制 - SEO 友好:URL 不含
#
,对搜索引擎 SEO 优化友好 - 需要服务器配置支持:由于 URL 不含
#
,在页面刷新或者初次加载时,服务器会将其识别为对应路径的静态资源,去请求对应的资源。因此需要服务器配置路由表,如果找不到对应资源,就根据对应的路由返回对应的归属页面。也可以使用一些中间件connect-history-api-fallback
在 node 服务中处理。
了解一个几个相关的 api:
window.history.go()
可以跳转到浏览器会话历史中的指定的某一个记录页window.history.forward()
指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同window.history.back()
返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同window.history.pushState()
可以将给定的数据压入到浏览器会话历史栈中window.history.replaceState()
将当前的会话页面的 url 替换成指定的数据window.addEventListener("popstate", (e) => {})
当 history 发生变化时触发
3、Abstract(Memory)内存模式
Memory 模式是一种抽象的路由模式,它不依赖于浏览器的 URL,而是完全在内存中管理路由状态。这种模式通常用于那些不需要浏览器 URL 和前进/后退功能的场景,例如服务器端渲染(SSR)、单元测试、小程序或桌面应用。Memory 模式的核心思想是将路由状态保存在内存中,而不是依赖浏览器的历史记录或 URL。
特点:
- 路由规则和状态信息保存在内存里
- 依赖 Node.js 环境,不依赖于浏览器
- 不支持普通 web 应用,适用于服务端渲染、小程序、单元测试、桌面应用等不涉及浏览器和 URl 的景。
- 不需要服务器配置
四、路由和视图协同
在单页应用中,页面跳转与路由的关联是实现用户界面动态更新的关键。当用户与应用交互时,例如点击一个链接或按钮,前端路由系统会拦截这些交互事件,根据定义的路由规则来更新页面内容,而不是重新加载整个页面。
1、页面跳转的触发方式
页面跳转可以由以下几种方式触发:
- 链接点击:用户点击带有
<a>
标签的链接。 - 地址栏输入:用户直接在浏览器地址栏输入 URL 或者修改当前的 URL。
- 表单提交:带有
action
属性的表单提交。 - JavaScript 代码:通过 JavaScript 封装前端路由组件,使用组件由对外暴露的方法来动态修改
window.location
或使用history
API。 - 浏览器的前进、后退、刷新按钮:用户直接点击浏览器页面上的前进、后退、刷新按钮
2、二者的协同工作
前端路由与页面跳转的协同工作主要依赖于以下几个步骤:
- 页面跳转:用户通过前文所述方式触发页面跳转。
- 事件监听:前端路由系统通过事件检测到页面跳转。
- 事件拦截:当这些事件被触发时,前端路由会根据事件来源选择性拦截默认的事件处理行为。( 例如:阻止链接的默认跳转,否则会触发页面刷新,重新请求服务器,破坏前端路由的管控。)
- 路径解析:前端路由解析当前触发事件的路径信息,可能是 hash 值、路径字符串等。
- 路由匹配:将解析得到的路径与路由规则进行匹配,找到对应的路由处理函数。
- 视图更新:执行路由处理函数,更新页面视图,可能是渲染新的组件或更新现有组件的状态。
五、路由的懒加载
1、路由的懒加载
路由的懒加载指的是在用户访问到某个路由时,才加载该路由对应的组件代码。这种方式可以减少应用的初始负载,因为不是所有的组件代码都会在开始时加载。
2、懒加载的常见实现方式
2.1、基于 Vue
在 Vue 中,可以使用 component
和 import()
语法来定义路由组件,从而实现懒加载。
const routes = [ { path: '/home', component: () =>
import('./components/Home.vue') }, { path: '/about', component: () =>
import('./components/About.vue') } // 其他路由... ];
2.2、基于 React
在 React 中,可以使用 React.lazy
和 Suspense
来配合 import()
实现组件的懒加载。
import React, { Suspense, lazy } from "react";
const Home = lazy(() => import("./components/Home"));
const About = lazy(() => import("./components/About"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Home />
<About />
</Suspense>
);
}
2.3、基于 AngularJs
在 Angular 中,可以在路由配置中使用 loadChildren
属性来配合 import()
实现模块的懒加载。
const routes: Routes = [
{
path: "home",
loadChildren: () => import("./home/home.module").then((m) => m.HomeModule),
},
{
path: "about",
loadChildren: () =>
import("./about/about.module").then((m) => m.AboutModule),
},
// 其他路由...
];
3、基于路由懒加载的代码分割
在现代前端框架中,代码分割是优化应用性能的重要手段。它们允许开发者将代码拆分成多个小块,按需加载,从而减少初始加载时间,加快首次渲染速度。
代码分割是将代码分成多个块(chunks),然后按需加载这些块的过程。它通常与懒加载结合使用,以优化应用的加载时间。
代码分割通常是通过构建工具(如 Webpack)来实现的。以下是一个简单的 Webpack 配置示例,演示了如何进行代码分割:
// Webpack配置文件(webpack.config.js)
module.exports = {
// 入口文件
entry: "./src/index.js",
// 输出配置
output: {
filename: "[name].bundle.js",
path: __dirname + "/dist",
chunkFilename: "[name].bundle.js",
publicPath: "dist/",
},
// 模块分割配置
optimization: {
splitChunks: {
chunks: "all", // webpack 会自动识别import语法,进行代码切割
},
},
// 其他配置...
};
六、路由的预加载
路由预加载是在用户访问某个路由之前,提前加载该路由对应的组件。这可以进一步优化用户体验,特别是在用户可能很快就会访问到某个路由的场景下。
1、Vue
在 Vue 中,可以使用 Webpack 的 魔法注释 来为路由组件指定加载优先级,实现路由预加载:
const router = new VueRouter({
routes: [
{
path: "/home",
component: () =>
import(/* webpackPreload: true */ "./components/Home.vue"),
},
{
path: "/about",
component: () => import("./components/About.vue"),
},
// 其他路由...
],
});
2、React
在 React 中,可以使用 React.lazy 和 Suspense 结合 IntersectionObserver 来实现路由预加载:
import React, { Suspense, lazy } from "react";
import { useIntersectionObserver } from "react-intersection-observer";
const Home = lazy(() => import("./components/Home"));
const About = lazy(() => import("./components/About"));
function App() {
const { ref, inView } = useIntersectionObserver({
onEnter: () => {
if (inView) {
// 当组件进入视图时,预加载对应的组件
Home.preload();
About.preload();
}
},
});
return (
<div ref={ref}>
<Suspense fallback={<div>Loading...</div>}>
<Home />
<About />
</Suspense>
</div>
);
}
七、路由组件缓存
缓存路由组件可以避免在每次路由切换时重新渲染组件,从而提高性能。对于某些频繁切换的组件,这是一种有效的优化策略。
1、Vue
在 Vue 中,可以使用 <keep-alive>
标签来缓存组件:
<template>
<router-view></router-view>
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
</template>
2、React
在 React 中,可以使用 React.memo
或 useMemo
来缓存组件:
import React, { useMemo } from "react";
import { Route, Switch } from "react-router-dom";
const Home = React.memo(() => <div>Home</div>);
const About = React.memo(() => <div>About</div>);
function App() {
const homeComponent = useMemo(() => <Home />, []);
const aboutComponent = useMemo(() => <About />, []);
return (
<Switch>
<Route exact path="/" component={homeComponent} />
<Route path="/about" component={aboutComponent} />
</Switch>
);
}
在这个配置中,splitChunks
选项用于定义代码分割的行为。设置 chunks: 'all'
意味着对所有模块进行分割,包括异步和非异步模块。
八、路由匹配算法优化
优化路由匹配算法可以减少路由解析的时间,提高路由的匹配效率。
1、实现高效的路由匹配
优化策略:
- 调整路由配置的先后顺序,把经常访问的路由放在前边
- 使用嵌套结构,使用
children
属性包裹子路由 - 尽量避免使用动态路由传参
- 尽量使用 history 模式,比 hash 模式性能好
其他
- 在 Vue 中,可以通过配置路由的
meta
字段来优化路由匹配,例如使用正则表达式或更精确的路径匹配规则。 - 在 React 中,可以自定义路由匹配逻辑,例如使用
matchPath
函数来实现更灵活的路径匹配。 :::
2、减少路由跳转时的重绘和重排
在单页应用中,路由跳转可能会导致页面的重绘和重排,这可能会影响性能。以下是一些减少重绘和重排的策略:
2.1 使用 CSS 的 transform 和 opacity 属性
利用 CSS 的 transform 和 opacity 属性进行动画处理,因为这些属性不会触发重排。
.fade-enter {
opacity: 0;
transition: opacity 0.3s ease-in;
}
.fade-enter-active {
opacity: 1;
}
2.2 使用虚拟 DOM 的优化特性
在 React 中,利用 React.memo
、useMemo
和 useCallback
等特性来减少不必要的组件渲染。
2.3 避免不必要的 DOM 操作
在 Vue 中,使用 v-show
而不是 v-if
来控制元素的显示和隐藏,以避免不必要的 DOM 创建和销毁。
3、使用服务端渲染(SSR)
服务端渲染可以减少首屏加载时间,因为服务器可以预先渲染好页面,而不是在客户端执行 JavaScript 来构建页面。
在 Vue 中,可以使用 vue-server-renderer
来实现服务端渲染。
在 React 中,可以使用 react-dom/server
来实现服务端渲染。
通过上述优化策略,可以显著提升前端路由的性能,提供更加流畅的用户体验。在实际应用中,应根据具体情况选择合适的优化方案。 前端路由与页面状态管理
前端路由不仅仅是页面跳转那么简单,它与页面状态管理紧密相关。在单页应用中,页面状态通常包括应用的当前路由、组件的状态、用户输入等。有效的页面状态管理对于确保用户在导航过程中的体验至关重要。
九、页面状态保持、管理
前端路由与页面状态的关联体现在以下几个方面:
- 路由变化触发页面状态更新:当用户导航到不同的路由时,前端路由系统会触发相应的状态更新,如组件的重新渲染。
- 页面状态影响路由行为:页面状态的变化可能会影响路由的解析和行为,例如,根据组件状态动态添加或删除路由。
- 状态持久化:前端路由可以与浏览器的历史记录栈(history stack)结合,实现页面状态的持久化,用户可以通过浏览器的前进和后退按钮来恢复之前的状态。
为了更好地管理页面状态,开发者通常会使用状态管理库,如 Vuex、Redux 或 MobX。
以下是如何将这些状态管理库与前端路由集成:
1、Vuex 与 Vue Router 的集成
在 Vue 应用中,Vuex 是官方推荐的状态管理库。以下是一个简单的集成示例:
// store.js
import Vue from "vue";
import Vuex from "vuex";
import router from "./router"; // 引入路由实例
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// 应用状态
},
mutations: {
// 状态突变
},
actions: {
// 提交突变
},
modules: {
// 模块化
},
});
// 在路由守卫中使用Vuex状态
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
// 检查用户认证状态
if (!store.getters.isAuthenticated) {
next("/login");
} else {
next();
}
} else {
next();
}
});
1.2 Redux 与 React Router 的集成
在 React 应用中,Redux 是非常流行的状态管理库。以下是一个简单的集成示例:
// store.js
import { createStore } from "redux";
import rootReducer from "./reducers";
import { routerMiddleware } from "react-router-redux";
import { createHistory } from "history";
import { routerReducer } from "react-router-redux";
export const history = createHistory();
const store = createStore(
rootReducer,
applyMiddleware(routerMiddleware(history))
);
// 在组件中使用react-router-redux的connect函数来连接Redux和React Router
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
const mapStateToProps = (state, ownProps) => {
// 从Redux状态中提取所需数据
};
const mapDispatchToProps = (dispatch, ownProps) => {
// 绑定动作到Redux的dispatch函数
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(MyComponent)
);
通过将状态管理库与前端路由集成,开发者可以更好地控制应用的状态,实现复杂的业务逻辑,并提供更加一致和流畅的用户体验。
2、页面状态管理与性能优化
页面状态管理不仅关乎用户体验,也影响应用的性能。以下是一些优化页面状态管理的策略:
- 避免不必要的渲染:通过合理的状态更新和组件拆分,减少不必要的渲染。
- 使用不可变数据结构:在 Redux 等库中,使用不可变数据结构可以帮助避免不必要的渲染,因为不可变数据结构在每次更新时都会生成新的对象。
- 懒加载和代码分割:结合前端路由的懒加载和代码分割,减少初始加载时间和应用的整体大小。
- 使用缓存:对于不经常变化的数据,可以使用缓存来减少数据请求和处理的次数。
通过这些策略,开发者可以确保应用在保持良好用户体验的同时,也具备高效的性能表现。
十、主流框架的实现
1、基于 Vue 的路由
Vue.js 提供了官方的路由管理器 Vue Router,它允许你为单页应用定义路由规则,并通过组件来渲染对应的视图。
<script setup>
import { ref, computed } from "vue";
import Home from "./Home.vue";
import About from "./About.vue";
import NotFound from "./NotFound.vue";
const routes = {
"/": Home,
"/about": About,
};
const currentPath = ref(window.location.hash);
window.addEventListener("hashchange", () => {
currentPath.value = window.location.hash;
});
const currentView = computed(() => {
return routes[currentPath.value.slice(1) || "/"] || NotFound;
});
</script>
<template>
<a href="#/">Home</a> | <a href="#/about">About</a> |
<a href="#/non-existent-path">Broken Link</a>
<component :is="currentView" />
</template>
2、基于 React 的路由
React 有多个路由库,其中最流行的是 React Router。React Router 为 React 应用提供了路由功能,允许你在应用中设置多个路由,并在用户导航时渲染相应的组件。
// React Router 示例
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "./components/Home";
import About from "./components/About";
const App = () => (
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Router>
);
export default App;
3、基于 Angular 的路由
Angular 提供了一个强大的路由模块,它允许你创建单页应用中的多个视图,并且可以嵌套路由。
// Angular 路由示例
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { RouterModule, Routes } from "@angular/router";
import { HomeComponent } from "./home.component";
import { AboutComponent } from "./about.component";
const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "about", component: AboutComponent },
];
@NgModule({
imports: [BrowserModule, RouterModule.forRoot(routes)],
declarations: [HomeComponent, AboutComponent],
bootstrap: [HomeComponent],
})
export class AppModule {}
更新日志
e7112
-1于