基本使用
~ pnpm add react-router-dom
~ pnpm add @types/react-router-dom -D
路由页面
// src/pages/Home.tsx
const Home: FC = () => Home Page;
export default Home;
// src/pages/About.tsx
const About: FC = () => About Page;
export default About;
嵌套路由
// src/pages/User.tsx
type UserParams = { username: string };
const User: FC = () => {
const { username } = useParams();
return current user is {username};
};
const UsersIndex: FC = () => {
const match = useRouteMatch();
return (
-
User List
-
Pincman
Please select a user.
);
};
export default UsersIndex;
链接菜单
// src/components/Menu.tsx
const Menu: FC = () => (
-
Home
-
About
-
Users
);
export default Menu;
构建路由
// src/App.tsx
...
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const App: FC = () => {
return (
);
};
export default App;
路由模式
路由模式有真实地址
和hash
两种,真实地址
在生产环境下需要服务器软件URL重写(如: nginx)的支持,其生成的url
是example.com/some/path
这样的格式.而hash
则不需要,只要有CDN的地方扔上去就可以,其url
看上去是这样example.com/#/your/page
真实地址
通过BrowserRouter
组件生成,hash
模式通过HashRouter
生成,他们可以通过使用Router
这个底层组件自由切换,如下
安装history
history
版本必须为4.x.x,因为react-router v5版本还不支持history
v5
~ pnpm add history@"<5.0.0"
// src/App.tsx
const App: FC<{ hash?: boolean }> = ({ hash }) => {
const historyCreator = hash ? createHashHistory() : createBrowserHistory();
return (
...
);
};
现在如果需要hash
模式,可以这样
// src/main.tsx
ReactDOM.render(
,
document.getElementById('root'),
);
当然也可以把hash
选项放到状态管理(比如我们前面讲得context
+userReducer
或者后续将会讲到的redux,[mobx][]等)中手动更换,这样会更灵活方便,甚至还可以把这个配置放到localstorage中储存!
精确匹配
Route
组件用于根据URL路径来匹配路由,Switch
组件则用于匹配第一个路由,如果在Route
列表外部不包装Switch
则会匹配所有符合条件的路由,试一下把App.tsx
中的Switch
组件去除,然后访问/about
,这时Home
页面也会呈现,因为/about
同时匹配了/
由于Switch
总是匹配第一个符合的路由,那么如果我们把Home
放到最前面会发生什么?这将导致我们访问任何其它路由都只会显示Home
页面,这时候就需要为/
路由添加添加一个exact
参数来设置精确匹配了
// src/App.tsx
const App: FC<{ hash?: boolean }> = ({ hash }) => {
...
};
这时访问/
则只会匹配Home
页面了
路由导航
导航组件
一般导航用Link
就可以了,但是在需要对匹配的链接添加一个CSS类的时候可以使用NavLink
(如导航栏),具体使用如下
React
路由跳转
Redirect
组件用于设置跳转路由,如下
// src/App.tsx
...
在导航链接上添加/test
// src/components/Menu.tsx
...
-
redirect to home
编程式导航
通过useHistory
这个hook可以获得history
对象,通过这个对象可以实现push
,replace
等方式的手动跳转
const Menu: FC = () => {
const history = useHistory();
return (
...
);
};
export default Menu;
代码分割
动态导入
通过动态导入的方式实现代码分割,可以使每个组件和页面只在需要的时候加载
动态导入需要安装@loadable/component
这个库
~ pnpm add @loadable/component
~ pnpm add @types/loadable__component -D
现在可以尝试一下动态导入About
页面
// src/App.tsx
const About = loadable(() => import('./pages/About'));
当我们从http://localhost:4000
导航到/about
时,开发者工具
->networks
中会显示一个异步加载的About.tsx
页面,表示已经异步加载
对于每个页面都进行一次loadable
的HOC包装是一件非常繁琐的事情,为了简单,可以一次性把所有需要的页面进行代码分割出去,并通过页面名称的字符串来动态加载页面,在vitejs下使用glob
的方式实现,而cra或者webpack等可以直接使用import(
./${props.page})
的方式
vitejs通过const pages = import.meta.glob('../../pages/**/*.{tsx,jsx}')
这样的语法可加载pages
目录下的所有页面并最终生成如下代码
const modules = {
'./dir/foo.js': () => import('./dir/foo.js'),
'./dir/bar.js': () => import('./dir/bar.js')
}
我们在添加一个函数,作用是通过正则去除前缀路径,比如../../pages/
和后缀,比如.tsx
,.jsx
等,就获得最终的动态导入对象
// src/components/Router/view.tsx
const getAsyncImports = (imports: Record Promise>, reg: RegExp) => {
return Object.keys(imports)
.map((key) => {
const names = reg.exec(key);
return Array.isArray(names) && names.length >= 2
? { [names[1]]: imports[key] }
: undefined;
})
.filter((m) => !!m)
.reduce((o, n) => ({ ...o, ...n }), []) as unknown as Record Promise>;
};
const pages = getAsyncImports(
import.meta.glob('../../pages/**/*.{tsx,jsx}'),
/..\/..\/pages\/([\w+.?/?]+).tsx|.jsx/i,
);
最终pages
对象会像这样
如果生产环境下,后缀会是
jsx
const modules = {
Home: () => import('../../pages/Home.tsx')
About: () => import('../../pages/About.tsx'),
'users/Index': () => import('../../pages/users/Index.tsx')
}
现在loadable('Home')
就会异步加载Home
页面了
export const AsyncPage = ({ page }: { page: string }) => {
const View = loadable(pages[page], {
cacheKey: () => page.replaceAll('/', '-'),
});
return ;
};
修改App.tsx
试一下
添加"加载中"组件
为了保证不会因为
loadding
组件的每次计算而导致页面总是被重新渲染,使用useMemo
缓存
const Loading: FC = () => (
加载中
);
export const AsyncPage: FC<{ page: string; loading?: JSX.Element }> = ({ page, loading }) => {
const fallback = useMemo(() => loading ?? , []);
const View = loadable(pages[page], {
cacheKey: () => page.replaceAll('/', '-'),
fallback,
});
return ;
};
以上代码看不出效果,可以使用p-min-delay
来做一下延迟测试(请注意: 生产环境中不要添加延迟),并且需要设置一下超时
~ pnpm add p-min-delay promise-timeout
~ pnpm add @types/promise-timeout -D
代码
const ViewHoc = loadable(() => timeout(pMinDelay(pages[page](), 500), 12000), {
cacheKey: () => page.replaceAll('/', '-'),
fallback,
});
render函数
[react router][]加载组件页面有三种方案
component
加载,如<Route path="/home" component={Home} />
children
方式加载,如<Route path="/home"><Home /></Route>
render
加载,如<Route path="/home" render={Home} />
通过component
和render
加载,可像页面传递route props
,而component
每次都会创建新的渲染实例,所以一般会选择render
函数来加载页面
首先定义一下类型
export interface PageOptions {
page: string;
loading?: JSX.Element;
}
export type PageProps<
// 传递给Page的额外参数
T extends Record = Record,
Path extends string = string,
Params extends { [K: string]: string | undefined } = ExtractRouteParams,
> = PageOptions &
T & {
route: RouteComponentProps;
};
export interface RouteProps
extends PageOptions,
Omit {
// 传递给Page的额外参数
params?: Record;
改造AsyncPage
export const AsyncPage: FC = ({ page, loading, ...rest }) => {
...
return ;
};
定义一个AsyncRoute
作为异步路由组件
export const AsyncRoute: FC = (props) => {
const { page, loading, params = {}, ...rest } = props;
return (
(
)}
/>
);
};
在App.tsx
中使用
同时也可以在User/Index
页面中使用
提取单个用户详情组件为独立页面
// src/pages/users/Detail.tsx
type UserParams = { username: string };
const UserDetail: FC = () => {
const { username } = useParams();
return current user is {username};
};
export default UserDetail;
提取用户列表组件为独立页面
// src/pages/users/List.tsx
const UserList: FC = () => {
return User List;
};
export default UserList;
原来的用户页面修改为用户布局页面
// src/pages/users/Index.tsx
const UsersIndex: FC = () => {
const match = useRouteMatch();
const [count, setCount] = useState(0);
return (
{count}
-
User List
-
Pincman
);
};
export default UsersIndex;
缓存布局
运行上面的代码会发现每次在访问子页面时会造成Index
重新渲染,表现为点击"增数"按钮count
变成1
,在点击User list
或者Pincman
链接,count
重置为0
,造成以上问题的原因是loadable
会在每次访问时生成一个新的实例,解决办法就是上一节讲到的缓存hook-useMemo
单独使用useMemo
会导致页面一直缓存无法渲染,所以需要一个判断重新渲染页面时机的变量,此变量可以是match.url
,因为切换子页面时,父级布局页的match.url
是不会变动的
export const AsyncPage: FC = ({ page, loading, ...rest }) => {
const { url } = rest.route.match;
const View = useMemo(
() =>
loadable(() => timeout(pMinDelay(pages[page](), 500), 12000), {
cacheKey: () => page.replaceAll('/', '-'),
fallback,
}),
[url],
);
...
};
现在试试点击"增数"再切换子页面,已经不会再重新渲染父级了
为了后续的配置式路由可以更简便地实现,我们还可以为AsyncRoute
和AsyncRoute
添加一个children
export const AsyncPage: FC = ({ page, loading, children, ...rest }) => {
return {children} ;
};
export const AsyncRoute: FC = (props) => {
...
render={(route) => (
{children}
)}
};
const UserIndex: FC = ({ children }) => {
...
return (
...
{children}
);
};
export default UserIndex;
现在可以在App.tsx
中添加所有路由配置了
可用API
这部分介绍一些常用的[react router][]的API
因为本教程是Typescript编码,所以不必把所有的API参数和属性列出来,大家可以点开类型文件自己查看
Hooks及函数
修改render
有了Hooks
之后不必要再往render
里的组件传递route
参数
// src/components/Router/types.ts
export type PageProps<
// 传递给Page的额外参数
T extends Record = Record,
> = PageOptions & T;
export const AsyncPage: FC = ({ page, loading, children, ...rest }) => {
const { url } = useRouteMatch();
...
};
// src/components/Router/view.tsx
export const AsyncRoute: FC = (props) => {
const { page, loading, params = {}, children, ...rest } = props;
...
{children}
};
useHistory
用于编程化导航,比如history.push("/home");
useLocation
获取当前URL的location
对象,比如location.pathname
useRouteMatch
通过path
参数与某个Route
的path
匹配,如果不传入参数,则与当前导航到的Route
匹配,匹配后即可获取其被导航后的信息,比如match.url
matchPath
在组件渲染的生命周期之前获取match
对象,其结果与渲染后使用useRouteMatch
一样,用法如下
matchPath("/users/2", {
path: "/users/:id",
exact: true,
strict: true
});
// 结果
// {
// isExact: true
// params: {
// id: "2"
// }
// path: "/users/:id"
// url: "/users/2"
// }
组件
Router
: 底层路由构建器BrowserRouter
: html5 history api路由构建器HashRouter
: hash路由构建器MemoryRouter
: 不把URL写入浏览器地址栏而把历史记录保存在内存中的路由构建器,适用于测试或[react native][]等Switch
: 在路由列表中只渲染第一个与当前地址匹配的路由Route
: 定义路由,参数包含path
(可以是个数组,对应多个地址),extra
(精确匹配),render
/component
等Redirect
: 跳转路由,部分参数与Route
一样Link
: 通过组件模式导航路由NavLink
: 比Link
多了个可以为当前链接激活时添加的CSS类Prompt
: 在用户离开页面之前跳出"是否离开"提示
其它知识
以下知识后续教程会讲解
- 配置式路由
- 认证和权限路由
- 基于
context
的独立路由组件 - 路由切换时的转场动画
以下知识自行查看官网示例
- query: 通过
Link
组件中的query
参数可指定query
,如要获取query
看[这里][https://reactrouter.com/web/example/query-parameters] - 弹出框: 想要点击一个路由可弹出一个modal,看[这里][https://reactrouter.com/web/example/modal-gallery]