前端路由
多页应用时代,每次跳转都是整页刷新,白屏等待是常态。前端路由让单页应用(SPA)在不刷新页面的前提下切换视图,并把当前视图编码进 URL——可收藏、可分享、可后退。
一、前端路由与 SPA
| 维度 | 多页应用(MPA) | 单页应用(SPA) |
|---|---|---|
| 页面 | 每路由一个 HTML | 一个 HTML,JS 渲染切换 |
| 跳转 | 整页刷新 | 无刷新,局部切换 |
| 首屏 | 服务器直出 | 首屏 JS 执行后渲染 |
| 路由实现 | 服务器转发 | 前端路由(Hash / History) |
前端路由的本质:监听 URL 变化 → 解析路由参数 → 渲染对应组件。它不请求服务器,纯前端行为。
二、Hash 路由原理
URL 中 # 之后的部分称为 hash,如 https://example.com/#/user/1。
javascript
// 1. 读取当前 hash
console.log(location.hash); // "#/user/1"
// 2. 修改 hash(不会刷新页面,且会写入历史记录)
location.hash = "#/user/2";
// 3. 监听 hash 变化
window.addEventListener("hashchange", () => {
console.log("路由变了:", location.hash);
renderByHash(location.hash); // 根据 hash 渲染对应组件
});| Hash 路由特点 | 说明 |
|---|---|
| 服务端配置 | 无需任何配置,# 后内容不会发给服务器 |
| 兼容性 | 极好,老浏览器也能用 |
| SEO | 差,搜索引擎默认不执行 JS |
| URL 形态 | /#/about,不够美观 |
三、History 路由原理
HTML5 提供 history.pushState / replaceState 与 popstate 事件。
javascript
// 1. 改变 URL(不刷新页面,不触发 popstate)
history.pushState({ page: "about" }, "", "/about");
history.replaceState({ page: "home" }, "", "/home"); // 替换当前记录
// 2. 前进 / 后退时才触发 popstate
window.addEventListener("popstate", () => {
console.log("当前位置:", location.pathname);
renderByPath(location.pathname);
});| History 路由特点 | 说明 |
|---|---|
| URL 形态 | /about,美观、语义化 |
| 服务端配置 | 必须配置 fallback:任意路径都返回 index.html |
| 兼容性 | 现代浏览器全支持 |
| SEO | 配合服务端渲染(SSR)可被收录 |
注意:直接访问 /about 时,服务器需要把该请求重定向到 index.html(Nginx 示例):
nginx
location / {
try_files $uri $uri/ /index.html; # 找不到文件就回退到 index.html
}四、两种路由对比
| 对比项 | Hash 路由 | History 路由 |
|---|---|---|
| URL 形态 | /#/about | /about |
| 服务端配置 | 不需要 | 需要 fallback |
| 兼容性 | 全兼容 | 需要 pushState 支持 |
| SEO | 基本无效 | 配合 SSR 有效 |
| 刷新页面 | 正常 | 需要服务器回退,否则 404 |
| 使用场景 | 纯静态托管 | 有服务端控制权的项目 |
选择建议:能控制服务器就用 History;纯静态托管(GitHub Pages 等)用 Hash 或配置好 404 回退。
五、React Router
5.1 基础用法
jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
// 也可用 HashRouter:import { HashRouter } from "react-router-dom";
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
{ path: "/user/:id", element: <UserDetail /> },
]);
function App() {
return <RouterProvider router={router} />;
}声明式写法(Routes / Route):
jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<UserDetail />} />
<Route path="*" element={<NotFound />} /> {/* 兜底 */}
</Routes>
</BrowserRouter>
);
}5.2 跳转与取参
jsx
import { useNavigate, useParams, Link, NavLink } from "react-router-dom";
function UserList() {
const navigate = useNavigate();
return (
<>
{/* 声明式导航 */}
<Link to="/user/1">用户 1</Link>
<NavLink to="/user/2" className={({ isActive }) => (isActive ? "on" : "")}>用户 2</NavLink>
{/* 编程式导航 */}
<button onClick={() => navigate("/user/3")}>去用户 3</button>
<button onClick={() => navigate(-1)}>后退</button>
</>
);
}
function UserDetail() {
const { id } = useParams(); // 取路径参数
const [search] = useSearchParams(); // 取查询参数
return <div>用户 {id},来自 {search.get("from")}</div>;
}5.3 嵌套路由与懒加载
jsx
import { Suspense, lazy } from "react";
const Admin = lazy(() => import("./pages/Admin")); // 动态 import 拆包
function App() {
return (
<BrowserRouter>
<Routes>
{/* 嵌套路由:AdminLayout 内用 <Outlet /> 渲染子路由 */}
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminHome />} />
<Route path="users" element={<AdminUsers />} />
</Route>
{/* 懒加载 + 加载兜底 */}
<Route path="/admin/menu" element={
<Suspense fallback={<div>加载中…</div>}>
<Admin />
</Suspense>
} />
</Routes>
</BrowserRouter>
);
}5.4 重定向与守卫
jsx
import { Navigate } from "react-router-dom";
// 重定向
<Route path="/old" element={<Navigate to="/new" replace />} />
// 简单守卫:未登录跳转登录页
function RequireAuth({ children }) {
const isLogin = Boolean(localStorage.getItem("token"));
return isLogin ? children : <Navigate to="/login" replace />;
}六、Vue Router
6.1 创建路由
javascript
import { createRouter, createWebHistory } from "vue-router";
// 也可用 createWebHashHistory() 启用 Hash 模式
const routes = [
{ path: "/", component: () => import("./views/Home.vue") },
{
path: "/user/:id",
name: "user", // 命名路由
component: () => import("./views/UserDetail.vue"),
props: true, // 把 :id 作为 props 传入
},
{ path: "/:pathMatch(.*)*", component: () => import("./views/NotFound.vue") },
];
const router = createRouter({ history: createWebHistory(), routes });
export default router;入口挂载:
javascript
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
createApp(App).use(router).mount("#app");6.2 视图与导航
vue
<!-- App.vue:路由出口 -->
<router-view />
<!-- 声明式导航 -->
<router-link to="/">首页</router-link>
<router-link :to="{ name: 'user', params: { id: 1 } }">用户</router-link>vue
<script setup>
import { useRoute, useRouter } from "vue-router";
const route = useRoute(); // 当前路由信息(含 params、query)
const router = useRouter(); // 路由实例
function go() {
router.push("/user/1"); // 跳转
router.push({ name: "user", params: { id: 2 }, query: { from: "list" } });
router.replace("/home"); // 替换当前记录
router.back(); // 后退
}
console.log(route.params.id, route.query.from);
</script>6.3 路由守卫
| 守卫 | 触发时机 | 典型用途 |
|---|---|---|
beforeEach | 每次导航前 | 登录校验、页面标题 |
beforeResolve | 组件内守卫确认后 | 数据预取 |
afterEach | 导航完成后 | 埋点、滚动复位 |
beforeEnter | 进入特定路由前 | 路由级权限 |
javascript
router.beforeEach((to, from, next) => {
const needAuth = to.matched.some((r) => r.meta.requiresAuth);
if (needAuth && !localStorage.getItem("token")) {
next({ path: "/login", query: { redirect: to.fullPath } }); // 登录后回跳
} else {
next();
}
});
router.afterEach((to) => {
document.title = to.meta.title ? `${to.meta.title} - 我的站点` : "我的站点";
});6.4 动态路由
javascript
// 登录后按权限动态添加路由
router.addRoute({
path: "/admin",
component: () => import("./views/Admin.vue"),
meta: { requiresAuth: true },
});七、路由懒加载与代码分割
懒加载的本质是动态 import(),webpack/Vite 会把每个动态导入拆成独立 chunk,用到才加载。
javascript
// 未懒加载:首屏就把所有页面打包进主 bundle
import Home from "./views/Home.vue";
// 懒加载:单独 chunk,导航到该路由时才下载
const Home = () => import("./views/Home.vue");| 方式 | 效果 |
|---|---|
| 静态 import | 全部打进主包,首屏大 |
| 动态 import + 路由懒加载 | 按路由拆包,首屏小 |
| 预加载(prefetch) | 空闲时提前下载附近路由 |
React 侧用 React.lazy + Suspense,Vue 侧直接函数式组件即可,两者底层都是动态 import。
八、路由传参与重定向
| 传参方式 | 示例 | 获取方式 |
|---|---|---|
| 路径参数 | /user/1 | React useParams / Vue route.params |
| 查询参数 | /search?q=vue | React useSearchParams / Vue route.query |
| state 对象 | navigate("/x", { state }) | React useLocation().state |
| 命名路由 | Vue router.push({ name }) | 通过路由名定位 |
重定向汇总:
| 场景 | React Router | Vue Router |
|---|---|---|
| 静态重定向 | <Navigate to="/new" /> | { path: "/old", redirect: "/new" } |
| 动态重定向 | <Navigate to="/login" state= /> | next({ path: "/login" }) |
| 404 兜底 | <Route path="*" /> | /:pathMatch(.*)* |
九、要点速查
| 概念 | 一句话记忆 |
|---|---|
| SPA | 一个 HTML,前端路由无刷新切换视图 |
| Hash 路由 | # 后变化,hashchange 监听,无需服务端配置 |
| History 路由 | pushState 改变 URL,popstate 监听,需服务端 fallback |
| 懒加载 | 动态 import 按路由拆包,用到才下载 |
| 守卫 | 导航前后拦截,做鉴权与埋点 |
| 传参 | 路径参数 / 查询参数 / 路由 state |