likes
comments
collection
share

UmiMax快速介绍

作者站长头像
站长
· 阅读数 10

官网:umijs.org/docs/max/in…

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

整体文件夹结构如下:

UmiMax快速介绍
  • .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.tscountsrc/models 目录下不支持目录嵌套定义 model
src/pages/pageA/model.tspageA.model
src/pages/pageB/models/product.tspageB.product
src/pages/pageB/models/fruit/apple.tspageB.fruit.applepages/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 提供了一套状态管理方案:

UmiMax快速介绍

数据统一在 src/models 中的model管理,组件内尽可能的不去维护数据,而是通过connect去关联model中的数据。页面有操作的时候则触发一个action去请求后端接口以及修改model中的数据,将业务逻辑分离到一个环形的闭环中,使得数据在其中单向流动

配置dva

需要在配置文件中配置 dva: {}打开Umi内置的dva插件,如:

UmiMax快速介绍

添加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);