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中間層做前端與服務端的完全解耦

其他

  • 按照之前發佈的夥伴前端開發手冊及出的規範配合進行

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章