前阵子,我们凹凸实验室遵循React语法规范的多终端开发方案——Taro,终于对外开放了。欢迎收看星空(先播广告)。作为最早使用Taro开发的TOPLIFE小程序的开发者之一,自然走了很多弯路,打下了很多漏洞,帮助发现了很多bug。现在项目终于上线了,是时候总结分享给大家了。
TOPLIFE第一期开发的时候,实际使用的是WePY(芋头还没有开发出来),然后在第二期完全转为芋头开发。由于两个小程序开发框架已经在生产环境中使用和应用,比较它们之间的异同是很自然的。
基于组件的开发npm包支持ES6特性支持、Promise和Async Functions等CSS预编译器支持,以及在Sass/Stylus/postss等中使用Redux进行状态管理..............
开发风格实现原理WePY支持槽,Taro暂时不支持直接渲染Children
。最大的区别自然是开发风格的不同。WePY使用类似Vue的开发风格,Taro使用类似React的开发风格。可以说开发体验还是会有很大的差别。发布官方演示并简要说明。
style lang=' less ' @ color : # 4d 926 f;userinfo { color: @ color}/style template lang=' pug ' view(class=' container ')view(class=' user info ' @ tap=' tap ')mycom(: prop . sync=' my prop ' @ fn . user=' myevent ')text { { now } }/template script从' wepy '导入wepy;从导入mycom./components/mycom ';导出默认类Index扩展wepy . page { components={ mycom };data={ myprop: { }计算={ now () {返回新日期()。getTime();} };async OnLoad(){ wait sleep(3);console . log(' Hello World ');}睡眠(时间){返回新的Promise((解析,拒绝)=setTimeout(解析,时间* 1000));} }/script
从“@tarojs/taro”导入Taro,{ Component }从“@ tarojs/components”导入{ View,Button}导出默认类索引扩展组件{constructor () {super(.参数)this.state={title3360' home page ',list: [1,2,3]} } component willment(){ } component did mount(){ } component will update(nextProps,nextState){ } component diupdate(prev props,prev state){ } should component update(nextProps,nextState){ return true } add=(e)={//dosth } render(){ return(View class na
me='index'> <View className='title'>{this.state.title}</View> <View className='content'> {this.state.list.map(item => { return ( <View className='item'>{item}</View> ) })} <Button className='add' onClick={this.add}>添加</Button> </View> </View> ) }}可以见到在 WePY 里,css、template、script都放在一个wpy文件里,template还支持多种模板引擎语法,然后支持computed、watcher等属性,这些都是典型的Vue风格。
而在 Taro 里,就是彻头彻尾的 React 风格,包括constructor,componentWillMount、componentDidMount等各种 React 的生命周期函数,还有return里返回的jsx,熟悉 React 的人上手起来可以说是非常快了。
除此之外还有一些细微的差异之处:
总的来说,毕竟是两种不同的开发风格,自然还是会有许多大大小小的差异。在这里与当前很流行的小程序开发框架之一WePY进行简单对比,主要还是为了方便大家更快速地了解Taro,从而选择更适合自己的开发方式。
Taro官方提供的demo 是很简单的,主要是为了让大家快速上手,入门。那么,当我们要开发偏大型的项目时,应该如何使用Taro使得开发体验更好,开发效率更高?作为深度参与TOPLIFE小程序开发的人员之一,谈一谈我的一些实践体验及心得
使用taro-cli生成模板是这样的
├── dist 编译结果目录├── config 配置目录| ├── dev.js 开发时配置| ├── index.js 默认配置| └── prod.js 打包时配置├── src 源码目录| ├── pages 页面文件目录| | ├── index index页面目录| | | ├── index.js index页面逻辑| | | └── index.css index页面样式| ├── app.css 项目总通用样式| └── app.js 项目入口文件└── package.json
假如引入了redux,例如我们的项目,目录是这样的
├── dist 编译结果目录├── config 配置目录| ├── dev.js 开发时配置| ├── index.js 默认配置| └── prod.js 打包时配置├── src 源码目录| ├── actions redux里的actions| ├── asset 图片等静态资源| ├── components 组件文件目录| ├── constants 存放常量的地方,例如api、一些配置项| ├── reducers redux里的reducers| ├── store redux里的store| ├── utils 存放工具类函数| ├── pages 页面文件目录| | ├── index index页面目录| | | ├── index.js index页面逻辑| | | └── index.css index页面样式| ├── app.css 项目总通用样式| └── app.js 项目入口文件└── package.json
TOPLIFE小程序整个项目大概3万行代码,数十个页面,就是按上述目录的方式组织代码的。比较重要的文件夹主要是pages、components和actions。
pages里面是各个页面的入口文件,简单的页面就直接一个入口文件可以了,倘若页面比较复杂那么入口文件就会作为组件的聚合文件,redux的绑定一般也是在这里进行。
组件都放在components里面。里面的目录是这样的,假如有个coupon优惠券页面,在pages自然先有个coupon,作为页面入口,然后它的组件就会存放在components/coupon里面,就是components里面也会按照页面分模块,公共的组件可以建一个components/public文件夹,进行复用。
这样的好处是页面之间互相独立,互不影响。所以我们几个开发人员,也是按照页面的维度来进行分工,互不干扰,大大提高了我们的开发效率。
actions这个文件夹也是比较重要,这里处理的是拉取数据,数据再处理的逻辑。可以说,数据处理得好,流动清晰,整个项目就成功了一半,具体可以看下面***更好地使用redux***的部分。如上,假如是coupon页面的actions,那么就会放在actions/coupon里面,可以再一次见到,所有的模块都是以页面的维度来区分的。
除此之外,asset文件用来存放的静态资源,如一些icon类的图片,但建议不要存放太多,毕竟程序包有限制。而constants则是一些存放常量的地方,例如api域名,配置等等。
只要按照上述或类似的代码组织方式,遵循规范和约定,开发大型项目时不说能提高多少效率,至少顺手了很多。
redux大家应该都不陌生,一种状态管理的库,通常会搭配一些中间件使用。我们的项目主要是用了redux-thunk和redux-logger中间件,一个用于处理异步请求,一个用于调试,追踪actions。
相信大家都遇到过这种时候,接口返回的数据和页面显示的数据并不是完全对应的,往往需要再做一层预处理。那么这个业务逻辑应该在哪里管理,是组件内部,还是redux的流程里?
举个例子:
例如上图的购物车模块,接口返回的数据是
{code: 0,data: { shopMap: {...}, // 存放购物车里商品的店铺信息的map goods: {...}, // 购物车里的商品信息 ...}...}
对的,购车里的商品店铺和商品是放在两个对象里面的,但视图要求它们要显示在一起。这时候,如果直接将返回的数据存到store,然后在组件内部render的时候东拼西凑,将两者信息匹配,再做显示的话,会显得组件内部的逻辑十分的混乱,不够纯粹。
所以,我个人比较推荐的做法是,在接口返回数据之后,直接将其处理为与页面显示对应的数据,然后再dispatch处理后的数据,相当于做了一层拦截,像下面这样:
const data = result.data // result为接口返回的数据const cartData = handleCartData(data) // handleCartData为处理数据的函数dispatch({type: 'RECEIVE_CART', payload: cartData}) // dispatch处理过后的函数...// handleCartData处理后的数据{ commoditys: [{ shop: {...}, // 商品店铺的信息 goods: {...}, // 对应商品信息 }, ...]}
可以见到,处理数据的流程在render前被拦截处理了,将对应的商品店铺和商品放在了一个对象了.
这样做有几个好处
一个是组件的渲染更纯粹,在组件内部不用再关心如何将数据修修改改而满足视图要求,只需关心组件本身的逻辑,例如点击事件,用户交互等
二是数据的流动更可控,假如后续后台返回的数据有变动,我们要做的只是改变handleCartData函数里面的逻辑,不用改动组件内部的逻辑。
后台数据——>拦截处理——>期望的数据结构——>组件
实际上,不只是后台数据返回的时候,其它数据结构需要变动的时候都可以做一层数据拦截,拦截的时机也可以根据业务逻辑调整,重点是要让组件内部本身不关心数据与视图是否对应,只专注于内部交互的逻辑,这也很符合React本身的初衷,数据驱动视图。
connect大家都知道是用来连接store、actions和组件的,很多时候就只是根据样板代码复制一下,改改组件各自的store、actions。实际上,我们还可以做一些别的处理,例如:
export default connect(({ cart,}) => ({ couponData: cart.couponData, commoditys: cart.commoditys, editSkuData: cart.editSkuData}), (dispatch) => ({ // ...actions绑定}))(Cart)// 组件里render () {const isShowCoupon = this.props.couponData.length !== 0 return isShowCoupon && <Coupon />}
上面是很普通的一种connect写法,然后render函数根据couponData里是否数据来渲染。这时候,我们可以把this.props.couponData.length !== 0这个判断丢到connect里,达成一种computed的效果,如下:
export default connect(({ cart,}) => { const { couponData, commoditys, editSkuData } = cart const isShowCoupon = couponData.length !== 0 return { isShowCoupon, couponData, commoditys, editSkuData}}, (dispatch) => ({ // ...actions绑定}))(Cart)// 组件里render () { return this.props.isShowCoupon && <Coupon />}
可以见到,在connect里定义了isShowCoupon变量,实现了根据couponData来进行computed的效果。
实际上,这也是一种数据拦截处理。除了computed,还可以实现其它的功能,具体就由各位看官自由发挥了。
要说最大的感受,就是在开发的过程中,有时会忘记了自己在写小程序,还以为是在写React页面。是的,有次我想给页面绑定一个滚动事件,才醒悟根本就没有doucment.body.addEventListener这种东西。在使用WePY过程中,那些奇奇怪怪的语法还是时常提醒着我这是小程序,不是h5页面,而在用Taro的时候,这个差异化已经被消磨得很少了。尽管还是有一定的限制,但我基本上就是用开发React的习惯来使用Taro,可以说极大地提高了我的开发体验。
那Taro,或者是小程序开发,有没有什么要注意的地方?当然有,走过的弯路可以说是非常多了。