Ant Design Pro 使用小结

本文总结了在Ant Design Pro开发过程中遇到的问题及其解决方案,包括Eslint规范、删除layout模板页脚antd链接、在请求头添加acess_token、用户角色管理、错误边界处理、全局样式定制、集成Sentry错误监控、富文本编辑器集成、监听路由变化、临时token访问、自定义菜单栏渲染、用户权限处理、dva-loading优化以及条件渲染的注意事项等。
部署运行你感兴趣的模型镜像

前言

最近在使用Ant Design Pro来搭建一个后台管理系统,对在开发中遇到的问题进行了如下总结。

一、Eslint规范

引入jsx文件时省略 .jsx 后缀
// .eslintrc.js
 rules: {
  //  'react/jsx-filename-extension': [1, { extensions: ['.js'] }],
    'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
    ...
 }
支持路径别名
yarn add eslint-import-resolver-webpack eslint-import-resolver-alias
// .eslintrc.js
settings: {
   'import/resolver': {
     alias: {
       map: [['@', path.resolve('src')]],
     },
   },
 },
// config/config.js
import path from 'path';

export default {
...
 alias: {
    '@': path.resolve('../src'),
  },
};
在代码文件中以注释定义eslint
  • 临时在一段代码中取消eslint检查
    /* eslint-disable */
    
    // Disables all rules between comments 
    alert('123');
    
    /* eslint-enable */
    
  • 临时在一段代码中取消个别规则的检查 (如no-alert, no-console)
    /* eslint-disable no-alert, no-console */
    
    // Disables no-alert and no-console warnings between comments 
    alert(‘123’); 
    console.log(‘123’);
    
    /* eslint-enable no-alert, no-console */
    
  • 在整个文件中取消eslint检查
    /* eslint-disable */
    
    // Disables all rules for the rest of the file 
    alert('123');
    
  • 在整个文件中禁用某一项eslint规则的检查
    /* eslint-disable no-alert */
    
    // Disables no-alert for the rest of the file 
    alert('123');
    
  • 针对某一行禁用eslint检查
    alert('foo'); // eslint-disable-line
    
    // eslint-disable-next-line 
    alert('123');
    
  • 针对某一行的某一具体规则禁用eslint检查
    alert('123'); // eslint-disable-line no-alert
    
    // eslint-disable-next-line no-alert 
    alert('123');
    

二、layout模板去掉页脚antd链接

// src/layouts/BasicLayout.jsx
const defaultFooterDom = <DefaultFooter copyright="xx系统出品" links="" />;
...
return (
   <>
     {defaultFooterDom}
      <div style={{ padding: '0px 24px 24px', textAlign: 'center' }} />
   </>
 );

三、在请求头中添加acess_token

通常在项目开发中需要判断用户是否已成功登录并拥有访问权限,这时我们不会只依赖于后端的session。因为每个用户登录后,如果都需要服务器存储该用户的登录状态,这样也会影响服务端性能,所以我们通常选择在客户端(浏览器)存储token,在发送请求时,在请求头中添加token,后端就能依据token判断用户权限

因为antd pro已为开发者封装了src/utils/request.js文件,我们只需要使用里面的request方法,但是如果每次请求都要手动添加access_token,也太麻烦了,所以我们可以利用request.interceptors.request.use(url, options)方法统一处理

import { extend } from 'umi-request';
import { getToken } from './authority';

/**
 * 配置request请求时的默认参数
 */
const request = extend({
  // 默认错误处理
  errorHandler,
  credentials: 'include', // 默认请求是否带上cookie
});

/**
 * 配置request拦截器, 改变url 或 options.
 */
request.interceptors.request.use(async (url, options) => {
  const token = getToken();  // 获取存储在浏览器的token
  const defaultHeaders = {
    Accept: 'application/json',
  };
  if (token && token.access_token) {
   // 根据前后端约定的api在请求头里添加token
    defaultHeaders.Authorization = `Bearer ${token.access_token}`; 
  }
  const newOptions = { ...options, headers: defaultHeaders };
  if (
    newOptions.method === 'POST' ||
    newOptions.method === 'PUT' ||
    newOptions.method === 'DELETE'
  ) {
    if (!(newOptions.body instanceof FormData)) {
      newOptions.headers = {
        'Content-Type': 'application/json; charset=utf-8',
        ...newOptions.headers,
      };
      newOptions.body = JSON.stringify(newOptions.body);
    } else {
      // newOptions.body is FormData
      newOptions.headers = {
        ...newOptions.headers,
      };
    }
  }
  return {
    url,
    options: { ...newOptions },
  };
});

export default request;

四、用户角色

有时我们需要设置不同用户角色,用来判断该用户所拥有的权限能操作或访问哪些功能和页面,antd pro已经为我们预设了这一功能,我们只需要在此基础上进一步完善

  • 首先我们需要知道,antd pro在渲染页面时,是利用src/utils/authority.js里的getAuthority方法来获取当前用户的角色,以此来判断该用户的权限,所以我们应该在该方法里返回真正的用户角色
// src/utils/authority.js
// 这里只给出大致的实现思路
export function getAuthorityByToken() {
  const str = localStorage.getItem('api-token');  // 用token判断用户是否登录
  if (!str) {
    return ['guest'];
  }
  const expires = localStorage.getItem('api-token-expires');
  if (moment(expires).isBefore(moment(new Date()))) {  // 判断是否登录超时
    setUser({});
    return ['guest'];
  }
  const user = getUser();  // getUser方法将返回当前用户信息,需要自己完善
  if (!user || !user.roles) {
    return ['guest'];
  }
  return user.roles;
}

export function getAuthority() {
  return getAuthorityByToken();
}
  • 然后我们需要将路由权限应用起来
// config/config.js
// 推荐把路由配置封装到单独的文件中,利用export和import在config.js中配置
import pageRoutes from './router.config';

export default {
  ...
  routes: pageRoutes,
  ...
}
// config/router.config.js
export default [
  {
    path: '/user',
    component: '../layouts/UserLayout',
    routes: [
      {
        name: 'login',
        path: '/user/login',
        component: './User/Login',
      },
    ],
  },
  {
    path: '/',
    component: '../layouts/BasicLayout',
    // 这里的Routes属性,利用Authorized组件判断用户角色,具体说明请见官方文档
    Routes: ['src/pages/Authorized'],  
    // authority属性的值,可以是一个数组,包括了该路由下可访问的角色
    authority: [
      'guest',
      'admin',
      'manager'
    ],
    routes: [
      {
        path: '/',
        redirect: '/dashboard'
      },
      {
        path: '/dashboard',
        name: 'Dashboard',
        icon: 'smile',
        component: './DashBoard',
        authority: ['admin', 'manager', 'guest'],
      },
      {
        path: '/overview',
        name: 'Overview',
        icon: 'container',
        component: './Overview',
        authority: ['admin', 'manager'],
      }
      {
        component: './404',
      },
    ],
  },
];
  • 到此,我们可以尝试登陆不同角色的用户账号,会发现,admin和manager用户在左侧的菜单里将出现dashboard和overview的选项,即可以访问到这两个页面,而guest用户左侧只显示dashboard,overview页面是访问不了的。如果我们在浏览器地址栏里手动输入/overview,页面将会出现403页面,提示我们没有权限访问
  • 然而这还没有结束,当我们按照老方式创建一个二级路由
{
    path: '/overview',
    name: 'Overview',
    icon: 'container',
    authority: ['admin', 'manager'],
    routes: [
        {
            path: '/overview/one',
            name: 'One',
            icon: 'container',
            component: './OverView/One',
        },
        {
            path: '/overview/Two',
            name: 'Two',
            icon: 'container',
            component: './OverView/Two',
        },
    ],
},

然后我们登陆guest角色,在地址栏手动输入/overview/one,会发现页面还是正常渲染了,并没有阻止用户访问。这是因为我们虽然给/overview路由设置了authority,却没有给子路由设置,所以所有角色都能访问。我们需要对此进行修改

{
    path: '/overview',
    name: 'Overview',
    icon: 'container',
    // 这里还需要Routes属性,针对二级路由,我们需要进一步判断权限
    Routes: ['src/pages/Subscriped'],  
    authority: ['admin', 'manager'],
    routes: [
        {
            path: '/overview/one',
            name: 'One',
            icon: 'container',
            // 设置Routes后,这里的authority才会生效
            authority: ['admin', 'manager'], 
            component: './OverView/One',
        },
        {
            path: '/overview/Two',
            name: 'Two',
            icon: 'container',
            authority: ['admin', 'manager'],
            component: './OverView/Two',
        },
    ],
},

可以看到在上面设置Routes属性时,我利用了Subscriped组件进行判断,而不是原先的Authorized组件,主要区别是:Authorized组件用来判断用户是否有权限访问项目实际页面,如果没有会直接跳回登录页,提示用户重新登录,这种适用于在项目根路由处进行配置,因为我们通常首先访问根路由/,而不是某一具体页面

所以针对二级路由,我们可以增设一个新的判断权限的组件,当我们故意想看那些没有权限访问的页面时,页面应该显示的是403页面,提示用户没有相关权限。比如:当guest用户访问/overview时,页面应该显示“您没有访问权限”这样的页面,而不是直接跳回登录页

// src/pages/Subscriped.jsx
import React from 'react';
import { Result } from 'antd';
import RenderAuthorized from '@/components/Authorized'; 
import { getAuthority } from '@/utils/authority';

export default ({ children }) => {
  const authority = getAuthority();
  const Authorized = RenderAuthorized(authority);
  return (
    <Authorized
      authority={children.props.route.authority}
      noMatch={
        <Result
          status="403"
          title="403"
          subTitle="Sorry, you are not authorized to access this page."
        />
      }
    >
      {children}
    </Authorized>
  );
};

五、错误边界

在使用antd pro开发中,有时会遇到本地环境明明运行得好好地,然而一到生产环境,有时就会出现页面渲染出错,直接导致页面白屏。对于这种摸不着的抽风BUG,改起来真是心有余而力不足。为此,我们可以换种思路去优化它,主要就是使用React16引入的错误边界 (Error Boundaries)

官方介绍

部分 UI 的 JavaScript 错误不应该导致整个应用崩溃
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。

核心介绍如上,总而言之,就是当子组件渲染出错时,我们可以利用static getDerivedStateFromError()或者componentDidCatch()去渲染备用UI或者打印错误

由此,我们可以在BasicLayout组件中使用错误边界,从而优化项目,但是错误边界的用法只适用于class组件,为此,需要在BasicLayout组件中先进行修改

// src/layouts/BasicLayout.jsx
class BasicLayout extends React.Component {
  componentDidMount() {
    const { dispatch } = this.props;
    if (dispatch) {
      dispatch({
        type: 'user/fetchCurrent',
      });
    }
  }
  
   handleMenuCollapse = payload => {
    const { dispatch } = this.props;
    if (dispatch) {
      dispatch({
        type: 'global/changeLayoutCollapsed',
        payload,
      });
    }
  };  

render() {
    const {
      children,
      settings,
      location = {
        pathname: '/',
      },
      route: { routes },
    } = this.props;
    const { props } = this;

    const authorized = getAuthorityFromRouter(routes, location.pathname || '/') || {
      authority: undefined,
    };
    return (
      <ProLayout
        menuHeaderRender={() => (
          <Link to="/">
            <h1>Ant Design</h1>
          </Link>
        )}
        onCollapse={this.handleMenuCollapse}
        menuItemRender={(menuItemProps, defaultDom) => {
          if (menuItemProps.isUrl || menuItemProps.children || !menuItemProps.path) {
            return defaultDom;
          }
          return <Link to={menuItemProps.path}>{defaultDom}</Link>;
        }}
        breadcrumbRender={(routers = []) => [
          {
            path: '/',
            breadcrumbName: formatMessage({
              id: 'menu.home',
              defaultMessage: 'Home',
            }),
          },
          ...routers,
        ]}
        itemRender={(routeRender, params, routesRender, paths) => {
          const first = routesRender.indexOf(routeRender) === 0;
          return first ? (
            <Link to={paths.join('/')}>{routeRender.breadcrumbName}</Link>
          ) : (
            <span>{routeRender.breadcrumbName}</span>
          );
        }}
        footerRender={footerRender}
        menuDataRender={menuDataRender}
        formatMessage={formatMessage}
        rightContentRender={() => <RightContent />}
        {...props}
        {...settings}
      >
        <Authorized authority={authorized.authority} noMatch={noMatch}>
          { children }
        </Authorized>
      </ProLayout>
    );
  }
}

export default connect(({ global, settings }) => ({
  collapsed: global.collapsed,
  settings,
}))(BasicLayout);

接着在此基础上,引入错误边界

class BasicLayout extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false  // 初始时,无任何错误
    };
  }
  // 关键语句:当子组件出错时,更新state
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  render () {
    const { hasError } = this.state;
    return (<>
        <ProLayout >
          <Authorized authority={authorized.authority} noMatch={noMatch}>
             {/* 根据hasError,当出错时,渲染错误提示组件(备用UI) */}
             {hasError ? <Result status="error" title="页面出错了!" /> : children}
          </Authorized>
        </ProLayout>
    </>)
  }
}

由此,当页面出错时,会去渲染备用UI,进行用户提示,并且此时左侧的菜单栏还能正常使用。这就保证了如果其中的某些 UI 组件崩溃,其余部分仍然能够交互。但是,要注意错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。

六、全改全局样式

修改全局主题色

Ant Design Pro默认的界面UI是以#1890ff蓝色为主题色,并且侧边栏默认为dark背景色,但是在项目正式上线前,我们希望能对界面UI进行修改,以此提高用户体验。比如,我希望将界面UI调整为主题色为绿色,而侧边栏为light背景色。由此,我们可以在config/config.jsconfig/defaultSettings.js中进行修改

// config/config.js
export default {
 theme: {
    // ...darkTheme,
    '@primary-color': '#30C27C', // 全局主色
    '@link-color': '#30C27C', // 链接色
    '@info-color': '#30C27C', // 提示色
  },
}
// config/defaultSettings.js
export default {
  navTheme: 'light',  // 侧边栏主题
  primaryColor: 'daybreak',
  layout: 'sidemenu',
  contentWidth: 'Fluid',
  fixedHeader: false,
  autoHideHeader: false,
  fixSiderbar: true,  // 是否固定侧边栏
  colorWeak: false,
  menu: {
    locale: false,
  },
  title: '测试项目',  // 网页标题名
  pwa: false,
  iconfontUrl: '',
};

修改后,需要重新运行项目,之后我们就可以看到界面UI已发生变化

修改全局样式

上述讲到我们可以在config.js文件中配置主题色,但如果你在项目中有用到DatePicker组件,你会发现页脚部分的tag的颜色并没有发生改变,还是以蓝色为背景色,这就会出现如下图那样的小问题

这时我们可以F12看下Elements面板,会发现这个tag的类名是ant-tag ant-tag-blue。所以要解决这个问题,我们有两种办法:1. 将tag的类名改为ant-tag ant-tag-green 2. 全局修改这个日期选择器页脚的tag样式

由于不知道第一种方案该怎样实现,于是此次采用修改全局样式
这边还要解释下为什么是修改全局样式,而不是直接给DatePicker加自定义类名,并在less文件中修改。显而易见,我们在DatePicker上添加的类名,控制的样式只是对外层div有用,实际上控制tag样式的是如下标签

所以需要修改的是全局样式

:global {
  .ant-calendar-footer-extra {
    // 修改日期选择器页脚tag样式
    .ant-tag {
      color: rgb(82, 196, 26);
      background: rgb(246, 255, 237);
      border-color: rgb(183, 235, 143);
    }
  }
}

修改效果如下:

修改某些地方的全局样式

如果像上述那样修改全局样式的做法,我们可以看到项目中所有运用到修改过的那个组件的地方,样式都会发生变化。但有时,我们只需要在某个页面改变这个组件的样式。以Select组件为例,我希望它在这个页面中,背景色为透明,并且边框没有圆角

  • 首先尝试给Select组件加个filter-item类名,然后再使用:global修改
<Select
   className={styles['filter-item-bg']}
   ...
>
...
:global {
  .filter-item-bg {
    .ant-select-selection{
        background-color: transparent;
        border-radius: 0;
    }
  }
}

可是修改后,会发现并没有任何变化。这是因为Ant Design Pro默认使用CSS Module,所以当我们使用styles['filter-item-bg']给组件引入样式时,CSS Module的构建工具将会把类名编译成一个哈希字符串,于是产生了一个独一无二的className,避免与其他选择器重名,我们可以在Elements面板中看到,实际确实如此。但是为什么放在:global中,我们写的样式没有生效?

CSS Modules 允许使用:global(.className)的语法,声明一个全局规则。凡是这样声明的class,都不会被编译成哈希字符串 --摘自(CSS Modules 用法教程)

也就是说,这个组件的类名已经被编译成一个哈希字符串,然而在less文件中我们在:global(.className)声明的class却没有被编译,于是,实际上这两个类名是不相同的,样式也就不起作用。
继续往下看阮老师的博客,我们可以看到解决方案就是使用普通的class写法,它将引用全局class


依照提示修改为:

<Select
   className="filter-item-bg"
   ...
>

接着我们会发现样式已经成功修改!

七、集成sentry

yarn add @sentry/browser
  • src/index.js(入口文件)处初始化sentry
    由于本项目下无index.js文件,所以笔者改成在global.jsx中初始化
    import * as Sentry from '@sentry/browser';
    Sentry.init({ dsn: 'https://xxxx' });  // 项目的dsn
    
  • 在组件中使用sentry监听异常。笔者这里选择在BasicLayout.jsx
    /*
        最常见的就是在componentDidCatch钩子函数中捕获异常,
        因为一旦在整个组件(包括子组件)中发生异常,componentDidCatch就会被调用,同时我们能获取对应的error信息
        配合 static getDerivedStateFromError() 使用,当发生异常,选择渲染备用UI组件
    */
    componentDidCatch(error, errorInfo) {
        Sentry.withScope(scope => {
          scope.setExtras(errorInfo);
          Sentry.captureException(error);
        });
      }
    
  • 如果我们存在多个不同的sentry dsn:
    • 本地环境使用测试地址,生产环境使用正式地址:可以在初始化sentry时,利用process.env.NODE_ENV判断是development还是production
    • 若需要根据不同环境手动指定sentry地址,可以在config目录下新建sentryConfig.js记录
      export default {
        MODE_TEST: 'test', // 测试环境
        MODE_RELEASE: 'release', // 正式环境
        TEST_DSN: 'https://dsn1', // 测试环境
        RELEASE_DSN: 'https://dsn2', // 正式环境
      };
      
      接着可以在组件中引入TEST_DSNRELEASE_DSN变量启动sentry
      或者你也可以利用config.js中的define去定义一个全局变量,用来判断
      // config/config.js
      import sentryConfig from './sentryConfig';
      
      define: {
          SENTRY_MODE: sentryConfig.MODE_TEST, // 修改 sentry的dsn  
          // MODE_TEST - 测试环境    MODE_RELEASE - 正式环境
      },
      
      接着在src目录下的js文件中,我们可以直接使用SENTRY_MODE这个变量
      import sentryConfig from '../config/sentryConfig';
      
      if (SENTRY_MODE === sentryConfig.MODE_TEST) {
            Sentry.init({ dsn: sentryConfig.TEST_DSN });
      } else if (SENTRY_MODE === sentryConfig.MODE_RELEASE) {
            Sentry.init({ dsn: sentryConfig.RELEASE_DSN });
      }
      

八、集成富文本编辑器

针对用antd pro框架搭建的React项目,笔者选用Tinymce这款富文本编辑器

  • yarn add @tinymce/tinymce-react@3.6.0
  • yarn add tinymce@5.2.2
  • node_modules\tinymce\skins文件夹包含了tinymce编辑器需要用到的静态样式文件和字体,拷贝skins文件夹到public目录下,以便本地能访问
  • 封装自定义的MyTinyMce组件
    import React, { PureComponent } from 'react';
    import { Editor } from '@tinymce/tinymce-react';
    import tinymce from 'tinymce/tinymce'; // eslint-disable-line
    import 'tinymce/themes/silver'; // 配置 tinymce 主题
    // 按需引入tinymce的插件
    import 'tinymce/plugins/paste'; // 针对ctrl + c/v 触发文本更新事件
    import 'tinymce/plugins/preview';
    import 'tinymce/plugins/table';
    import 'tinymce/plugins/image';
    import 'tinymce/plugins/link';
    import 'tinymce/plugins/print';
    import 'tinymce/plugins/autolink';
    import 'tinymce/plugins/fullscreen';
    import 'tinymce/plugins/autosave';
    import 'tinymce/plugins/autoresize';
    import 'tinymce/plugins/hr';
    import 'tinymce/plugins/code';
    import 'tinymce/plugins/lists';
    import 'tinymce/plugins/media';
    import 'tinymce/plugins/anchor';
    import 'tinymce/plugins/advlist';
    
    class MyTinyMce extends PureComponent {
      render() {
        // 需要向封装的Tinymce组件传递 onChange 和 value 属性
        const { onChange, value } = this.props;
        
        // 页面上编辑器上方的功能菜单栏会依据给 Editor 组件传入的 toolbal 属性来排列
        return (
          <Editor
            value={value}
            onEditorChange={content => onChange(content)}
            init={{
              height: 300,
              menubar: false,
              plugins: [
                'advlist autolink lists link image print preview anchor',
                'code fullscreen',
                'media table paste code',
              ],
              toolbar:
                'formatselect forecolor backcolor bold italic underline strikethrough image media table alignleft aligncenter alignright alignjustify bullist numlist outdent indent removeformat hr paste code link undo redo preview fullscreen',
            }}
          />
        );
      }
    }
    
    export default MyTinyMce;
    
自定义的MyTinymce组件与受控组件

上述封装代码中,通过给自定义的MyTinymce组件传递onChangevalue属性来使其成为一个受控组件,控制编辑器文本的显示和更新。基于此,我们可以结合antdForm组件的this.props.form.getFieldDecorator来包装自定义的MyTinymce组件

<Form.Item label="商品描述">
    {getFieldDecorator('bodyHtml', {
        initialValue: initBodyHtml || '<p></p>',
    })(<TinyMce />)}
</Form.Item>

antd
图片源于Ant Design官网

九、监听路由变化

需求场景:当我们需要在项目中实现一方用户能创建消息发送给某用户,某用户能实时接收到全部消息的功能,并且基于项目结构我们不想利用socket.io来改写功能,此时就可以考虑换个思路去实现。
我们可以考虑当路由改变或页面刷新时,就发送请求获取用户当前未读消息,虽然这种实现思路略有瑕疵,但确实是一种可行方案。

// src\layouts\BasicLayout.jsx
componentDidMount() {
    const {  history } = this.props;

    // 通过 history.listen 监听路由变化,在此方法内可以发送请求
    history.listen(route => {
        console.log(route)
    })
}

十、通过临时token访问项目

对于使用OAuthJWT授权方案获取access_token来判断用户是否有访问权限的系统,若正面临的一个需求是:通过后端生成的临时token,让外部用户在访问本项目的页面url中携带一个token参数,前端拦截这个参数并请求后端接口来解析这个临时token,从而能获取真正的access_token,前端依此获取用户信息,从而实现外部用户在无需登录的情况下也能访问本项目特定页面
这个需求实现的关键点在于应该在哪块代码中或在哪个代码文件中实现这个功能。首先想到的是在BasicLayout.jsx中通过componentDidMount钩子函数中对当前访问的url进行判断,若携带token参数,则发起请求解析临时token。但明显是行不通的,对于无访问权限的用户,在进入主页面前就会通过路由权限判断为无访问权限,重定向回登录页面
最后选择在src\components\Authorized\CheckPermissions.jsx模块下实现功能。当用户访问项目主页面时,都会通过CheckPermissions.jsx里的代码逻辑进行权限检查,未通过则重定向回登录页

const checkPermissions = (authority, currentAuthority, target, Exception) => {
  const { props = {} } = target || {};
  const { pathname, query } = props.location || {};
  if (query && query.token) {
   // 若页面url携带token参数,则发起请求解析token
    parseToken(query.token)
      .then(res => {
        if (res && res.token) {
          // 若解析成功则保存返回的access_token
          setAuthorityByToken(token);
          // 通过access_token,发送请求获取用户信息
          return fetchCurUser();
        }
        return null;
      })
      .then(user => {
        if (user && user.id) {
         // 如果获取用户信息成功,则代表身份认证成功,设置前往用户原访问页面
          setUser(user);
          window.location.href = pathname;
        }
      });
  }

  const expires = localStorage.getItem('api-token-expires');
  if (moment(expires).isBefore(moment(new Date()))) {
    notification.error({
      message: '权限失效,请重新登录',
    });
    setUser({});
    setAuthorityByToken();
    return <Redirect to="/user/login" />;
  }

  ...
};

上述是笔者的实现方式,若有更好的实现方法,欢迎提出

十一、自定义菜单栏渲染

ant design pro默认渲染的菜单栏样式如下

若想自定义菜单栏的样式,如利用Badge组件提示未读消息数量或用多个图标装饰,可以在BasicLayout.jsx里进行修改

<ProLayout
	...
	menuItemRender={(menuItemProps, defaultDom) => {
		if (menuItemProps.isUrl || menuItemProps.children || !menuItemProps.path) {
			return defaultDom;
		}
		// 示例,可以根据当前渲染菜单项的path来判断,并自定义渲染
        if(menuItemProps.path === 'dashboard') return <Link to='/dashboard'>Dashboard</Link>
		return <Link to={menuItemProps.path}>{defaultDom}</Link>;
	}}
	...

十二、用户权限

对于中大型后台管理系统来说,需要更细化的用户鉴权方式。通常会选择在获取用户信息时,将该用户所拥有的权限数据一并返回,从而用来判断页面上某些显示和操作功能是否向用户开放。
如果单纯将这些用户信息利用状态管理器dva来存取,在下面这个场景:当用户刷新页面时,状态管理器中的数据会丢失,这时会利用存储在浏览器本地的access_token重新发起请求获取用户信息,但同时页面仍在重新渲染,甚至在请求完成前就已渲染完毕,这时会出现用户权限数据丢失,导致页面渲染不完整的情况。
解决上述问题的方法有三:一是等待请求完成后才渲染页面,显然不太实际,用户无需因为只是刷新了下页面就要等待额外的时间;二是请求完成并且用户数据更新后再次渲染页面,但是实际上利用connectmodel state获取的数据不能触发渲染(如有错误请指出);所以最好的办法是利用存储在浏览器本地如localStorage里的用户信息进行鉴权

// BasicLayout.jsx
...
async componentDidMount() {
    const { dispatch } = this.props;
    const currentUser = JSON.parse(window.localStorage.getItem('current-user')) || {};
    if (dispatch) {
     // 同步获取本地存储的用户信息
      dispatch({
        type: 'user/saveCurrentUser',
        payload: currentUser,
      });
      // 异步获取用户信息
      dispatch({
        type: 'user/fetchCurrent',
      });
    }
}
...

BasicLayout.jsx文件中做了上述修改。利用两次dispatch,先同步本地存储的用户信息到项目状态管理器中,等到请求完成后,再更新为最新的用户信息。这么做考虑点有下:一是如刷新页面或者在access_token未过期前重新访问项目,正常情况下本地保存的用户信息是完整的,可以用此初始化项目数据,若丢失则会跳转到登录页,且登录的流程中恰恰是等到获取了完整的用户信息后才能访问项目主页面,所以基本上不会出现问题。

十三、dva-loading

antd pro中利用dva-loading插件封装了loading状态,发起异步请求时为true,请求结束为false,在实际开发中可以利用loading值给出更好的用户提示

// dva中存在loading模块
connect(({ app, loading }) => ({ 
    app, 
    loading,
    loading1: loading.models.app, // 监听app模块下的所有异步请求,在所有异步请求完成前,loading1值为true,完成后为false
    loading2: loading.effects['app/getData'], //监听app下的getData异步请求
    loading3: loading.global()  // 监听全局所有的异步请求
}))(App);

按上面的介绍,我们可以拿到对应的loading值,为true,表示正在请求数据,给用户“正在加载中…”的提示。但是这个条件判断有一点不足,具体看下述场景
开发中,我们一般会在componentDidMount中发起异步请求获取数据,并可能会利用到类似<Spin spinning={loading} />给予用户加载中的提示,但是要注意的是,初始化应用时,loading值实际上为undefined ,并且在非常短的时间间隔后,在异步请求发起时,才会变为true,如果按上述方式使用loading,会发现页面一开始没有正在加载数据的提示,之后才会出现,这点实际上可以被优化,只需要给loading赋上初始值,值为undefined时设置为默认值true,之后loading值会根据异步请求完成情况,自动变更为truefalse

class App extends Component {
   componentDidMount(){
     const { dispatch } = this.props
     dispatch({
       type: 'app/getData'
     })
   }
   
   render(){
      const { loading } = this.props
      return (
        <Spin tip="Loading..." spinning={loading}>
           <div>hello world</div>
        </Spin>
      )
   }
}

connect(({ app, loading }) => ({ 
    app, 
    // 默认值true
    loading: loading.effects['app/getData'] ?? true, 
}))(App);

十四、条件渲染

开发中常用到条件渲染
一般写法

isExist ? <div>hello world</div> : null

简化后

isExist && <div>hello world</div>

上述写法有一定缺陷,当前面的条件判断后的值为非布尔类型,比如值为数值0,预期是判为false,并且渲染后面的元素,但实际上渲染出来的是数值0,并且后面的元素不会被渲染

// 预期当data数组的长度不为0时渲染后面的div
// 实际上当data数组长度为0时渲染出来的是 数值0 而非 div
data.length && <div>hello world</div>

应该改写为

data.length > 0 && <div>hello world</div>

以上就是笔者的一些个人总结,后续会不定期更新。对于本文不足的地方,欢迎大家讨论指正

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值