React+Antd+TypeScript 开发规范

React+Antd+TypeScript 规范整合

1.TypeScript代码标准化规则

提取出部分适用于项目中的官方要求的的TypeScript用于约束贡献者的编码规范 [typescript官方规范]
出自:深圳市伙伴行网络科技有限公司 编辑:前端开发-毛昱宇

(https://zhongsp.gitbooks.io/typescript-handbook/doc/wiki/coding_guidelines.html?q=)

命名

  • 使用PascalCase为类型命名。
type ColumnProps = xxx
  • 不要使用I做为接口名前缀。(定义props和state 使用IProps / IState)
  • 使用PascalCase为枚举值命名。
  • 使用camelCase为函数命名。
const handleTableChange = ():void => xxx
  • 使用camelCase为属性或本地变量命名。
  • 不要为私有属性名添加_前缀。
  • 尽可能使用完整的单词拼写命名。

风格

  • 使用arrow函数(箭头函数)代替匿名函数表达式。
  • 只要需要的时候才把arrow函数的参数括起来。
    比如,(x) => x + x是错误的,下面是正确的做法:
    1. x => x + x
    2. (x,y) => x + y
    3. <T>(x: T, y: T) => x === y
  • 开大括号总是放在其关联语句的同一行(大括号不换行)。
  • 小括号里开始不要有空白.
    逗号,冒号,分号后要有一个空格。比如:
    1. for (var i = 0, n = str.length; i < 10; i++) { }
    2. if (x < 10) { }
    3. function f(x: number, y: string): void { }
  • 每个变量声明语句只声明一个变量
    (比如 使用 var x = 1; var y = 2; 而不是 var x = 1, y = 2;)。

类型

  • 除非类型/函数需要在多个组件中共享,否则不要导出(export)

  • 在文件中,类型定义应该放在顶部

null和undefined

  • 使用 undefined,不要使用 null

    typeof null == "object"
    

注释

  • 为函数,接口,枚举类型和类使用JSDoc风格的注释。JSDoc

2.React+antd+typescript代码规范

注释

使用 koroFileHeader 插件用于快速生成开发者注释。koroFileHeader是一款用于在vscode中用于生成文件头部注释和函数注释的插件,支持所有主流语言,功能强大,灵活方便,文档齐全。

  • 文件顶部的注释,包括描述、作者、日期、最后编辑时间,最后编辑人

    /*
     * @Description: app im菜单配置列表中的表格模块
     * @Author: maoyuyu
     * @Date: 2019-08-05 10:31:12
     * @LastEditTime: 2019-08-14 17:08:33
     * @LastEditors: Please set LastEditors
     */
    

    文件头部添加注释快捷键windowctrl+alt+i,macctrl+cmd+i

    在光标处添加函数注释windowctrl+alt+t,macctrl+cmd+t

    vscode setting 配置如下

    "fileheader.configObj": {
      "autoAdd": false, // 自动添加头部注释开启才能自动添加
    },
    "fileheader.customMade": {
      "Author":"[you name]",
      "Date": "Do not edit", // 文件创建时间(不变)
      "LastEditors": "[you name]", // 文件最后编辑者
      "LastEditTime": "Do not edit", // 文件最后编辑时间
      "Description":""
    },
    "fileheader.cursorMode": {
      "Author":"[you name]",
      "description": "", 
      "param": "", 
      "return":""
    }
    
  • 业务代码的注释

  • /*业务代码注释*/
    
  • 变量的注释

  • interface IState {
      // 名字
      name: string;
      // 电话
      phone: number;
      // 地址
      address: string;
    }
    
  • 公共方法/私有方法的注释

  • /**
     * 用于获取富文本通知的格式
     * @param formatContent 格式化后的content(后台返回)
     * @param useEdit 是否用于修改(true:修改通知的反显)
     */
    getContent(formatContent:string, useEdit:boolean=false){
      try{
        return useEdit? formatContent : formatContent.replace(reg,'')
      }catch(err){
        return ""
      }
    }
    

引用组件顺序

  • 第三方组件库 ==> 公共组件 ==> 业务组件 ==>utils ==> map ==> css 样式

  • //react
    import * as React from "react"
    import { SFC } from "react"
    //dva
    import { connect } from "dva"
    import { Link } from 'dva/router';
    //antd
    import { Table,Dropdown, Menu,Button, Icon,Modal } from 'antd';
    import { ColumnProps } from "antd/lib/table/interface"
    //公共组件
    import { DefaultProps } from "@/interface/global"
    import { IMList } from "@/interface/Operations/AppImMenu/list"
    import { Pagination } from "@/interface/BrokersBusiness/BuildingManage/buildingList"
    import { appImModelAction } from "./model"
    //util
    import { parseQuery, } from '@/utils/utils';
    //map
    import { androidType, iosType } from "../common/maps"
    //less
    import styles from "./AppImMenuList.less"
    

引号

  • 使用双引号或es6反引号

命名

  • 同上typescript 代码标准化规则
  • 定义props 和 state 接口名以大写字母I为前缀
interface IProps extends DefaultProps {
  appNoticeModel: AppNoticeProps,
  officeTree: any[],
}

interface IState {
  id?: number | null,
  uploading?: boolean,
  excludeControls?:BuiltInControlType[]
}

项目中关闭tslint中的interface以大写字母I为前缀的规则,认为会影响interface的阅读。前缀I的命名规则放在prop 和state的描述上,用于识别interface的作用。

  • 常量: 全大写风格,大写字母、数字和下划线,单词之间以下划线分隔,例如:ABC_TEST。禁止汉字、特殊符号、小写字母。同时以const 定义变量
const UPLOADURL:string = "/api/file/upload"

interface声明顺序

  • 日常用到比较多的是四种,只读参数放第一位,必选参数第二位,可选参数次之,不确定参数放最后
interface iProps {
  readonly x: number;
  readonly y: number;
  name: string;
  age: number;
  height?: number;
  [propName: string]: any;
}

state定义

  • 定义state前加上只读属性(readonly),用于防止无意间对state的直接修改,或者在constructor定义
readonly state = {
    id: null,
    imageList: [],
    smallImageList: [],
    uploading: false,
  }

//constructor

constructor(props){
  super(props)
  this.state = {
    id: null,
    imageList: [],
    smallImageList: [],
    uploading: false,
  }
}

声明规范

  • 不要使用 var 声明变量(已加入tslint套餐)
  • 不会被修改的变量使用 const 声明(常量必须使用const声明)
  • 去除声明但未被引用的代码
  • 不允许有空的代码块

渲染默认值

  • 添加非空判断可以提高代码的稳健性,例如后端返回的一些值,可能会出现不存在的情况,应该要给默认值.
// bad
render(){
  {name}
}

// good
render(){
  {!!name || '--'}
}

/*=================================*/
// bad
this.setState({
  status: STATUS.READY,
  apps: list,
  total: totalCount,
  page: page,
});


// good 
const { list, totalCount } = await getPeopleList(keyword, page, pageSize);
this.setState({
  status: STATUS.READY,
  apps: list || [],
  total: totalCount || 0,
  page: page,
});

数据格式转换

  • 把字符串转整型可以使用+号
let maxPrice = +form.maxPrice.value;
let maxPrice = Number(form.maxPrice.value);
  • 转成 boolean 值用!!
let mobile = !!ua.match(/iPhone|iPad|Android|iPod|Windows Phone/);

使用 location 跳转前需要先转义

// bad
window.location.href = redirectUrl + '?a=10&b=20';

// good
window.location.href = redirectUrl + encodeURIComponent('?a=10&b=20');

业务代码里面的异步请求需要 try catch

  • ajax 请求,使用 try catch,错误提示后端返回,并且做一些失败后的状态操作例如进入列表页,我们需要一个 loading 状态,然后去请求数据,可是失败之后,也需要把 loading 状态去掉,把 loading 隐藏的代码就写在 finally 里面。

    getStudentList = async () => {
      try {
        this.setState({
          loading: true,
          isEmpty: false
        });
        await getStudentList({});
        this.setState({
          loading: false,
          isEmpty: true
        });
      } catch (e) {
        // TODO
        console.log(e)
      } finally {
        //  失败之后的一些兜底操作
        this.setState({
          loading: false,
          isEmpty: true
        });
      }
    };
    

setState使用

  • 使用setState 函数的写法
//bad
this.setState({
  a:300
})

//good
this.setState(
  (state,props) => {
    return {
      a:300
    }
  }
)

原因:

//对象式
//state.count = 1
function incrementMultiple() {
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
}
//console.log(this.state.count) //2
//函数式
function increment(state, props) {
  return {count: state.count + 1};
}
function incrementMultiple() {
  this.setState(increment);
  this.setState(increment);
  this.setState(increment);
}
//console.log(this.state.count) //4

setState将要修改的值加入队列进行批量处理

function incrementMultiple() {
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
}

//等同于
function incrementMultiple() {
	const count = this.state.count
  this.setState({count: count + 1});
  this.setState({count: count + 1});
  this.setState({count: count + 1});
}

而函数式setState每次调用内部会获取上一次修改的值再批量更新

  • 避免使用setState同步情况
//bad
setTimeout(()=>{
 	this.setState({info}) 
})

setState一般情况下为异步操作,只有当使用js原生方法调用时才会出现同步情况。setState同步不会批量处理修改,会造成性能降低。

判断

  • 使用定义好的常量代替type值的判断
// bad
if (type !== 0) {
  // TODO
}

// good
const STATUS: Record<string, any> = {
  READY: 0,
  FETCHING: 1,
  FAILED: 2
};

if (type === STATUS.READY) {
  // TODO
}

// best
enum STATUS {
  // 就绪
  READY = 0,
  // 请求中
  FETCHING = 1,
  // 请求失败
  FAILED = 2,
}
  • 减少多个boolean值变量的联合判断?
if((type1 == 0 || type1 == 1) && type2 !== 0){
	pass
}

该代码判断冗余且难以维护,应维护成标识

/**
 * 用于根据type值判断是否显示附件和喜报
 */
enum TypeFlag {
  Empty = 0,//type值为空
  ShowHapplyNews = 1,//type值为1
  ShowOther = 1 << 1//type值不为1,切type值存在
};

//标识维护/控制器
private showUploadRule(type){
  let KEY = TypeFlag.Empty
  if(type == 1){
    KEY |= TypeFlag.ShowHapplyNews
  }
  if(type && type !==1){
    KEY |= TypeFlag.ShowOther
  }
  if(type && type == 3){
    KEY &= ~TypeFlag.ShowOther
  }
  
  return KEY
}

//判断是否有权限 (type == 1)
{this.showUploadRule(type) & TypeFlag.ShowOther ? 
  <FormItem label="附件"  >
    ...pass
	 </FormITem>
  :null}

具体使用规则参见typescript枚举

公共组件开发规则

  • 目录
|- components
  |- component1
    |- index.tsx
    |- component1.tsx
    |- aaa.tsx
    |- bbb.tsx
  |- component2
    |- index.js
    |- component2.tsx
    |- aaa.tsx
    |- bbb.tsx
    |- ccc.tsx
//aaa.tsx
import * as React from "react"
...
export default class Aaa extends React.component<IProps, IState>{
  pass
}

//component1.tsx
import * as React from "react"
import Aaa from "./aaa"
...

export default class Component1 extends React.component<IProps, IState>{
  render {
    return (
      <Aaa>
  			pass
  		</Aaa>
    )
  }
}

//index.tsx
import Component1 from "./component1"
export default Component1

代码过滤掉你没考虑到的情况

  • 例如一个函数,你只想操作字符串,那你必须在函数开头就只允许参数是字符串
function parse (str:string){
  if (typeof(str) === 'string' ) {

  }
}

####不再使用react 即将废弃的生命周期函数

componentWillMount
componentWillReceiveProps
componentWillUpdate

//bad
componentWillMount(){
  formItemFun(this.props.form, map);
}

//good 直接在构造函数中构造
constructor(props:IProps){
  super(props)
  formItemFun(this.props.form, map);
}

react17将会正式废弃这三个生命周期函数

for-in 中一定要有 hasOwnProperty 的判断(即禁止直接读取原型对象的属性)

//bad
const arr = [];
const key = '';

for (key in obj) {
  arr.push(obj[key]);
}

//good
const arr = [];
const key = '';

for (key in obj) {
  if (obj.hasOwnProperty(key)) {
    arr.push(obj[key]);
  }
}

//或者使用Object.keys()
for (key of Object.keys(obj)) {
  arr.push(obj[key]);
}

防止 xss 攻击

  • input,textarea 等标签,不要直接把 html 文本直接渲染在页面上,使用 xssb 等过滤之后再输出到标签上;
import { html2text } from 'xss';
render(){
  <div
  dangerouslySetInnerHTML={{
    __html: html2text(htmlContent)
  }}
/>
}

禁止使用 dangerouslySetInnerHTML属性

在组件中获取真实 dom

  • 使用 16 版本后的 createRef()函数
class MyComponent extends React.Component<iProps, iState> {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }
}

使用私有属性取代state状态

  • 对于一些不需要控制ui的状态属性,我们可以直接绑到this上, 即私有属性,没有必要弄到this.state上,不然会触发渲染机制,造成性能浪费 例如请求翻页数据的时候,我们都会有个变量。
// bad
state: IState = {
  pageNo:1,
  pageSize:10
};

// good 
queryParams:Record<string,any> = {
  pageNo:1,
  pageSize:10
}

代码粒度

  • 超过两次使用的代码用函数分离
//判断是编辑/新增
isEdit = () => {
	return !!parseQuery(location.search).id || parseQuery(location.search).id == 0
}

//提交表单
formSaveParams = (fieldsValue: FieldsValue) => {
  let formFieldsValue:FieldsValue & {edit?:boolean} = {...fieldsValue}
  if(this.isEdit()){
    formFieldsValue = {
      ...formFieldsValue,
      id:this.state.id,
      edit:true
    }
  }
  return formFieldsValue
}

render(){
  return (
    <PageHeaderWrapper title={`${this.isEdit() ? '编辑' : '添加'}IM配置`} content="请按照页面要求填写数据">
      pass
    </PageHeaderWrapper>
  )
}
  • 工具函数和业务逻辑抽离,表单校验和业务抽离、事件函数和业务抽离,ajax和业务抽离。
componentDidMount(){
  this.getList()
}		

/*获取列表 */
getList = () => {
  const { form } = this.props
  const type = form.getFieldValue('type')
  const page = {
    currentPage: 1,
    pageRows: 10,
  }
  const formData = {
    type,
  }
  this.upDate(page, formData)

}

//ajax
upDate = (page, formData?) => {
  const { dispatch } = this.props
  dispatch(appNoticeModelAction('fetchList')(payload));
}

if else 等判断太多了,后期难以维护

  • 抽离config.js 对配置进行统一配置
例如你的业务代码里面,会根据不同url参数,代码会执行不同的逻辑.
/info?type=wechat&uid=123456&
const qsObj = qs(window.location.url)
const urlType = qsObj.type
// bad 
if (urlType === 'wechat') {
    doSomeThing()
} else if () {
    doSomeThing()
} else if () {
    doSomeThing()
} else if () {
    doSomeThing()
}

// good 
config.t
const urlTypeConfig: Record<string, typeItem> = {
  'wechat': { // key 就是对应的type
    name: 'wechat', 
    show: ['header', 'footer', 'wechat'] // 展示什么,可能是异步的
    pession: ['admin'], // 权限是什么,可能是异步的
  },
  'zhifubao': { // key 就是对应的type
    name: 'zhifubao', 
    show: ['header', 'footer', 'zhifubao'] // 展示什么,可能是异步的
    pession: ['admin'], // 权限是什么,可能是异步的
  },
}

// 业务逻辑
const qsObj = qs(window.location.url)
const urlType = qsObj.type
urlTypeConfig.forEach(item => {
  if(urlType === item.type) {
    doSomeThing(item.show)
  }
})

3.使用接口对象类型

对于ant design 内置的拥有字段效验功能的类与接口需要强制在代码中使用

事件对象类型

ClipboardEvent<T = Element> 剪贴板事件对象
DragEvent<T = Element> 拖拽事件对象
ChangeEvent<T = Element> Change 事件对象
KeyboardEvent<T = Element> 键盘事件对象
MouseEvent<T = Element> 鼠标事件对象
TouchEvent<T = Element> 触摸事件对象
WheelEvent<T = Element> 滚轮事件对象
AnimationEvent<T = Element> 动画事件对象
TransitionEvent<T = Element> 过渡事件对象
import { MouseEvent } from 'react';

interface IProps {
  onClick(event: MouseEvent<HTMLDivElement>): void;
}

Table

import { ColumnProps } from "antd/lib/table/interface"

interface IMList {
	id: string;
	routeKey: string;
	routeValue: string;
	type: number;
	remark: string;
	createDate: string;
	updateDate: string;
	delFlag: number;
}

const columns: Array<ColumnProps<IMList>> = [
    {
      title: <div className={styles.tableTeCenter}>路由key</div>,
      dataIndex: 'routeKey',
      fixed: tableWidth < 1470 ? 'left' : false,
      render: (val, item, index) => <div className={styles.tableCenter}>{index + 1}</div>,
    },
    {
      title: <div className={styles.tableTeCenter}>路由value</div>,
      dataIndex: 'routeValue',
      render: (val, item) => <div className={styles.tableCenter}>{`${val || ''}`}</div>,
    },
    {
      title: <div className={styles.tableTeCenter}>类型</div>,
      dataIndex: 'type',
      render: (val, item) => <div className={styles.tableCenter}>{renderFloor(val, item)}</div>,
    },
    {
      title: <div className={styles.tableTeCenter}>描述</div>,
      dataIndex: 'describe',
      render: (val, item) => (
        <div className={styles.tableCenter}>{renderHouseAreaPrice(val, item, 1)}</div>
      ),
    },
  ]

Form

  • 引入form的方法
import { FormComponentProps } from "antd/lib/form/Form";//内置form的属性和方法

interface IProps extends DefaultProps, FormComponentProps {
  appNoticeModel: AppNoticeProps,
  officeTree: any[],
}

####表单字段效验

使用json2ts插件 可以直接通过json数据生成interface

  • 先通过接口文档的请求示例使用json2ts确定好FieldsValue接口。然后对表单提交fieldsValue字段进行校验
interface FieldsValue {
  id?:string|number,
  productId?:number,
  platform?:string,
  adLocation?:number,
  effectiveFrequency?:number,
  effectiveImmediately?:number,
  minVersion?:string,
  title?:string,
  imageUrl?:any,
  smallImageUrl?:any,
  jumpRouteId?:string,
  jumpRouteKey?:string,
  params?:Params,
  describe?:string,
}

handleSubmit = () => {
  const { id } = this.state
  const { form, dispatch } = this.props;
  //确定fieldsValue对象为FieldsValue类型
  form.validateFields((err, fieldsValue: FieldsValue) => {
    if (err) return
    dispatch(appAdvModelAction('submitAdvForm')({...fieldsValue,id,}))
  })
}
  • 对Details处理
//interface.ts
export interface Detail {
	id?: string;
	type?: number;
	range?: number;
	otherRange?: string;
	effectiveImmediately?: number;
	isEffective?: number;
	title?: string;
	attchUrlList?: AttchUrl[];
	imageUrl?: string;
	content?: EditorState;
	clicks?: number;
	createTime?: number;
	activityTime?: number;
	officeId?: string;
	officeName?: string;
	createId?: string;
	createName?: string;
}

//interface.ts
//页面需要的details类型,对接口返回的进行重写
export interface RenderDetail extends Omit<Detail,"imageUrl" | "attchUrlList"> {
	imageUrl?:{
		[key:string]:any
	},
	attchUrlList?:any
}
  

//model.ts
//后台返回的details信息
export interface AppNoticeProps {
    detail:Detail,
}

...
	reducers:{
    saveDetail(state,{ payload }){
            const processedDetail:RenderDetail = {
                ...(payload as Detail) || null,
                content:BraftEditor.createEditorState(payload.content||""),
                imageUrl:defaultImg(payload.imageUrl),
            }
            return {
                ...state,
                detail:processedDetail || {}
            }
        }
  }

//page.tsx
interface IProps {
    detail:RenderDetail,
}


export default Page extends React.Component<IProps,IState>{
  render(){
    return (
    	<Form>
        ...
        <EffectiveImmediately initValue={detail.effectiveImmediately}/>
      </Form>
    )
  }
}

组件开发原则

单一职责原则(SPR)

  • 目录结构
|- components
  |- component1
    |- index.tsx
    |- component1.tsx
    |- aaa.tsx
    |- bbb.tsx
  |- component2
    |- index.js
    |- component2.tsx
    |- aaa.tsx
    |- bbb.tsx
    |- ccc.tsx

每个页面一个目录,通过入口index.tsx暴露出。遵循单一职责原则,component.tsx只负责数据ajax请求和提交。 单一职责原则(SPR)

Flux

  • dva设计遵循flux模型,view层只提交dispatch,model层负责action与store管理。因此数据操作应当尽量放在model层处理。
/*提交表单*/
//page.tsx
  handleSubmit = () => {
    const { id } = this.state
    const { form, dispatch } = this.props;
    form.validateFields((err, fieldsValue: FieldsValue) => {
      if (err) return
      dispatch(appAdvModelAction('submitAdvForm')({...fieldsValue,id,}))
    })
  }
  
 //model.ts
  ...
  	effects:{
      *submitAdvForm({ payload }, { call, put }) {
            yield put({
                type: 'changeState',
                payload: {editLoading:true},
            });
            let processedPayload:FieldsValue = {
                ...(payload as FieldsValue),
                params: payload.params.map(key => payload.paramsSave[key]),
                imageUrl: payload.imageUrl[0].saveUrl,
                smallImageUrl: payload.smallImageUrl[0].saveUrl,
                platform: payload.platform.join(","),
            }
            if(!processedPayload.id && processedPayload.id !==0) delete processedPayload.id
            const res = yield call(adConfigSave, processedPayload);
            if(res.code == 200){
                message.success("保存成功")
                yield put(routerRedux.push(routerConfig('AppAdvMenuList')));  

            }else{
                message.error('保存失败,' + res.msg);
            }
            yield put({
                type: 'changeState',
                payload: {editLoading:false},
            });
        },
    }
/*获取详情*/
//page.tsx
...
render(){
  return (
    <Form>
      {/*默认值直接获取,不在页面中进行任何处理*/}
    	<Platform initValue={detail.platform} allowClear={true}/>
      <Params initValue={detail.params}  />
      <SmallImageUrl initValue={detail.smallImageUrl} />
    </Form>
  )
}

//models.ts
...
reducers:{
  saveDetail(state,{ payload }){
    let processedPayload:Detail = {
      ...(payload as Omit<FieldsValue,"id">),
      platform:payload.platform.split(",").map(k=>+k),
      jumpRoute:(payload.jumpRouteKey && payload.jumpRouteId) ? payload.jumpRouteKey + "&&" + payload.jumpRouteId : "",
      imageUrl:payload.imageUrl && defaultImg(payload.imageUrl),
      smallImageUrl:payload.smallImageUrl && defaultImg(payload.smallImageUrl)
    }
    return {
      ...state,
      detail:processedPayload
    }
  }
}

数据与视图完全分离增加页面的复用性,不需要对每个接口返回的不同数据做不同的处理。同样model层数据使用纯js代码处理,降低与视图耦合,方便测试。也方便迁移,如后续使用node中间层做前端与服务端的完全解耦

其他

  • 按照之前发布的伙伴前端开发手册及出的规范配合进行

发布了149 篇原创文章 · 获赞 51 · 访问量 23万+

猜你喜欢

转载自blog.csdn.net/caoyan0829/article/details/101022271