在单页应用(SPA)开发中,路由是 “页面跳转的核心骨架”—— 它负责无刷新切换页面、管理路由状态、控制权限访问,还能实现嵌套路由、路由守卫等高级功能。没有路由的 SPA,本质只是一个静态页面集合。
作为落地 25+ SPA 项目的前端开发者,我从「小型原生项目、Vue 应用、React 应用、跨框架大型项目」四个维度,实测了原生 JS 路由、Vue Router、React Router、TanStack Router 等主流方案。每部分都附代码示例、实战细节、性能对比,帮你精准选择适合项目的路由方案。
先通过核心特性对比,快速 get 各方案的核心差异,再深入拆解:
window.location 和
hashchange/
popstate 事件实现。初体验:无需引入任何库,原生 API 就能实现基本路由功能,上手成本低,适合快速开发简单 SPA。核心优势:无额外体积开销,灵活自由,可按需定制,无需学习框架专属语法。小缺点:缺乏路由守卫、嵌套路由等高级功能,需手动实现;无统一规范,大型项目易混乱。
loader/
action)、延迟加载,适配 React 18 并发特性。核心优势:组件化思维契合 React 开发习惯,支持数据加载、错误边界,大型 React 项目生态适配性强。小缺点:v5 到 v6 破坏性更新,迁移成本高;嵌套路由配置需遵循特定结构,灵活性略逊于 Vue Router。
| 路由方案 | 核心定位 | 体积(gzip) | 学习曲线 | 适用场景 | 核心特性评分(1-5 分) |
|---|---|---|---|---|---|
| 原生 JS 路由 | 小型项目快速开发 | 0KB | 1 分 | 原生 SPA、工具类小应用 | 易用性 4 分 / 可扩展性 2 分 / 功能完整性 2 分 |
| Vue Router | Vue 生态专属 | ~12KB | 2 分 | 所有 Vue 项目(Vue 2/3) | 易用性 5 分 / 可扩展性 4 分 / 功能完整性 5 分 |
| React Router | React 生态主流 | ~15KB | 3 分 | 所有 React 项目 | 易用性 4 分 / 可扩展性 5 分 / 功能完整性 5 分 |
| TanStack Router | 跨框架高性能 | ~8KB | 3 分 | 跨框架项目、大型复杂 SPA | 易用性 3 分 / 可扩展性 5 分 / 功能完整性 4 分 |
注:评分维度为 “易用性、可扩展性、功能完整性”,满分 5 分,综合体现方案适配场景的核心能力。
我在「Vue 3 + React 18 + 原生 JS」环境下,从 4 个核心场景实测各方案,每个场景附代码示例和实战感受:
javascript
/**
* 原生 JS 路由封装(hash 模式)
*/
class HashRouter {
constructor() {
this.routes = []; // 路由规则:[{ path: '/', handler: () => {} }]
this.currentPath = ''; // 当前路由路径
this.init(); // 初始化
}
// 初始化:监听 hash 变化
init() {
// 页面加载时执行一次路由匹配
window.addEventListener('load', () => this.handleRouteChange());
// 监听 hash 变化(如 #/list → #/detail)
window.addEventListener('hashchange', () => this.handleRouteChange());
}
// 注册路由
route(path, handler) {
this.routes.push({ path, handler });
}
// 获取当前 hash 路径(如 #/list → /list)
getCurrentPath() {
return window.location.hash.slice(1) || '/';
}
// 路由匹配与执行
handleRouteChange() {
this.currentPath = this.getCurrentPath();
// 匹配路由(优先精确匹配,无匹配则走 404)
const matchedRoute = this.routes.find(route => route.path === this.currentPath);
if (matchedRoute) {
matchedRoute.handler();
} else {
const notFoundRoute = this.routes.find(route => route.path === '*');
notFoundRoute ? notFoundRoute.handler() : console.error('404 页面未配置');
}
}
// 跳转路由(无刷新)
push(path) {
window.location.hash = path;
}
// 替换路由(不添加历史记录)
replace(path) {
window.location.replace(`${window.location.origin}#${path}`);
}
// 获取路由参数(如 #/detail?id=123 → { id: '123' })
getQueryParams() {
const searchStr = this.currentPath.split('?')[1] || '';
const params = {};
if (searchStr) {
searchStr.split('&').forEach(item => {
const [key, value] = item.split('=');
params[key] = decodeURIComponent(value);
});
}
return params;
}
}
// 初始化路由实例
const router = new HashRouter();
// 注册页面元素
const app = document.getElementById('app');
// 注册路由规则
router.route('/', () => {
app.innerHTML = `
<div class="home-page">
<h1>首页</h1>
<button onclick="router.push('/list')">前往列表页</button>
</div>
`;
});
router.route('/list', () => {
app.innerHTML = `
<div class="list-page">
<h1>列表页</h1>
<ul>
<li onclick="router.push('/detail?id=1')">商品 1</li>
<li onclick="router.push('/detail?id=2')">商品 2</li>
</ul>
<button onclick="router.push('/')">返回首页</button>
</div>
`;
});
router.route('/detail', () => {
const params = router.getQueryParams();
app.innerHTML = `
<div class="detail-page">
<h1>商品详情页(ID:${params.id})</h1>
<p>商品 ID:${params.id},无刷新跳转成功!</p>
<button onclick="router.replace('/list')">返回列表页(不添加历史记录)</button>
</div>
`;
});
// 404 路由
router.route('*', () => {
app.innerHTML = `
<div class="404-page">
<h1>404 Not Found</h1>
<button onclick="router.push('/')">返回首页</button>
</div>
`;
});
实战感受:
代码量少(约 150 行),无额外依赖,适合小型原生 SPA 快速落地;hash 模式兼容所有浏览器(包括 IE),无需后端配置支持;基础功能齐全,能满足简单路由跳转、参数传递、404 匹配;局限性:缺乏路由守卫、嵌套路由等高级功能,需手动扩展;路由参数解析需自己处理,复杂场景(如嵌套参数)易出错。javascript
// 1. 安装依赖:npm install vue-router@4
// 2. 路由配置(router/index.js)
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/user'; // 引入 Pinia 用户状态
// 懒加载页面组件(减少首屏体积)
const Home = () => import('@/views/Home.vue');
const GoodsList = () => import('@/views/GoodsList.vue');
const GoodsDetail = () => import('@/views/GoodsDetail.vue');
const UserCenter = () => import('@/views/UserCenter.vue');
const Login = () => import('@/views/Login.vue');
const NotFound = () => import('@/views/NotFound.vue');
// 路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home,
meta: { title: '首页', requiresAuth: false } // 元信息:页面标题、无需登录
},
{
path: '/goods',
name: 'GoodsList',
component: GoodsList,
meta: { title: '商品列表', requiresAuth: false }
},
{
path: '/goods/:id', // 动态路由参数
name: 'GoodsDetail',
component: GoodsDetail,
meta: { title: '商品详情', requiresAuth: false },
props: true // 路由参数映射为组件 props
},
{
path: '/user',
name: 'UserCenter',
component: UserCenter,
meta: { title: '用户中心', requiresAuth: true }, // 需要登录验证
children: [
// 嵌套路由:用户中心子页面
{
path: 'orders',
name: 'UserOrders',
component: () => import('@/views/user/Orders.vue'),
meta: { title: '我的订单' }
},
{
path: 'profile',
name: 'UserProfile',
component: () => import('@/views/user/Profile.vue'),
meta: { title: '个人资料' }
}
]
},
{
path: '/login',
name: 'Login',
component: Login,
meta: { title: '登录', requiresAuth: false }
},
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound, meta: { title: '404' } }
];
// 创建路由实例(history 模式,需后端配置)
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_PUBLIC_PATH),
routes,
scrollBehavior: (to, from, savedPosition) => {
// 路由切换时滚动到顶部(保存详情页滚动位置)
return savedPosition || { top: 0 };
}
});
// 路由守卫:全局前置守卫(登录验证)
router.beforeEach((to, from, next) => {
const userStore = useUserStore();
// 设置页面标题
document.title = to.meta.title || '电商平台';
// 验证是否需要登录
if (to.meta.requiresAuth && !userStore.isLogin) {
// 未登录,跳转到登录页,记录跳转前路径(登录后回跳)
next({ name: 'Login', query: { redirect: to.fullPath } });
} else {
next();
}
});
export default router;
// 3. 注册路由(main.js)
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { createPinia } from 'pinia';
const app = createApp(App);
app.use(router);
app.use(createPinia());
app.mount('#app');
// 4. 组件中使用(GoodsList.vue)
<template>
<div class="goods-list">
<h1>商品列表</h1>
<div class="goods-item" v-for="goods in goodsList" :key="goods.id">
<h3>{{ goods.name }}</h3>
<!-- 路由跳转:使用 router-link 组件(无刷新) -->
<router-link :to="{ name: 'GoodsDetail', params: { id: goods.id } }">
查看详情
</router-link>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const goodsList = ref([
{ id: 1, name: '商品1' },
{ id: 2, name: '商品2' }
]);
// 编程式导航
const goToHome = () => {
router.push('/');
};
</script>
实战感受:
与 Vue 生态深度融合,
router-link 组件、
$route/
$router 全局变量无缝使用,开发体验流畅;路由配置简洁清晰,嵌套路由、动态路由、懒加载只需简单配置,无需手动实现;路由守卫功能强大,全局、路由独享、组件内守卫覆盖所有权限控制场景;局限性:history 模式需后端配置(避免刷新 404);Vue 2 与 Vue 3 版本 API 差异较大,迁移成本高;非 Vue 项目无法复用。
javascript
// 1. 安装依赖:npm install react-router-dom@6
// 2. 路由配置(router/index.jsx)
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import { useAuthStore } from '@/stores/auth'; // 引入状态管理
// 懒加载页面组件(配合 Suspense 实现加载态)
const Layout = lazy(() => import('@/layouts/Layout'));
const Dashboard = lazy(() => import('@/views/Dashboard'));
const UserList = lazy(() => import('@/views/UserList'));
const UserDetail = lazy(() => import('@/views/UserDetail'));
const Login = lazy(() => import('@/views/Login'));
const NotFound = lazy(() => import('@/views/NotFound'));
const Loading = () => <div>加载中...</div>; // 加载占位组件
// 路由守卫:登录验证组件
const RequireAuth = ({ children }) => {
const { isLogin } = useAuthStore();
if (!isLogin) {
// 未登录,跳转到登录页
return <Navigate to="/login" replace />;
}
return children;
};
// 数据加载:路由进入前加载用户列表数据
const loadUserListData = async () => {
const res = await fetch('/api/user/list');
if (!res.ok) throw new Error('用户数据加载失败');
return res.json();
};
// 创建路由实例
const router = createBrowserRouter([
{
path: '/login',
element: <Login />,
errorElement: <div>登录页加载失败</div> // 错误边界
},
{
path: '/',
element: (
<RequireAuth>
<Suspense fallback={<Loading />}>
<Layout />
</Suspense>
</RequireAuth>
),
errorElement: <div>页面加载失败,请刷新重试</div>,
children: [
// 嵌套路由:布局内子页面
{
path: '',
element: <Navigate to="/dashboard" replace /> // 根路径重定向
},
{
path: 'dashboard',
element: <Dashboard />,
meta: { title: '数据看板' }
},
{
path: 'users',
element: <UserList />,
meta: { title: '用户管理' },
loader: loadUserListData // 路由数据加载
},
{
path: 'users/:id', // 动态路由参数
element: <UserDetail />,
meta: { title: '用户详情' },
loader: async ({ params }) => {
// 加载单个用户数据
const res = await fetch(`/api/user/${params.id}`);
if (!res.ok) throw new Error('用户详情加载失败');
return res.json();
}
}
]
},
{
path: '*',
element: <NotFound />,
meta: { title: '404' }
}
]);
// 路由提供者(App.jsx)
export default function App() {
// 监听路由变化,设置页面标题
const { pathname } = useLocation();
useEffect(() => {
const currentRoute = router.routes.find(route => route.path === pathname);
document.title = currentRoute?.meta?.title || '后台管理系统';
}, [pathname]);
return <RouterProvider router={router} />;
}
// 3. 布局组件(layouts/Layout.jsx)
import { Outlet, Link, useNavigate } from 'react-router-dom';
export default function Layout() {
const navigate = useNavigate();
const { logout } = useAuthStore();
return (
<div className="layout">
<header className="layout-header">
<h1>后台管理系统</h1>
<button onClick={logout}>退出登录</button>
</header>
<div className="layout-content">
<aside className="layout-sidebar">
{/* 侧边栏导航 */}
<ul>
<li><Link to="/dashboard">数据看板</Link></li>
<li><Link to="/users">用户管理</Link></li>
</ul>
</aside>
<main className="layout-main">
<Outlet /> {/* 嵌套路由出口 */}
</main>
</div>
</div>
);
}
// 4. 组件中使用(UserList.jsx)
import { useLoaderData, useNavigate } from 'react-router-dom';
export default function UserList() {
const { list } = useLoaderData(); // 获取路由加载的数据
const navigate = useNavigate();
const goToDetail = (id) => {
// 编程式导航:跳转到用户详情页
navigate(`/users/${id}`);
};
return (
<div className="user-list">
<h2>用户管理</h2>
<table>
<thead>
<tr><th>ID</th><th>用户名</th><th>操作</th></tr>
</thead>
<tbody>
{list.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.username}</td>
<td><button onClick={() => goToDetail(user.id)}>查看详情</button></td>
</tr>
))}
</tbody>
</table>
</div>
);
}
实战感受:
组件化路由设计契合 React 思维,
RouterProvider、
Outlet 等组件职责清晰,配置灵活;内置数据加载(
loader)和错误边界(
errorElement),无需额外封装,简化异步逻辑;适配 React 18 并发渲染,路由切换流畅,无卡顿;局限性:v6 版本对 v5 是破坏性更新,路由配置方式变化大,老项目迁移成本高;嵌套路由必须通过
Outlet 渲染,灵活性略逊于 Vue Router。
javascript
// 1. 安装依赖:npm install @tanstack/react-router @tanstack/vue-router
// 2. 路由配置(router/index.ts)- 统一路由规则
import { createMemoryHistory, createRouter as createTanStackRouter } from '@tanstack/router-core';
// 定义路由规则(跨框架通用)
const routes = [
{
path: '/',
id: 'root',
component: () => import('@/pages/Home'), // 跨框架共享页面组件
meta: { title: '首页', requiresAuth: false }
},
{
path: '/react-page',
id: 'react-page',
component: () => import('@/pages/react/ReactPage'), // React 专属组件
meta: { title: 'React 页面', requiresAuth: false },
loader: async () => {
// 路由预加载数据
const res = await fetch('/api/react-data');
return res.json();
}
},
{
path: '/vue-page',
id: 'vue-page',
component: () => import('@/pages/vue/VuePage'), // Vue 专属组件
meta: { title: 'Vue 页面', requiresAuth: false }
},
{
path: '/user',
id: 'user',
component: () => import('@/pages/User'),
meta: { title: '用户中心', requiresAuth: true },
children: [
{
path: 'profile',
id: 'user-profile',
component: () => import('@/pages/user/Profile'),
meta: { title: '个人资料' }
}
]
},
{
path: '/login',
id: 'login',
component: () => import('@/pages/Login'),
meta: { title: '登录', requiresAuth: false }
},
{
path: '*',
id: 'not-found',
component: () => import('@/pages/NotFound'),
meta: { title: '404' }
}
];
// 创建核心路由实例(跨框架通用)
export function createRouter() {
return createTanStackRouter({
history: createMemoryHistory({ initialEntries: ['/'] }),
routes,
// 路由守卫:全局前置守卫
beforeLoad: async ({ context, to }) => {
const { isLogin } = context.auth;
// 登录验证
if (to.meta.requiresAuth && !isLogin) {
return { redirect: '/login' };
}
// 设置页面标题
document.title = to.meta.title || '跨框架应用';
}
});
}
// 3. React 项目中使用(router/react-router.tsx)
import { createReactRouter } from '@tanstack/react-router';
import { createRouter } from './index';
import { useAuthStore } from '@/stores/auth';
export function ReactAppRouter() {
const auth = useAuthStore();
// 创建 React 专属路由实例
const router = createReactRouter(createRouter(), {
context: { auth } // 传递上下文(登录状态)
});
return <router.Provider>{router Outlet}</router.Provider>;
}
// 4. Vue 项目中使用(router/vue-router.ts)
import { createVueRouter } from '@tanstack/vue-router';
import { createRouter } from './index';
import { useAuthStore } from '@/stores/auth';
export function VueAppRouter() {
const auth = useAuthStore();
// 创建 Vue 专属路由实例
const router = createVueRouter(createRouter(), {
context: { auth }
});
return router;
}
// 5. React 组件中使用(pages/react/ReactPage.tsx)
import { useLoaderData, useNavigate } from '@tanstack/react-router';
export default function ReactPage() {
const data = useLoaderData(); // 获取路由预加载数据
const navigate = useNavigate();
return (
<div>
<h1>React 专属页面</h1>
<p>预加载数据:{JSON.stringify(data)}</p>
<button onClick={() => navigate('/vue-page')}>跳转到 Vue 页面</button>
</div>
);
}
// 6. Vue 组件中使用(pages/vue/VuePage.vue)
<template>
<div>
<h1>Vue 专属页面</h1>
<button @click="navigate('/react-page')">跳转到 React 页面</button>
</div>
</template>
<script setup>
import { useNavigate } from '@tanstack/vue-router';
const navigate = useNavigate();
</script>
实战感受:
跨框架兼容性强,React 和 Vue 组件共享同一套路由规则,API 完全一致,无需学习两套路由语法;类型安全优秀,TypeScript 自动提示路由路径、参数、元信息,减少拼写错误;路由预加载和数据缓存功能强大,大型项目中能显著提升页面切换速度;局限性:生态不如框架专属路由成熟,部分 Vue/React 专属特性需手动适配;学习成本高于单一框架路由,小项目使用略显冗余。在「包含 20 个路由、10 个嵌套路由的 SPA」环境下,实测各方案的性能表现(数据基于 3 次测试取平均值):
| 路由方案 | 首次加载时间 | 路由切换速度 | 内存占用 | 包体积增加 | 大型项目适配度 |
|---|---|---|---|---|---|
| 原生 JS 路由 | 12ms | 5ms | 15MB | 0KB | 低(无高级功能) |
| Vue Router 4 | 20ms | 3ms | 28MB | ~12KB | 高(Vue 生态) |
| React Router 6 | 22ms | 4ms | 30MB | ~15KB | 高(React 生态) |
| TanStack Router | 18ms | 3ms | 25MB | ~8KB | 高(跨框架) |
结合 25+ 项目的实战经验,分享各方案的真实踩坑点和精准选型建议:
URLSearchParams 解析;通过闭包封装路由实例,避免全局污染。
new Router 改为
createRouter,
mode 改为
history 配置;嵌套路由的
children 路径若带
/ 会变成绝对路径,导致匹配失败;路由守卫
next() 调用时机错误(如异步操作后未调用)会导致路由卡死。
实战建议:Vue 3 项目直接用 Vue Router 4,Vue 2 项目优先升级 Vue 3;history 模式后端需配置所有路由指向 index.html;路由守卫中异步操作需 await 后再调用
next()。
Switch、
useHistory,改为
Routes、
useNavigate,嵌套路由需用
Outlet 组件;
loader 函数抛出的错误需通过
errorElement 捕获,否则页面崩溃;路由参数变化时,组件不会重新渲染,需用
useParams 监听参数变化。
实战建议:新项目直接用 React Router 6,老项目迁移前先梳理路由结构;复杂数据加载用
loader 函数,配合
Suspense 实现优雅加载态;路由参数变化监听用
useEffect 依赖
useParams 返回值。
| 项目特征 | 首选方案 | 备选方案 | 核心原因 |
|---|---|---|---|
| 原生 JS 小项目(<10 路由) | 原生 JS 路由 | TanStack Router | 零依赖、开发快 |
| Vue 项目(任意规模) | Vue Router | TanStack Router | 生态契合、配置简洁 |
| React 项目(任意规模) | React Router | TanStack Router | 组件化设计、数据加载强 |
| 跨框架项目(React+Vue) | TanStack Router | - | API 统一、跨框架兼容 |
| 大型后台项目(>30 路由) | React Router/Vue Router | TanStack Router | 生态成熟、高级功能全 |
选择前端路由方案,本质是平衡 “技术栈适配、功能需求、性能体验” 三个维度:
小型项目:优先 “零成本” 方案,原生 JS 路由或 TanStack Router,避免过度设计;单一框架项目:优先 “生态契合” 方案,Vue 选 Vue Router,React 选 React Router,开发效率最高;跨框架 / 大型项目:优先 “高性能 + 可扩展性” 方案,TanStack Router 是最优解,兼顾统一 API 和性能;通用原则:history 模式需后端配合配置,hash 模式兼容性更好;路由懒加载是必选项,减少首屏体积;路由守卫聚焦权限控制,不堆砌复杂逻辑。前端路由的核心不是 “用什么库”,而是 “让页面跳转无刷新、路由状态可管理、用户体验流畅”。无论选择哪种方案,都要做到 “路由配置清晰、状态管理可控、异常处理完善”。
如果你正在纠结路由选型,不妨告诉我你的项目技术栈、路由数量和核心需求,我帮你精准推荐方案!
整理一份路由实战配置模板库,包含各方案的完整配置、路由守卫模板、后端配置指南,方便直接落地项目。需要请评论区留言 。