跟 ant design pro 5.x 学习 react 的使用与代码风格

react 项目写法各异,这时候参考一个权威的样例,就会很有参考价值。无论是否示使用 ant design pro ,通过项目,都可以有很多的启发。

项目结构

如果拿不准如何搭建react 项目的目录结构,这时 ant design pro 的 src 项目结构,也许是一个参考的模板。

components 公共组件

components 公共组件,里面多个目录,每个目录为一个组件

  • index.tsx 组件本身
  • index.less 组件样式

如果该组件依赖子组件,可以在自身目录下创建

  • NoticeIcon.tsx
  • NoticeList.less
  • NoticeList.tsx

样式文件,如果是模块化的模式,是可以被多个组件引用。

pages 页面组件

pages 页面在antd pro中相当于路由定义,虽然不用它这种模式,但路径即路由这种方式也可以学习的。

里面多个目录,每个目录为一个组件,或模块。

当该目录作为组件时:

  • index.tsx 组件本身
  • index.less 组件样式

当页面比较简单,也可以写在 pages 或模块下。

当存在子组件,可以写在组件目录的 components 下。

好处是,不容易跟其他页面组件混乱。而公共组件的子组件不需要components,因为他外层目录本来就是这个,而且公共组件一般引用的组件会少一些,也不必单独放在目录下。

样式

公共样式

可以看到有两个公共样式文件:

  • global.less 公共样式及ant组件样式重置
  • utils/utils.less 工具mixin

没有明确引入它们的地方。可以模仿文件组成

组件模块化样式

antd pro 所有组件都是外部 less 文件,通过模块化样式引入。这种用法也是比较标准的用法了,需要学习并使用。

看代码可见,只要是类名,都是模块化的,即使父级用了 classNama={styles.a} ,子级也不能通过 classNama="b" 使用。

类名下的标签、伪类等,可以生效。

.a {
  .b {
    input {
    }
    .c {
    }
  }
}

应该使用:

<div className={styles.a}>
  <div className={styles.b}>
    <div className={styles.c}>
    </div>
  </div>
</div>

类名复杂的:

import classNames from 'classnames';

const inputClass = classNames(styles.input, {
  [styles.show]: searchMode,
});

样式穿透

:global(内部类名) 即可实现穿透

.lang {
  width: 100%;
  height: 40px;
  line-height: 44px;
  text-align: right;
  :global(.ant-dropdown-trigger) {
    margin-right: 24px;
  }
}

如果是多个穿透样式:

.tabs {
  :global {
    .ant-tabs-nav-list {
      margin: auto;
    }

    .ant-tabs-nav-scroll {
      text-align: center;
    }
    .ant-tabs-bar {
      margin-bottom: 0;
    }
  }
}

ts 使用

组件 Props 类型定义

组件 Props 的类型,跟当前组件关联性高,直接在定义组件前定义。

export interface UpdateFormProps {
  onCancel: (flag?: boolean, formVals?: FormValueType) => void;
  onSubmit: (values: FormValueType) => Promise<void>;
  updateModalVisible: boolean;
  values: Partial<TableListItem>;
}

const UpdateForm: React.FC<UpdateFormProps> = (props) => {

};

数据格式定义

在 services 目录下定义的接口,数据格式定义在 API.d.ts

declare namespace API {
  // 用户基本信息
  export interface CurrentUser {
    avatar?: string;
    name?: string;
    title?: string;
    group?: string;
    signature?: string;
    tags?: {
      key: string;
      label: string;
    }[];
    userid?: string;
    access?: 'user' | 'guest' | 'admin';
    unreadCount?: number;
  }
}

d.ts 结尾的文件会被 TypeScript 默认导入到全局

export async function query() {
  return request<API.CurrentUser[]>('/api/users');
}

全局类型避免了复杂的引用

为 Window 增加参数

/src/typings.d.ts 文件做一些全局的类型

interface Window {
  ga: (
    command: 'send',
    hitType: 'event' | 'pageview',
    fieldsObject: GAFieldsObject | string,
  ) => void;
  reloadAuthorized: () => void;
}

定义全局变量的类型(变量在其他地方被全局注入,如环境变量)

declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

数据请求

数据请求接口:

export async function updateRule(params: TableListParams) {
  return request('/api/rule', {
    method: 'POST',
    data: {
      ...params,
      method: 'update',
    },
  });
}

把处理接口的操作,封装为函数。这类增、删、改的函数不需要放在组件内,返回成功或失败。即简化组件内的函数,不是必要的就可以提取到外面。

/**
 * 更新节点
 * @param fields
 */
const handleUpdate = async (fields: FormValueType) => {
  const hide = message.loading('正在配置');
  try {
    await updateRule({
      name: fields.name,
      desc: fields.desc,
      key: fields.key,
    });
    hide();

    message.success('配置成功');
    return true;
  } catch (error) {
    hide();
    message.error('配置失败请重试!');
    return false;
  }
};

直接在回调函数中,即可调用并处理成功失败的逻辑。

<UpdateForm
  onSubmit={async (value) => {
    const success = await handleUpdate(value);
    if (success) {
      handleUpdateModalVisible(false);
      setStepFormValues({});
      if (actionRef.current) {
        actionRef.current.reload();
      }
    }
  }}
  onCancel={() => {
    handleUpdateModalVisible(false);
    setStepFormValues({});
  }}
  updateModalVisible={updateModalVisible}
  values={stepFormValues}
/>

全局状态

定义全局状态

在 app.tsx 中,定义了全局状态。可见这就是一个类似于 reducer 函数的定义。

  • 定义了 state : settings 和 currentUser
  • 定义了异步 action : fetchUserInfo
export async function getInitialState(): Promise<{
  settings?: LayoutSettings;
  currentUser?: API.CurrentUser;
  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
  const fetchUserInfo = async () => {
    try {
      const currentUser = await queryCurrent();
      return currentUser;
    } catch (error) {
      history.push('/user/login');
    }
    return undefined;
  };
  // 如果是登录页面,不执行
  if (history.location.pathname !== '/user/login') {
    const currentUser = await fetchUserInfo();
    return {
      fetchUserInfo,
      currentUser,
      settings: defaultSettings,
    };
  }
  return {
    fetchUserInfo,
    settings: defaultSettings,
  };
}

获取和改变全局状态

组件中,可以直接获取和改变全局状态:

const { initialState, setInitialState } = useModel('@@initialState');

setInitialState({ ...initialState, currentUser: undefined });

可见在新版中,dva那一套已经被隐藏或弃用,全局状态已经非常的简洁。