使用官方的教学项目
npx create-react-app router-tutorial
安装 react-router 依赖
cd router-tutorial
npm add react-router-dom@6 history@5
react-router-dom是浏览器端的基于react-router库的库,所以装了这个以后就不用再手动装react-router了
修改App.js和 index.js到简单的样子
//src/App.js
export default function App() {
return (
<div>
<h1>Bookkeeper!</h1>
</div>
);
}
// src/index.js
import { render } from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
render(<App />, rootElement);
然后启动项目,然后我们可以在基础上修改了。
# probably this
npm start
# or this
npm run dev
#or
yarn start
01.BrowserRouter
连接你的app到浏览器的URL。
用BrowserRouter包裹在你的App的外面
//src/index.js
import { render } from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
render(
<BrowserRouter>
<App />
</BrowserRouter>,
rootElement
);
02.添加一些链接Link
在src/App.js
里添加一些链接和全局导航。
import { Link } from "react-router-dom";
export default function App() {
return (
<div>
<h1>Bookkeeper</h1>
<nav
style={{
borderBottom: "solid 1px",
paddingBottom: "1rem"
}}
>
<Link to="/invoices">Invoices</Link> |{" "}
<Link to="/expenses">Expenses</Link>
</nav>
</div>
);
}
现在点击那些链接,你会发现地址栏会发生改变,也可以用前进后退在历史记录中移动
03.添加一些路由
src/routes/invoices.jsx
src/routes/expenses.jsx
//src/routes/expenses.jsx
export default function Expenses() {
return (
<main style={{ padding: "1rem 0" }}>
<h2>Expenses</h2>
</main>
);
}
//src/routes/invoices.jsx
export default function Invoices() {
return (
<main style={{ padding: "1rem 0" }}>
<h2>Invoices</h2>
</main>
);
}
接下来我们需要在index.js里面创建路由配置告诉app如何渲染不同的url
//src/index.js
import { render } from "react-dom";
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
import App from "./App";
import Expenses from "./routes/expenses";
import Invoices from "./routes/invoices";
const rootElement = document.getElementById("root");
render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />} />
</Routes>
</BrowserRouter>,
rootElement
);
04.嵌套路由
我们注意到点击链接的时候,App中的布局消失了。只剩下Expenses或Invoices这两个路由指向的内容
嵌套路由的作用就是共享部分UI
我们需要两步操作实现这一点
首先index.js里面对路由进行嵌套。这样两个组件就变成了App组件的子节点
import { render } from "react-dom";
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
import App from "./App";
import Expenses from "./routes/expenses";
import Invoices from "./routes/invoices";
const rootElement = document.getElementById("root");
render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />} />
</Route>
</Routes>
</BrowserRouter>,
rootElement
);
当路由拥有子节点的时候会发生两件事
- 路由的url嵌套 (
"/" + "expenses"
and"/" + "invoices"
) - 子路由组件匹配的时候也会渲染父组件共享的部分
接下来我们在App.jsx添加一个Outlet作为渲染子节点路由的地方
//src/App.jsx
import { Outlet, Link } from "react-router-dom";
export default function App() {
return (
<div>
<h1>Bookkeeper</h1>
<nav
style={{
borderBottom: "solid 1px",
paddingBottom: "1rem"
}}
>
<Link to="/invoices">Invoices</Link> |{" "}
<Link to="/expenses">Expenses</Link>
</nav>
<Outlet />
</div>
);
}
这下我们就可以在两个路由间切换保持共享的布局了。
05.给Invoices路由添加数据
我们模拟真实使用场景,给Invoices路由造点假数据
// src/data.js
let invoices = [
{
name: "Santa Monica",
number: 1995,
amount: "$10,800",
due: "12/05/1995"
},
{
name: "Stankonia",
number: 2000,
amount: "$8,000",
due: "10/31/2000"
},
{
name: "Ocean Avenue",
number: 2003,
amount: "$9,500",
due: "07/22/2003"
},
{
name: "Tubthumper",
number: 1997,
amount: "$14,000",
due: "09/01/1997"
},
{
name: "Wide Open Spaces",
number: 1998,
amount: "$4,600",
due: "01/27/2998"
}
];
export function getInvoices() {
return invoices;
}
然后我们修改invoices.jsx组件,获取并且渲染数据
//src/routes/invoices.jsx
import { Link } from "react-router-dom";
import { getInvoices } from "../data";
export default function Invoices() {
let invoices = getInvoices();
return (
<div style={{ display: "flex" }}>
<nav
style={{
borderRight: "solid 1px",
padding: "1rem"
}}
>
{invoices.map(invoice => (
<Link
style={{ display: "block", margin: "1rem 0" }}
to={`/invoices/${invoice.number}`}
key={invoice.number}
>
{invoice.name}
</Link>
))}
</nav>
</div>
);
}
06.添加一个不匹配路由
我们可以发现,当我们输入一个没有分配地址的路由的时候,会显示空白页。
实际上有一个好办法就是把这些不匹配的路由都导入一个404页面。
我们添加一个"*"路由,这个路由会匹配所有没有匹配其他路由的路由
// src/index.js
<Routes>
<Route path="/" element={<App />}>
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />} />
<Route
path="*"
element={
<main style={{ padding: "1rem" }}>
<p>There's nothing here!</p>
</main>
}
/>
</Route>
</Routes>
07.读取url参数
下面我们添加一些新组件,用于显示固定年份的invoice
// src/routes/invoice.jsx
export default function Invoice() {
return <h2>Invoice #???</h2>;
}
然后我们在invoices路由下面添加这个子路由
// src/index.js
<Routes>
<Route path="/" element={<App />}>
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />}>
<Route path=":invoiceId" element={<Invoice />} />
</Route>
<Route
path="*"
element={
<main style={{ padding: "1rem" }}>
<p>There's nothing here!</p>
</main>
}
/>
</Route>
</Routes>
我们刚刚创建的路由是匹配 "/invoices/2005" and "/invoices/1998"这种格式的。
然后我们还需要在invoices.jsx 添加一个outlet,不然显示不出来子路由的内容
然后我们在invoice.jsx 文件中获取url参数
// src/routes/invoice.jsx
import { useParams } from "react-router-dom";
export default function Invoice() {
let params = useParams();
return <h2>Invoice: {params.invoiceId}</h2>;
}
接着我们在data.js里面添加一个根据年份返回对应年份数据的函数
//...
export function getInvoices() {
return invoices;
}
export function getInvoice(number) {
return invoices.find(
invoice => invoice.number === number
);
}
然后我们就能用这个函数获取数据并且渲染出来了。
import { useParams } from "react-router-dom";
import { getInvoice } from "../data";
export default function Invoice() {
let params = useParams();
let invoice = getInvoice(parseInt(params.invoiceId, 10));
return (
<main style={{ padding: "1rem" }}>
<h2>Total Due: {invoice.amount}</h2>
<p>
{invoice.name}: {invoice.number}
</p>
<p>Due Date: {invoice.due}</p>
</main>
);
}
08.index路由
这可能是react router 里面最难理解的概念。
当我们浏览 invoices 路由的子路由内容,之后我们点击invoices路由的链接,我们发现右侧变成了空白。
我们可以添加一个index路由解决这个问题
// src/index.js
<Routes>
<Route path="/" element={<App />}>
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />}>
<Route
index
element={
<main style={{ padding: "1rem" }}>
<p>Select an invoice</p>
</main>
}
/>
<Route path=":invoiceId" element={<Invoice />} />
</Route>
<Route
path="*"
element={
<main style={{ padding: "1rem" }}>
<p>There's nothing here!</p>
</main>
}
/>
</Route>
</Routes>
接下来我们发现点击invoices路由的时候会默认显示index路由的内容而不是空白。
index路由和其他路由不同的地方是它没有path属性,他和父路由共享同一个路径。
下面几点可以帮助你理解这个概念
- index路由渲染在父路由的outlet,而且路由地址和父路由相同
- index路由在父路由匹配并且其他子路由不匹配的时候 匹配
- index路由是一个父节点默认的子节点
- index路由在用户还没有点击导航中的链接时渲染
09.高亮激活的链接
通常我们需要,特别是在导航列表里面,需要展示给用户当前激活的链接是哪个。
西面我们把invoices.jsx中的Link换成NavLink
import { NavLink, Outlet } from "react-router-dom";
import { getInvoices } from "../data";
export default function Invoices() {
let invoices = getInvoices();
return (
<div style={{ display: "flex" }}>
<nav
style={{
borderRight: "solid 1px",
padding: "1rem"
}}
>
{invoices.map(invoice => (
<NavLink
style={({ isActive }) => {
return {
display: "block",
margin: "1rem 0",
color: isActive ? "red" : ""
};
}}
to={`/invoices/${invoice.number}`}
key={invoice.number}
>
{invoice.name}
</NavLink>
))}
</nav>
<Outlet />
</div>
);
}
我们做了以下3件事
替换Link为NavLink
我们用函数改变样式
我们该百年了链接颜色通过NavLink传递过来的isActive属性
我们也可以利用className做到一样的效果。
// normal string
<NavLink className="red" />
// function
<NavLink className={({ isActive }) => isActive ? "red" : "blue"} />
10.url搜索参数
搜索参数就类似于url参数,但是他们在url中所处的位置不同。
不是由/
分隔,他们出现在一个?
之后,类似于这样的形式 "/login?success=1"or
"/shoes?brand=nike&sort=asc&sortby=price
react router 提供了 useSearchParams 用于读取和操作搜索参数。它有点像useState,不同点是useState是操作内存中的数据,
而他是设置url 搜索参数中的state
// routes/invoices.jsx
import {
NavLink,
Outlet,
useSearchParams
} from "react-router-dom";
import { getInvoices } from "../data";
export default function Invoices() {
let invoices = getInvoices();
let [searchParams, setSearchParams] = useSearchParams();
return (
<div style={{ display: "flex" }}>
<nav
style={{
borderRight: "solid 1px",
padding: "1rem"
}}
>
<input
value={searchParams.get("filter") || ""}
onChange={event => {
let filter = event.target.value;
if (filter) {
setSearchParams({ filter });
} else {
setSearchParams({});
}
}}
/>
{invoices
.filter(invoice => {
let filter = searchParams.get("filter");
if (!filter) return true;
let name = invoice.name.toLowerCase();
return name.startsWith(filter.toLowerCase());
})
.map(invoice => (
<NavLink
style={({ isActive }) => ({
display: "block",
margin: "1rem 0",
color: isActive ? "red" : ""
})}
to={`/invoices/${invoice.number}`}
key={invoice.number}
>
{invoice.name}
</NavLink>
))}
</nav>
<Outlet />
</div>
);
}
11.自定义行为
接着上一节的程序,
我们发现如果我们点击了过滤出来的链接,就不会保持过滤了,input会被清空。
我们能够在点击新链接的时候保持查询字符串,只要我们组合 Navlink和useLocation,组成一个我们自己的QueryNavLink
import { useLocation, NavLink } from "react-router-dom";
function QueryNavLink({ to, ...props }) {
let location = useLocation();
return <NavLink to={to + location.search} {...props} />;
}
类似于 useSearchParams
, useLocation
也会返回一个location告诉我们一些信息。就类似于下面的格式
{
pathame: "/invoices",
search: "?filter=sa",
hash: "",
state: null,
key: "ae4cz2j"
}
12.编程式导航
有时我们需要更改URL,
我们添加一个按钮,将invoice删除,然后导航到索引路由。
首先我们在data.js里添加下面用于删除一个invoice的函数
export function deleteInvoice(number) {
invoices = invoices.filter(
invoice => invoice.number !== number
);
}
然后我们添加删除按钮
// src/routes/invoice.jsx
import { useParams, useNavigate } from "react-router-dom";
import { getInvoice, deleteInvoice } from "../data";
export default function Invoice() {
let navigate = useNavigate();
let params = useParams();
let invoice = getInvoice(parseInt(params.invoiceId, 10));
return (
<main style={{ padding: "1rem" }}>
<h2>Total Due: {invoice.amount}</h2>
<p>
{invoice.name}: {invoice.number}
</p>
<p>Due Date: {invoice.due}</p>
<p>
<button
onClick={() => {
deleteInvoice(invoice.number);
navigate("/invoices");
}}
>
Delete
</button>
</p>
</main>
);
}