57

新的小程序开发框架?- Taro的深度实践体验

 5 years ago
source link: https://segmentfault.com/a/1190000015432413?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

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

与wepy比较

当初开发TOPLIFE第一期的时候,用的其实是 wepy (那时Taro还没有开发完成),然后在第二期才全面转换为用 Taro 开发。作为两个小程序开发框架都使用过,并应用在生产环境里的人,自然是要比较一下两者的异同点。

相同点

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

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

不同点

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

开发风格

最大的不同之处,自然就是开发风格上的差异, wepy 使用的是类Vue开发风格, Taro 使用的是类React开发风格,可以说开发体验上还是会有较大的区别。贴一下官方的demo简单阐述下

wepy demo

<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 demo

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 都放在一个wepy文件里, template 还支持多种模板引擎语法,然后支持 computedwatcher 等属性,这些都是典型的vue风格

而在taro里,就是彻头彻尾的react风格,包括 constructorcomponentWillMountcomponentDidMount 等各种react的生命周期函数,还有 return 里返回的 jsx ,熟悉react的人上手起来可以说是非常快了

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

  • 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

比较常见的一种项目目录组织方式,相比初始模板多了几个文件夹,用于存放redux相关的内容及其他的一些东西,整个项目结构相信还是比较直观,简单明了的

更好地使用redux

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

数据预处理

相信大家都遇到过这种时候,接口返回的数据和页面显示的数据并不是完全对应的,往往需要再做一层预处理。那么这个业务逻辑应该在哪里管理,是组件内部,还是 redux 的流程里?

举个例子:

7Bf6Nnn.png!web

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

{
    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 ,还可以实现其它的功能,具体就由各位看官自由发挥了

一些需要注意的地方

那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粉的一员,可以说是相当的兴奋了~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK