参考链接: reacttraining.com/react-route…
安装
npm install --save react-router-dom
例子
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/user">User</Link>
</li>
<li>
<Link to="/user/1">User 1</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/user/:id" component={UserId} />
<Route path="/user" component={User} />
<Route path="/" component={Home} />
</Switch>
</div>
</Router>
);
}
function Home() {
return <h2>Home</h2>;
}
function User() {
return <h2>User</h2>;
}
function UserId() {
return <h2>User 1</h2>;
}
基础组件
BrowserRouter 和 HashRouter
路由渲染方式,传统的 / 或者 hash 跳转
-
<BrowserRouter> -
<HashRouter>
路由匹配组件 Switch 和 Route
渲染
如果 <Switch> 组件下的 <Route> 与当前 URL 匹配,则渲染对应 Route 组件中的元素,如果没有匹配的 URL,则 render null。
路由匹配规则
注意:不是整个 URL (只要 URL 能将 Route 中的规则能从头到尾都匹配上,就算匹配成功)
例如将上例修改下 /user/:id 和 user 两条路由规则的顺序,点击 User 1 渲染的也是 User(因为 /user 也能匹配到 /user/1)
<Route path="/user" component={User} />
<Route path="/user/:id" component={UserId} />
所以,<Route path="/"> 不能写在头部,不然所有的路由都会被匹配到这条规则
如果需要完全匹配整个路由的话,可以使用 exact
(<Router exact path="/"> 表示只有路由为 / 的时候,才会渲染,其他不渲染)
<Route exact path="/" component={Home} />
<Route exact path="/user" component={User} />
<Route exact path="/user/:id" component={UserId} />
路由导航
Link
React Route 提供 <Link> 组件来生成路由导航,渲染出 <a> 标签
NavLink
<NavLink> 是个特殊的导航标签,当路由匹配的时候,可以渲染相关元素
activeClassName(string):设置选中样式,默认值为activeactiveStyle(object):当元素被选中时,为此元素添加样式exact(bool):为true时,只有当导致和完全匹配class和style才会应用strict(bool):为true时,在确定为位置是否与当前URL匹配时,将考虑位置pathname后的斜线isActive(func): 判断链接是否激活的额外逻辑的功能
<NavLink to="/user" activeStyle={{backgroundColor: "yellow"}} exact>User</NavLink>
Redirect
路由重定向
<Route exact path="/">
<Redirect to="/user" />
</Route>
服务端渲染
参考链接:Using React Router 4 with Server-Side Rendering
例子
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter as Router, Route, Switch, Link } from 'react-router-dom';
import fs from 'fs';
import path from 'path';
import express from 'express';
const app = express();
function App() {
return (
<div>
<ul>
<li><Link to="/" >Home</Link></li>
<li><Link to="/todos">todos</Link></li>
<li><Link to="/posts">posts</Link></li>
</ul>
<Switch>
<Route exact path="/">Home</Route>
<Route exact path="/todos">todos</Route>
<Route exact path="/posts">posts</Route>
</Switch>
</div>
)
}
app.get('/*', function (req, res) {
const context = {};
const app = renderToString(
<Router location={req.url} context={context}>
<App />
</Router>
);
fs.readFile(path.resolve('./index.html'), 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops, better luck next time!');
}
return res.send(
data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
);
})
})
app.listen(5000, () => {
console.log(`😎 Server is listening on port 5000`);
});
Redirect
在静态路由中,使用 <Redirect> 进行重定向时,React Router 会自动将带有重定向 URL 的属性添加到 context 对象中
//...
function App() {
...
<Redirect to='/todos' />
...
}
fs.readFile(path.resolve('./index.html'), 'utf8', (err, data) => {
...
if(context.url) {
console.log(context.url);
return res.redirect(301, context.url);
}
...
})
添加具体的 context 属性
添加到 staticContext 属性中
例如 301、302、401、404 等状态值
function Status({code, children}) {
return (
<Route
render={({staticContext}) => {
if(staticContext) staticContext.status= code;
return children;
}}
/>
)
}
function NotFound() {
return (
<Status code={404}>
<div>
<h1>Sorry, can't find that.</h1>
</div>
</Status>
)
}
function App() {
return (
<div>
<ul>
<li><Link to="/" >Home</Link></li>
<li><Link to="/todos">todos</Link></li>
<li><Link to="/posts">posts</Link></li>
</ul>
<Switch>
<Route exact path="/">Home</Route>
{/* <Route exact path="/todos">todos</Route> */}
<Route exact path="/posts">posts</Route>
<NotFound />
</Switch>
</div>
)
}
app.get('/*', function (req, res) {
...
if(context.status === 404) {
res.status(404);
}
...
})
数据加载
数据加载完后,再渲染页面
小例子
- 获取数据接口 (
isomorphic-fetch)
// loadData.js
import 'isomorphic-fetch';
export default resourceType => {
return fetch(`https://jsonplaceholder.typicode.com/${resourceType}`)
.then(res => {
return res.json();
})
.then(data => {
// only keep 10 first results
return data.filter((_, idx) => idx < 10);
});
};
- 路由文件,可以使用
loadData用于加载数据
// router.js
import App from './App';
import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';
import loadData from './loadData';
const Routes = [
{
path: '/',
exact: true,
component: Home
},
{
path: '/posts',
component: Posts,
loadData: () => loadData('posts')
},
{
path: '/todos',
component: Todos,
loadData: () => loadData('todos')
},
{
component: NotFound
}
];
export default Routes;
- 在服务端使用
matchPath找到当前路由,并判断当前路由是否具有loadData属性。如果有的话,则调用loadData方法去获取数据,并将数据添加到服务端响应中(下例中,还将数据添加到了浏览器全局变量__ROUTE_DATA__中)
// index.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter as Router, matchPath } from 'react-router-dom';
import express from 'express';
import fs from 'fs';
import path from 'path';
import serialize from 'serialize-javascript';
import Routes from './routes';
import App from './app';
const app = express();
app.get('/*', function(req, res) {
const currentRoutes = Routes.find(route => matchPath(req.url, route)) || {};
let promise;
if(currentRoutes.loadData) {
promise = currentRoutes.loadData();
} else {
promise = Promise.resolve(null);
}
promise.then(data => {
const context = {data}; // 加载到的数据将被添加到 context 中
const app = renderString(
<Router location={req.url} context={context}>
<App />
</Router>
);
fs.readFile(path.resolve('./index.html'), (err, indexData) => {
if(err) {
console.error('Something went wrong', err);
return res.status(500).send('Oops, better luck next time!')
}
if(context.status === 404) {
res.status(404);
}
if(context.url) {
return res.redirect(301, context.url)
}
return res.send(
indexData
.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
.replace(
'</body>',
`<script>window.__ROUTE_DATA__ = ${data}</script></body>`
)
);
})
});
})
app.listen(5000, () => {
console.log(`😎 Server is listening on port 5000`);
});
- 上例中,加载到的数据已被添加到
context对象中,在服务端渲染时,我们将从staticContext对象中获取到
// todo.js
import React from 'react';
import loadData from './loadData';
class Todos extends React.Component {
constructor(props) {
super(props);
if (props.staticContext && props.staticContext.data) {
this.state = {
data: props.staticContext.data
};
} else {
this.state = {
data: []
};
}
}
componentDidMount() { // 在浏览器端才会调用
// why of a use setTimeout?
// This is just so that we can for the next JavaScript tick to ensure that __ROUTE_DATA__
// is available.
setTimeout(() => {
if (window.__ROUTE_DATA__) {
this.setState({
data: window.__ROUTE_DATA__
});
delete window.__ROUTE_DATA__;
} else {
loadData('todos').then(data => { // 此时是浏览器端再发请求,与服务端无关
this.setState({
data
});
});
}
}, 0);
}
render() {
const { data } = this.state;
return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>;
}
}
export default Todos;
React Router Config提供matchRoutes和renderRoutes方法
如果匹配嵌套路由的话, matchPath 只能匹配到单条,matchRoutes 则可以匹配到所有的
例如下例,
{
path: '/todos',
component: Todos,
loadData: () => loadData(),
routes: [
{
path: '/todos/:id',
exact: true,
component: TodosId,
loadData: () => loadData()
}
]
}
mathPath 匹配到的结果
const currentRoutes = Routes.find(route => matchPath(req.url, route)) || {};
console.log(currentRoutes);
/**
{
path: '/todos',
component: [Function: Todos],
loadData: [Function: loadData],
routes: [
{
path: '/todos/:id',
exact: true,
component: [Function: TodosId],
loadData: [Function: loadData]
}
]
}
**/
matchRoutes 匹配到的结果
const matchingRoutes = matchRoutes(Routes, req.url);
console.log(matchingRoutes);
/**
[
{
route: {
path: '/todos',
component: [Function: Todos],
loadData: [Function: loadData],
routes: [Array]
},
match: { path: '/todos', url: '/todos', isExact: false, params: {}}
},
{
route: {
path: '/todos/:id',
exact: true,
component: [Function: TodosId],
loadData: [Function: loadData]
},
match: {
path: '/todos/:id',
url: '/todos/1',
isExact: true,
params: [Object]
}
}
]
**/
所以如果是有嵌套路由的话,则可以使用 matchRoutes
import { matchRoutes } from 'react-router-config';
// ...
const matchingRoutes = matchRoutes(Routes, req.url);
let promises = [];
matchingRoutes.forEach(route => {
if (route.loadData) {
promises.push(route.loadData());
}
});
Promise.all(promises).then(dataArr => {
// render our app, do something with dataArr, send response
});
// ...
renderRoutes:接受一个路由静态文件,返回 Route 组件;当使用 matchRoutes 的时候,应该使用 renderRoutes 方法
// app.js
import React from 'react';
import { renderRoutes } from 'react-router-config';
import { Switch, NavLink } from 'react-router-dom';
import Routes from './routes';
import Todos from './Todos';
export default props => {
return (
<div>
<ul>
<li><NavLink to="/" >Home</NavLink></li>
<li><NavLink to="/todos">todos</NavLink></li>
<li><NavLink to="/posts">posts</NavLink></li>
</ul>
<Switch>
{renderRoutes(Routes)}
</Switch>
</div>
);
};
- 上述例子
路由懒加载
使用 @loadable/component 插件为例
// home.js
import React from 'react';
export default function Home() {
return (
<h2>Home</h2>
)
}
// user.js
import React from 'react';
export default function User() {
return (
<h2>User</h2>
)
}
// app.js
import React from 'react';
import loadable from "@loadable/component";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from 'react-router-dom'
function Loading() {
return (
<div>
等待....
</div>
)
}
const HomeComponent = loadable(() => import('./home'), {
fallback: <Loading />
});
const UserComponent = loadable(() => import('./user'), {
fallback: <Loading />
});
function App() {
return (
<Router>
<div>
<ul>
<li>
<Link to='/'>Home</Link>
</li>
<li>
<Link to='/user'>User</Link>
</li>
</ul>
<Switch>
<Route exact path='/' component={HomeComponent} />
<Route exact path='/user' component={UserComponent} />
</Switch>
</div>
</Router>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
可见,User 组件只有在点击使用的时候,才会被加载到
滚动还原
为什么需要有滚动还原:Automatic scroll restoration in Single Page Applications (SPA)
- 浏览器按回退键,滚动位置可能会有跳动
- 不同浏览器的表现可能不一致
都还原到网页顶部
function ScrollToTop() {
const {pathname} = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
const App = () => (
<Router>
<ScrollToTop />
<nav>
<ul>
<li>
<Link to='/home'>Home</Link>
</li>
<li>
<Link to='/users'>Users</Link>
</li>
</ul>
</nav>
<Switch>
<Route exact path='/home' component={Home} />
<Route exact path='/users' component={User} />
</Switch>
</Router>
)
- 不加
<ScrollToTop />前,滚动条不会被改变
- 加
<ScrollToTop />后,路由跳转,还是浏览器点击前进/后退,都会置顶
如果想在切换 tab 的时候,不置顶,则可修改如下
import { useEffect } from "react";
function ScrollToTopOnMount() {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return null;
}
动态路由
传统的路由都是需要这种路由配置文件
// express
app.get("/", handleIndex);
app.get("/invoices", handleInvoices);
app.listen();
// Angular
const appRoutes: Routes = [
{
path: "crisis-center",
component: CrisisListComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)]
})
export class AppModule {}
在 React Router 在 v4 版本后,开始脱离这种静态路由文件配置模式,而是直接将路由写在 render 中
如下例,点击 dashboard 将显示 dashboard,不点击的话,则 render null
const App = () => (
<BrowserRouter>
<Dashboard />
</BrowserRouter>
)
const Dashboard = () => (
<div>
<nav>
<Link to="/dashboard">Dashboard</Link>
</nav>
<div>
<Route path='/dashboard'>
Dashboard
</Route>
</div>
</div>
)
子路由
新版的 React Router,路由可以像一个组件一样定义
const App = () => (
<BrowserRouter>
<div>
<Route path="/tacos" component={Tacos} />
</div>
</BrowserRouter>
)
const Tacos = ({match}) => (
<div>
<Route path={match.url + "/carnitas"}>
carnitas
</Route>
</div>
)
响应式路由
有了动态路由之后,就可以通过媒体查询实现动态的路由匹配
const App = () => (
<BrowserRouter>
<div>
<Route path="/invoices" component={Invoices} />
</div>
</BrowserRouter>
)
const Invoices = () => {
return (
<div>
<Media query="(max-width: 599px)"> // react-media是React的CSS媒体查询组件
{matches =>
matches ? (
<Switch>
<Route exact path="/invoices/small">
small
</Route>
<Redirect from="/invoices" to="/invoices/small" />
</Switch>
) : (
<Switch>
<Route exact path="/invoices/large">
Large
</Route>
<Redirect from="/invoices" to="/invoices/large" />
</Switch>
)
}
</Media>
</div>
)
}
也可参考这篇文章:Conditional Routing with React Router v4
测试
使用 <MemoryRouter>
- 例子:
// app.js
import React from 'react';
import {
Route,
Link
} from 'react-router-dom'
const App = () => (
<div>
<Route
exact
path="/"
render={() => (
<div>
<h1>Welcome</h1>
</div>
)}
/>
<Route
path="/dashboard"
render={() => (
<div>
<h1>Dashboard</h1>
<Link to="/" id="click-me">
Home
</Link>
</div>
)}
/>
</div>
);
export default App;
// app.test.js
import React from 'react';
import { render, unmountComponentAtNode } from "react-dom";
import { MemoryRouter } from "react-router-dom";
import { act } from 'react-dom/test-utils';
import App from './app';
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// 退出时进行清理
unmountComponentAtNode(container);
container.remove();
container = null;
});
it('navigates home when you click the link', async () => {
render(
<MemoryRouter initialEntries={['/dashboard']}>
<App />
</MemoryRouter>,
container
)
act(() => {
const goHomeLink = document.querySelector('#click-me');
goHomeLink.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(document.body.textContent).toBe('Welcome');
})
- 测试
location
官网中说,不应该在用例中,经常测试 location 、 history,但是如果需要的话(例如验证地址栏中是否设置了新的查询参数),则可以在测试中更新一条可以更新变量的路由
it("clicking filter links updates product query params", () => {
let history, location;
render(
<MemoryRouter initialEntries={["/dashboard"]}>
<App />
<Route
path="*"
render={({ history, location }) => {
history = history;
location = location;
return null;
}}
/>
</MemoryRouter>,
container
);
act(() => {
// example: click a <Link> to /products?id=1234
});
// assert about url
expect(location.pathname).toBe("/products");
const searchParams = new URLSearchParams(location.search);
expect(searchParams.has("id")).toBe(true);
expect(searchParams.get("id")).toEqual("1234");
});
其他方案
-
如果你的测试环境具有浏览器全局变量
window.location和window.history(这是通过JSDOM在Jest中的默认设置,但您无法重置测试之间的历史记录),则也可以使用BrowserRouter -
您可以将基本路由器与历史包中的历史道具一起使用,而不是将自定义路由传递给
MemoryRouter
// app.test.js
import { createMemoryHistory } from "history";
import { Router } from "react-router";
test("redirects to login page", () => {
const history = createMemoryHistory();
render(
<Router history={history}>
<App signedInUser={null} />
</Router>,
node
);
expect(history.location.pathname).toBe("/login");
});
Redux 整合
例如当
- 该组件通过
connect()连接到redux - 组件不是路由组件(
<Route component={SomeConnectedThing} />中someConnectedThing组件)
这些情况下,当路由变化时,组件却没有更新
解决方法:
// before
export default connect(mapStateToProps)(Something)
// after
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))
深度整合
例如
- 同步路由和
store中的数据 - 通过
dispath actions进行导航 Have support for time travel debugging for route changes in the Redux devtools.
官方建议:上述情况的话, 不要在 Redux store 中使用路由,如果要使用的话,建议使用 Connected React Router( React Router v4 和 Redux 的第三方绑定)