UmiMax快速介绍
UmiMax脚手架
% pnpm dlx create-umi@latest
┌ create-umi
│
○ Pick Umi App Template
│ Ant Design Pro
│
○ Pick Npm Client
│ pnpm
│
○ Pick Npm Registry
│ taobao
│
└ You're all set!
% pnpm dev
在Umi Max项目中命令行请使用max
,而不是原来的umi
整体文件夹结构如下:

- .umirc.ts --- 相当于原config/config.ts文件
- src/app.ts --- 运行时配置,用到比较多的方法:onRouteChange、request、render
布局与菜单
启用方式
在配置文件中如下配置
export default defineConfig({
...
layout: {
title: 'umi max测试',
},
...
});
扩展的路由配置
routes: [
{
path: '/',
redirect: '/home',
},
{
name: '首页',
path: '/home',
component: './Home',
icon: 'HomeOutlined',
},
{
name: '权限演示',
path: '/access',
component: './Access',
},
{
name: ' CRUD 示例',
path: '/table',
component: './Table',
},
],
配置选项如下:
- name:菜单上显示的名称,没有则不展示该菜单
- icon:菜单上显示的antd的icon,为了按需加载layout插件会帮你自动转化为Antd icon的dom。支持类型可以在antd icon中找到
- access:当Layout插件配合plugin-access插件使用时生效
- locale:菜单的国际化配置,国际化的key是
menu.${submenu-name}.${name}
- flatMenu:默认为false,为true时在菜单中只隐藏此项,子项往上提,仍旧展示。打平菜单,如果只想要子级的menu不展示自己的,可以配置为true
- xxxRender:xxxRender设置为false,即可不展示部分layout模块
- headerRender=false 不显示顶栏
- footerRender=false 不显示页脚
- menuRender=false 不显示菜单
- menuHeaderRender=false 不显示菜单的 title 和 logo
- hideInXXX:hideInXXX可以管理menu的渲染
- hideChildrenInMenu=true 隐藏子菜单
- hideInMenu=true 隐藏自己和子菜单
- hideInBreadcrumb=true 在面包屑中隐藏
export const routes: IBestAFSRoute[] = [
{
path: '/welcome',
component: 'IndexPage',
name: '欢迎', // 兼容此写法
icon: 'testicon',
// 更多功能查看
// https://beta-pro.ant.design/docs/advanced-menu
// ---
// 新页面打开
target: '_blank',
// 不展示顶栏
headerRender: false,
// 不展示页脚
footerRender: false,
// 不展示菜单
menuRender: false,
// 不展示菜单顶栏
menuHeaderRender: false,
// 权限配置,需要与 plugin-access 插件配合使用
access: 'canRead',
// 隐藏子菜单
hideChildrenInMenu: true,
// 隐藏自己和子菜单
hideInMenu: true,
// 在面包屑中隐藏
hideInBreadcrumb: true,
// 子项往上提,仍旧展示,
flatMenu: true,
},
];
antd
启用方式
在配置文件.umirc.ts中如下配置
export default {
antd: {
// configProvider
configProvider: {},
// themes
dark: true,
compact: true,
// babel-plugin-import
import: true,
// less or css, default less
style: 'less',
// shortcut of `configProvider.theme`
// use to configure theme token, antd v5 only
theme: {},
// antd <App /> valid for version 5.1.0 or higher, default: undefined
appConfig: {}
},
};
配置选项
- dark:开启暗色主题。默认为false
- compact:开启紧凑主题。默认为false
- import:配置antd的babel-plugin-import按需加载
- style:配置使用antd的样式。值为less或css,默认less
- configProvider:配置antd的configProvider
- theme:配置antd@5的theme token,等同于配置configProvider.theme,且该配置项拥有更高的优先级
- appConfig:配置antd的App包裹组件
数据流
@umi/max内置了数据流管理插件,它是一种基于hooks范式的轻量级数据管理方案,可以在Umi项目中管理全局的共享数据。
创建Model
可以在 src/models , src/pages/xxxx/models/ 目录中
,和 src/pages/xxxx/model.{js,jsx,ts,tsx} 文件
引入Model文件。 Model文件允许使用 .(tsx|ts|jsx|js)
四种后缀格式,命名空间(namespace) 生成规则如下。
路径 | 命名空间 | 说明 |
---|---|---|
src/models/count.ts | count | src/models 目录下不支持目录嵌套定义 model |
src/pages/pageA/model.ts | pageA.model | |
src/pages/pageB/models/product.ts | pageB.product | |
src/pages/pageB/models/fruit/apple.ts | pageB.fruit.apple | pages/xxx/models 下 model 定义支持嵌套定义 |
当我们需要获取Model中的全局数据时,调用该命名空间即可。例如,对于Model文件userModel.ts,它的命名空间为userModel
// src/models/userModel.ts
import { useState } from 'react';
import { getUser } from '@/services/user';
export default function Page() {
const [user, setUser] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
getUser().then((res) => {
setUser(res);
setLoading(false);
});
}, []);
return {
user,
loading,
};
};
使用Model
在某个组件中使用全局的 Model。以用户信息为例,只需要调用useModel这一钩子函数。其中,useModel() 方法传入的参数为Model的命名空间
// src/components/Username/index.tsx
import { useModel } from 'umi';
export default function Page() {
const { user, loading } = useModel('userModel');
return (
{loading ? <></>: <div>{user.username}</div>}
);
}
请求
它基于axios和ahooks的useRequest提供了一套统一的网络请求和错误处理方案
import { request, useRequest } from 'umi';
request;
useRequest;
构建时配置
export default {
request: {
dataField: 'data'
},
};
构建时配置可以为useRequest配置dataField ,该配置的默认值是data。该配置的主要目的是方便useRequest直接消费数据。如果想要在消费数据时拿到后端的原始数据,需要在这里配置dataField 为 '' 。
服务端返回数据:
{
success: true,
data: 123,
code: 1,
}
那么useRequest就可以直接消费data。其值为123,而不是{ success, data, code }
运行时配置
在src/app.ts中可以通过配置reques 项,来为项目进行统一的个性化的请求设定。
除了errorConfig、requestInterceptors、responseInterceptors以外其它配置都直接透传axios的request配置。在这里配置的规则将应用于所有的request和useRequest方法。
import type { RequestConfig } from 'umi';
export const request: RequestConfig = {
timeout: 1000,
// other axios options you want
errorConfig: { // =======统一的错误处理方案=======
errorHandler(){
},
errorThrower(){
}
},
requestInterceptors: [ // =======为request方法添加请求阶段的拦截器=======
// 直接写一个 function,作为拦截器
(url, options) =>
{
// do something
return { url, options }
},
// 一个二元组,第一个元素是 request 拦截器,第二个元素是错误处理
[(url, options) => {return { url, options }}, (error) => {return Promise.reject(error)}],
// 数组,省略错误处理
[(url, options) => {return { url, options }}]
],
responseInterceptors: [ // =======为request方法添加响应阶段的拦截器=======
// 直接写一个 function,作为拦截器
(response) =>
{
// 不再需要异步处理读取返回体内容,可直接在data中读出,部分字段可在 config 中找到
const { data = {} as any, config } = response;
// do something
return response
},
// 一个二元组,第一个元素是 request 拦截器,第二个元素是错误处理
[(response) => {return response}, (error) => {return Promise.reject(error)}],
// 数组,省略错误处理
[(response) => {return response}]
]
};
API
useRequest
以在组件内通过useRequest简单便捷的消费数据:
配置:
export default {
request: {
dataField: 'data'
},
};
使用:
import { useRequest } from 'umi';
export default function Page() {
const { data, error, loading } = useRequest(() => {
return services.getUserList('/api/test');
});
if (loading) {
return <div>loading...</div>;
}
if (error) {
return <div>{error.message}</div>;
}
return <div>{data.name}</div>;
};
request
request接收的options如下:
request('/api/user', {
params: { name : 1 },
timeout: 2000,
// other axios options
skipErrorHandler: true, // true代表某个请求跳过错误处理
getResponse: false, // true代表拿到axios完整的response结构
requestInterceptors: [], // 为request注册拦截器
responseInterceptors: [], // 为request注册拦截器
}
RequestConfig
这是一个接口的定义。注意,在导入时要加 type
import type { RequestConfig } from 'umi';
export const request:RequestConfig = {};
运行时配置示例
一个完整的运行时配置示例:
import { RequestConfig } from './request';
// 错误处理方案: 错误类型
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3,
REDIRECT = 9,
}
// 与后端约定的响应数据格式
interface ResponseStructure {
success: boolean;
data: any;
errorCode?: number;
errorMessage?: string;
showType?: ErrorShowType;
}
// 运行时配置
export const request: RequestConfig = {
// 统一的请求设定
timeout: 1000,
headers: {'X-Requested-With': 'XMLHttpRequest'},
// 错误处理: umi@3 的错误处理方案。
errorConfig: {
// 错误抛出
errorThrower: (res: ResponseStructure) => {
const { success, data, errorCode, errorMessage, showType } = res;
if (!success) {
const error: any = new Error(errorMessage);
error.name = 'BizError';
error.info = { errorCode, errorMessage, showType, data };
throw error; // 抛出自制的错误
}
},
// 错误接收及处理
errorHandler: (error: any, opts: any) => {
if (opts?.skipErrorHandler) throw error;
// 我们的 errorThrower 抛出的错误。
if (error.name === 'BizError') {
const errorInfo: ResponseStructure | undefined = error.info;
if (errorInfo) {
const { errorMessage, errorCode } = errorInfo;
switch (errorInfo.showType) {
case ErrorShowType.SILENT:
// do nothing
break;
case ErrorShowType.WARN_MESSAGE:
message.warn(errorMessage);
break;
case ErrorShowType.ERROR_MESSAGE:
message.error(errorMessage);
break;
case ErrorShowType.NOTIFICATION:
notification.open({
description: errorMessage,
message: errorCode,
});
break;
case ErrorShowType.REDIRECT:
// TODO: redirect
break;
default:
message.error(errorMessage);
}
}
} else if (error.response) {
// Axios 的错误
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
message.error(`Response status:${error.response.status}`);
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
// `error.request` 在浏览器中是 XMLHttpRequest 的实例,
// 而在node.js中是 http.ClientRequest 的实例
message.error('None response! Please retry.');
} else {
// 发送请求时出了点问题
message.error('Request error, please retry.');
}
},
},
// 请求拦截器
requestInterceptors: [
(config) => {
// 拦截请求配置,进行个性化处理。
const url = config.url.concat('?token = 123');
return { ...config, url};
}
],
// 响应拦截器
responseInterceptors: [
(response) => {
// 拦截响应数据,进行个性化处理
const { data } = response;
if(!data.success){
message.error('请求失败!');
}
return response;
}
]
};
dva
很多时候我们需要纯净的UI组件,除了渲染逻辑,不再杂糅其他(比如网络请求)。这样我们就要想办法把与渲染无关的业务逻辑抽离出来,形成独立的层(在Umi中就是 src/models 文件夹中所管理的model
)去管理。让所有组件降级为无状态组件,仅仅依赖props渲染。这样UI层面就不需关心渲染无关的逻辑,专注做UI渲染。(注:这里说的组件主要是指page下面的页面组件,对于component下的组件本身就应该是比较通用的组件,更应该仅仅依赖props渲染,它们也不应该有model,数据应该通过在页面组件中通过props传递过去)。
Umi管理状态
Umi内置了Dva 提供了一套状态管理方案:
数据统一在 src/models 中的model管理,组件内尽可能的不去维护数据,而是通过connect去关联model中的数据。页面有操作的时候则触发一个action去请求后端接口以及修改model中的数据,将业务逻辑分离到一个环形的闭环中,使得数据在其中单向流动
配置dva
需要在配置文件中配置 dva: {}
打开Umi内置的dva插件,如:
添加model
Umi会默认将 src/models 下的model定义自动挂载
,只需要在model文件夹中新建文件即可新增一个model用来管理组件状态。
在2.0后,对于某个 page 文件夹下面的model也会默认挂载
。但是需要注意的是model的namespace是全局的,所以需要保证namesapce唯一(默认是文件名)。对于大部分项目,推荐统一放到model中进行管理即可。
import { queryUsers, queryUser } from '../../services/user';
export default {
state: {
user: {},
},
effects: {
*queryUser({ payload }, { call, put }) {
const { data } = yield call(queryUser, payload);
yield put({ type: 'queryUserSuccess', payload: data });
},
},
reducers: {
queryUserSuccess(state, { payload }) {
return {
...state,
user: payload,
};
},
},
test(state) {
console.log('test');
return state;
},
};
组件和model进行connect
新建完成model之后可以在组件中通过ES6的Decorator把model和组件connect到一起。然后可以在组件中通过this.props.[modelName] 的方式来访问model中的数据
。(在对应的model中,默认namespace即为文件名)
import React, { Component } from 'react';
import { connect } from 'umi';
@connect(({ user }) => ({
user,
}))
class UserInfo extends Component {
constructor(props) {
super(props);
}
render() {
return <div>{this.props.user.name}</div>;
}
}
export default UserInfo;
...
const {
location, dispatch, global, rpc, login
} = props;
...
export default connect(({ rpc, global, login }) => ({ rpc, global, login }))(AddCaseForm);
组件中dispatch事件
connect方法同时也会添加 dispatch 到 this.props 上,可以在用户触发某个事件的时候调用它来触发model中的effects或者reducer来修改model中的数据
import React, { Component } from 'react';
import { connect } from 'umi';
@connect(({ user }) => ({
user,
}))
class UserInfo extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div
onClick={() => {
this.props.dispatch({
type: 'user/test',
});
}}
>
{this.props.user.name}
</div>
);
}
}
export default UserInfo;
修改数据
dispatch一个action之后会按照action中的type找到定义在model中的一个effect或者reducer。如果是effect,那么可以去请求后端数据,然后再触发一个reducer来修改数据。通过reducer修改数据之后组件便会按照最新的数据更新。
文档说明
model组成
一个 model 中可以定义如下几个部分:
- namespace:model 的命名空间,唯一标识一个model,如果与文件名相同可以省略不写
- state:model中的数据
- effects:异步action,用来发送异步请求
- reducers:同步 action,用来修改state
connect
connect 是用来将model和组件关联在一起的,它会将相关数据和 dispatch 添加到组件的 props 中
通过注解的方式调用connect,等同于export default connect(mapModelToProps)(UserInfo)
import React, { Component } from 'react';
import { connect } from 'umi';
const mapModelToProps = allModels => {
return {
test: 'hello world',
// props you want connect to Component
};
};
@connect(mapModelToProps)
class UserInfo extends Component {
render() {
return <div>{this.props.test}</div>;
}
}
export default UserInfo;
dispatch
在使用connect将组件和model关联在一起的同时框架也会添加一个this.props.dispatch
的方法,通过该方法可以触发一个action到model中
render () {
return <div onClick={() => {
this.props.dispacth({
type: 'modelnamespace/actionname',
sometestdata: 'xxx',
othertestata: {},
}).then(() => {
// it will return a promise
// action success
});
}}>test</div>
}
Reducer
reducer是一个函数,用来处理修改数据的逻辑(同步,不能请求后端)
。接受state和action,返回老的或新的state 。即: (state, action) => state
增删改数据:
exports default {
namespace: 'todos',
state: [],
reducers: {
add(state, { payload: todo }) {
return state.concat(todo);
},
remove(state, { payload: id }) {
return state.filter(todo => todo.id !== id);
},
update(state, { payload: updatedTodo }) {
return state.map(todo => {
if (todo.id === updatedTodo.id) {
return { ...todo, ...updatedTodo };
} else {
return todo;
}
});
},
},
};
嵌套数据的增删改:(建议最多一层嵌套)
app.model({
namespace: 'app',
state: {
todos: [],
loading: false,
},
reducers: {
add(state, { payload: todo }) {
const todos = state.todos.concat(todo);
return { ...state, todos };
},
},
});
Effect
effects是定义在model中的。它也是一种类型的action,主要用于和后端的异步通讯
。通过effects请求后端发送和接收必要的数据之后可以通过put方法再次发送一个reducer来修改数据。
effects中定义的action都必须是通过 * 定义的Generator函数,然后在函数中通过关键字yield来触发异步逻辑。
export default {
namespace: 'todos',
effects: {
*addRemote({ payload: todo }, { put, call }) {
yield call(addTodo, todo);
yield put({ type: 'add', payload: todo });
},
},
};
put:用于触发action
yield put({ type: 'todos/add', payload: 'Learn Dva' });
call:用于调用异步逻辑,支持promise
const result = yield call(fetch, '/todos');
select:用于从state里获取数据
const todos = yield select(state => state.todos);
转载自:https://juejin.cn/post/7217814237788078138