💕💕😍👍初心:希望帮助更多像我一样无助且迷茫,想提升自我的小伙伴,同时提升自己,分享自己的学习方法。
Umi从零搭建后中后台系统保姆级记录教程。
前言
在学习此教程之前你大概要会用HTML/CSS/JS 其次是React和Ant Design UI,其次是要理解为什么要用Umi,他能解决什么问题,带来什么好处,本文不做介绍,纯实践,分享如何去学习。 如果你是有丰富经验的大佬,可以直接去看官方文档,如果和我一样菜,可以看我的文档一步一步实践。
学习本文你会了解和应用到以下内容:
UMI官网(目前访问需要梯子)。ProComponents (好像访问也需要梯子)。 React和 Ant Design
教程
教程正式开始了。
环境信息
本教程开发环境如下,如果你需要安装环境或者切换node版本,请访问我这篇教程juejin.cn/post/724327...
Node :18.16.0 NPM:9.5.1
安装创建&&启动查看项目
官网上面也有相应的教程,你可以按我的过程走,也可以按官网的走,大同小异。
使用 npx create-umi@latest
在项目目录创建umi+ant design pro项目,
安装好后我们可以看到项目中出现了一堆似曾相识的目录,我们先启动项目看看是什么B样子。使用npm run dev
启动项目 启动项目之后,是这个样子。~额真丑啊😒😒😒。。。。
附上一张全过程图片,但是糊了应该
项目分析
具体的目录结构自己仔细斟酌一下官网的教程,讲的很细致了。这里不做解释umijs.org/docs/guides...
这里分享一个创建完项目后的目录结构和package.json,可以看到帮我们默认安装好了antd的一套插件。
ts
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-components": "^2.4.4",
"@umijs/max": "^4.0.72",
"antd": "^5.4.0"
坑:ts报错 解决方法重启ts服务试试,试试就逝世
依赖安装好后我们开始项目的初始化,包括基本的布局,请求封装,路由配置等功能。
tips:在看以下内容大概了解一下什么是'运行时配置'和'构建时配置'两个概念,虽然我也没看懂,配就完了。
配置文件
这里项目中给我简单弄了几个示例的页面。我们在此基础上进行更改。 分析结构目录,我们可以看出大量的配置文件都在 .umirc.ts文件, 然后再看这个官方教程的解释 我们在根目录下创建config/config.ts文件,然后把.umirc.ts文件内容复制粘贴到config/config.ts,并删除.umirc.ts(因为.umirc.ts优先级较高),这个时候我们的配置文件就都在config.ts里面了,保存运行,出来的效果是一样的
设置全局加载页面
1.根据文档,我们可以看出在src下loading.tsx就是全局的加载动画页面。
2.在src下新建global.less,写一下loading动画的居中和loading的css动画
css
.global-loading-body {
height: 100vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.loader {
color: #1a88ff;
font-size: 12px;
margin: 50px auto;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
-webkit-animation: load4 1s infinite linear;
animation: load4 1.3s infinite linear;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
@-webkit-keyframes load4 {
0%,
100% {
box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
}
12.5% {
box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
25% {
box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
37.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em,
0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
50% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em,
0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
62.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
}
75% {
box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em,
2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em,
-2em -2em 0 0;
}
87.5% {
box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
}
}
@keyframes load4 {
0%,
100% {
box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
}
12.5% {
box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
25% {
box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0,
0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
37.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em,
0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
50% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em,
0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
62.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
}
75% {
box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em,
2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em,
-2em -2em 0 0;
}
87.5% {
box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em,
0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
}
}
}
3.在新建的loading.tsx中写入加载动画页面
ts
import json from '@/assets/json/loading.json';
import { Player } from '@lottiefiles/react-lottie-player';
export default function loading() {
return (
<div className="global_loading_body">
<Player
autoplay
loop
src={json}
style={{ height: '150px', width: '150px' }}
></Player>
</div>
);
}
这样用满网速访问,我们就可以看到我们的loading页面了。
layout样式配置
1.根据官网的介绍,在config.ts下面进行构件配置,在app.ts进行运行时配置。查看下app.ts
默认代码他有导出一个getInitialState函数,和layout函数,其中getInitialState函数是做登录逻辑的函数,他会返回登录的信息,比如他给写死的name:'@umijs.max'这里先不管,后面会用到。
2.修改菜单布局的配置 以下代码配置了我们菜单的布局样式,菜单的logo图标,具体内容参照procomponents.ant.design/components/... 进行配置。
ts
// 运行时配置
import { RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
import { message } from 'antd';
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
export async function getInitialState(): Promise<{
name: string;
avatar?: string;
}> {
return {
name: '小荣',
avatar:
'https://github.com/XiaoRongwen/imgs/blob/master/avatar.jpg?raw=true',
};
}
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
//initialState上面登录函数返回的信息
return {
logo: 'https://github.com/XiaoRongwen/imgs/blob/master/logo.png?raw=true', //左上角Logo
title: 'xxxx运营平台', //左上角Logo后面的名字
menu: {
locale: false, //菜单是否国际化
},
layout: 'mix', //菜单的方式,有mix,top,side三种,这里用mix
splitMenus: true, // 这里用了mix才会生效,bia
avatarProps: {
src: initialState?.avatar || undefined, //右上角头像
title: initialState?.name || '用户', //右上角名称
},
token: {
//菜单的样式配置
// colorBgAppListIconHover: 'rgba(0,0,0,0.06)',
// colorTextAppListIconHover: 'rgba(255,255,255,0.95)',
// colorTextAppListIcon: 'rgba(255,255,255,0.85)',
sider: {
//侧边菜单的配置 ,这里具体看文档
// colorBgCollapsedButton: '#fff',
// colorTextCollapsedButtonHover: '#1677ff',
// colorTextCollapsedButton: 'rgba(0,0,0,0.45)',
colorMenuBackground: '#fff',
// colorBgMenuItemCollapsedElevated: 'rgba(0,0,0,0.85)',
colorMenuItemDivider: 'rgba(255,255,255,0.15)',
colorBgMenuItemHover: 'rgba(0,0,0,0.06)',
colorBgMenuItemSelected: 'rgba(0,0,0,0.05)',
colorTextMenuSelected: '#1677ff',
colorTextMenuItemHover: '#1677ff',
// colorTextMenu: 'rgba(255,255,255,0.75)',
// colorTextMenuSecondary: 'rgba(255,255,255,0.65)',
colorTextMenuTitle: 'rgba(255,255,255,0.95)',
colorTextMenuActive: '#1677ff',
colorTextSubMenuSelected: '#1677ff',
},
},
};
};
配置完成后我们的布局就成了这样
请求封装
1.了解请求封装 umi已经帮我们封装了请求方法,也就是 Umi Reuest,可以搜一下这个库,我只需要对他进行一些配置即可。 umijs.org/docs/max/re... 具体的解释可以看官网的这个介绍。
他也给出了一套配置好的demo,我们根据需求再去进行配置修改。
2.具体进行配置 根据上面图片(即文档说明),我们在app.ts
中导出request的配置即可,主要对请求拦截,请求 响应处理做了一些配置,类似于axios的配置。配置完成后app.ts
如下
js
// 运行时配置
import { RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
import { message } from 'antd';
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
export async function getInitialState(): Promise<{
name: string;
avatar?: string;
}> {
return {
name: '小荣',
avatar:
'https://github.com/XiaoRongwen/imgs/blob/master/avatar.jpg?raw=true',
};
}
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
//initialState上面登录函数返回的信息
return {
logo: 'https://github.com/XiaoRongwen/imgs/blob/master/logo.png?raw=true', //左上角Logo
title: 'xxxx运营平台', //左上角Logo后面的名字
menu: {
locale: false, //菜单是否国际化
},
layout: 'mix', //菜单的方式,有mix,top,side三种,这里用mix
splitMenus: true, // 这里用了mix才会生效,bia
avatarProps: {
src: initialState?.avatar || undefined, //右上角头像
title: initialState?.name || '用户', //右上角名称
},
token: {
//菜单的样式配置
// colorBgAppListIconHover: 'rgba(0,0,0,0.06)',
// colorTextAppListIconHover: 'rgba(255,255,255,0.95)',
// colorTextAppListIcon: 'rgba(255,255,255,0.85)',
sider: {
//侧边菜单的配置 ,这里具体看文档
// colorBgCollapsedButton: '#fff',
// colorTextCollapsedButtonHover: '#1677ff',
// colorTextCollapsedButton: 'rgba(0,0,0,0.45)',
colorMenuBackground: '#fff',
// colorBgMenuItemCollapsedElevated: 'rgba(0,0,0,0.85)',
colorMenuItemDivider: 'rgba(255,255,255,0.15)',
colorBgMenuItemHover: 'rgba(0,0,0,0.06)',
colorBgMenuItemSelected: 'rgba(0,0,0,0.05)',
colorTextMenuSelected: '#1677ff',
colorTextMenuItemHover: '#1677ff',
// colorTextMenu: 'rgba(255,255,255,0.75)',
// colorTextMenuSecondary: 'rgba(255,255,255,0.65)',
colorTextMenuTitle: 'rgba(255,255,255,0.95)',
colorTextMenuActive: '#1677ff',
colorTextSubMenuSelected: '#1677ff',
},
},
};
};
export const request: RequestConfig = {
timeout: 1000,
// other axios options you want
errorConfig: {
errorHandler(error: any) {
const { response } = error;
if (response && response.status === 500) {
message.error('请求错误:服务器故障,请稍后再试');
}
},
errorThrower() {},
},
// 请求拦截
requestInterceptors: [
(config: any) => {
let token = localStorage.getItem('token') || '';
if (token.startsWith('"')) {
token = JSON.parse(token);
}
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
},
(error: any) => {
return error;
},
],
// 相应拦截
responseInterceptors: [
(response: any) => {
const { data, message } = response;
if (!data.success) {
message.error(message);
}
return response;
},
],
};
路由配置
1.路由配置解释
umi有一套文件路由系统,但是用不惯,我们就先用路由配置文件去配置。 看到配置文件中的路由我们会很奇怪,routes的配置也挺正常的,但是引入的component是./Home
指向哪里的?这个时候我们再去翻一下文档:umijs.org/docs/guides...
看到这里就~ 哦 ~幡然醒悟。。。。。。。。他是从src/page下./Home
找的,这就懂了。然后你就可以欢快的新建你的页面了。
2.单独把路由文件提出来
按正常思维逻辑我们要单独把router文件独立出来进行配置, 在配置文件config目录下新建一个router.ts,把路由内容复制过来并到处,在config.ts引入并使用路由配置。 3.创建子路由且展示侧边菜单栏
在router.ts文件中新增一个路,我们拿客户管理作为一个示例。 在src/pages/下面新建一个CustomerManage/index.tsx
,CustomerManage/ClientAuthentication/index.tsx
,CustomerManage/index.tsx
。三个文件,分别代表客户管理页面
及子页面客户列表
,客户认证
.
创建好后我们在router.ts去进行路由的配置。在路由CURD示例后面加入以下内容。
ts
{
name: '客户管理',
path: '/customer-manage',
component: './CustomerManage',
routes: [
{
name: ' 客户列表',
icon: 'TeamOutlined',
path: '/customer-manage/customer-list',
component: './CustomerManage/CustomerList',
},
{
name: ' 客户认证',
icon: 'FileProtectOutlined',
path: '/customer-manage/authentication',
component: './CustomerManage/ClientAuthentication',
},
],
},
这个时候我们在运行项目就成了这个样子
4.创建单独登录页面(没有菜单的页面)
在src/pages下面创建登录页面Login/index.tsx
,我们打开Pro Components
的地址,发现他有登录表单的功能,我直接点击链接去CV!CV!CV! procomponents.ant.design/components/...
CV过来其实是有一些小问题的(框架更新,但是文档没有及时更新导致的问题,我已经修改好了),代码如下
ts
import {
AlipayOutlined,
LockOutlined,
MobileOutlined,
TaobaoOutlined,
UserOutlined,
WeiboOutlined,
} from '@ant-design/icons';
import {
LoginFormPage,
ProFormCaptcha,
ProFormCheckbox,
ProFormText,
} from '@ant-design/pro-components';
import { Divider, Space, Tabs, message } from 'antd';
import type { CSSProperties } from 'react';
import { useState } from 'react';
import { history } from 'umi';
type LoginType = 'phone' | 'account';
const iconStyles: CSSProperties = {
color: 'rgba(0, 0, 0, 0.2)',
fontSize: '18px',
verticalAlign: 'middle',
cursor: 'pointer',
};
export default () => {
const items = [
{ label: '账户密码登录', key: 'account' },
{ label: '手机号登录', key: 'phone' },
];
const [loginType, setLoginType] = useState<LoginType>('phone');
const onSubmit = async (formData: any) => {
console.log(formData);
history.push('/');
};
return (
<div
style={{
backgroundColor: 'white',
height: '100vh',
width: '100vw',
}}
>
<LoginFormPage
onFinish={onSubmit}
backgroundImageUrl="https://gw.alipayobjects.com/zos/rmsportal/FfdJeJRQWjEeGTpqgBKj.png"
logo="https://github.githubassets.com/images/modules/logos_page/Octocat.png"
title="Github"
subTitle="全球最大的代码托管平台"
// activityConfig={{
// style: {
// boxShadow: '0px 0px 8px rgba(0, 0, 0, 0.2)',
// color: '#fff',
// borderRadius: 8,
// backgroundColor: '#1677FF',
// },
// title: '活动标题,可配置图片',
// subTitle: '活动介绍说明文字',
// action: (
// <Button
// size="large"
// style={{
// borderRadius: 20,
// background: '#fff',
// color: '#1677FF',
// width: 120,
// }}
// >
// 去看看
// </Button>
// ),
// }}
actions={
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
}}
>
<Divider plain>
<span
style={{ color: '#CCC', fontWeight: 'normal', fontSize: 14 }}
>
其他登录方式
</span>
</Divider>
<Space align="center" size={24}>
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
height: 40,
width: 40,
border: '1px solid #D4D8DD',
borderRadius: '50%',
}}
>
<AlipayOutlined style={{ ...iconStyles, color: '#1677FF' }} />
</div>
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
height: 40,
width: 40,
border: '1px solid #D4D8DD',
borderRadius: '50%',
}}
>
<TaobaoOutlined style={{ ...iconStyles, color: '#FF6A10' }} />
</div>
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
height: 40,
width: 40,
border: '1px solid #D4D8DD',
borderRadius: '50%',
}}
>
<WeiboOutlined style={{ ...iconStyles, color: '#333333' }} />
</div>
</Space>
</div>
}
>
<Tabs
centered
items={items}
activeKey={loginType}
onChange={(activeKey) => setLoginType(activeKey as LoginType)}
></Tabs>
{loginType === 'account' && (
<>
<ProFormText
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={'prefixIcon'} />,
}}
placeholder={'请输入账号/邮箱/电话号码'}
rules={[
{
required: true,
message: '请输入用户名!',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={'prefixIcon'} />,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '请输入密码!',
},
]}
/>
</>
)}
{loginType === 'phone' && (
<>
<ProFormText
fieldProps={{
size: 'large',
prefix: <MobileOutlined className={'prefixIcon'} />,
}}
name="mobile"
placeholder={'手机号'}
rules={[
{
required: true,
message: '请输入手机号!',
},
{
pattern: /^1\d{10}$/,
message: '手机号格式错误!',
},
]}
/>
<ProFormCaptcha
fieldProps={{
size: 'large',
prefix: <LockOutlined className={'prefixIcon'} />,
}}
captchaProps={{
size: 'large',
}}
placeholder={'请输入验证码'}
captchaTextRender={(timing, count) => {
if (timing) {
return `${count} ${'获取验证码'}`;
}
return '获取验证码';
}}
name="captcha"
rules={[
{
required: true,
message: '请输入验证码!',
},
]}
onGetCaptcha={async () => {
message.success('获取验证码成功!验证码为:1234');
}}
/>
</>
)}
<div style={{ marginBlockEnd: 24 }}>
<ProFormCheckbox noStyle name="autoLogin">
自动登录
</ProFormCheckbox>
<a style={{ float: 'right' }}>忘记密码 </a>
</div>
</LoginFormPage>
</div>
);
};
这样我们的登录页面就做好了,怎么 跳转过去呢? 细心的同学会发现我们创建的所有路由都是带菜单的,那怎么不带菜单单独一个页面呢?看文档!!! umijs.org/docs/guides... 大致意思就是默认所有路由都会带这个菜单(layouts),想要不带,就需要在路由下面添加一个 layout:false
我们在路由配置文件下 根目录/config/router.ts添加
js
{
name: 'Login',
path: '/login',
component: './Login',
layout: false,
},
此时我们访问http://localhost:8000/login 就不会出现菜单了,能够正常访问到登录页面了。
总结
由于片段真的太长了,实在肝不动了,请关注下一章节,下一章节将讲述后台管理常用的面包屑,表单配置,antd主题定义。