路由设计

在浏览器支持了 History API 之后,很多网站为了提升用户体验都采用基于 History 的单页面应用(SPA)方式,这种方式在页面切换时几乎不需要等待,页面也不会出现白屏,在用户体验上有非常大的提高。采用 SPA 的方式,所有页面路由都需要在前端代码中定义,在 React 领域我们通常使用 react-router 的方案,本文就来介绍如何在 SPA 应用中设计路由。

路由配置

在模板中,路由与菜单一样也是按照一定的约定进行配置,用来描述路由的结构关系。路由主要分为 路由配置 和 路由渲染 两部分:

  • 路由配置:src/config/routes.js
  • 路由生成:src/router.js

这样设计的目的主要是分离路由配置信息和路由渲染部分,让开发者大多数时候只需要关注路由配置,路由配置协议如下:

  1. const routerConfig = [
  2. // 分组路由,children 里的路由会将父节点的 component 作为布局组件
  3. {
  4. path: '/user',
  5. component: UserLayout,
  6. children: [
  7. {
  8. path: '/login',
  9. component: UserLogin,
  10. },
  11. {
  12. path: '/',
  13. // 重定向
  14. redirect: '/user/login',
  15. },
  16. {
  17. // 404 没有匹配到的路由
  18. component: NotFound,
  19. },
  20. ],
  21. },
  22. // 非分组路由
  23. {
  24. path: '/about',
  25. component: About,
  26. },
  27. ];

注意:路由有一个按顺序匹配的规则,从上到下一旦命中路由匹配规则就会停止遍历,因此如果你在最前面配置了 / 这样一个路由,则所有的路由都会命中该规则,导致其他路由没有效果,所以在开发时要注意路由的顺序以及 exact 属性的使用。

路由渲染

完成路由配置后,我们通过 src/router.jsx 的生成逻辑将这些路由渲染出来,即转换成使用 react-router 的代码(注意我们默认支持了两层路由嵌套,如需支持更多层级请修改 src/router.jsx 这个文件),具体代码如下:

  1. import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
  2. import routes from '@/config/routes';
  3. const RouteItem = (props) => {
  4. const { redirect, path: routePath, component, key } = props;
  5. if (redirect) {
  6. return (
  7. <Redirect exact key={key} from={routePath} to={redirect} />
  8. );
  9. }
  10. return (
  11. <Route key={key} component={component} path={routePath} />
  12. );
  13. };
  14. const router = () => {
  15. return (
  16. <Router>
  17. <Switch>
  18. {routes.map((route, id) => {
  19. const { component: RouteComponent, children, ...others } = route;
  20. return (
  21. <Route
  22. key={id}
  23. {...others}
  24. component={(props) => {
  25. return (
  26. children ? (
  27. <RouteComponent key={id} {...props}>
  28. <Switch>
  29. {children.map((routeChild, idx) => {
  30. const { redirect, path: childPath, component } = routeChild;
  31. return RouteItem({
  32. key: `${id}-${idx}`,
  33. redirect,
  34. path: childPath && path.join(route.path, childPath),
  35. component,
  36. });
  37. })}
  38. </Switch>
  39. </RouteComponent>
  40. ) : (
  41. <>
  42. {RouteItem({
  43. key: id,
  44. ...props,
  45. })}
  46. </>
  47. )
  48. );
  49. }}
  50. />
  51. );
  52. })}
  53. </Switch>
  54. </Router>
  55. );
  56. };

至此,路由设计方案基本分析完成,这里可以总结如下:

  • 考虑到每个路由都能配置任意不同的布局,如果多个路由共享一个布局且不希望在切换路由的时候刷新布局,你可以将这几个路由放在一个路由组下。
  • 路由组的情况,真实路径需要将路由组的路径和当前路径拼起来,如果路由配置的路由路径是 /login,路由组的路径是 /user,那么页面的实际路由地址是 /user/login

路由跳转

通过 Link 组件跳转

通过 <Link /> 标签组件可实现路由跳转,使用方式:

  1. import { Link } from 'react-router-dom';
  2. function Demo() {
  3. return (
  4. <div>
  5. <Link to="/courses?sort=name" />
  6. {/* 可以携带额外的数据 `state` 到路由中。 */}
  7. <Link
  8. to={{
  9. pathname: '/courses',
  10. search: '?sort=name',
  11. hash: '#the-hash',
  12. state: { fromDashboard: true },
  13. }}
  14. />
  15. </div>
  16. )
  17. }

通过 withRouter 方法调用实现跳转

如果调用方法的地方在 React 组件内部,可以直接在组件上添加 withRouter 的装饰器,然后组件内可以通过 props 获取到相关 API:

  1. import React from 'react';
  2. import { withRouter } from 'react-router-dom';
  3. function ShowTheLocation(props) {
  4. const { history, location } = props;
  5. const handleHistoryPush = () => {
  6. history.push('/new-path');
  7. };
  8. return (
  9. <div>
  10. <div>当前路径: {location.pathname}</div>
  11. <button onClick={handleHistoryPush}>点击跳转新页面</button>
  12. </div>
  13. );
  14. }
  15. export default withRouter(ShowTheLocation);

方法调用:History API

如果前两种方式都无法实现,比如在 React 组件外部需要使用路由跳转等操作,则需要单独使用 history 的三方包,一般情况下不推荐这种情况,实际业务里应该很少需要。首先添加依赖:

  1. $ npm install --save history

创建一个公共的 histroy 对象:

  1. // /src/utils/history.js
  2. // 这里以 BrowserHistory 为例,如果是 HashHistory 参考文档使用即可
  3. import { createBrowserHistory } from 'history';
  4. export default createBrowserHistory();

然后在代码中引入并使用:

  1. // /src/utils/request.js
  2. import history from './history';
  3. export default function checkAuth() {
  4. ajax('/api/checkAuth').then((res) => {
  5. if (res.data.noAuth) {
  6. history.push('/page/noAuth');
  7. }
  8. });
  9. }

常见问题

为什么浏览器里的地址都带着 #

前端路由通常有两种实现方式:HashHistory 和 BrowserHistory,路由都带着 # 说明使用的是 HashHistory。这两种方式优缺点如下:

特点\方案 HashRouter BrowserRouter
美观度 不好,有 # 号
易用性 简单 中等,需要 server 配合
依赖 server 不依赖 依赖
跟锚点功能冲突 冲突 不冲突
兼容性 IE8 IE10

开发者可以根据自己的实际情况选择对应方案。

如何使用 BrowserHistory?

首先在路由渲染部分将最外层由 HashRouter 替换为 BrowserRouter:

  1. - import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
  2. + import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';

本地调试支持,在 ice.config.js 中配置 webpack-dev-server 的配置:

  1. module.exports = {
  2. devServer: {
  3. historyApiFallback: true,
  4. }
  5. }

线上运行时需要 server 端支持,否则会出现刷新 404 问题,具体方案请参考社区文档: