优雅的React写法
组件
尽量分模块划分为不同的组件,每个组件文件中最多只能有一个组件类,且组件的样式划分到每个组件中。文件结构如下:
- course_pay // 页面名称
- AreaDetail // 组件文件夹
- index.jsx // 组件jsx
- index.scss // 组件样式
- Coupon
- index.jsx
- index.scss
- FooterBar
- index.jsx
- index.scss
- offpack
- phone.png
- time.png
- Container.jsx // 容器组件
- index.html
- index.jsx // 组合action,连接store和页面
- index.scss// 容器层面样式
- preload-init.js
- reducer.js
- store.js
- AreaDetail // 组件文件夹
容器组件与展示组件
容器组件负责业务逻辑,展示组件负责渲染,展示组件尽量做成无状态组件。
class Container extends Component {
getData() => {
this.props.getData();
}
render() {
return (
<div className="container">
{
props.children && props.children.map((item, index) => {
return (
<ChildCard
key={item.id}
{...item}
/>
);
})
}
</div>
)
}
}
import './index.scss'; // 通过这种方式来引入本组件的css
const ChildCard = ({name, avatar}) => {
return (
<div className="ChildCard">
<img className="avatar" src={avatar} alt="孩子头像" />
<div calssName="name">{name}</div>
</div>
);
}
export default ChildCard;
在设计组件的时候,把页面分为逻辑部分和渲染部分,逻辑部分尽量放到container容器组件中,尽量不负责渲染等工作,仅仅负责将展示组件给组合在一起,有利于逻辑的复用;而展示展示部分,按照一定的粒度分为多个展示组件,每个组件内部负责css样式和JSX结构,尽量不要使用状态,将展示组件写成上面的无状态组件,看起来更加清晰、轻量,复用也更加简单。
PureComponent
pureComponent可以帮助我们做浅比较,从一定程度上避免重复渲染。
import React, { PureComponent } from 'react';
class Child extends PureComponent {
render (
<div className="child">{this.props.name}</div>
);
}
export default Child;
LIST组件
在开发中,经常要渲染列表,不知道具体要渲染多少个,此时要给每个li组件附一个key值,且key值应该是id等具有真实含义的值,而不应该是index等这些数据。
<ul className="cardlist-all-course">
{courseList.map((item, index) => {
return (
<CourseCard
key={item.cid} // key不能是index
{...{
cid: item.cid,
teacherName: item.teacherName || '',
grade: item.grade,
subject: item.subject,
hasLine: index < courseList.length - 1,
}}
/>
);
})}
</ul>
高阶组件(组合和继承)
在不需要用高阶组件的地方尽量不要用高阶组件,要用高阶组件的情况下,尽量选择组合的方式。
class FooterBar extends Component {
render() {
if (utils.getParams(coupon) === 'QQZONEVIP') {
return <FooterBarQQzondVip />
}
return <FooterBarBasic />
}
}
class FooterBarQQzondVip extends Component {
beforePay = () => {
this.props.judgeQQzoneVip()
.then(() => {
this.setState({ isGoPay: true });
}).catch(() => {
console.log('not qq zone vip');
})
}
render() {
return (
<FooterBarBasic
beforePay={this.beforePay}
isGoPay={this.state.isGoPay}
/>
);
}
}
class FooterBarBasic extends Component {
componentWillReceiveProps(nextProps) {
if(nextProps.isGoPay && !this.props.isGoPay) {
this.pay();
}
}
beforePay = () => {
this.props.beforePay();
}
pay = () => { // do something }
render() {
return (
<div className="footer-bar">
<button onClick={this.beforePay}>购买</button>
</div>
);
}
}
PropTypes
使用PropTypes做React类型检查,一来可以在开发环境下协助定位问题,二来可以清晰的看到组件接口及含义。propTypes类型检查只有在dev模式下有用,在Production下没有用,应该进行去除,可以使用transform-react-remove-prop-types
的库来进行移除proptypes操作。
import React from 'react';
import PropTypes from 'prop-types';
import './index.scss'
const ChildCard = ({name, avatar}) => {
return (
<div className="ChildCard">
<img className="avatar" src={avatar} alt="孩子头像" />
<div calssName="name">{name}</div>
</div>
);
}
ChildCard.propTypes = {
name: PropTypes.string.isRequired, // 名称
avatar: PropTypes.string, // 头像
}
export default ChildCard;
defaultProps
不要检查某个props值是否存在,而是使用defaultProps去预设Props。
方法
生命周期函数
除了render函数是必须的外,其他生命函数在不需要的时候都不要写。constructor只有在需要定义state的时候是必须的。
事件响应函数命名
事件响应函数满足以下条件:
- 第一个单词为handle;
- 最后一个单词为要响应的的事件,如Click;
- 中间词汇根据功能来定义。
方法定义及绑定this
使用箭头函数,直接在定义的时候就可以绑定this,一般情况下,直接使用箭头函数就好。除了生命周期函数外,都使用箭头函数。
class FooterBarBasic extends Component {
handlePayClick = (id) => {
this.props.beforePay(id);
}
render() {
return (
<div className="footer-bar">
<button onClick={() => { this.handlePayClick(this.props.id); }}>购买</button>
</div>
);
}
}
尽量不要传参
上面加入函数需要参数,则需要使用onClick={() => { this.beforePay(this.props.id); }
或者`onClick={this.beforePay.bind(this.this.props.id) }
`的方式。但是,这两种方式都会动态生成函数,会造成浪费。可以说,只要在JSX给函数传参,那么久会动态生成函数,所以,要避免传递参数。需要的参数尽量在函数内部通过props或者state来获取。
class FooterBarBasic extends Component {
handlePayClick = () => {
this.props.beforePay(this.props.id);
}
render() {
return (
<div className="footer-bar">
<button onClick={this.handlePayClick}>购买</button>
</div>
);
}
}
此外,如果每个li元素要绑定函数,则可以把函数参数放到元素data属性上。例如:
function handleClick(e) {
// const { cid, name } = e.currentTarget.dataset; // ie10及以下不能使用dataset
const cid = e.currentTarget.getAttribute('data-cid'); // 从data-属性上取值
const name = e.currentTarget.getAttribute('data-name');
window.open(`//fudao.qq.com/pc/task_detail.html?cid=${cid}&title=${encodeURIComponent(`${name}课程任务详情页`)}`);
}
const CourseCard = ({ cid, name, grade, subject, plan, teacherName, studyTerm, totalLesson, hasLine }) => {
const className = classNames({
'course-card': true,
line: hasLine,
});
return (
<div className={className} data-cid={cid} data-name={name} onClick={handleClick}>
<div className="title">
<CourseIcon type="grade" name={grade} />
<CourseIcon type="subject" name={subject} />
{name}
</div>
<div className="plan">{`${plan} ${teacherName}`}</div>
<div className="lesson">学习进度 {`${studyTerm} / ${totalLesson}`}</div>
{hasLine && <div className="line" />}
</div>
);
};
方法长度
一个方法的长度不要超过20行,超过的话,就需要进行拆分。特别是生命周期中的函数,一定不要太长。
使用get、set
在render中,有时候需要缓存一些数据或者判断条件等,此时最好使用get或者set。
// bad
class Coupon extends Component {
render() {
let valueSales = props.sales
? (`省 ¥${props.sales}`)
: (props.coupon_disc
? ('最多省¥' + (props.coupon_disc / 100).toFixed(2))
: '无可用优惠券');
return (
<div className="coupon">
<label className="formLabel">优惠券</label>
<span className={valueClass}>{valueSales}</span>
</div>
)
}
}
把一些值放到get属性中去获取
// good
class Coupon extends Component {
get valueSales () {
if (props.sales) {
return `省 ¥${props.sales}`;
} else if (props.coupon_disc) {
return '最多省¥' + (props.coupon_disc / 100).toFixed(2));
}
return '无可用优惠券';
}
render() {
return (
<div className="coupon">
<label className="formLabel">优惠券</label>
<span className={valueClass}>{this.valueSales}</span>
</div>
)
}
}
变量
const、let
现在我们都是用的ES6的写法,尽量使用const、let,避免使用var来定义变量。在React中,props以及props中的值都是不建议做修改的,所以上级组件传过来的props数据,使用const来定义;自己在组件中定义的可能改变的变量使用let。优先级为:const > let >> var。
赋值与解构
另外,在进行对象变量赋值时,尽量使用简写,如果名称不一致,可以进行解构赋值。例如:
class FooterBarBasic extends Component {
pay = () => {
const {
course_id,
isjoin,
cou_id: coupon_id, // 解构命名
} = this.props;
this.props.pay({
course_id,
isjoin,
cou_id, // 最后一项也要带逗号,方便代码比对和添加参数
});
}
render() {
return (
<div className="footer-bar">
<button onClick={this.pay}>购买</button>
</div>
);
}
}
异步请求
Promise
在React和Redux中,我们经常需要发起ajax请求来获取数据。每次发起ajax请求后,都需要dispatch来把获取的数据加到props中,这个就需要新建actionType以及reducer函数。然而,有些时候,我们只需要知道成功或者失败,并不需要获取数据,此时就可以使用Promise来处理。使用promise不仅可以避免react中ajax请求发起和处理结果的隔离感,更能少些很多代码。但是,promise中的数据获取和新的props的数据时间顺序是不可控的,如果对获取值的时间顺序有要求,不要相信promise中返回的数据(因为这个时候,props的新数据可能还没有拿到)。
class FooterBarBasic extends Component {
pay = () => {
const {
course_id,
isjoin,
cou_id: coupon_id, // 解构命名
} = this.props;
this.props.pay({
course_id,
isjoin,
cou_id, // 最后一项也要带逗号,方便代码比对和添加参数
}).then(() => {
location.href = 'http://fudao.qq.com/pay_success.html';
}).catch(() => {
alert('支付失败,请重试!');
});
}
render() {
return (
<div className="footer-bar">
<button onClick={this.pay}>购买</button>
</div>
);
}
}
Redux
Reducer简写
避免reducer中大量重复的代码。
const reduer = function (state = {}, action) {
switch (action.type) {
case TYPE1:
case TYPE2:
case TYPE3:
return Object.assign({}, state, action.data); // 一样的处理方式进行合并
case PAID:
return Object.assign({}, state, action.data, {paid: true});
default:
return state;
}
};
combineReducer
随着应用变得复杂,需要对reducer函数进行拆分,拆分后的每一块独立负责管理state的一部分。combineReducer函数可以帮助我们组合reducer函数,它的作用仅仅如此,我们也可以自己来组合reducer。
export default combineReducer({
course: courseReducer,
pay: payReducer,
})
调试工具
store.js中可以使用如下写法,有如下功能:
- dev模式下,可以使用redux-logger功能;
- dev模式下,可以使用chrome内嵌的redux-devtools工具;
- dev模式下,可以使用eruda的手机内嵌控制台台;
- dev模式下,可以使用react-perf性能测试工具;
- production模式下,不会有上述库。
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import reducer from './reducers';
let middleware = applyMiddleware(thunkMiddleware);
if (process.env.NODE_ENV !== 'production') {
// dev 模式
let createLogger = require('redux-logger');
let reduxDevTool = require('redux-devtools-extension'); // redux工具
let eruda = require('eruda'); // 手机内嵌控制台
let Perf = require('react-addons-perf'); // react性能测试
middleware = applyMiddleware(thunkMiddleware, createLogger());
middleware = reduxDevTool.composeWithDevTools(middleware);
eruda.init();
window.Perf = Perf;
}
let __initialState = window.__initialState;
export default createStore(
reducer,
__initialState,
middleware
);
代码优雅写法
- 渠道上报
ext: ['', 'QQWALLET', 'QZONEVIP', 'QQVIP'].indexOf(utils.getParams('coupon'))
- 组合action
import {
getCourseDetail,
checkSubpayStatus,
} from 'pages/action_creators/course';
import {
pay,
resetPayFailCode,
} from 'pages/action_creators/pay';
const action = Object.assign({
getCourseDetail,
checkSubpayStatus,
pay,
resetPayFailCode,
});
使用模板字符串
const url = `${location.protocol}//fudao.qq.com/other_pay.html` + `?_bid=2379&course_id=${props.cid}&subpay_code=${props.subpayCode}`;
加载中&加载失败
render() {
const props = this.props;
if (!props.cid && props.courseDetailFail) {
return <PageFail />;
} else if (!props.cid) {
return <FetchingLoading />;
}
return <div>加载成功</div>
}
- classname
let footerBarClass = classnames({
footerBar: true,
showOtherPay
});