015 在 Umi 中使用 Ant Design 编写全局布局

4,587 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

前面14天的内容,我们几乎都在谈论 Umi 的相关概念,从这节课开始,我们就会真正进入实战阶段,如果需要给它取一个小标题,那我大概率会用《如何手写 Ant Design Pro》

这节课我们来实现页面级整体布局,所谓布局简单的理解就是网页的整体框框,也可以看成是,所有页面都共用的部分。 比如在 pc 上比较常见的上中下布局,在 app 上的底部 tabs 、全局浮动球等都属于布局需求。

约定的全局布局

约定式路由时的全局布局文件,实际上是在路由外面套了一层。比如,你的路由是:

约定 src/layouts/index.tsx 为全局路由,实际上是在路由外面套了一层返回一个 React 组件,并通过 useOutlet hook 或者 Outlet 组件渲染子组件。

比如以下目录结构,

.
└── src
    ├── layouts
    │   └── index.tsx
    └── pages
        ├── index.tsx
        └── users.tsx

会生成路由,

[
  { exact: false, path: '/', component: '@/layouts/index',
    routes: [
      { exact: true, path: '/', component: '@/pages/index' },
      { exact: true, path: '/users', component: '@/pages/users' },
    ],
  },
]

从组件角度可以简单的理解为如下关系:

<layout>
  <page>index</page>
  <page>users</page>
</layout>

一个自定义的全局 layout 如下:

import React from "react";
import { useOutlet } from "umi";

const Layout = () => {
  const outlet = useOutlet();
  return (
    <div>
      Layout
      {outlet}
    </div>
  );
};

export default Layout;

不同的全局 layout

你可能需要针对不同路由输出不同的全局 layout,Umi 不支持这样的配置,但你仍可以在 src/layouts/index.tsx 中对 location.path 做区分,渲染不同的 layout 。

比如想要针对 /login 输出简单布局,

import React from "react";
import { useOutlet } from "umi";

export default function(props) {
  const outlet = useOutlet();
  if (props.location.pathname === '/login') {
    return <SimpleLayout>{ outlet }</SimpleLayout>
  }

  return (
    <>
      <Header />
      { outlet }
      <Footer />
    </>
  );
}

使用 Ant Design 实现基本布局

Ant Design 提供了好几种的布局方式,几乎中后台的所有布局都包括了。

antdlayout.jpg

详细的范例可以参考 Ant Design 官网,这里我们用最常见的 顶部导航 Header、侧边栏 Sider、内容区 Content、底部区域 Footer 的布局来做演示。

安装 Ant Design 和图标库

pnpm i antd @ant-design/icons

使用 umi antd 插件

config/config.ts 配置中,修改 plugins 配置

import { defineConfig } from "umi";

export default defineConfig({
  // 最终值在插件中设置,所以这里不用写
  //   title: "Hello Umi",
  plugins: [
    require.resolve("@umijs/plugins/dist/model"),
+   require.resolve("@umijs/plugins/dist/antd"),
  ],
  model: {},
  antd: {},
});

开启 antd 插件功能

config/config.ts 配置中,新增 antd 配置,这里是一次强调,添加完插件,要记得添加对应的配置。

Umi 中部分插件是默认开启,就无须配置。正常的插件都是配置开关。所有的插件都可以通过配置值为 false,来关闭它。

import { defineConfig } from "umi";

export default defineConfig({
  // 最终值在插件中设置,所以这里不用写
  //   title: "Hello Umi",
  plugins: [
    require.resolve("@umijs/plugins/dist/model"),
    require.resolve("@umijs/plugins/dist/antd"),
  ],
  model: {},
+ antd: {},
});

新建 Layout 页面

新建页面文件 src/layouts/index.tsx,写出整体布局(用法来自 Ant Design 官网)

import { Layout } from "antd";
import React from "react";

const { Header, Content, Footer, Sider } = Layout;

const App: React.FC = () => {
  return (
    <Layout style={{ minHeight: "100vh" }}>
      <Sider></Sider>
      <Layout>
        <Header style={{ padding: 0 }} />
        <Content style={{ margin: "0 16px" }}></Content>
        <Footer style={{ textAlign: "center" }}></Footer>
      </Layout>
    </Layout>
  );
};

export default App;

编写 Sider 和 Menu

import {
  DesktopOutlined,
  FileOutlined,
  PieChartOutlined,
  TeamOutlined,
  UserOutlined,
} from "@ant-design/icons";
import type { MenuProps } from "antd";
import { Breadcrumb, Layout, Menu } from "antd";
import React, { useState } from "react";

const { Header, Content, Footer, Sider } = Layout;

type MenuItem = Required<MenuProps>["items"][number];

function getItem(
  label: React.ReactNode,
  key: React.Key,
  icon?: React.ReactNode,
  children?: MenuItem[]
): MenuItem {
  return {
    key,
    icon,
    children,
    label,
  } as MenuItem;
}

const items: MenuItem[] = [
  getItem("Option 1", "1", <PieChartOutlined />),
  getItem("Option 2", "2", <DesktopOutlined />),
  getItem("User", "sub1", <UserOutlined />, [
    getItem("Tom", "3"),
    getItem("Bill", "4"),
    getItem("Alex", "5"),
  ]),
  getItem("Team", "sub2", <TeamOutlined />, [
    getItem("Team 1", "6"),
    getItem("Team 2", "8"),
  ]),
  getItem("Files", "9", <FileOutlined />),
];

const App: React.FC = () => {
  const [collapsed, setCollapsed] = useState(false);

  return (
    <Layout style={{ minHeight: "100vh" }}>
      <Sider
        collapsible
        collapsed={collapsed}
        onCollapse={(value) => setCollapsed(value)}
      >
        <div
          style={{
            height: "32px",
            margin: "16px",
            color: "#fff",
            textAlign: "center",
            fontSize: "16px",
          }}
        >
          Umi 4
        </div>
        <Menu
          theme="dark"
          defaultSelectedKeys={["1"]}
          mode="inline"
          items={items}
        />
      </Sider>
      {/* Layout 略 */}
    </Layout>
  );
};

export default App;

编写 Footer

<Footer style={{ textAlign: "center" }}>
    Umi@4 实战小册 Created by xiaohuoni
</Footer>

编写 Content 和面包屑

<Content style={{ margin: "0 16px" }}>
    <Breadcrumb style={{ margin: "16px 0" }}>
        <Breadcrumb.Item>User</Breadcrumb.Item>
        <Breadcrumb.Item>Bill</Breadcrumb.Item>
    </Breadcrumb>
    <div style={{ padding: 24, minHeight: 360 }}>Bill is a cat.</div>
</Content>

渲染当前页面

前面提到过,我们使用 useOutlet hook 或者 Outlet 组件渲染子组件。将上面 Content 中的 Bill is a cat. 替换成 outlet 即可。

import { Outlet } from "umi";

// 其他内容略

<div style={{ padding: 24, minHeight: 360 }}>
    <Outlet />
</div>

运行效果

执行 pnpm start 或者 npx umi dev,启动 umi 的开发服务,通过浏览器访问 http://127.0.0.1:8888/

umilayout.jpg

源码归档