Taro实践 - TOPLIFE小程序 开发体验

来自团队支持 TOPLIFE小程序 业务的小伙伴,关于 Taro 的一篇使用感觉,但愿对你们有所帮助。javascript

前言

前阵子,来自咱们凹凸实验室的遵循 React 语法规范的多端开发方案 - Taro 终于对外开源了,欢迎围观star(先打波广告)。做为第一批使用了 Taro 开发的TOPLIFE小程序的开发人员之一,天然是走了很多弯路,躺了很多坑,也帮忙找过很多bug。如今项目总算是上线了,那么,也是时候给你们总结分享下了。css

与WePY比较

当初开发TOPLIFE第一期的时候,用的实际上是WePY(那时Taro尚未开发完成),而后在第二期才全面转换为用 Taro 开发。做为两个小程序开发框架都使用过,并应用在生产环境里的人,天然是要比较一下二者的异同点。html

相同点

  • 组件化开发
  • npm包支持
  • ES6+特性支持,PromiseAsync Functions
  • CSS预编译器支持,Sass/Stylus/PostCSS等
  • 支持使用Redux进行状态管理
  • …..

相同的地方也不用多说什么,都2018年了,这些特性的支持都是为了让小程序开发变得更现代,更工程化,重点是区别之处。java

不一样点

  • 开发风格
  • 实现原理
  • WePY支持slot,Taro暂不支持直接渲染children

开发风格react

最大的不一样之处,天然就是开发风格上的差别,WePY使用的是类Vue开发风格, Taro 使用的是类 React 开发风格,能够说开发体验上仍是会有较大的区别。贴一下官方的demo简单阐述下。git

WePY demogithub

<style lang="less">
    @color: #4D926F;
    .userinfo {
        color: @color;
    }
</style>
<template lang="pug">
    view(class='container')
        view(class='userinfo' @tap='tap')
            mycom(:prop.sync='myprop' @fn.user='myevent')
            text {{now}}
</template>

<script>
    import wepy from 'wepy';
    import mycom from '../components/mycom';

    export default class Index extends wepy.page {
        
        components = { mycom };
        data = {
            myprop: {}
        };
        computed = {
            now () { return new Date().getTime(); }
        };
        async onLoad() {
            await sleep(3);
            console.log('Hello World');
        }
        sleep(time) {
            return new Promise((resolve, reject) => setTimeout(resolve, time * 1000));
        }
    }
</script>
复制代码

Taro demonpm

import Taro, { Component } from '@tarojs/taro'
import { View, Button } from '@tarojs/components'

export default class Index extends Component {
  constructor () {
    super(...arguments)
    this.state = {
      title: '首页',
      list: [1, 2, 3]
    }
  }

  componentWillMount () {}

  componentDidMount () {}

  componentWillUpdate (nextProps, nextState) {}

  componentDidUpdate (prevProps, prevState) {}

  shouldComponentUpdate (nextProps, nextState) {
    return true
  }

  add = (e) => {
    // dosth
  }

  render () {
    return (
      <View className='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 里,csstemplatescript都放在一个wpy文件里,template还支持多种模板引擎语法,而后支持computedwatcher等属性,这些都是典型的Vue风格。json

而在 Taro 里,就是彻头彻尾的 React 风格,包括constructorcomponentWillMountcomponentDidMount等各类 React 的生命周期函数,还有return里返回的jsx,熟悉 React 的人上手起来能够说是很是快了。redux

除此以外还有一些细微的差别之处:

  • WePY 里的模板,或者说是wxml,用的都是小程序里原生的组件,就是小程序文档里的各类组件;而Taro里使用的每一个组件,都须要从@tarojs/components里引入,包括ViewText等基础组件(这种作实际上是为了转换多端作准备)
  • 事件处理上
    • Taro 中,是用click事件代替tap事件
    • WePY使用的是简写的写法@+事件;而Taro则是on+事件名称
    • 阻止冒泡上WePY用的是@+事件.stop;而Taro则是要显式地使用e.stopPropagation()来阻止冒泡
    • 事件传参WePY能够直接在函数后面传参,如@tap="click({{index}})";而Taro则是使用bind传参,如onClick={this.handleClick.bind(null, params)}
  • WePY使用的是小程序原生的生命周期,而且组件有pagecomponent的区分;Taro 则是本身实现了相似React 的生命周期,并且没有pagecomponent的区分,都是component

总的来讲,毕竟是两种不一样的开发风格,天然仍是会有许多大大小小的差别。在这里与当前很流行的小程序开发框架之一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万行代码,数十个页面,就是按上述目录的方式组织代码的。比较重要的文件夹主要是pagescomponentsactions

  • pages里面是各个页面的入口文件,简单的页面就直接一个入口文件能够了,假若页面比较复杂那么入口文件就会做为组件的聚合文件,redux的绑定通常也是在这里进行。

  • 组件都放在components里面。里面的目录是这样的,假若有个coupon优惠券页面,在pages天然先有个coupon,做为页面入口,而后它的组件就会存放在components/coupon里面,就是components里面也会按照页面分模块,公共的组件能够建一个components/public文件夹,进行复用。

    这样的好处是页面之间互相独立互不影响。因此咱们几个开发人员,也是按照页面的维度来进行分工,互不干扰,大大提升了咱们的开发效率。

  • actions这个文件夹也是比较重要,这里处理的是拉取数据,数据再处理的逻辑。能够说,数据处理得好,流动清晰,整个项目就成功了一半,具体能够看下面***更好地使用redux***的部分。如上,假如是coupon页面的actions,那么就会放在actions/coupon里面,能够再一次见到,全部的模块都是以页面的维度来区分的。

除此以外,asset文件用来存放的静态资源,如一些icon类的图片,但建议不要存放太多,毕竟程序包有限制。而constants则是一些存放常量的地方,例如api域名,配置等等。

只要按照上述或相似的代码组织方式,遵循规范和约定,开发大型项目时不说能提升多少效率,至少顺手了不少。

更好地使用redux

redux你们应该都不陌生,一种状态管理的库,一般会搭配一些中间件使用。咱们的项目主要是用了redux-thunkredux-logger中间件,一个用于处理异步请求,一个用于调试,追踪actions

数据预处理

相信你们都遇到过这种时候,接口返回的数据和页面显示的数据并非彻底对应的,每每须要再作一层预处理。那么这个业务逻辑应该在哪里管理,是组件内部,仍是redux的流程里?

举个例子:

mage-20180612151609

例如上图的购物车模块,接口返回的数据是

{
	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能够作更多的事情

connect你们都知道是用来链接storeactions和组件的,不少时候就只是根据样板代码复制一下,改改组件各自的storeactions。实际上,咱们还能够作一些别的处理,例如:

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,或者是小程序开发,有没有什么要注意的地方?固然有,走过的弯路能够说是很是多了。

页面栈只有10层
  • 估计是每一个页面的数据在小程序内部都有缓存,因此作了10层的限制。带来的问题就是假如页面存在循环跳转,即A页面能够跳到B页面,B页面也能够跳到A页面,而后用户从A进入了B,想返回A的时候,每每是直接在B页面里点击跳转到A,而不是点返回回到A,如此一来,10层很快就突破了。因此咱们本身对navigateTo函数作了一层封装,防止溢出。
页面内容有缓存
  • 上面说到,页面内容有缓存。因此假如某个页面是根据不一样的数据渲染视图,新渲染时会有上一次渲染的缓存,致使页面看起来有个闪烁的变化,用户体验很是很差。其实解决的办法也很简单,每次在componentWillUnmount生命周期中清理一下当前页面的数据就行了。小程序说到底不是h5,不会说每次进入页面就会刷新,也不会离开就销毁,刷新,清理数据的动做都须要本身再生命周期函数里主动触发。
不能随时地监听页面滚动事件
  • 页面的滚动事件只能经过onPageScroll来监听,因此当我想在组件里进监听操做时,要将该部分的逻辑提早到onPageScroll函数,提升了抽象成本。例如我须要开发一个滚动到某个位置就吸顶的tab,原本能够在tab内部处理的逻辑被提早了,减小了其可复用性。
Taro开发须要注意的地方
  • 原本也想详细描述下的,不过在咱们几位大佬的努力,加班加点下,已经开发出eslint插件,及补充完整了 Taro 文档。你们只要遵循eslint插件规范,查看文档,应该不会有太大问题,有问题欢迎提issue

总结

总的来讲,用 Taro 来开发小程序体验仍是很不错的,最重要的是,可使用jsx写小程序了!!!做为React粉的一员,能够说是至关的兴奋了~

最后,欢迎关注 github.com/nervjs/taro

相关文章
相关标签/搜索