Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[项目] 导航菜单 & 路由管理功能 #2031

Closed
5 tasks done
chenbin92 opened this issue May 28, 2019 · 23 comments
Closed
5 tasks done

[项目] 导航菜单 & 路由管理功能 #2031

chenbin92 opened this issue May 28, 2019 · 23 comments
Assignees

Comments

@chenbin92
Copy link
Collaborator

chenbin92 commented May 28, 2019

背景

原先方案存在的问题:

  • 路由与 Layout 耦合,维护起来不方便
  • 路由扁平,不支持任何级别的嵌套

路由方案

最终方案如下:

src/config/routes.js 文件内容

import React from 'react';

import UserLayout from './layouts/UserLayout';
import BasicLayout from './layouts/BasicLayout';

const UserLogin = React.lazy(() => import('./pages/UserLogin'));
const UserRegister = React.lazy(() => import('./pages/UserRegister'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Charts = React.lazy(() => import('./pages/Charts'));
const BasicCharts = React.lazy(() => import('./pages/BasicCharts'));
const Terms = React.lazy(() => import('./pages/Terms'));
const Result = React.lazy(() => import('./pages/Result'));
const BasicList = React.lazy(() => import('./pages/BasicList'));
const ProjectList = React.lazy(() => import('./pages/ProjectList'));
const BasicTable = React.lazy(() => import('./pages/BasicTable'));
const GeneralTable = React.lazy(() => import('./pages/GeneralTable'));
const Profile = React.lazy(() => import('./pages/Profile'));
const Setting = React.lazy(() => import('./pages/Setting'));
const Fail = React.lazy(() => import('./pages/Fail'));
const Empty = React.lazy(() => import('./pages/Empty'));
const Forbidden = React.lazy(() => import('./pages/Forbidden'));
const NotFound = React.lazy(() => import('./pages/NotFound'));
const ServerError = React.lazy(() => import('./pages/ServerError'));

const routerConfig = [
  {
    path: '/user',
    component: UserLayout,
    children: [
      {
        path: '/login',
        component: UserLogin,
      },
      {
        path: '/register',
        component: UserRegister,
      },
      {
        path: '/',
        redirect: '/user/login',
      },
      {
        component: NotFound,
      },
    ],
  },
  {
    path: '/',
    component: BasicLayout,
    children: [
      {
        path: '/dashboard/monitor',
        component: Dashboard,
      },
      {
        path: '/chart/general',
        component: Charts,
      },
      {
        path: '/chart/basic',
        component: BasicCharts,
      },
      {
        path: '/list/basic',
        component: BasicList,
      },
      {
        path: '/list/general',
        component: ProjectList,
      },
      {
        path: '/result/success',
        component: Result,
      },
      {
        path: '/result/fail',
        component: Fail,
      },
      {
        path: '/table/basic',
        component: BasicTable,
      },
      {
        path: '/profile/basic',
        component: Profile,
      },
      {
        path: '/profile/general',
        component: Terms,
      },
      {
        path: '/table/general',
        component: GeneralTable,
      },
      {
        path: '/account/setting',
        component: Setting,
      },
      {
        path: '/exception/500',
        component: ServerError,
      },
      {
        path: '/exception/403',
        component: Forbidden,
      },
      {
        path: '/exception/204',
        component: Empty,
      },
      {
        path: '/exception/404',
        component: NotFound,
      },
      {
        // Redirect 重定向
        path: '/',
        redirect: '/dashboard/monitor',
      },
      {
        // 404 没有匹配到的路由
        component: NotFound,
      },
    ],
  },
];

export default routerConfig;

src/router.jsx

import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import React, { Suspense } from 'react';
import path from 'path';
import routes from '@/routerConfig';
import PageLoading from '@/components/PageLoading';

const RouteItem = (props) => {
  const { redirect, path: routePath, component, key } = props;
  if (redirect) {
    return (
      <Redirect
        exact
        key={key}
        from={routePath}
        to={redirect}
      />
    );
  }
  return (
    <Route
      key={key}
      component={component}
      path={routePath}
    />
  );
};

const router = () => {
  return (
    <Router>
      <Switch>
        {routes.map((route, id) => {
          const { component: RouteComponent, children, ...others } = route;
          return (
            <Route
              key={id}
              {...others}
              component={(props) => {
                return (
                  children ? (
                    <RouteComponent key={id} {...props}>
                      <Suspense fallback={<PageLoading />}>
                        <Switch>
                          {children.map((routeChild, idx) => {
                            const { redirect, path: childPath, component } = routeChild;
                            return RouteItem({
                              key: `${id}-${idx}`,
                              redirect,
                              path: childPath && path.join(route.path, childPath),
                              component,
                            });
                          })}
                        </Switch>
                      </Suspense>
                    </RouteComponent>
                  ) : (
                    <Suspense fallback={<PageLoading />}>
                      {
                        RouteItem({
                          key: id,
                          ...props,
                        })
                      }
                    </Suspense>
                  )
                );
              }}
            />
          );
        })}
      </Switch>
    </Router>
  );
};

export default router;

可视化管理路由

功能

  • 导航面板
    • 树级拖拽
    • 增删改导航、导航组和外链
  • adapter
    • 生成 menuConfig

路由管理

菜单管理

@boiawang
Copy link
Member

导航配置

iceworks 导航配置生成 menuConfig(headerMenuConfig 和 asideMenuConfig)

数据字段如下:

{
    name: 'ice',
    path: 'https://alibaba.github.io/ice',
    // 是否是外链
    external: true,
    // 是否新打开窗口
    newWindow: true,
    // icon 先用 string,未来是否扩展 component 方案待定
    icon: 'help',
    // 导航组下有 children
    children: []
}

路由配置

iceworks 路由配置生成 routerConfig

数据字段如下:

{
    path: '/dashboard/monitor',
    component: Dashboard
}

链路方案

  • 通过 babylon 解析读取数据 menuConfig 或 routerConfig,初始数据展示在 iceworks
  • 在 iceworks 中完成修改配置生成文件内容覆盖 menuConfig 或 routerConfig

布局配置(TODO)

布局方案这块还需要确定 UI 规范以及组件,这次版本不会增加布局配置的能力,未来确定了方案再拿出来讨论。

@chenbin92
Copy link
Collaborator Author

chenbin92 commented May 30, 2019

  1. 导航配置和路由配置和原因方案是一样的,没问题
  2. 路由配置是否需要考虑 exactredirect 需求,因为你现在设计了多级导航,多级导航的 redirect 是很常见的需求

@alvinhui
Copy link
Collaborator

+1

@boiawang
Copy link
Member

boiawang commented May 31, 2019

路由配置

背景

  • 因为之前 Layout 和 routerConfig 耦合,代码结构不够清晰给开发带来了困惑,所以需要将 routerConfig 从 Layout 中抽离出来。
  • 实现一个路由可视化配置方案提效开发。

技术方案

routerConfig.js 实例如下:

import Page1 from './pages/Page1';
import Page2 from './pages/Page2';
import Page3 from './pages/Page3';
import Page4 from './pages/Page4';
import Page5 from './pages/Page5';

import Layout1 from './layouts/layout1';
import Layout2 from './layouts/layout2';
import Layout3 from './layouts/layout3';
import Layout4 from './layouts/layout4';

export default [
  {
    path: '/project/admin',
    component: Page4,
    layout: Layout3
  },
  {
    path: '/project/user',
    component: Page5,
    layout: Layout4
  },
  {
    path: '/project',
    layout: layout1,
    children: [
      {
        path: '/detail',
        component: Page1
      },
      {
        path: '/list',
        component: Page2
      }
    ]
  },
  {
    path: '/help',
    component: Page3,
    layout: Layout2
  },
];

router.jsx 代码如下:

/**
 * 定义应用路由
 */
import { Switch, Route } from 'react-router-dom';
import React from 'react';
import routerConfig from './routerConfig';

const router = () => {
  return (
    <Switch>
      {routerConfig.map((router) => {
        const { layout: Layout, component: Component } = router;

        // 判断是否为组
        if (router.children) {
          return (
            <Route path={router.path} component={router.layout}>
              {router.children.map((routeChild) => {
                return <Route path={routeChild.path} component={routeChild.component} />;
              })}
            </Route>
          );
        }

        return <Route path={router.path} component={() => {
          return (
            <Layout>
              <Component />
            </Layout>
          );
        }} />;
      })}
    </Switch>
  );
};

export default router;

技术问题

  • 需要保证路由顺序,如 /project、/project/list,需要把 /project/list 提前
  • 已有项目不能使用路由配置功能,在 iceworks 面板上指引用户迁移

Action

  • [] 路由配置 UI
    • [] 创建路由组、路由
    • [] 路由组下可以创建路由
    • [] 路由可以被拖拽
  • [] 生成文件 routerConfig.js
  • [] 模板修改 @思忠

@chenbin92
Copy link
Collaborator Author

还需要考虑现有用户项目的识别和迁移问题

@boiawang
Copy link
Member

@chenbin92 已有项目不能使用路由配置功能,在 iceworks 面板上指引用户迁移。

@boiawang
Copy link
Member

boiawang commented May 31, 2019

导航配置

  • asideMenuConfig 导航菜单数据
  • headerMenuConfig 头部右边菜单数据

技术方案

导航组件

实现一个数据驱动的导航组件 IceNav

<IceNav config={[
  {
    name: '反馈',
    path: 'https://github.com/alibaba/ice',
    external: true,
    newWindow: true,
    icon: 'email',
  },
  {
    name: '帮助',
    path: 'https://alibaba.github.io/ice',
    external: true,
    newWindow: true,
    icon: 'help',
  },
]} />

Layout 中引入 menuConfig 以及显示导航

headerMenuConfig 显示在 LayoutHeader, asideMenuConfig 显示在 LayoutAside

import { asideMenuConfig, headerMenuConfig } from '../../menuConfig';

const App = () => {
  render() {
    return (
      <Layout>
        <Layout.Header>
          <IceNav config={headerMenuConfig} />
        </Layout.Header>
        <Layout.Section>
          <Layout.Aside>
            <IceNav config={asideMenuConfig} direction="ver" />
          </Layout.Aside>
          <Layout.Main>{this.props.children}</Layout.Main>
        </Layout.Section>
      </Layout>
    );
  };

Action

  • [] 菜单配置 UI
    • [] 创建导航组、导航、链接
    • [] 可以拖拽实现结构变化
  • [] 生成文件 menuConfig

@alvinhui
Copy link
Collaborator

asideMenuConfig 和 headerMenuConfig 都显示在 LayoutHeader

这个不能够吧。

@chenbin92
Copy link
Collaborator Author

chenbin92 commented May 31, 2019

  1. LayoutHeader 这个命名有些奇怪,而且还要包含 asideMenuConfigheaderMenuConfig,上述代码的职责和组件名对不上,直接就是 Layout 即可
import { asideMenuConfig, headerMenuConfig } from '../../menuConfig';

const App = () => {
  render() {
    return (
      <Layout>
        <Layout.Header>
          <IceNav config={asideMenuConfig} />
          <IceNav config={headerMenuConfig} />
        </Layout.Header>
        <Layout.Main>{this.props.children}</Layout.Main>
      </Layout>
    );
  }
};
  1. 下面代码也比较奇怪,<IceNav> 调用两次传值不一样,又都包括在 <Layout.Header> 下面
 <Layout.Header>
       <IceNav config={asideMenuConfig} />
       <IceNav config={headerMenuConfig} />
 </Layout.Header>

@chenbin92 chenbin92 reopened this May 31, 2019
@chenbin92
Copy link
Collaborator Author

chenbin92 commented May 31, 2019

iceworks 上面交互图是怎样的,补充一下基本的交互草图吧

@imsobear
Copy link
Collaborator

想到一个问题,在实际项目里,很可能出现 90% 的页面都是一个 Layout,然后由另外 1-2 个页面是其他 Layout,这时候这样的路由分层感觉就不太合理了:

import Page1 from './pages/Page1';
import Page2 from './pages/Page2';
import Page3 from './pages/Page3';
import Page4 from './pages/Page4';
import Page5 from './pages/Page5';

import Layout1 from './layouts/layout1';
import Layout2 from './layouts/layout2';

export default [
  {
    path: '/help',
    component: Page3,
    layout: Layout2
  },
  {
    path: '/about',
    component: Page3,
    layout: Layout2
  },
  {
    path: '/',
    layout: layout1,
    children: [
      {
        path: '/project/list',
        component: Page1
      },
      {
        path: '/project/:id',
        component: Page2
      },  
      {
        path: '/user/list',
        component: Page3
      },
      {
        path: '/user/:id',
        component: Page4
      },
      {
        path: '/page/list',
        component: Page5
      },
      {
        path: '/page/:id',
        component: Page6
      }
    ]
  },
];

@boiawang
Copy link
Member

boiawang commented Jun 3, 2019

介绍

iceworks 项目中有两个文件 routerConfig.jsrouter.js,我们需要约定 routerConfig 的协议和生成规则。

  • routerConfig.js 文件:路由列表,包含整个项目路由配置信息
  • router.js 文件:读 routerConfig,生成 react-router-dom 的结构

针对协议的设计列了以下三种方式:

第一种:

routerConfig.js

import Page1 from './pages/Page1';
import Page2 from './pages/Page2';
import Page3 from './pages/Page3';
import Page4 from './pages/Page4';
import Page5 from './pages/Page5';

import Layout1 from './layouts/layout1';
import Layout2 from './layouts/layout2';
import Layout3 from './layouts/layout3';

export default [
  {
    path: '/help',
    component: Page3,
    layout: Layout2
  },
  {
    path: '/about',
    component: Page3,
    layout: Layout2
  },
  {
    path: '/project/list',
    component: Page1,
    layout: Layout1
  },
  {
    path: '/project/:id',
    component: Page2,
    layout: Layout1
  },  
  {
    path: '/user/list',
    component: Page3,
    layout: Layout3
  },
  {
    path: '/user/:id',
    component: Page4,
    layout: Layout3
  },
  {
    path: '/page/list',
    component: Page5,
    layout: Layout3
  },
  {
    path: '/page/:id',
    component: Page6,
    layout: Layout3
  }
];

router.js 如下:

import routerConfig from './routerConfig';
import React from 'react';

/**
 * 根据路由信息扁平化地生成如下 Route 节点
 * 
 * <Switch>
 *   <Route path="/help" component={Layout2+Component} />
 *   <Route path="/about" component={Layout2+Component} />
 *   <Route path="/project/list" component={Layout1+Component} />
 *   <Route path="/project/:id" component={Layout1+Component} />
 *   <Route path="/user/list" component={Layout3+Component} />
 *   <Route path="/user/:id" component={Layout3+Component} />
 *   <Route path="/page/list" component={Layou3+Component} />
 *   <Route path="/page/:id" component={Layout3+Component} />
 * </Switch>
 */
const App = () => {
  render() {
    return (
      <Switch>
        {routerConfig.map((item) => {
          return (
            <Route exact={item.exact} path={item.path} component={() => {
              const Layout = item.layout;
              const Component = item.Component;

              return (
                <Layout>
                  <Component />
                </Layout>
              );
            }} />
          );
        })}
      </Switch>
    );
  }
};

export default App;

优点

  • 协议扁平易懂,实现成本低

缺点

  • 如果两个路由页面A和B用了同一个 Layout,切换的时候会造成整个页面刷新,体验差

第二种:

routerConfig.js 协议和上面一样

import Page1 from './pages/Page1';
import Page2 from './pages/Page2';
import Page3 from './pages/Page3';
import Page4 from './pages/Page4';
import Page5 from './pages/Page5';

import Layout1 from './layouts/layout1';
import Layout2 from './layouts/layout2';
import Layout3 from './layouts/layout3';

export default [
  {
    path: '/help',
    component: Page3,
    layout: Layout2
  },
  {
    path: '/about',
    component: Page3,
    layout: Layout2
  },
  {
    path: '/project/list',
    component: Page1,
    layout: Layout1
  },
  {
    path: '/project/:id',
    component: Page2,
    layout: Layout1
  },  
  {
    path: '/user/list',
    component: Page3,
    layout: Layout3
  },
  {
    path: '/user/:id',
    component: Page4,
    layout: Layout3
  },
  {
    path: '/page/list',
    component: Page5,
    layout: Layout3
  },
  {
    path: '/page/:id',
    component: Page6,
    layout: Layout3
  }
];

router.js 如下:

import routerConfig from './routerConfig';
import React from 'react';

/**
 * 根据 layout 分组路由,生成如下带父子路由的结构
 * 
 * <Switch>
 *   <Route path="/help" component={Layout2+Component} />
 *   <Route path="/about" component={Layout2+Component} />
 *   <Route path="/" component={Layout1}>
 *      <Route path="/project/list" component={ProjectList} />
 *      <Route path="/project/:id" component={ProjectDetail} />
 *   </Route>
 *   <Route path="/" component={Layout3}>
 *      <Route path="/user/list" component={UserList} />
 *      <Route path="/user/:id" component={User} />
 *      <Route path="/page/list" component={PageList} />
 *      <Route path="/page/:id" component={PageDetail} />
 *   </Route>
 * </Switch>
 */
const App = () => {
  render() {
    return (
      <Switch>
        // 实现部分略
      </Switch>
    );
  }
};

export default App;

优点

  • 协议扁平易懂,实现成本低

缺点

  • 需要将同个 Layout 的页面分组生成一个父 Route,path 是 /,看起来会有点奇怪。

第三种:

routerConfig.js 如下:

import Page1 from './pages/Page1';
import Page2 from './pages/Page2';
import Page3 from './pages/Page3';
import Page4 from './pages/Page4';
import Page5 from './pages/Page5';

import Layout1 from './layouts/layout1';
import Layout2 from './layouts/layout2';
import Layout3 from './layouts/layout3';

export default [
  {
    path: '/help',
    component: Page3,
    layout: Layout2
  },
  {
    path: '/about',
    component: Page3,
    layout: Layout2
  },
  {
    path: '/project',
    layout: Layout1,
    children: [
      {
        path: '/project/list',
        component: Page1
      },
      {
        path: '/project/:id',
        component: Page2
      },
    ]
  },
  {
    path: '/user',
    layout: Layout3,
    children: [
      {
        path: '/user/list',
        component: Page3
      },
      {
        path: '/user/:id',
        component: Page4
      },
      {
        path: '/page/list',
        component: Page5
      },
      {
        path: '/page/:id',
        component: Page6
      }
    ]
  }
];

router.js 如下:

import routerConfig from './routerConfig';
import React from 'react';

/**
 * 路由结构和 routerConfig 协议保持一致
 * 
 * <Switch>
 *   <Route path="/help" component={Layout2+Component} />
 *   <Route path="/about" component={Layout2+Component} />
 *   <Route path="/project" component={Layout1}>
 *      <Route path="/project/list" component={ProjectList} />
 *      <Route path="/project/:id" component={ProjectDetail} />
 *   </Route>
 *   <Route path="/user" component={Layout3}>
 *      <Route path="/user/list" component={UserList} />
 *      <Route path="/user/:id" component={User} />
 *      <Route path="/page/list" component={PageList} />
 *      <Route path="/page/:id" component={PageDetail} />
 *   </Route>
 * </Switch>
 */
const App = () => {
  render() {
    return (
      <Switch>
        // 实现部分略
      </Switch>
    );
  }
};

export default App;

优点

  • 协议结构和生成的 Route 结构一样

缺点

@chenbin92
Copy link
Collaborator Author

  • 方式一页面存在刷新问题,不能接受
  • 方式二解决了方式一的刷新问题,path="/" 是必须的吗?
  • 方式三示例没有示例分组情况下 Layout 的处理,我理解也是需要向方式二一样有 path 的问题吧
    image

目前来看,方式二比较能接受,主要在于分组逻辑的实现是否优雅,分组后的代码只是注释的展示,path="/" 看起来还好,但不确定这个 path 是否是必须

@boiawang
Copy link
Member

boiawang commented Jun 4, 2019

@chenbin92 试了下不用 path="/" 是可以的

@boiawang
Copy link
Member

boiawang commented Jun 4, 2019

第二种实现代码如下:

Layout1.js:

export default function Layout1(props) {
  return (
    <div>
      <div>Layout1</div>
      {props.children}
    </div>
  );
}

Layout2.js:

export default function Layout2(props) {
  return (
    <div>
      <div>Layout2</div>
      {props.children}
    </div>
  );
}

Layout3.js:

export default function Layout3(props) {
  return (
    <div>
      <div>Layout2</div>
      {props.children}
    </div>
  );
}

routerConfig.js:

import Index from './pages/index';
import Home from './pages/Home';
import About from './pages/About';

import Layout1 from './layouts/Layout1';
import Layout2 from './layouts/Layout2';
import Layout3 from './layouts/Layout3';

/*
* @TODO: 这里未来做一件事:需要保证路由顺序,如 /project、/project/list,需要把 /project/list 提前(或者这步算法放到 iceworks 中实现)
*/
const routerConfig =  [
  {
    path: '/',
    component: Index,
    exact: true,
  },
  {
    path: '/home',
    component: Home,
    layout: Layout1
  },
  {
    path: '/about',
    component: About,
    layout: Layout1
  },
  {
    path: '/project/list',
    component: ProjectList,
    layout: Layout2,
    exact: true,
  },
  {
    path: '/project/:id(\\d+)',
    component: ProjectDetail,
    layout: Layout2
  },
  {
    path: '/user/list',
    component: UserList,
    layout: Layout3
  },
  {
    path: '/user/:id(\\d+)',
    component: UserDetail,
    layout: Layout3
  },
  {
    path: '/page/list',
    component: PageList,
    layout: Layout3
  },
  {
    path: '/page/:id(\\d+)',
    component: PageDetail,
    layout: Layout3
  }
];

export default routerConfig;

router.js:

import React from 'react';
import { Switch, Route, Link } from 'react-router-dom';
import routerConfig from './routerConfig';

const routerConfigGroup = groupByLayout(routerConfig);

export default function App() {
  return (
    <Switch>
      {routerConfigGroup.map((group) => {
        return (
          <Route component={group.layout}>
            {group.children.map((item) => {
              return (
                <Route exact={item.exact} path={item.path} component={item.component} />
              );
            })}
          </Route>
        );
      })}
    </Switch>
  );
}

/**
 * 先分组生成如下结构
 * [{
 *    layout: Layout1,
 *    children: [{
 *      path: '/page/list',
 *      component: PageList
 *    }]
 * }]
 **/
function groupByLayout(list) {
  return list.reduce((reduceList, item) => {
    const findParentLayout = reduceList.find(reduceItem => reduceItem.layout === item.layout);
    if (findParentLayout) {
      findParentLayout.children.push({
        path: item.path,
        component: item.component,
      });
    } else {
      reduceList.push({
        layout: item.layout,
        children: [{
          path: item.path,
          component: item.component,
          exact: item.exact,
        }]
      });
    }
    return reduceList;
  }, []);
};

@chenbin92 @alvinhui @imsobear

@chenbin92
Copy link
Collaborator Author

chenbin92 commented Jun 4, 2019

image

这个应该是放在最后? component: Index 是指什么,如果这里加个 Layout 会怎么样

@boiawang
Copy link
Member

boiawang commented Jun 4, 2019

@chenbin92

  1. index 是要放到最后,顺序问题的事情会在 iceworks 里保证。
  2. component: Index 这里的 Index 是指 Index Page
  3. 不管是有 Layout 还是没有 Layout 都会被分组,没有 Layout 会被分组到 layout: undefind 下面,假设 layout 为 LayoutA,会被分组到 layout: LayoutA 下面

@chenbin92
Copy link
Collaborator Author

chenbin92 commented Jun 4, 2019

@chenbin92

  1. index 是要放到最后,顺序问题的事情会在 iceworks 里保证。
  2. component: Index 这里的 Index 是指 Index Page
  3. 不管是有 Layout 还是没有 Layout 都会被分组,没有 Layout 会被分组到 layout: undefind 下面,假设 layout 为 LayoutA,会被分组到 layout: LayoutA 下面

就是没有 Layout 就只渲染当前组件吧,有的话就进行分组归类。

没问题了

@boiawang
Copy link
Member

boiawang commented Jun 6, 2019

翻到一个 stackoverflow 关于 layout 共用以及嵌套路由的讨论 以及官方也给出了共用 layout 的例子,得出路由第四种方案:

routerConfig.js:

import Page1 from './pages/Page1';
import Page2 from './pages/Page2';
import Page3 from './pages/Page3';
import Page4 from './pages/Page4';
import Page5 from './pages/Page5';

import Layout1 from './layouts/layout1';
import Layout3 from './layouts/layout3';

export default [
  {
    path: '/help',
    component: Page3,
  },
  {
    path: '/about',
    component: Page3,
  },
  {
    path: '/project',
    component: Layout1,
    routes: [
      {
        path: '/project/list',
        component: Page1
      },
      {
        path: '/project/:id',
        component: Page2
      },
    ]
  },
  {
    path: '/user',
    component: Layout3,
    routes: [
      {
        path: '/user/list',
        component: Page3
      },
      {
        path: '/user/:id',
        component: Page4
      },
      {
        path: '/user/page/list',
        component: Page5
      },
      {
        path: '/user/page/:id',
        component: Page6
      }
    ]
  }
];

router.js:

具体实现参考 例子

优点

  • component、routes 字段都是 react-router 中存在的概念,不像之前的 layout、children 是我们创造出来的
  • react-router 官网推荐用法
  • 结构上更清晰

缺点

  • 暂无

@imsobear
Copy link
Collaborator

imsobear commented Jun 17, 2019

路由数据定义:

// src/config/routes.js
import BasicLayout from '@/layouts/BasicLayout';
import Home from '@/pages/Home';
import ProjectList from '@/pages/ProjectList';
import ProjectDetail from '@/pages/ProjectDetail';

const routes = [{
  path: '/',
  component: Home,
  layout: BasicLayout,
  exact: true,
}, {
  path: '/project',
  layout: BasicLayout,
  children: [{
    path: '/detail',
    component: ProjectDetail,
  }, {
    path: '/list',
    component: ProjectList,
  }],
}];

export default routes;

路由定义与渲染:

// src/router.jsx
<Router basename={getBasename()}>
    <Switch>
      {routes.map((route, id) => {
        const { component, layout, children, ...others } = route;

        if (!children) {
          const RouteLayout = layout;
          const RouteComponent = component;

          return (
            <Route
              {...others}
              component={(props) => {
                return (
                  <RouteLayout key={id}>
                    <RouteComponent {...props} />
                  </RouteLayout>
                );
              }}
            />
          );
        } else {
          const RouteLayout = component;
          return (
            <RouteLayout key={id}>
              <Switch>
                {route.children.map((routeChild, idx) => {
                  routeChild.path = path.join(route.path, routeChild.path);
                  const RouteComponent = routeChild.component;
                  return (
                    <Route
                      key={`${id}-${idx}`}
                      {...routeChild}
                      component={RouteComponent}
                    />
                  );
                })}
              </Switch>
            </RouteLayout>
          );
        }
      })}
    </Switch>
</Router>

chenbin92 pushed a commit that referenced this issue Jun 19, 2019
* feat: add navigation and router config #2031

* chore: lint error

* refactor: add routes to router

* chore: change cn to cx

* fix: fix routeConfig bug and code lint

* chore: change navgation global scss

* feat: add router and navigation when create page (#2173)

* feat: add router and navigation when create page

* chore: first create page

* chore: fix code lint

* chore: fix code lint

* chore: improve interaction

* style: change menu tree style

* chore: fix code lint

* chore: change style inject
@temper357
Copy link
Collaborator

temper357 commented Jun 19, 2019

config中还缺少Redirect和未匹配路由的配置,写了个提案,大家看下 @imsobear @boiawang @chenbin92 @alvinhui

在config中增加以下两种配置:

  1. Route上有redirect属性时渲染为Redirect,可配置多个
  2. Route上无path时作为未匹配路由放在最后渲染

config配置:

const routerConfig = [
  {
    path: '/user',
    component: UserLayout,
    routes: [
      {
        path: '/login',
        component: UserLogin,
      },
     {
        component: NotFound,  // 无path代表未匹配路由
      }
    ]
  },
  {
    path: '/',
    component: BasicLayout,
    routes: [
      {
        path: '/dashboard/monitor',
        component: Dashboard,
      },
      { 
        path: '/',  // 有redirect属性渲染为Redirect,可配置多个
        redirect: '/dashboard/monitor'
      },
      {
        path: '/login',
        redirect: '/user/login'
      },
      {
        component: NotFound,  // 无path代表未匹配路由
      }
  }
]

router.js渲染逻辑:

    <Router>
      <Switch>
        {routes.map((route, id) => {
          const { component, children, ...others } = route;
          const RouteComponent = component;
          return (
            <Route
              key={id}
              {...others}
              component={(props) => {
                return (
                  <RouteComponent key={id} {...props}>
                    {children && (
                      <Switch>
                        {/* 普通路由 */}
                        {children.filter(routeChild => routeChild.path && routeChild.component)
                          .map((routeChild, idx) => {
                            const { component } = routeChild;
                            return (
                              <Route
                                key={`route-${id}-${idx}`}
                                component={component}
                                path={path.join(route.path, routeChild.path)}
                              />
                            );
                          })}
                        {/* Redirect路由 */}
                        {children.filter(routeChild => routeChild.redirect)
                          .map((routeChild, idx) => {
                            const { redirect } = routeChild;
                            return (
                              <Redirect
                                key={`redirect-${id}-${idx}`}
                                exact
                                from={path.join(route.path, routeChild.path)}
                                to={redirect}
                              />
                            );
                          })}
                        {/* 未匹配路由 */}
                        {children.filter(routeChild => !routeChild.path)
                          .map((routeChild, idx) => {
                            const { component } = routeChild;
                            return (
                              <Route
                                key={`notfound-${id}-${idx}`}
                                component={component}
                              />
                            );
                          })}
                      </Switch>
                    )}
                  </RouteComponent>
                );
              }}
            />
          );
        })}
      </Switch>
    </Router> 

该方案有个缺点是由于layout分组的存在,未匹配路由需要在每个分组下定义一次,如果定义在与layout平级,由于BasicLayout的路由是'/',路由会进入BasiceLayout走不到未匹配路由。

另外layout这个字段感觉对用户有一定理解成本,直接用component代替是否更好,也可以再讨论下。

@boiawang
Copy link
Member

boiawang commented Jun 19, 2019

第一种方案,在 children 中增加 redirect 类型:

const routerConfig = [
  {
    path: '/user',
    component: UserLayout,
    children: [
      {
        path: '/login',
        component: UserLogin,
      },
      {
        path: '/',
        redirect: '/user/login'
      },
     {
        component: NotFound,  // 无path代表未匹配路由
      }
    ]
  },
  {
    path: '/',
    component: BasicLayout,
    children: [
      {
        path: '/dashboard/monitor',
        component: Dashboard,
      },
      { 
        path: '/',  // 有from和to属性渲染为Redirect,可配置多个
        redirect: '/dashboard/monitor'
      },
      {
        component: NotFound,  // 无path代表未匹配路由
      }
    ]
  }
];

第二种方案:

这个方案有个问题,需要在 router.js 中分析出 redirectConfig 里面的结构,代码还比较复杂

const routerConfig = [
  {
    path: '/user',
    component: UserLayout,
    children: [
      {
        path: '/login',
        component: UserLogin,
      },
     {
        component: NotFound,  // 无path代表未匹配路由
      }
    ]
  },
  {
    path: '/',
    component: BasicLayout,
    children: [
      {
        path: '/dashboard/monitor',
        component: Dashboard,
      },
      {
        component: NotFound,  // 无path代表未匹配路由
      }
    ]
  }
];

const redirectConfig = [
  {
    from: '/', // 需要根据 from 匹配到 routerConfig 哪一组,将 Redirect 塞到某个 layout 下
    to: '/dashboard/monitor',
  },
  {
    from: '/user',
    to: '/user/login'
  },
];

第三种方案:

需要有个理解成本,redirectConfig 是根节点的 redirects,每个 layout 有自己的 redirects

const routerConfig = [
  {
    path: '/user',
    component: UserLayout,
    // layout 下有自己的 redirects
    redirects: [
      {
        from: '/user',
        to: '/user/login'
      },
    ],
    children: [
      {
        path: '/login',
        component: UserLogin,
      },
     {
        component: NotFound,  // 无path代表未匹配路由
      }
    ]
  },
  {
    path: '/',
    component: BasicLayout,
    redirects: [
      {
        from: '/',
        to: '/dashboard/monitor',
      },
    ],
    children: [
      {
        path: '/dashboard/monitor',
        component: Dashboard,
      },
      {
        component: NotFound,  // 无path代表未匹配路由
      }
    ]
  },
  {
    path: '/list',
    component: List
  },
];

// 这里放根节点的 redirects
const redirectConfig = [
  {
    from: '/a',
    to: '/list',
  },
];

以上方案,我比较支持第一种,有更好的方案可以提供。

@boiawang
Copy link
Member

boiawang commented Jun 24, 2019

最终方案如下:

RouterConfig.js 文件内容

import React from 'react';

import UserLayout from './layouts/UserLayout';
import BasicLayout from './layouts/BasicLayout';

const UserLogin = React.lazy(() => import('./pages/UserLogin'));
const UserRegister = React.lazy(() => import('./pages/UserRegister'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Charts = React.lazy(() => import('./pages/Charts'));
const BasicCharts = React.lazy(() => import('./pages/BasicCharts'));
const Terms = React.lazy(() => import('./pages/Terms'));
const Result = React.lazy(() => import('./pages/Result'));
const BasicList = React.lazy(() => import('./pages/BasicList'));
const ProjectList = React.lazy(() => import('./pages/ProjectList'));
const BasicTable = React.lazy(() => import('./pages/BasicTable'));
const GeneralTable = React.lazy(() => import('./pages/GeneralTable'));
const Profile = React.lazy(() => import('./pages/Profile'));
const Setting = React.lazy(() => import('./pages/Setting'));
const Fail = React.lazy(() => import('./pages/Fail'));
const Empty = React.lazy(() => import('./pages/Empty'));
const Forbidden = React.lazy(() => import('./pages/Forbidden'));
const NotFound = React.lazy(() => import('./pages/NotFound'));
const ServerError = React.lazy(() => import('./pages/ServerError'));

const routerConfig = [
  {
    path: '/user',
    component: UserLayout,
    children: [
      {
        path: '/login',
        component: UserLogin,
      },
      {
        path: '/register',
        component: UserRegister,
      },
      {
        path: '/',
        redirect: '/user/login',
      },
      {
        component: NotFound,
      },
    ],
  },
  {
    path: '/',
    component: BasicLayout,
    children: [
      {
        path: '/dashboard/monitor',
        component: Dashboard,
      },
      {
        path: '/chart/general',
        component: Charts,
      },
      {
        path: '/chart/basic',
        component: BasicCharts,
      },
      {
        path: '/list/basic',
        component: BasicList,
      },
      {
        path: '/list/general',
        component: ProjectList,
      },
      {
        path: '/result/success',
        component: Result,
      },
      {
        path: '/result/fail',
        component: Fail,
      },
      {
        path: '/table/basic',
        component: BasicTable,
      },
      {
        path: '/profile/basic',
        component: Profile,
      },
      {
        path: '/profile/general',
        component: Terms,
      },
      {
        path: '/table/general',
        component: GeneralTable,
      },
      {
        path: '/account/setting',
        component: Setting,
      },
      {
        path: '/exception/500',
        component: ServerError,
      },
      {
        path: '/exception/403',
        component: Forbidden,
      },
      {
        path: '/exception/204',
        component: Empty,
      },
      {
        path: '/exception/404',
        component: NotFound,
      },
      {
        // Redirect 重定向
        path: '/',
        redirect: '/dashboard/monitor',
      },
      {
        // 404 没有匹配到的路由
        component: NotFound,
      },
    ],
  },
];

export default routerConfig;

router.jsx

import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import React, { Suspense } from 'react';
import path from 'path';
import routes from '@/routerConfig';
import PageLoading from '@/components/PageLoading';

const RouteItem = (props) => {
  const { redirect, path: routePath, component, key } = props;
  if (redirect) {
    return (
      <Redirect
        exact
        key={key}
        from={routePath}
        to={redirect}
      />
    );
  }
  return (
    <Route
      key={key}
      component={component}
      path={routePath}
    />
  );
};

const router = () => {
  return (
    <Router>
      <Switch>
        {routes.map((route, id) => {
          const { component: RouteComponent, children, ...others } = route;
          return (
            <Route
              key={id}
              {...others}
              component={(props) => {
                return (
                  children ? (
                    <RouteComponent key={id} {...props}>
                      <Suspense fallback={<PageLoading />}>
                        <Switch>
                          {children.map((routeChild, idx) => {
                            const { redirect, path: childPath, component } = routeChild;
                            return RouteItem({
                              key: `${id}-${idx}`,
                              redirect,
                              path: childPath && path.join(route.path, childPath),
                              component,
                            });
                          })}
                        </Switch>
                      </Suspense>
                    </RouteComponent>
                  ) : (
                    <Suspense fallback={<PageLoading />}>
                      {
                        RouteItem({
                          key: id,
                          ...props,
                        })
                      }
                    </Suspense>
                  )
                );
              }}
            />
          );
        })}
      </Switch>
    </Router>
  );
};

export default router;

@boiawang boiawang added Kit and removed PRO labels Jun 25, 2019
chenbin92 pushed a commit that referenced this issue Jun 27, 2019
* feat: add navigation and router config #2031

* chore: lint error

* refactor: add routes to router

* chore: change cn to cx

* fix: fix routeConfig bug and code lint

* chore: change navgation global scss

* feat: add router and navigation when create page (#2173)

* feat: add router and navigation when create page

* chore: first create page

* chore: fix code lint

* chore: fix code lint

* chore: improve interaction

* style: change menu tree style

* feat: add header menu config tab

* chore: change return type

* chore: remove external link text

* chore: remove useless file

* chore: flat datasource in menustore

* chore: change create router interaction

* chore: layout do not use lazy

* chore: remove page remove router and menu

* fix: remove path must exist

* fix: lint

* chore: lint code

* chore: fix disabled boolean

* chore: intl optimization
@imsobear imsobear closed this as completed Jul 3, 2019
imsobear added a commit that referenced this issue Jul 8, 2019
* feat: stark biz template support

* feat: stark biz template support

* fix: update template package

* chore: fix lint

* chore: code optim

* refactor: code optim

* fix: catch open editor error

* refacotr: refactor socket logic

* fix: add some tips in material page

* fix: repeat listen

* fix: use NoData component

* fix: fix typos (#2191)

* fix: fix typos

* fix: use injectIntl instead FormattedMessage

* fix: fix tslint & code style

* [iceworks]feat: add navigation and router config #2031 (#2107)

* feat: add navigation and router config #2031

* chore: lint error

* refactor: add routes to router

* chore: change cn to cx

* fix: fix routeConfig bug and code lint

* chore: change navgation global scss

* feat: add router and navigation when create page (#2173)

* feat: add router and navigation when create page

* chore: first create page

* chore: fix code lint

* chore: fix code lint

* chore: improve interaction

* style: change menu tree style

* chore: fix code lint

* chore: change style inject

* refactor: remove panel setting

* refactor: remove env

* refactor: rename guide to quick start

* refactor: Slimming Server (#2192)

* refactor: storage for adapter

* refactor: keep adapter module method params as same

* refactor: socket emit

* refactor: remove controller

* refactor: rename API project to adapter

* refactor: rewrite by review

* chore: merge router and menu

* refactor: router and menu

* refactor: project.adapter

* chore: code style

* fix: event name (#2206)

* fix: same of warning (#2208)

* fix: add rename to set (#2211)

* fix: array type

* feat: add start guide

* feat: support dark theme in material page (#2210)

* refactor: material category (#2215)

* refactor: category define in server

* chore: rename api

* chore: a to span

* chore: lint

* feat: write log to global

* feature: project panels settings

* feat: init panel settings

* feat: set panel is available

* feat: sortable for panels

* feat: style for panel settings

* feat: filter panel settings

* chore: rename

* feat: refresh panel store when panel is set to available

* feat: add infos to Router and Menu

* fix: merge bug

* fix: same of warning

* fix: add rename to set

* refactor: category define in server

* chore: rename api

* chore: a to span

* chore: lint

* refactor: useProject

* fix: do not need map twice time

* chore: rename

* fix: sort panel

* refactor: refactor panel setting

* refacotr: separate panle config

* feat: todo panel (#2213)

* feat: todo panel

* fix: use reddir & fix code

* fix: add file filter

* feat: show start guide panel

* feat: todo config support

* [优化] Icon 优化 (#2225)

* chore: icon upgrade

* fix: code lint

* fix: task setting

* chore: remove log

* feature: page builder

* fix: fix theme init bug & locale (#2232)

* fix: fix locale

* fix: fix theme init

* fix: remove locale

* refactor: style checking (#2233)

* chore: fix icon bug

* chore: icon tooltip upgrade

* chore: create router error tips

* feat: add changeId && bug fix

* chore: add changeId for app.ts

* feat: server i18n (#2246)

* refactor: refactor adapter

* fix: dev start error due to teminal not initiated

* fix: upperCamelCase module name

* chore: remove unnecessary field

* fix: add interface

* fix: dev status lost

* fix: adapter middleware error

* fix: dev status lost

* fix: dev start error due to teminal not initiated (#2259)

* fix: PR review

* fix: lint

* fix: dev status lost (#2262)

* fix: dev start error due to teminal not initiated

* fix: dev status lost

* fix: dev status lost

* fix: PR review

* fix: lint

* fix: remame adapter (#2268)

* feat: hide settings when ice.config.js not found

* fix: user refresh would trigger setState which will lead to infinite render

* fix: fix module name (#2271)

* feat: dynamic load adapter

* fix: change lodash import (#2272)

* feat: add react v1,v2, v3 adapter

* feat: no adapter hint

* chore: remove unused dependencies

* feat: start web server for iceworks

* fix: router adapter path

* chore: base adpater use kit v3.0 structure

* fix: lint

* feat: revert icestore related PR

* fix: fix todo icon (#2279)

* chore: remove unused field

* chore: update default adapter

* fix: PR review suggestions

* [Iceworks]vue adapter (#2280)

* feat: add vue adapter

* fix: change doc name

* feat: adapter support extends

* fix: fix config

* fix: fix type & filename

* fix: limit project name length

* feat: show current theme

* feat: support xterm closed

* fix: different task's status save in global (#2288)

* feat: navigation optim

* feat: add navigation and router config #2031

* chore: lint error

* refactor: add routes to router

* chore: change cn to cx

* fix: fix routeConfig bug and code lint

* chore: change navgation global scss

* feat: add router and navigation when create page (#2173)

* feat: add router and navigation when create page

* chore: first create page

* chore: fix code lint

* chore: fix code lint

* chore: improve interaction

* style: change menu tree style

* feat: add header menu config tab

* chore: change return type

* chore: remove external link text

* chore: remove useless file

* chore: flat datasource in menustore

* chore: change create router interaction

* chore: layout do not use lazy

* chore: remove page remove router and menu

* fix: remove path must exist

* fix: lint

* chore: lint code

* chore: fix disabled boolean

* chore: intl optimization

* fix: display none style which result to xterm width not 100% (#2289)

* feature:  前台一些异步操作后的 UI 处理 (#2290)

* feat: global bar state as store

* feat: async show log

* refactor: use store.writeLog

* chore: lint

* feat: add node remote log

* fix: fix comment

* chore: folder structure optimize

* fix: getRecommendScaffolds

* fix: project name overflow

* fix: icestore dependency & user data fetch

* fix: do not show panel setting when no projects

* fix: Git 面板相关 Bug (#2298)

* fix: git status handle error

* fix: refresh git store after updated remote url

* fix: git get branches

* feat: show tip when no file changes

* feat: support theme

* feat: support multiple languages for the guide panel

* feat: add interaction effects

* chore: rename setting panel

* refactor: refactor task logic

* chore: move task modal

* feat: quick run task support

* fix: material page bugfix (#2231)

* fix: force check lazyload components after category change

* feat: delete material confirm

* fix: fix linter error

* chore: update english tips

* chore: fix typo

* feat: i18n support

* chore: add @babel/runtime

* fix: lint error (#2306)

* feature: refactor panel head

* refactor: title & description for panels

* feat: panel head

* chore: remove title

* refactor: remove style

* refactor: change PanelHead props

* fix: use current theme

* fix: set xterm height

* chore: add onBeforeunload

* fix: ast parse error

* chore: fix typo

* fix: larger tab size (#2312)

* refactor: refactor theme

* chore: fix lint

* chore: code optim

* chore: fix lint

* fix: fix dev task

* refactor: refactor panel head

* refactor: refactor register store

* refactor: load adapter optim

* fix: remove adapter router

* fix: add package error

* feat: add goldlog

* feat: browser delect

* feat: optimize tip icon (#2320)

* [iceworks] feat i18n log (#2314)

* feat: i18n log replace

* fix: fix plugin load

* fix: fix vue config

* feat: add en-us config

* fix: fix adapter update

* fix: fix type

* refactor: status rename to isWorking

* refactor: refactor writeGlobalLog

* Fix vue adapter (#2323)

* fix: fix vue config

* fix: rm des

* fix: add quick task config

* fix: clear log error

* fix: hide categories panel when only one category (#2328)

* fix: hide categories when categories.length <=1

* chore: add MaterialPanel component

* fix: engineering configuration write failed

* fix: hide the build panel by default

* fix: kill process pid

* fix: fix i18n setting (#2332)

* fix: fix git panel bug

* feat: add log

* chore: rename gloglog to goldlog

* fix: add scripts files

* iceworks/cli  (#2336)

* chore: fix typo

* feat: add log

* chore: rename gloglog to goldlog

* Adapter 根据条件决定前端面板是否显示 (#2319)

* chore: notes of method

* feat: filter panels

* chore: remove console

* chore: simple code

* feat: dynamic set port

* fix: page panel bugs

* fix: fix git branch

* chore: rename

* fix: block dir

* chore: fix lint

* [iceworks] optimization routerConfig

* chore: optimization style

* chore: routerConfig import support @

* feat: create page do not display router group when no groups

* fix: fix lint

* chore: code optim

* feat: reload adapter support

* feat: i18n

* feat: add adapter docs

* chore: fix lint

* chore: fix typo

* feat: need to pass parameter --sticky when starting the cluster

* chore: remove mock data

* chore: code optim

* fix: no need

* Project panel bug fix (#2347)

* fix: repeated listen

* fix: set reset modal if project is verified

* fix: page form style

* fix: fix tarball download

* feature: refresh project panel when page is visible (#2348)

* fix: repeated listen

* fix: set reset modal if project is verified

* fix: page form style

* feat: useVisibilityChange

* feat: set notes

* [iceworks] fix: adpater-react-v1 getConfCLI error (#2353)

* fix: adpater-react-v1 getConf error

* fix: lint

* feat: refactor log data structure

* chore: code optim

* feat: refresh adapter's locale

* fix: always ensure consistency of return values of functions

* fix: fix open editor bug (#2360)

* chore: adjust style

* feat: new materials (#2366)

* style: code formatting

* feat: generate project (#2364)

* [iceworks] add loading and error status to async actions (#2349)

* feat: add ActionStatus Component used to render async actions' status

* fix: action return

* feat: add catch for refresh

* fix: new store logic

* fix: set reset modal is projectStore is validate

* chore: hasProjects

* feat: use modal loading for async

* fix: if project refresh error do not refresh panels

* feat: page set loading and error

* feat: showMessage

* fix: typo

* fix: typo

* feat: loading && error for layout

* feat: loading && error for menu

* feat: loading && error for git

* refactor: use showMessage

* refactor: use showMessage

* refactor: use showMessage

* refactor: use showMessage

* chore: lint

* feat: add catch and error handler

* feat: loading and error for dependency

* fix: add timeout for error

* feat: add errorType

* feat: add notFound & redirect path

* feat: create router add global valid

* fix: state modify error

* fix: lint

* fix: router store

* fix: MenuPanel children proptype

* fix: remove page remove routers

* fix: lint

* feat: loading for buildPage

* fix: lint

* feat:  add loading theme

* fix: define isLoading

* fix: error message style

* fix: lint

* feat: adjust code

* chore: remove useless

* fix: err fix

* chore: release 3.0-beta (#2363)

* chore: update package version

* chore: update version
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants