My Little World

重新认识路由

路由管理,就是让你的页面能够根据 URL 的变化进行页面的切换,这是前端应用中一个非常重要的机制

路由的核心逻辑就是根据 URL 路径这个状态,来决定在主内容区域显示什么组件。

1
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
36
37
38
// 自定义路由
const MyRouter = ({ children }) => {
const routes = _.keyBy(
children.map((c) => c.props),
"path",
);
const [hash] = useHash();
const Page = routes[hash.replace("#", "")]?.component;
// 如果路由不存在就返回 Not found.
return Page ? <Page /> : "Not found.";
};


const Route = () => null;

// 应用
function SamplePages {
return (
<div className="sample-pages">
{/* 定义了侧边导航栏 */}
<div className="sider">
<a href="#page1">Page 1</a>
<a href="#page2">Page 2</a>
<a href="#page3">Page 3</a>
<a href="#page4">Page 4</a>
</div>
<div className="exp-15-page-container">
{/* 定义路由配置 */}
<MyRouter>
<Route path="page1" component={Page1} />
<Route path="page2" component={Page2} />
<Route path="page3" component={Page3} />
<Route path="page4" component={Page4} />
</MyRouter>
</div>
</>
);
};

使用React Router

简单应用

1
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
36
37
38
39
40
41
// 从 react-router-dom 引入路由框架提供的一些组件
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
// 引入了两个课程示例页面
import Counter from "./01/Counter";
import UserList from "./01/UserList";


// 使用数组定义了页面组件和导航的标题,从而方便下面的渲染逻辑
const routes = [
["01 Counter", Counter],
["01 UserList", UserList],
];
function App() {
return (
<BrowserRouter>
<div className="app">
<ul className="sider">
{routes.map(([label]) => (
<li>
<Link to={`/${label.replace(" ", "/")}`}>{label}</Link>
</li>
))}
</ul>
<div id="pageContainer" className="page-container">
<Switch>
{routes.map(([label, Component]) => (
<Route key={label} path={`/${label.replace(" ", "/")}`}>
<Component />
</Route>
))}
{/* 定义一个默认的路由 */}
<Route path="/" exact>
<h1>Welcome!</h1>
</Route>
<Route path="*">Page not found.</Route>
</Switch>
</div>
</div>
</BrowserRouter>
);
}

相关组件作用

BrowserRouter:标识用标准的 URL 路径去管理路由,比如 /my-page1 这样的标准 URL 路径。
除此之外,还有 MemoryRouter,表示通过内存管理路由;
HashRouter,标识通过 hash 管理路由。
自己实现的例子其实就是用的 hash 来实现路由。

Link:定义一个导航链接,点击时可以无刷新地改变页面 URL,从而实现 React Router 控制的导航。

Route: 定义一条路由规则,可以指定匹配的路径、要渲染的内容等等。

Switch:在默认情况下,所有匹配的 Route 节点都会被展示,但是 Switch 标记可以保证只有第一个匹配到的路由才会被渲染。

react router

使用嵌套路由:实现二级导航页面

需要路由框架具备两个能力:

能够模糊匹配。比如 /page1/general 、/page1/profile 这样两个路由,需要都能匹配到 Page1 这样一个组件。
然后 Page1 内部呢,再根据 general 和 profile 这两个子路由决定展示哪个具体的页面。

Route 能够嵌套使用。在我们自定义 Route 的例子中,Route 组件仅用于收集路由定义的信息,不渲染任何内容。
如果需要路由能嵌套使用,那就意味着需要在 Route 下还能嵌套使用 Route。而这在 React Router 是提供支持的。

1
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
36
37
38
39
40
41
42
43

import { BrowserRouter, Route, Link } from "react-router-dom";


const Page1 = () => {
return (
<div className="exp-15-page1">
<div className="exp-15-page1-header">
<Link to="/page1/general">General</Link>
<Link to="/page1/profile">Profile</Link>
<Link to="/page1/settings">Settings</Link>
</div>
<div className="exp-15-page1-content">
<Route path="/page1/general">General Page</Route>
<Route path="/page1/profile">Profile Page</Route>
<Route path="/page1/settings">Settings Page</Route>
</div>
</div>
);
};
const Page2 = () => "Page 2";
const Page3 = () => "Page 3";


function NestedRouting() {
return (
<BrowserRouter>
<h1>Nested Routing</h1>
<div className="exp-15-nested-routing">
<div className="exp-15-sider">
<Link to="/page1">Page 1</Link>
<Link to="/page2">Page 2</Link>
<Link to="/page3">Page 3</Link>
</div>
<div className="exp-15-page-container">
<Route path="/page1"><Page1 /></Route>
<Route path="/page2"><Page2 /></Route>
<Route path="/page3"><Page3 /></Route>
</div>
</div>
</BrowserRouter>
);
}

在 URL 中保存页面状态(页面参数)

在url携带页面相关信息,将页面的一些状态存放到 URL 中,
一方面可以提升用户体验,另一方面也可以简化页面之间的交互。

1
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

<Route path="/tabs-page/:activeTab" component={TabsPage} />

import { useCallback } from "react";
import { Tabs, Table } from "antd";
import { useHistory, useParams } from "react-router-dom";
import data from "../10/data";
import { useSearchParam } from "react-use";


const { TabPane } = Tabs;


export default () => {
// 通过 React Router 的 API 获取 activeTab 这个参数信息
const { activeTab = "users" } = useParams();
// 通过查询字符串获取当前的页码信息
const page = parseInt(useSearchParam("page"), 10) || 1;

// 通过 React Router 提供的 history 对象来操作 URL
const history = useHistory();
const handleTabChange = useCallback(
(tab) => history.push(`/15/TabsPage/${tab}`),
[history],
);
// 定义表格的翻页功能
const pagination = {
pageSize: 3,
current: page,
onChange: (p) => {
history.push(`/15/TabsPage/${activeTab}?page=${p}`);
},
};
return (
<div>
<h1>Tabs Page</h1>
<Tabs activeKey={activeTab} onChange={handleTabChange}>
<TabPane tab="Users" key="users">
<Table
dataSource={data}
columns={[
{ dataIndex: "name", title: "User Name" },
{ dataIndex: "city", title: "City" },
]}
pagination={pagination}
/>
</TabPane>
<TabPane tab="Jobs" key="jobs">
<Table
dataSource={data}
columns={[{ dataIndex: "job", title: "Job Title" }]}
pagination={pagination}
/>
</TabPane>
</Tabs>
</div>
);
}

注意

这里遵循了唯一数据源的原则,避免定义中间状态去存储 tab 和页码的信息,而是直接去操作 URL,这样可以让代码逻辑更加清晰和直观。

路由层面实现权限控制

利用前端路由的动态特性。路由是通过 JSX 以声明式的方式去定义的,这就意味着路由的定义规则是可以根据条件进行变化的,也就是所谓的动态路由。

1
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65


import { useState } from "react";
import { Button } from "antd";
import { Route, Link } from "react-router-dom";


// 定义了两个示例页面组件
const Page1 = () => "Page 1";
const Page2 = () => "Page 2";


// 定义了一个组件用户展示未登录状态
const UnauthedPage = () => (
<span style={{ color: "red" }}>Unauthorized, please log in first.</span>
);
export default () => {
// 模拟用户是否登录的状态,通过一个按钮进行切换
const [loggedIn, setLoggedIn] = useState(false);

// 定义了两套路由,一套用于登录后,一套用于未登录状态
const routes = loggedIn
? [
{
path: "/15/RouterAuth",
component: Page1,
},
{
path: "/15/RouterAuth/page1",
component: Page1,
},
{
path: "/15/RouterAuth/page2",
component: Page2,
},
]
// 如果未登录,那么对于所有 /15/RouterAuth 开头的路径,显示未授权页面
: [{ path: "/15/RouterAuth", component: UnauthedPage }];


return (
<div>
<h1>Router Auth</h1>
<Button
type={loggedIn ? "primary" : ""}
onClick={() => setLoggedIn((v) => !v)}
>
{loggedIn ? "Log Out" : "Log In"}
</Button>


<div className="exp-15-router-auth">
<div className="exp-15-sider">
<Link to="/15/RouterAuth/page1">Page 1</Link>
<Link to="/15/RouterAuth/page2">Page 2</Link>
</div>
<div className="exp-15-page-container">
{/* */}
{routes.map((r) => (
<Route path={r.path} component={r.component} />
))}
</div>
</div>
</div>
);

代码中核心的机制就在于我们根据登录状态,创建了不同的路由规则,这样就能在源头上对权限进行集中控制,避免用户未经授权就访问某些受保护的页面。

同时呢,因为在相同的 URL 下进行了信息提示,那么也就更容易实现用户登录后还能返回原页面的功能。