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
是錯誤的,下面是正確的做法:x => x + x
(x,y) => x + y
<T>(x: T, y: T) => x === y
- 開大括號總是放在其關聯語句的同一行(大括號不換行)。
- 小括號裏開始不要有空白.
逗號,冒號,分號後要有一個空格。比如:for (var i = 0, n = str.length; i < 10; i++) { }
if (x < 10) { }
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 */
文件頭部添加註釋快捷鍵:
window
:ctrl+alt+i
,mac
:ctrl+cmd+i
在光標處添加函數註釋:
window
:ctrl+alt+t
,mac
:ctrl+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中間層做前端與服務端的完全解耦
其他
- 按照之前發佈的夥伴前端開發手冊及出的規範配合進行