路由配置
刀刀
1/2/2025
0 字
0 分钟
本项目都是一级路由,没有二级路由,因此可以采用创建路由数组、通过循环的形式返回路由组件。
在根目录下创建 router
文件夹,在里面新建 routes.js
文件用于设置路由对象数组;index.js
文件用于处理数组生成路由组件。
routes.js
通过懒加载的形式引入除首页外的其他路由页面。
代码示例
import { lazy } from "react";
import Home from "@/views/Home.jsx";
const Detail = lazy(() => import("@/views/Detail.jsx"));
const Store = lazy(() => import("@/views/Store.jsx"));
const Personal = lazy(() => import("@/views/Personal.jsx"));
const Update = lazy(() => import("@/views/Update.jsx"));
const Page404 = lazy(() => import("@/views/Page404.jsx"));
const Login = lazy(() => import("@/views/Login.jsx"));
const routes = [
{
path: "/",
name: "home",
component: Home,
meta: {
title: "知乎日报-WebApp",
},
},
{
path: "/detail/:id",
name: "detail",
component: Detail,
meta: {
title: "新闻详情-知乎日报",
},
},
{
path: "/personal",
name: "personal",
component: Personal,
meta: {
title: "个人中心-知乎日报",
},
},
{
path: "/store",
name: "store",
component: Store,
meta: {
title: "我的收藏-知乎日报",
},
},
{
path: "/update",
name: "update",
component: Update,
meta: {
title: "个人信息修改-知乎日报",
},
},
{
path: "/login",
name: "login",
component: Login,
meta: {
title: "登录-知乎日报",
},
},
{
path: "/*",
name: "404",
component: Page404,
meta: {
title: "404-知乎日报",
},
},
];
export default routes;
index.js
返回路由配置
该文件引入路由对象数组,通过 .map()
的方法循环数组并返回路由组件 <Route />
,最后全局导出路由组件给 <App.js />
根组件使用。
由于采用了路由懒加载的设置,需要外层包裹一层 Suspense
标签
代码如下所示:
import React, { Suspense } from "react";
import routes from "./routes.js";
// 统一路由配置
const Element = (props) => {
};
const RouterView = () => {
return (
<Suspense
fallback={<div>loading</div>}
>
<Routes>
{routes.map((route) => {
let { name, path } = route;
return (
<Route key={name} path={path} element={<Element {...route} />} />
);
})}
</Routes>
</Suspense>
);
};
export default RouterView;
统一配置路由
通过 Element
函数统一配置路由,返回的是一个配置好的 JSX 组件给路由组件使用。配置信息分以下几个步骤:
- 获取
meta
元信息修改页面的title
标题 - 获取路由信息并传递给组件
- 获取组件并返回
- 后续实现路由守卫配置
代码示例
import {
Route,
Routes,
useNavigate,
useLocation,
useParams,
useSearchParams,
} from "react-router-dom";
// ...
// 统一路由配置
const Element = (props) => {
let { component: Component, meta } = props;
// 修改页面的Title
document.title = meta ? meta.title : "知乎日报-mobile";
// 路由守卫设置
// 获取路由信息,基于属性传递给组件
const navigate = useNavigate(),
location = useLocation(),
params = useParams(),
[usp] = useSearchParams();
return (
<Component
navigate={navigate}
location={location}
params={params}
usp={usp}
/>
);
};
懒加载配置
简单的 Loading
文本过于简陋,因此去查看有没有组件可以使用。通过查看官方文档,最终敲定使用 Mask
遮罩层与 DotLoading
加载图标,代码如下:
import { Mask, DotLoading } from "antd-mobile";
// ...
const RouterView = () => {
return (
<Suspense
fallback={
<Mask visible={true}>
<DotLoading color="white" />
</Mask>
}
>
<Routes>
{routes.map((route) => {
let { name, path } = route;
return (
<Route key={name} path={path} element={<Element {...route} />} />
);
})}
</Routes>
</Suspense>
);
};
修改样式:
.adm-dot-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 60px;
}
使用
在 App.js
根组件中引入导出的路由配置并使用,代码如下:
import RouterView from "@/router/index.js";
function App() {
return (
<>
<RouterView />
</>
);
}
export default App;
此时运行会报错,提示需要使用 router
包裹 routes
。在入口文件 index.js
中导入路由模式并包裹 <App />
组件,代码如下:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "@/App";
// ...
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<HashRouter>
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
</HashRouter>
</React.StrictMode>
);
目前为止,路由模块搭建完毕。
路由守卫
引入 redux
仓库内保存的数据,用于判断是否登录来判断能否进入登录后才能进入的页面。如果没有,则调用接口尝试获取数据,获取到数据则放行;获取数据失败则跳转到登录页。代码如下:
代码示例
import React, { Suspense } from "react";
import {
Route,
Routes,
useNavigate,
useLocation,
useParams,
useSearchParams,
Navigate,
} from "react-router-dom";
import routes from "./routes.js";
import { Mask, DotLoading, Toast } from "antd-mobile";
import store from "@/store";
import Api from "@/api";
const Element = async (props) => {
let { component: Component, meta, path } = props;
// 修改页面的Title
document.title = meta ? meta.title : "知乎日报-mobile";
// 路由守卫设置
let {
base: { info },
} = store.getState(),
checkList = ["/personal", "/store"];
if (!info && checkList.includes(path)) {
// 先获取用户信息
let res = await Api.userInfo();
info = res.data;
if (!info) {
// 还是没有信息,说明没登录,需要去重新登录
Toast.show({
icon: "fail",
content: "请先登录",
});
return (
<Navigate
to={{
pathname: "/login",
search: `?to=${path}`,
}}
/>
);
}
// 如果获取到信息,说明已登录,派发任务信息存储到容器中
store.dispatch(res);
}
// 获取路由信息,基于属性传递给组件
const navigate = useNavigate(),
location = useLocation(),
params = useParams(),
[usp] = useSearchParams();
return (
<Component
navigate={navigate}
location={location}
params={params}
usp={usp}
/>
);
};
const RouterView = () => {
//...
};
export default RouterView;
运行后发现报错,提示 Element
必须返回一个 JSX 组件而不是 Promise
对象。因为 Element
是函数式组件,也就是说 async
不能加在 Element
函数上。
转换思路,通过闭包 加 自运行函数的形式调用接口获取数据的形式。
代码示例
// 统一路由配置。不能把async家在这里,因为最终要返回一个 JSX 而不是 Promise
const Element = (props) => {
let { component: Component, meta, path } = props;
// 修改页面的Title
document.title = meta ? meta.title : "知乎日报-mobile";
// 路由守卫设置
let {
base: { info },
} = store.getState(),
checkList = ["/personal", "/store"];
(async () => {
if (!info && checkList.includes(path)) {
// 先获取用户信息
let res = await Api.userInfo();
info = res.data;
if (!info) {
// 还是没有信息,说明没登录,需要去重新登录
Toast.show({
icon: "fail",
content: "请先登录",
});
return (
<Navigate
to={{
pathname: "/login",
search: `?to=${path}`,
}}
/>
);
}
// 如果获取到信息,说明已登录,派发任务信息存储到容器中
store.dispatch(res);
}
})();
// ...
);
};
const RouterView = () => {
//...
};
export default RouterView;
但是保存后运行发现没有效果,他只是提示 “请先登录” ,但是没有跳转登录页。因为调用接口是异步操作,而 Element
需要立刻返回一个 JSX ,因此他无法基于异步操作,实现根据异步结果控制同步渲染。
不能加 async
,闭包异步函数运行比同步慢,那该怎么办呢?
可以用一个变量 isShow
判断,在异步函数执行完毕前先渲染 Loading
效果的遮罩层,然后接口调用完毕后再 return
返回对应的页面或登录页,实现跳转。
isShow
在用户未登录且跳转到需要登陆的页面时,其值为 false
表示需要做校验,否则用户登录了或跳转到首页、详情页这种不需要登录的页面,其值为 true
表示不需要做校验。
如果不需要做校验,直接返回对应 JSX 组件,否则返回 Loading
加载遮罩层。
需要校验的时候,再通过闭包 加 自运行函数 调用接口获取最新的数据,并保存到 redux
内。
代码示例
const isLoading = (path) => {
let {
base: { info },
} = store.getState(),
checkList = ["/personal", "/store"];
return !info && checkList.includes(path);
};
// 统一路由配置。不能把async家在这里,因为最终要返回一个 JSX 而不是 Promise
const Element = (props) => {
let { component: Component, meta, path } = props;
let isShow = !isLoading(path);
let [_, setRandom] = useState(0);
// 路由守卫设置
useEffect(() => {
if (isShow) return;
(async () => {
// 先获取用户信息
let res = await Api.userInfo();
let info = res.data;
if (!info) {
// 还是没有信息,说明没登录,需要去重新登录
Toast.show({
icon: "fail",
content: "请先登录",
});
navigate({
pathname: '/login',
search: `?to=${path}`
}, { replace: true })
return
}
// 如果获取到信息,说明已登录,派发任务信息存储到容器中
store.dispatch(res);
setRandom(+new Date());
})();
});
// 修改页面的Title
document.title = meta ? meta.title : "知乎日报-mobile";
// 获取路由信息,基于属性传递给组件
const navigate = useNavigate(),
location = useLocation(),
params = useParams(),
[usp] = useSearchParams();
return !isShow ? (
<Mask visible={true}>
<DotLoading color="white" />
</Mask>
) : (
<Component
navigate={navigate}
location={location}
params={params}
usp={usp}
/>
);
};
⚠ 注意
- 这里
setRandom
的作用是更新useState
的值后触发render
的更新,实现redux
仓库的数据更新后也能更新视图。 - 如果没登录,路由实际上是从首页跳转到个人页,然后判断出未登录再跳转到登录页,登录后再跳转回个人页,此时路由栈是:首页、个人页、个人页,需要返回两次才是首页。因此在跳转到登录页时要做
replace
操作。
总结
在实现业务时应该按照标准的函数组件的操作,基于状态、变量、周期函数,统一处理登录状态的校验:分析是否需要校验:isShow
true
不需要校验,直接渲染需要渲染的视图即可false
需要校验,执行以下操作:- 先渲染
Loading
异步操作的遮罩层 - 在周期函数中做校验,根据校验结果来决定操作。校验通过,更新视图组件;校验不通过,提示并跳转登录页
- 先渲染