持续创作,加速成长!这是我参与「掘金日新计划 · 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 提供了好几种的布局方式,几乎中后台的所有布局都包括了。
详细的范例可以参考 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/