# 通过createBrowserRouter创建路由
1. 创建路由(routes.tsx)
import { createBrowserRouter } from 'react-router-dom';
import Home from './Home';
import About from './About';
import App from './App';
export const router = createBrowserRouter([
{
path: '/',
element: <App />, // App 作为顶级路由
children: [
{
path: 'home',
element: <Home />,
},
{
path: 'about',
element: <About />,
},
],
},
]);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2. 使用 RouterProvider 包裹应用(main.tsx 或 index.tsx)
import React from 'react';
import ReactDOM from 'react-dom';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes';
ReactDOM.render(
<RouterProvider router={router} />,
document.getElementById('root')
);
2
3
4
5
6
7
8
9
# React的beforeEach实现
Vue的路由守卫beforeEach在页面路由发生变化时便会执行。 React没有提供类似的Api,但是AppApp 组件在每次路由变化时都会被执行。于是我们可以通过 React Router 的一些功能来实现类似的效果。
使用 useEffect 和 useLocation 实现路由守卫
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
const App = () => {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
// 类似于 beforeEach 的效果
const handleRouteChange = () => {
// 这里可以执行路由守卫逻辑,比如权限验证
const isAuthenticated = false; // 假设为未登录状态
if (!isAuthenticated && location.pathname !== '/login') {
navigate('/login'); // 如果没有登录就跳转到登录页
}
};
handleRouteChange(); // 路由变化时触发
// 如果有条件,可以在此加入监听路由变化的逻辑
}, [location.pathname]);
return <App />;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
当我们满怀欣喜的以为就能大功告成时,却发现控制台报错了:
Uncaught Error: useLocation() may be used only in the context of a <Router> component.
原来这是因为 React Router提供的hooks useLocation,useNavigate必须在Router组件的内部使用
要解决这个问题,你需要确保 App 组件在 Router 组件的包裹下运行
在index.tsx中包裹
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
2
3
4
5
6
但是如果使用的是createBrowserRouter呢?
那我们就需要将App组件作为根路由
routes.tsx
export const routes: RouteConfig[] = [
{
path: "/login",
element: (
<Login />
),
meta: {
name: "登录页",
},
},
{
path: "/",
errorElement: <Error />,
element: <App />,
children: [
{ path: "", element: <Home /> },
{
path: "about1",
element: <About1 />,
},
{
id: "user",
path: "about",
element: <About />,
meta: { requireAuth: true },
loader: userLoader,
},
{ path: "test", element: <Test /> },
],
},
{ path: "*", element: <NotFound /> },
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
App.tsx
export default function App() {
const { isLogin } = useContext(GlobalContext);
const location = useLocation();
const matches = useMatches();
// to 即我们要跳转的页面路由元信息
const to = matches.find((item) => item.pathname === location.pathname);
console.log("to: ", to);
const navigate = useNavigate();
const handleRouteChange = () => {
console.log("------全局路由守卫------", isLogin);
// 如果未登录,且去的页面不是登录页,则重定向到登录页
if (!isLogin && location.pathname !== "/login") {
navigate("/login");
}
// 如果已登录,不可直接去登录页,应通过logout退出登录来跳转登录页
if (isLogin && location.pathname === "/login") {
navigate("/");
}
// 需要鉴权的页面
if (to?.handle?.meta?.requireAuth) {
// 判断有没有这个页面的权限
// ...
// 没有则跳转至指定页
navigate("/unpermission");
}
};
useEffect(() => {
handleRouteChange();
}, [location]);
return <AppLayout />;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
index.tsx
root.render(
<Suspense fallback={<Loading />}>
<Router />
</Suspense>
);
2
3
4
5
# 如果我同时还需要全局Context怎么办
将ContextProvider组件放在最外面
首先创建Context并提供Provider组件
interface IGlobalContext {
isLogin: boolean
setLogin: React.Dispatch<React.SetStateAction<boolean>>
}
export const GlobalContext = createContext<IGlobalContext>({
isLogin: JSON.parse(localStorage.getItem('isLogin') || 'false'),
setLogin: () => {}
})
// 创建 Provider 组件
interface GlobalProviderProps {
children: JSX.Element;
}
export const GlobalProvider: React.FC<GlobalProviderProps> = ({ children }) => {
const [isLogin, setLogin] = useState<boolean>(() => {
const storeValue = localStorage.getItem("isLogin");
return storeValue ? JSON.parse(storeValue) : false;
});
return (
<GlobalContext.Provider value={{ isLogin, setLogin }}>
{children}
</GlobalContext.Provider>);
};
// 创建自定义 hook,便于使用
export const useGlobalContext = () => {
const context = useContext(GlobalContext);
if (!context) {
throw new Error('useGlobalContext must be used within a GlobalProvider');
}
return context;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
在index.tsx中最外层用 GlobalProvider 包裹
root.render(
<GlobalProvider>
<Suspense fallback={<Loading />}>
<Router />
</Suspense>
</GlobalProvider>
);
2
3
4
5
6
7
请注意:当Context Provider的value属性发生变化时,所有使用 useContext 或 Context.Consumer 订阅了该 Provider 的组件都会重新渲染。
如果没有订阅该 Context 的组件不会受到影响,因此不会重新渲染。
React 使用的是浅比较(Object.is),如果 value 是一个引用类型(比如对象或数组),即使它的内容没有变化,只要引用变了,所有消费该 Context 的组件都会重新渲染。
最终实现
const routes = [
// ...
]
const router = createBrowserRouter(routes);
const Router = () => <RouterProvider router={router} />;
export default Router;
2
3
4
5
6
7
8
9
# 路由元信息meta的实现
React-Router6中的route.meta通过useMatches和route.handle来实现
{
path: "test",
element: <Test />,
handle: {
meta: { title: "Test", description: "This is the test page." },
},
},
2
3
4
5
6
7
在页面组件中使用useMatches
const location = useLocation();
const matches = useMatches();
// to 即我们要跳转的页面路由元信息
const to = matches.find((item) => item.pathname === location.pathname);
console.log("to: ", to);
2
3
4
5