打造属于你的Ant Design Pro V5(二)

5,507 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

助你快速了解 Ant Design Pro V5 详细配置,用于自己的项目,是真的香!

上篇文章讲了下有关Ant Design Por V5 的基础配置,接下来本菜鸟将详细讲讲实实在在的干货,主要从:国际化、数据的装载与搬运(useModel)、路由(菜单)、网络请求、数据模拟(mock) 五个方面详细解剖,

希望这篇文章能为小伙伴们一点微薄的帮助,有任何问题,或说的不对的地方,欢迎评论区讨论👏🏻👏🏻👏🏻

附上本菜鸟的 gitHub地址:Ant Design Pro V5 有喜欢的点个 Star 支持下~

去除国际化

国际化是Ant Design Pro 一个非常强大的功能,但对国内的项目并不需要要国际化,所以当自己的项目不需要这个功能时,我们可以考虑去除这个功能

我们只需要执行 npm run i18n-remove 这个命令即可,但此时我们再将 local 删掉,还是会发现有大量的报错原因是代码中只用了 umi的 useIntl 这个方法, 那么现在只需要把文件的所有代码删除就可以了

小问题

当我们去除国际化后我们还是会遇见一些小问题

1.浏览器自带的翻译功能

这是因为在 src/page/document.ejs 文件中的 lang 是 en 的原因

我们需要将它改为zh-CN就行了

2.Ant Design 的部分组件会显示英文(如日期组件)

这时我们还需要在 config/config.js 中的 locale 配置 default: 'zh-CN' 即可

数据的装载与搬运---useModel

上篇文章本菜鸟说过 V5 的useModel 相当于傻瓜式操作,不得不说蚂蚁就是牛!

首先 V5 自带一个全局状态 (initialState),用官方的话说是:

initialState 在 v5 中替代了原来的自带 model,global,login,setting 都并入了 initialState 中。我们需要删除 src/models/global.ts,src/models/login.ts,src/models/setting.ts ,并且将请求用户信息和登陆拦截放到 src/app.tsx

理解下官方的话:

  • 首先,他是一个由官方内置的model,可以当做最外层的model
  • 其次,我们可以将全局的状态放进去,以供单独的文件去调用,如:用户信息,权限等级等~

我们先来看看如何使用吧

import { useModel } from 'umi';

export default () => {
  const { initialState, loading, error, refresh, setInitialState } = useModel('@@initialState');
  return <>{initialState}</>
};

使用起来非常的方便,先介绍下所有参数的用途

initialState : 返回全局状态,也就是 getInitialState 的返回值

setInitialState: (state:any) => 手动设置 initialState 的值,手动设置完毕会将 loading 置为 false.

loading: getInitialState 是否处于 loading 状态,在首次获取到初始状态前,页面其他部分的渲染都会被阻止。loading 可用于判断 refresh 是否在进行中。

error: 当运行时配置中,getInitialState throw Error 时,会将错误储存在 error 中。

refresh: () => void 重新执行 getInitialState 方法,并获取新数据。

如何独自创建 model

讲完了官方自带的 initialState,接下来我们单独讲讲如何创建属于自己的 model

这块没有太多要讲解的地方,我们直接上代码吧~,以最简单的计时器即可

首先我们需要在 src/model 创建自己模块

文件位置 src/models/test/modelTest.ts

import { useState, useCallback } from 'react';

interface Props {
  count?: number
}

const initInfoValue: Props = {
  count: 1,
}

export default function modelTest() {

  const [init, setInitValue] = useState(initInfoValue);
  const [loading, setLoading] = useState(false);

  const waitTime = (time: number = 2000) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(true);
      }, time);
    });
  };

  const setInit = useCallback(async(res:any) => {
    setLoading(true)
    await waitTime()
    setLoading(false)
    setInitValue({count: res})
  }, [init])

  const setAdd= useCallback((res:any) => {
    setInitValue({ count: res +1})
  }, [init])

  return {
    loading,
    init,
    setAdd,
    setInit
  };
}

然后在所需的页面直接通过useModel,获取就 OK 了

import React from 'react';
import { useModel } from 'umi';
import { Button } from 'antd';

export const MockModel: React.FC<any> = () => {
  const { init, setInit, setAdd, loading } = useModel('test.modelTest');

  return <div>
    <div style={{ marginBottom: 14 }}> count 对应的值{init.count}</div>
    <Button loading={loading} style={{ marginBottom: 18 }} type='primary' onClick={() => setInit(5)} >设置count为5</Button>
    <br />
    <Button type='primary' onClick={() => setAdd(init.count)} >累加1</Button>
  </div>
}

总的来说,只要把对应的方法,值全部返回,然后在调用就OK了,是不是很简单~

性能优化

当我们存在 model 的数据越来越多,就需要使用 useModel 的第二个可选参数来进行性能优化,只消费model 中的部分参数,而不使用其他参数,并返回的值则是 useModel 最终的返回值

我们以上述为例

import React from 'react';
import { useModel } from 'umi';
import { Button } from 'antd';

export const MockModelRet: React.FC<any> = () => {
  const { init, setAdds } = useModel('test.modelTest', (ret) => {
    return {
      init: ret.init,
      setAdds: ret.setAdd
    }
  });

  return <div>
    <div style={{ marginBottom: 14 }}> count 对应的值{init.count}</div>
    <Button type='primary' onClick={() => setAdds(init.count)} >累加1</Button>
  </div>
}

路由详解(动态菜单)

路由的配置

首先,所有的路由都在config/routes下,我们配置路由都在此文件下进行

我们先建一个一级目录

export default [
  {
     path: '/test',
     name: '一级目录',
     icon: 'smile',
     component: './Welcome'
  }
]

看看此时实现的效果

image.png

而多级目录只需要使用 routes 这个参数即可,其他配置一样

我们在这里在创建一个二级目录,在创建一个二级目录的子目录,我们希望由二级目录跳入这个子目录,但在菜单上不显示,这时我们只需要使用hideInMenu即可在菜单上隐藏,但我们会发现变成了这样

image.png

这时我们发现了两个问题:一个是左边的菜单栏并没有高亮,二是面包屑展示的不对,这是因为我们写的路劲不对,我们需要把这个子页面挂载二级目录的下面就能完美解决了~

export default [
	{
    path: '/test',
    name: '一级目录',
    icon: 'smile',
    routes: [
      {
        path: '/test',
        redirect: '/test/twotest',
      },
      {
        path: '/test/twotest',
        name: '二级目录',
        component: './Welcome',
      },
      {
        path: '/test/twotest/threetest',
        name: '二级目录的子页面',
        component: './Welcome',
        hideInMenu: true
      }
    ]
  }
]

效果:

image.png

我们再来总结下常用路由的参数(其余的参数可看官网):

  • path: 地址栏的访问路径

  • name : 名称

  • icon:前面的小图标

  • component:对应的文件夹目录

  • redirect:重定向后的地址

  • authority:权限,大型项目不建议使用,直接用动态菜单即可

  • hideInMenu: 是否影藏菜单栏

  • routes:对应的子路由

动态菜单

在 V5 中,提供两种,一种是 权限, 一种是动态菜单,在这里建议使用动态菜单,所以只介绍下动态菜单的用法。

所谓动态菜单,需要接口的配合,返回什么菜单就展示什么,在本人的案例中,我通过 mock 模拟出数据,并将它放入 utils/initData

image.png

有感兴趣的同学可以去 GitHub 上下载看看,我这里讲下我使用中发现的问题:

  1. 动态路由里的地址只能根据已配置原有的路由去打包,如果没有则会出现问题,(也就是说,不管有没有动态路由,都要在原有的路径里写上对应的名字)
  2. 其中配置的 component 无用,redirect的也无用。
  3. 动态路由的 icon不显示
  4. 动态路由就算隐藏了对应的路径,也可以通过地址栏输入地址进入到该页面
  5. redirect 重定向不管用,就算设置上也是默认原有的路由的重定向定义的,这样点击头部的时候会跳转到原有的页面
  6. 登录,登录的时候跳转的页面如果没有重定向地址,会跳转 / ,应为动态路由里的重定向不管用,所以会跳向原有的/页面

如有写的不对请留言指出~

解决方法:

针对问题1和问题2,我们必须跟后端协商好,这个路由必须与接口返回的字段对应,并且只需要返回对应的名称、icon、路径即可

问题3,我们需要单独写个方法来适配就行了

使用: menuData: formatter(menuData.data)

const formatter = (data: any[]) => {
  data.forEach((item) => {
    if (item.icon) {
      const { icon } = item;
      const v4IconName = toHump(icon.replace(icon[0], icon[0].toUpperCase()));
      const NewIcon = allIcons[icon] || allIcons[''.concat(v4IconName, 'Outlined')];

      if (NewIcon) {
        try {
          // eslint-disable-next-line no-param-reassign
          item.icon = React.createElement(NewIcon);
        } catch (error) {
          console.log(error);
        }
      }
    }

    if (item.routes || item.children) {
      const children = formatter(item.routes || item.children); // Reduce memory usage

      item.children = children;
    }
  });
  return data;
};

const toHump = (name: string) => name.replace(/-(\w)/g, (all: string, letter: any) => letter.toUpperCase());

针对问题5和问题6,我们进行详细的描述下:

比如说我现在原有的页面配置上A页面(第一个页面),但我在其权限下不想展示A页面,不显示的时候,就会出现这个问题

解决方法

在点击头部的方法和登录的方法(不包括重定向)跳转到获取路由的第一个上,并且,将取消原有路由的重定向。在getInitialState上统一设置,如果路径是/则自动获取第一个参数的路径,就能解决了~

链接的桥梁--网络请求

网络请求可以说是前端与后端的,我们需要这个桥将后端绑定起来

对于一个系统来说,请求的方法与接受的参数都是统一的,所以我们需要集中配置我们的请求模块,来适配自己的系统。

在 V5 中设置请求的模块在src/app.tsx

在V5中我们需要在 umi 中引入,并且相对于 V4 ,V5扩张了一个配置 skipErrorHandler, 这个配置的作用是:跳过默认的错误处理,用于处理特殊的接口

import { request } from 'umi';

request('/api/user', {
  params: {
    name: 1,
  },
  skipErrorHandler: true,
});

统一地址

在上篇文章介绍了分模块打包 其本质就是通过不同的命令,打出不同的包,请求的接口就需要在这里通过prefix来配置

export const request: RequestConfig = {
  prefix: process.env.NODE_ENV === "production" ? host : '/api/',
};

拦截器

每个系统对应的后端请求都不相同,所以应该在请求前和请求后做一些特定的处理,以此来帮助我们快速开发,比如:在请求的时,请求头上加入 token

因此,V5提供了两种方式,一是中间件(middlewares),另一种则是拦截器

这两种方式都可以优雅地做网络请求前后的增强处理,但中间件使用起来比较复杂,所以这里只介绍拦截器

请求拦截--requestInterceptors

首先,我们来说说请求拦截需要配置什么

  • 后端的不同发送网络的格式,方式都不通,比如说配置 Content-Type
  • 此外,现在大多数项目都会有一个token,用来判断

这里要说明一点 token 通常需要存储到本地的,原因是每次启动项目都会用到,当然缓存能少用就少用,尽量使用数据流做处理。

因此 我们需要设置一个变量来存储 token

另外我们需要注意一点,在未登录的时候并无token,并且在退出登录后,要清空缓存

废话有点多~ 直接看代码吧~

/**请求拦截 */
export const requestInterceptors: any = (url: string, options: RequestInit) => {
  if (storageSy.token) {
    const token = `Bearer ` + localStorage.getItem(storageSy.token);//存储的token
    options.headers = {
      ...options.headers,
      "Authorization": token,
      'Content-Type': 'application/json',
    }
  }
  return { url, options };
}

响应拦截 responseInterceptors

跟请求拦截一样,我们先来说说响应拦截做的的做了什么吧

  • 统一的错误处理,如:在网络不好的情况下,请求不到数据,这时我们可以给一个统一的提示语来告诉用户
  • 统一报错,有的时候返回的状态是不正常的,这时我们就可以做处理,给出接口给的提示语,并且我在这里统一设置了一下,如果返回的不是 200(成功)将统一设置为 false, 这样就不需要用 catch 来进行捕获了。
  • 用户登录时限,一个系统中,我们希望用户登录这个是有时效性,这是接口就回返回特定的状态码,来告诉我们用户信息不匹配,或者登录时间到了,这时我们需要在响应拦截中给出对应的提示,并清空缓存,退出系统 关于第二点,其实有个小问题,就是他什么都不返回,但状态码为 200 ,这时在具体页面中如何判定成功呢,其实只要判定 返回的类型 不等于 布尔值就行了
// 响应拦截
export const responseInterceptors:any = async (response: Response) => {
  if (!response) {
    notification.error({
      description: '您的网络发生异常,无法连接服务器',
      message: '网络异常',
    });
    return;
  }
  const data = await response.clone().json();
  if ([10001,10008].includes(data.resultCode)) {
    message.error(data.message);
    localStorage.clear();
    return false;
  }
  if (data.code !== 200) {
    message.error(data.message);
    return false;
  }
  return data.data;
}

数据模拟--mock

mock 是什么呢? 他是模拟接口请求的数据,并且可以随机生成测试数据,当后端还未好时,我们可以与后端沟通变量的名称,之后再靠 mock 来模拟数据,实现开发,等后端好了,直接替换接口就行了~~

文档请参考: mock官网

mock服务

mock 是模拟的接口,所以在正式打包后,mock 数据是无法使用,那么如果在开发时候不用mock数据,该怎么处理呢?

命令 npm run start:no-mock 就行

独立的mock服务

我们知道,有的时候 mock 数据 是可以和真实的 Api 请求并存的

但在打包后 mock 是无法使用的,那么能否启动一个 mock 服务呢?然后通过 nginx 代理到这个mock服务呢?

官方给出了一种方法: umi-serve

安装命令 yarn add global umi-serve

为了方便起见

我们可以再 package.json 中的 script 中加入 "serve":"umi-serve" 即可

下次启动 umi-serve 服务,只需在控制台中输入:npm run serve ,即可。

GET请求和POST请求

这块内容比较简单,就没必要说了,直接提供两种请求方式就ok了

  'GET /api/form/queryDetail': async (req: Request, res: Response) => {
    const { detail } = req.query;
    if (detail === 'introduce') {
      res.send(
        resData({
          list: introduce,
          anchorList: introduceAnchorList
        }
      ))
    } 
    
    res.send({
      code: 400,
      detail,
      message: '请输入参数'
    })
  }
  
    'POST /api/domesy/queryDetail': async (req: Request, res: Response) => {
    const { detail } = req.query
    if(detail === 'welcome') {
      res.send(
        resData({
          list: welcome,
          anchorList: welcomeAnchorList
        }
      ))
      return
    }

    res.send({
      code: 400,
      detail,
      message: '请输入参数'
    })
  },

到此,就结束了,希望本篇文章对你有微末的帮助~~~

关于 Ant Design Pro V5 的基本配置请看 打造属于你的Ant Design Pro V5(一)

关于 Ant Design Pro V5 工作流程请看 打造属于你的Ant Design Pro V5(三)