• 基本结构
    • 路由
    • 菜单
    • 面包屑
  • 需求实例
    • 新增页面
    • 新增布局模板
    • 带参数的路由/菜单
    • 嵌套布局
    • 嵌套路由同级展示
    • 隐藏菜单
    • 隐藏面包屑
  • 常见问题
    • 为什么我的路由列表里有些条目没有渲染?
    • Link to 属性
    • 关于 dynamicWrapper

    路由和菜单是组织起一个应用的关键骨架,我们的脚手架提供了一些基本的工具及模板,帮助你更方便的搭建自己的路由/菜单。

    如果你想了解更多关于 browserHistoryhashHistory,请参看 构建和发布。

    注意:我们的脚手架依赖 dva@2,路由方面是基于 react-router@4 的实现,在写法以及 API 上与之前的版本有较大不同,所以,在开始前你需要具备一些相关的基础知识。这里给出几篇关键文档:

    • react-router
    • Migrating from v2/v3 to v4
    • dva@2.0
    • Webpack Code Splitting

    基本结构

    在这一部分,脚手架通过结合一些配置文件、基本算法及工具函数,搭建好了路由和菜单的基本框架,主要涉及以下几个模块/功能:

    • 路由生成 结合路由信息的配置文件和预设的基本算法,提供了在不同层级文件中自动生成路由列表的能力。
    • 菜单生成 由确定数据结构的菜单配置文件生成,其中的菜单项名称,嵌套路径与路由有一定关联关系。
    • 面包屑 组件 PageHeader 中内置的面包屑也可由脚手架提供的配置信息自动生成。

    下面简单介绍下各个模块的基本思路,如果你对实现过程不感兴趣,只想了解应该怎么实现相关需求,可以直接查看需求实例。

    路由

    目前在脚手架中,除了顶层路由,其余路由列表都是自动生成,其中最关键的就是中心化配置文件 src/common/router.js,它的主要作用有两个:

    • 配置路由相关信息。如果只考虑生成路由,你只需要指定每条配置的路径及对应渲染组件。
    • 输出路由数据,并将路由数据(routerData)挂载到每条路由对应的组件上。

    这样我们得到一个基本的路由信息对象,它的结构大致是这样:

    1. {
    2. '/dashboard/analysis': {
    3. component: DynamicComponent(),
    4. name: '分析页',
    5. },
    6. '/dashboard/monitor': {
    7. component: DynamicComponent(),
    8. name: '监控页',
    9. },
    10. '/dashboard/workplace': {
    11. component: DynamicComponent(),
    12. name: '工作台',
    13. },
    14. }

    为了帮助自动生成路由,在 src/utils/utils.js 中提供了工具函数 getRoutes,它接收两个参数:当前路由的 match 路径及路由信息 routerData,主要完成两个工作:

    • 筛选路由信息,筛选的算法为只保留当前 match.path 下最邻近的路由层级(更深入的层级留到嵌套路由中自行渲染),举个例子(每条为一个 route path):

      1. // 当前 match.path 为 /
      2. /a // 没有更近的层级,保留
      3. /a/b // 存在更近层级 /a,去掉
      4. /c/d // 没有更近的层级,保留
      5. /c/e // 没有更近的层级,保留
      6. /c/e/f // 存在更近层级 /c/e,去掉
    • 自动分析路由 exact 参数,除了下面还有嵌套路由的路径,其余路径默认设为 exact。

    经过 getRoutes 处理之后的路由数据就可直接用于生成路由列表:

    1. // src/layouts/BasicLayout.js
    2. getRoutes(match.path, routerData).map(item => (
    3. <Route
    4. key={item.key}
    5. path={item.path}
    6. component={item.component}
    7. exact={item.exact}
    8. />
    9. ))

    注意:如果你不需要这种筛选及分析功能,可以不使用 getRoutes,直接基于 routerData 自行处理。

    菜单

    菜单信息配置在 src/common/menu.js 中,它的作用是:

    • 配置菜单相关数据,菜单项的跳转链接为配置项及其所有父级配置 path 参数的拼接。
    • src/common/router.js 提供路由名称(name)等数据,根据拼接好的跳转链接来匹配相关路由。

    如果你的项目并不需要菜单,你也可以直接在 src/common/router.js 中配置 name 信息。

    配置文件输出的菜单数据,可以直接提供给侧边栏组件 SiderMenu使用。除了生成菜单,菜单数据还可辅助生成重定向路由等模块,参考 BasicLayout.js#L154。

    面包屑

    之前提到的路由信息 routerData 可以直接传递给 PageHeader 组件用以生成面包屑,你可以用 props 或者 context 的方式进行传递。脚手架里的示例。

    需求实例

    上面对这部分的实现概要进行了介绍,接下来通过实际的案例来说明具体该怎么做。

    新增页面

    脚手架默认提供了两种布局模板:基础布局 - BasicLayout 以及 账户相关布局 - UserLayout

    基础布局

    账户相关布局

    如果你的页面可以利用这两种布局,那么只需要在路由及菜单配置中增加一条即可:

    1. // src/common/router.js
    2. '/dashboard/test': {
    3. component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/Test')),
    4. },
    1. // src/common/menu.js
    2. const menuData = [{
    3. name: 'dashboard',
    4. icon: 'dashboard', // https://demo.com/icon.png or <Icon type="dashboard" />
    5. path: 'dashboard',
    6. children: [{
    7. name: '分析页',
    8. path: 'analysis',
    9. }, {
    10. name: '监控页',
    11. path: 'monitor',
    12. }, {
    13. name: '工作台',
    14. path: 'workplace',
    15. }, {
    16. name: '测试页',
    17. path: 'test',
    18. }],
    19. }, {
    20. // 更多配置
    21. }];

    加好后,会默认生成相关的路由及导航。

    新增布局模板

    如果提供的布局不能满足你的要求,就需要自己新建 Layout 模板了。假设有两个新的页面需要使用新模板,你需要先配置好路由及菜单:

    1. // src/common/router.js
    2. '/new': {
    3. component: dynamicWrapper(app, ['monitor'], () => import('../layouts/NewLayout')),
    4. },
    5. '/new/page1': {
    6. component: dynamicWrapper(app, ['monitor'], () => import('../routes/New/Page1')),
    7. },
    8. '/new/page2': {
    9. component: dynamicWrapper(app, ['monitor'], () => import('../routes/New/Page2')),
    10. },
    1. // src/common/menu.js
    2. const menuData = [{
    3. name: '新布局',
    4. icon: 'table',
    5. path: 'new',
    6. children: [{
    7. name: '页面一',
    8. path: 'page1',
    9. }, {
    10. name: '页面二',
    11. path: 'page2',
    12. }],
    13. }, {
    14. // 更多配置
    15. }];

    在根路由中增加这组新模板:

    1. // src/router.js
    2. <Router history={history}>
    3. <Switch>
    4. <Route path="/new" render={props => <NewLayout {...props} />} />
    5. <Route path="/user" render={props => <UserLayout {...props} />} />
    6. <Route path="/" render={props => <BasicLayout {...props} />} />
    7. </Switch>
    8. </Router>

    然后在你的新模板中,仿照 src/layouts/BasicLayout.jssrc/layouts/UserLayout.js 生成路由列表即可。

    带参数的路由/菜单

    脚手架默认支持带参数的路由、菜单及面包屑配置,直接在路由的 key 以及菜单中的 path 配置即可:

    1. // src/common/router.js
    2. '/dashboard/:workplace': {
    3. component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Workplace')),
    4. },
    5. '/:list/table-list': {
    6. component: dynamicWrapper(app, ['rule'], () => import('../routes/List/TableList')),
    7. },
    1. // src/common/menu.js
    2. const menuData = [{
    3. name: 'dashboard',
    4. icon: 'dashboard',
    5. path: 'dashboard',
    6. children: [{
    7. name: '分析页',
    8. path: 'analysis',
    9. }, {
    10. name: '监控页',
    11. path: 'monitor',
    12. }, {
    13. name: '工作台',
    14. path: ':workplace',
    15. }],
    16. }, {
    17. name: '列表页',
    18. icon: 'table',
    19. path: ':list',
    20. children: [],
    21. }, {
    22. // 更多配置
    23. }];

    嵌套布局

    有时在当前 layout 下还需要嵌套其他布局,例如有几个页面都需要展示同一个模块,你可以把这部分提炼出来变成一个新的布局,再到该布局下生成路由列表。与新增布局模板 的区别,只是不需要将它增加到根路由中。具体可以参照 src/common/router.js /list/search 相关配置,及相关组件文件。

    嵌套路由同级展示

    脚手架默认使用工具函数 getRoutes 对 routerData 进行处理,然后生成路由列表,根据基本算法,在每一级组件中只会渲染当前 match.path 下最邻近的路由,所以,如果你要实现嵌套路由的同级展示(如:将 /list/search/list/search/projects 在同一个地方渲染),就需要手动获取该路由的数据并添加在合适的地方。

    1. {/* src/layouts/BasicLayout.js 类比你的上层 layout 组件 */}
    2. <Content style={{ margin: '24px 24px 0', height: '100%' }}>
    3. <div style={{ minHeight: 'calc(100vh - 260px)' }}>
    4. <Switch>
    5. {/* 默认生成的路由列表,不包含 /list/search/projects */}
    6. {
    7. getRoutes(match.path, routerData).map(item => (
    8. <Route
    9. key={item.key}
    10. path={item.path}
    11. component={item.component}
    12. exact={item.exact}
    13. />
    14. ))
    15. }
    16. {/* 补充 /list/search/projects 的路由 */}
    17. <Route exact path="/list/search/projects" component={routerData['/list/search/projects'].component} />
    18. <Redirect exact from="/" to="/dashboard/analysis" />
    19. <Route render={NotFound} />
    20. </Switch>
    21. </div>
    22. </Content>

    同时在嵌套 layout 的文件中去掉这一条路由(如果还有下层路由需要 render)。

    1. {/* src/routes/List/List.js 类比你的嵌套 layout 组件 */}
    2. <Switch>
    3. {
    4. getRoutes(match.path, routerData).filter(item => item.path !== '/list/search/projects').map(item =>
    5. (
    6. <Route
    7. key={item.key}
    8. path={item.path}
    9. component={item.component}
    10. exact={item.exact}
    11. />
    12. )
    13. )
    14. }
    15. </Switch>

    如果你的项目中大部分路由都需要同级渲染,也可以不使用 getRoutes。

    隐藏菜单

    如果需要隐藏某条菜单项,可以在该条数据中增加 hideInMenu 参数。

    1. // src/common/menu.js
    2. const menuData = [{
    3. name: 'dashboard',
    4. icon: 'dashboard',
    5. path: 'dashboard',
    6. children: [{
    7. name: '分析页',
    8. path: 'analysis',
    9. }, {
    10. name: '监控页',
    11. path: 'monitor',
    12. }, {
    13. name: '工作台',
    14. path: 'workplace',
    15. hideInMenu: true, // 隐藏该条
    16. }],
    17. }, {
    18. name: '表单页',
    19. icon: 'form',
    20. path: 'form',
    21. hideInMenu: true, // 隐藏该组
    22. children: [{
    23. name: '基础表单',
    24. path: 'basic-form',
    25. }, {
    26. name: '分步表单',
    27. path: 'step-form',
    28. }, {
    29. name: '高级表单',
    30. path: 'advanced-form',
    31. }],
    32. }, {
    33. // 更多配置
    34. }];

    隐藏面包屑

    如需隐藏面包屑中的某个层级,可以增加 hideInBreadcrumb 属性。

    1. // src/common/router.js
    2. '/dashboard/analysis': {
    3. component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')),
    4. hideInBreadcrumb: true, // 隐藏该条
    5. },
    6. '/dashboard/monitor': {
    7. component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/Monitor')),
    8. },

    常见问题

    为什么我的路由列表里有些条目没有渲染?

    框架默认使用 getRoutes 函数对配置的路由列表进行筛选处理,对于类似 /ant-design/ant-design-pro 的路由,我们默认你希望它在 /ant-design 里嵌套展示(如果有配置 /ant-design),所以在渲染 /ant-design 的地方不会同时渲染 /ant-design/ant-design-pro,如须将这两条同级展示,参见嵌套路由同级展示。

    react-router@4 中,这一属性变为必选项,如果值为 undefined,会引发一系列报错和警告,这一点需要格外注意。相关文档及 issue:

    • react-router@4 Link
    • 相关讨论

    关于 dynamicWrapper

    1. import dynamic from 'dva/dynamic';
    2. // wrapper of dynamic
    3. const dynamicWrapper = (app, models, component) => dynamic({
    4. app,
    5. // eslint-disable-next-line no-underscore-dangle
    6. models: () => models.filter(m => !app._models.some(({ namespace }) => namespace === m)).map(m => import(`../models/${m}.js`)),
    7. // add routerData prop
    8. component: () => {
    9. const routerData = getRouterData(app);
    10. return component().then((raw) => {
    11. const Component = raw.default || raw;
    12. return props => <Component {...props} routerData={routerData} />;
    13. });
    14. },
    15. });

    为了代码的简洁性,我们对 dva/dynamic 进行了二次封装,需要注意的是这里使用了 Webpack Code Splitting 的动态 importdvadynamic 方法已经帮我们封装好了 Promise 动态加载的相关事宜,所以我们只需要直接使用即可。