前面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/