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中间层做前端与服务端的完全解耦
其他
- 按照之前发布的伙伴前端开发手册及出的规范配合进行