React初接触
最初接触的前端框架是angular 1.x,对于当时习惯jquery写页面模式的我来说冲击还是很大的。前端也可以用后端的方式使用mvc的模式来构建,而且前端项目也能够处理着么复杂的逻辑(以前写php的时候,项目的逻辑主要是放在后端做的,通过Controller来渲染不同的页面,前端反反而只是单纯对数据的渲染)。当时对我冲击最大的反而不是mvc这样大的模式,而是数据绑定真是太好用了。
相信之前基于jquery方式实现前端页面的时候,都会因为繁琐的dom操作而感到烦恼,因此当时angular的数据绑定大大减轻了开发的压力,项目构建也变的简单很多。当然,angular也存在很多问题,个人感觉angular的思想很大一部分来自后端开发,这对于前端工程师可能天然有些不太友好,另一方面就是被吐槽很多的,angular这个框架太重了。不仅仅是代码的大小,对于一些简单的项目,我们不需要angular这么复杂的框架,这点特别是体现在移动端上。
因此,当时Vue的出现也给我带来了很大惊喜。Vue框架很轻,一个简单的MVVM框架,只是将View层和Modal层进行了绑定。而且Vue还保留了很多Dom的细节,对于前端工程师来说可以迅速的转换。实习期间一直使用Vue在移动端进行开发,在移动端表现也十分友好,配合其它一些库可以迅速的搭建一个公众号应用。
真正接触React是工作后,之前对于React其实一直是一个比较抵抗的状态,一个是jsx的写法当时不太适应,另一个方面是感觉React能做的Vue也能够实现,因此对于React的理解都不太深。
使用React开发
真正工作后,第一个任务便是对之前的一个React项目进行维护和改造。当时React给我的第一个感觉就是混乱!由于历史原因,项目立项的要求是比较快的能够上线,因此没有很好的预估,并且没有引入redux。因此整个代码中你就能看到无数的回调,以及constructor中state里的一大串参数。
习惯Vue开发模式后,这个项目的代码给我的感觉就是乱,一方面因为它本身比较复杂,另一方面Vue作为一个Vm框架,它有一个model层来对组件的数据做一个管理,而React作为一个单纯的View层框架,每一个组件只能通过state来维持自己的状态。因此当遇到组件间需要进行通信的时候,react简直是灾难性的。项目中我见过大量的函数在父组件中进行绑定,然后传给子组件进行调用,有些函数甚至向下传导了数层,同时,由于状态都是组件自己维护的,因此在一个组件中你也能见到大量的setState的方法。这样在后期维护的过程中是十分不方便的,有时候需要加功能的时候,我需要从一个组件一层一层的跳到多个组件中来寻找一些方法的调用。
当然,react肯定也有自己的好处,作为一个单纯的View层,react组件间的耦合度更低了,也是第一次的体会到了“搭积木”的开发模式。react的生态圈十分完善,除去各个公司自己的组件库,很多开源的组件库也是十分优秀的,比如google的material-UI以及蚂蚁金服的ant-design。使用react可以迅速的搭建一个前端项目。
Redux的引入
之前也说过没有Redux的react项目是十分混乱的,因此Redux的引入给React项目带了很大的改观。Redux的使用过程中,我感觉它给我带来以下好处:
- 有一个统一的地方来管理我们的数据
- 组件间的通信变的更加简单
Redux像一个全局的Model,管理项目中的数据,这样的话我们在设计组件的时候我们可以将UI逻辑和数据逻辑做更深的抽离。在我开发的过程中,对于一些组件的实现我会尽可能的将它设计成一个stateless(无状态)并且可以pure render的组件。通俗来说就是在设计组件的时候会把它想象成一个盒子,它不包含任何业务逻辑,它的作用就是把我给它的数据进行渲染,它的表现形式也完全由传递过来的属性决定。这个思想跟函数的解耦以及Spring中的切片模式是同样的概念,组件相当于一个函数,这个函数中不依赖其它对象也不保存状态。
另一个方面是组件间的通信,Redux是一个单向的数据流:
组件触发动作-->动作修改Redux中数据-->绑定Redux中数据的组件重新渲染
Redux(store)就像是一个全局的Model,组件View和Model进行绑定,当Model被修改后,绑定的组件也会进行重新渲染。这样的设计模式下,我们可以将组件间的通信逻辑抽象到数据层。没有Redux的开发模式下,当组件触发一个动作,比如Button的点击,我需要考虑到这次点击有没有其它组件在监听,父组件需不需要对这个动作反馈,如果其它组件或者父组件也关心这个动作的话,我需要在这些组件中构建监听方法,bind后传到触发动作的组件中供调用。从上面的描述可以看到这样的方式很繁琐,当逻辑复杂后,就会出现大量的bind函数。引入Redux后,各个组件只需要绑定自己关注的数据,组件的动作也可以抽像成对数据的修改,当数据修改后,绑定这个数据的组件也会重新渲染。
Redux带来的一些问题
Redux给我的开发带来了很大遍历,React构建前端应用也不会出现像之前提到的混乱问题。但是它同样给我带来了一些烦恼。
组件的过度设计
之前说过,在对组件进行设计的时候,我回尽可能的将组件的UI逻辑和数据逻辑(或者说项目逻辑)进行抽离,因此在写代码的时候常常会出现这样的问题,写着写着,突然觉得这段代码需不需要抽离出来成一个单独的组件呢?这时候我会花大量的时间去把这个组件的逻辑抽离出来,把它想象成一个公共组件,同时考虑各种扩展性,这样花费大量时间最后发现这个组件其实只在一个页面被引用了。
其实很多时候这样的事情是不需要的,特别是我们引入了如material或者ant这样的前端组件库的时候。将组件细力度化,构造更多的通用组件这种工作可能更多的是在实现组件库的时候考虑的,现实的工作中我更多是类似 “搭积木”的工作,其实并没有必要去实现很多的通用组件。现在我在构建前端项目的时候通常会有一个View文件夹和一个Components文件夹,View里面通常放的是页面组件,Components里面也只有少部分是通用组件,大部分是因为本身逻辑很复杂而被抽离出来的组件。
.
├── common // Utils方法和一些静态变量
├── components // 通用组件和一些项目组件
├── main.js //入口函数
├── redux // reducers 和 actions
└── views // 页面组件
是否保留State
刚接触Redux的时候,发现有很多的观点认为应用Redux后,需要尽量的去避免State的使用,这样的目的是尽量将组件抽离成stateless的UI组件,想法其实跟上面挺相似的。在我的实际开发中感觉这样的想法可能有点极端,一些不需要共享的状态完全可以交个组件自己去管理,Redux管理太多的数据也会使得项目逻辑变的太复杂。
组件从哪获取属性
在使用Redux后,我常常比较纠结,组件的属性从哪获得?是从父组件中传递过来呢还是直接map到redux上呢?如果是都是从父组件上继承的话,那么一些封装到很深的组件就需要多次传递,这样对于后期开发和debug都会带来额外的开销。如果直接从redux的store中获取,组件和redux连接太紧密了,有时候逻辑没写对,组件渲染不符合预期但是很难定位到问题在哪。现在对于大部分逻辑比较复杂的组件我都是直接从store中获取数,一些简单的组件则是从父亲组件那里获取。这都需要根据项目做具体调整,但是在一些场景中,可能需要直接从父组件中获取,一个是可能需要做后端渲染直出的时候,另一个是组件可能需要跨平台的时候。
Redux相关优化
在使用Redux的过程中,也有很多地方并没有考虑清楚,其实发现还是很多地方可以优化。
MapDispatchToProps
在实际开发过程中,redux我使用的是react-redux这个库,里面的connect函数有两个参数,一个是MapStateToProps,另一个是MapDispatchToProps,MapDispatchToProps这个参数,这个参数的作用是将一些dispatch合并成function,然后作为Props中的属性传给组件。在很长一段时间里,我都没有使用这个参数,因为我感觉直接在组件中调用dispatch(action(payload))足够解决问题。但是当项目变的复杂,同时需要别人来维护的时候,问题就体现出来了。
比如一个很常用的例子,当一个Button点击后,我们希望改变它当前的disable状态,在使用redux的方式下我通常会这样实现功能:
// 组件监听
<Button onClick={this.handleClick} disable={this.props.currentStatus}/>
// 监听的回调
handleClick() { this.props.dispatch(CHANGESTATUS(XX))}
这样看起来并没什么问题,但是当需要dispatch多个action后事件就会变的比较复杂了。
// 组件监听
<Button
onClick={this.handleClick}
disable={this.props.currentStatus}>
{`${this.props.content}-${this.props.user}`}
</Button>
// 监听的回调
handleClick() {
this.props.dispatch(CHANGESTATUS(XX));
this.props.dispatch(CHANGECONTENT(XX));
this.props.dispatch(CHANGEUSER(XX));
}
这样的话一个点击动作会dispatch多个actions,同时这些actions都是直接对数据的修改,从代码看很难准确的确定这些actions是干嘛用的。因此当我把这样的代码交给另外的维护者的时候,他可能需要去看看这些action都做了什么事情然后确定这次点击做了什么。
这些dispatch action可能更像是对数据库的增删改查,对于熟悉后端开发来说,他们很有可能会在上面重新进行一层封装,类似于dao层上的biz业务层。对于复杂的react项目同样需要这样一层封装。MapDispatchToProps就是进行这样的工作。
// 对dispatch actions进行封装
changeButtonContent(){
this.props.dispatch(CHANGESTATUS(XX));
this.props.dispatch(CHANGECONTENT(XX));
this.props.dispatch(CHANGEUSER(XX));
}
// 组件监听
<Button
onClick={this.handleClick}
disable={this.props.currentStatus}>
{`${this.props.content}-${this.props.user}`}
</Button>
// 监听的回调
handleClick() {
this.changeButtonContent();
}
这样的话代码逻辑就要清晰很多,而且很多时候我们会在一个地方对changeButtonContent这样的函数进行一个统一的维护,而不是在各个组件中进行申明,这样配合丰富的注释,代码的逻辑将更加清晰。更加深入的话我们可以在这些统一维护的函数之上接着封装一层Controller层,事件的触发将被这个Controller handle,来决定调用哪个函数来处理事件。当然这样就变成类似于angular那种MVC框架了,是否要用还是需要根据项目的具体场景来做。
immutable.js
redux的要求reducer每次处理action都需要返回一个新的state。在开发过程中,如果没有immutable.js 的话,通常是实用Object.assign来实现,但是Object.assign无法对一些数据进行深拷贝,比如一个Object里的array,而react属性里面的数组却是传引用。因此开发的时候经常需要注意对这个array进行slice(0)操作,以获得一个不同引用的array。
immutable对js中的一些常用数据结构进行了封装,同时提供了新的api接口,每次调用这些接口都将是一个新的对象,从而避免了上面的问题。只是为了避免深拷贝带来的问题,loadsh类似的库就能实现,为什么需要使用immutable,还需要重新学习一套新的api?immutable带了更多的可能是后期对react项目的优化,配合shouldComponentUpdate可以减少很多多余的渲染。这方面没有做过相关实践,不过这篇文章写的挺好。
最后
现在前端发展越来越快了,而且更加多的向跨平台发展了,从最初的react native,以及现在的weex,还有很多需要学习的。
单身狗的十一😂,做些纪录,加油共勉。
网友评论