Taro实践 - 快速开发【知乎】多端应用

avatar
全栈开发工程师 @京东

来自团队小伙伴 - 阿集,关于 Taro 上手实践的一篇文章,希望对大家的使用有所帮助。

1. Taro 简介

Taro 是由凹凸实验室打造的一套遵循 React 语法规范的多端统一开发框架。

使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信小程序、H5、App 端等)运行的代码。目前 Taro 支持编译出支持微信小程序、H5 运行的代码,RN、快应用及支付宝小程序的支持还在开发中。

具体介绍请看文章 《多端统一开发框架 - Taro 》,以及GitHub仓库 https://github.com/NervJS/taro

2. 前言

为了学习 Taro,本人在 github 找了知乎的小程序demo,本文通过修改该代码,实现了 Taro 版的知乎H5、小程序 demo ,对 Taro 有兴趣的同学可以 star 或 fork 下来学习,GitHub地址

3. 安装

安装 Taro 开发工具 @tarojs/cli

使用 npm 或者 yarn 全局安装

npm install -g @tarojs/cli
// 或
yarn global add @tarojs/cli

下载代码

git clone https://github.com/jimczj/taro_zhihu
# 安装依赖
cd taro_zhihu
npm i

4. 运行代码

文件目录如下:

├── dist                   编译结果目录
├── config                 配置目录
|   ├── dev.js             开发时配置
|   ├── index.js           默认配置
|   └── prod.js            打包时配置
├── src                    源码目录
|   ├── pages              页面文件目录
|   |   ├── index          index页面目录
|   |   |   ├── index.js   index页面逻辑
|   |   |   └── index.css  index页面样式
|   ├── app.css            项目总通用样式
|   └── app.js             项目入口文件
└── package.json

进入项目目录开始开发,可以选择小程序预览模式,或者h5预览模式,若使用微信小程序预览模式,则需要自行下载并打开微信开发者工具,选择预览项目根目录。

微信小程序编译预览模式:

# npm script
npm run dev:weapp
# 或 仅限全局安装
taro build --type weapp --watch

H5编译预览模式:

# npm script
npm run dev:h5
# 或 仅限全局安装
taro build --type h5 --watch

5. 开发前注意

若使用 微信小程序预览模式 ,则需下载并使用 微信开发者工具 添加项目进行预览,此时需要注意微信开发者工具的项目设置

  • 需要设置关闭ES6转ES5功能,开启可能报错
  • 需要设置关闭上传代码时样式自动补全,开启可能报错
  • 需要设置关闭代码压缩上传,开启可能报错

项目设置

6. 功能实现详解

6.1 小程序全局配置

app.json文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。从原来的 app.json 转化为 Taro 项目的 app.js 几乎没有什么成本,配置基本一致。只是有一点需要注意,静态图片资源文件夹要放在src目录下面,这样代码在编译打包的时候,才会把图片资源给复制打包过去。我一开始将静态图片资源文件夹跟src同层级,然后各种找不到图片资源,浪费了许多时间。好了,话不多说,看看下面代码对比,你就清楚这迁移无比轻松,转写成 React 写法,看起来也顺眼多了。

原微信小程序 app.json (有删减)代码如下:

{
  'pages':[
    'pages/index/index',
    'pages/discovery/discovery',
    'pages/more/more',
    'pages/answer/answer',
    'pages/question/question'
  ],
  'window':{
    'backgroundTextStyle':'light',
    'navigationBarBackgroundColor': '#0068C4',
    'navigationBarTitleText': '知乎',
    'navigationBarTextStyle':'white',
    'enablePullDownRefresh':true
  },
  'tabBar': {
    'color': '#626567',
    'selectedColor': '#2A8CE5',
    'backgroundColor': '#FBFBFB',
    'borderStyle': 'white',
    'list': [{
      'pagePath': 'pages/index/index',
      'text': '首页',
      'iconPath': 'images/index.png',
      'selectedIconPath': 'images/index_focus.png'
    }, {
      'pagePath': 'pages/discovery/discovery',
      'text': '发现',
      'iconPath': 'images/discovery.png',
      'selectedIconPath': 'images/discovery_focus.png'
    },  {
      'pagePath': 'pages/more/more',
      'text': '我的',
      'iconPath': 'images/burger.png',
      'selectedIconPath': 'images/burger_focus.png'
    }]
  }
}

转写成Taro 代码如下:

import Taro, { Component } from '@tarojs/taro'
import Index from './pages/index'

import './app.scss'

class App extends Component {
  config = {
    pages: [
      'pages/index/index',
      'pages/discovery/discovery',
      'pages/more/more',
      'pages/answer/answer',
      'pages/question/question'
    ],
    window: {
      backgroundTextStyle: 'light',
      navigationBarBackgroundColor: '#0068C4',
      navigationBarTitleText: 'Taro知乎',
      navigationBarTextStyle: 'white',
      enablePullDownRefresh: true
    },
    tabBar: {
      color: '#626567',
      selectedColor: '#2A8CE5',
      backgroundColor: '#FBFBFB',
      borderStyle: 'white',
      list: [{
        pagePath: 'pages/index/index',
        text: '首页',
        iconPath: './asset/images/index.png',
        selectedIconPath: './asset/images/index_focus.png'
      },{
        pagePath: 'pages/discovery/discovery',
        text: '发现',
        iconPath: './asset/images/discovery.png',
        selectedIconPath: './asset/images/discovery_focus.png'
      }, 
      {
        pagePath: 'pages/more/more',
        text: '我的',
        iconPath: './asset/images/burger.png',
        selectedIconPath: './asset/images/burger_focus.png'
      }]
    }
  }
  render () {
    return (
      <Index />
    )
  }
}

Taro.render(<App />, document.getElementById('app'))

6.2 首页

页面效果如下:

功能描述:上滑刷新,下拉加载更多,数据请求,点击跳转

刷新及继续加载的动作, 依靠的是ScrollView组件,并在组件上绑定 onScrolltoupperonScrolltolower 来绑定滚动到顶部及底部所触发的事件, 同时 upperThresholdlowerThreshold 能够调整触发时距边界的距离。

数据请求 api 使用 Taro.request, 跟wx.request 使用方法基本一致,不同的是 Taro.request 天然支持 promise 化,mock 数据使用 easy-mock 来提供 。

点击跳转使用 Taro.navigateTo,跟 wx.navigateTo 也基本一致,在写跳转的时候,一开始想用匿名函数的形式写,发现 Taro 目前还不支持,据说就要支持了,Taro 的迭代速度还是很快的。

将首页进行页面重构的时候,遇到最费时间的问题,应该是wxss转化为scssTaro 的组件转化为微信小程序和 web 后,标签是不一样的,比如Image组件会变成imageimg标签,然而原来的wxss里面使用了标签名来命名 css,这样一来就造成了微信小程序跟 web 样式表现不一致。所以不建议使用标签名命名 css,建议直接用 class。

export default class Index extends Component {
  config = {
    navigationBarTitleText: '首页'
  }
  constructor() {
    super(...arguments)
    this.state = {
      loading:true,
      list:[]
    }
  }
  componentDidMount () { 
    // 获取远程数据
    this.updateList()
  }
  updateList() {
    Taro.showLoading({title: '加载中'})
    Taro.request({
      url: 'https://easy-mock.com/mock/5b21d97f6b88957fa8a502f2/example/feed'
    }).then(res => {
      Taro.hideLoading()
      if (res.data.success) {
        this.setState({
          loading:false,
          list:res.data.data
        })
      }
    })
  }
  appendNextPageList() {
    Taro.showLoading({title: '加载中'})
    Taro.request({
      url: 'https://easy-mock.com/mock/5b21d97f6b88957fa8a502f2/example/feed'
    }).then(res => {
      Taro.hideLoading()
      if (res.data.success) {
        this.setState({
          list: this.state.list.concat(res.data.data)
        })
      }
    })
  }
  render () {
    return (<ScrollView className='container'
        scrollY
        scrollWithAnimation
        scrollTop='0'
        lowerThreshold='10'
        upperThreshold='10'
        onScrolltoupper={this.updateList}
        onScrolltolower={this.appendNextPageList}
        >
        <View className='search flex-wrp'>
          <View className='search-left flex-item'>
              <View className='flex-wrp'>
                <View className='flex1'><Image src={searchPng}></Image></View>
                <View className='flex6'><Input type='text' placeholder='搜索话题, 问题或人' placeholderClass='search-placeholder'/></View>
              </View>
          </View>
          <View className='search-right flex-item'>
              <Image src={lightingPng}></Image>
          </View>
        </View>
        {
          this.state.loading 
          ? <View className='txcenter'><Text>加载中</Text></View>
          : this.state.list.map((item,index)=>{
          return <Feed key={item} />})
        }
      </ScrollView>
    )
  }
}

6.3 Taro 组件

其实我一直搞不懂微信小程序的 Component 组件跟Page 页面的生命周期函数要搞的不一样,页面的生命周期方法有 onLoad、onReady、onUnload 等,而到了组件中则是 created、attached 、ready 等,相比之下 Taro 就比较统一了,不管是页面还是组件,写法都跟 React 的生命周期一致,统一的api开发起来也顺畅了许多。

然而 Taro 组件目前还是有很多局限,比如,不支持直接渲染 children, 即不支持 this.props.childrenprops 不能传递jsx

在抽象组件的时候,主要有以下注意点

  • 在写法上,Taro 组件首字母要大写并采用驼峰命名法,比如在 wxml里面的标签是view、scroll-view、image,在 Taro 要写成View、ScrollView、ImageTaro 的事件绑定事件绑定都以 on 开头并采用驼峰命名法
// 小程序代码
<scroll-view scroll-y='true' class='container' bindscrolltoupper='upper' upper-threshold='10' lower-threshold='5' bindscrolltolower='lower'>
</scroll-view>

// Taro 代码
<ScrollView className='container'
        scrollY
        scrollWithAnimation
        scrollTop='0'
        lowerThreshold='10'
        upperThreshold='10'
        onScrolltoupper={this.upper.bind(this)}
        onScrolltolower={this.lower.bind(this)}
        >
</ScrollView>
  • 小程序引用本地静态资源直接在 src 写上相对路径,Taro 引用本地静态资源需要先 import 进来再使用,为了让 h5 部署的时候图片路径不出错,最好把图片放在服务器上,然后直接写 http 路径
// 小程序 引用本地静态资源
<image src='../../images/search.png'></image>

// Taro 引用本地静态资源
import searchPng from '../../asset/images/search.png'
// ...此处省略无数代码
<Image src={searchPng}></Image>

// 最好把图片放在服务器上,然后写http 路径
<Image src='https://image.ibb.co/kUissy/search.png'></Image> 
  • 遍历列表的区别,小程序使用模版语言,而 Taro 使用 jsx
// 小程序
<block wx:for='{{feed}}' wx:for-index='idx' wx:for-item='item' data-idx='{{idx}}'>
    <view class='feed-item'>
        ...
    </view>
</block>

// Taro 代码
{
    this.state.list.map((item,index)=>{
       return <Feed {...item} key={index} />
    })
}

答案组件在抽象过程中,想直接写成纯函数形式

// 暂不支持这种写法
const Text = ({ answer }) => 
  <p>{answer}</p>

发现目前还不支持,于是老老实实写成class的形式了:

export default class Feed extends Component {
  navigateTo(url) {
    Taro.navigateTo({url:url})
  }
  render() {
    return (
      <View className='feed-item'>
        <View className='feed-source'>
          <View className='avatar flex1'>
              <Image src={this.props.feed_source_img}></Image>
          </View>
          <View className='flex8'>
            <Text className='feed-source-txt'>{this.props.feed_source_name}{this.props.feed_source_txt}</Text>
          </View>
          <View className='flex1'>
            <Image className='item-more' mode='aspectFit' src={more}></Image>
          </View>
        </View>
        <View className='feed-content'>
            <View className='question' onClick={this.navigateTo.bind(this,'/pages/question/question')}>
                <View className='question-link'>
                    <Text>{this.props.question}</Text>
                </View>
            </View>
            <View className='answer-body'>
                <View>
                    <Text className='answer-txt' onClick={this.navigateTo.bind(this,'/pages/answer/answer')} >{this.props.answer_ctnt}</Text>
                </View>
                <View className='answer-actions'>
                    <View className='like dot'>
                        <View>{this.props.good_num} 赞同 </View>
                    </View>
                    <View className='comments dot'>
                        <View>{this.props.comment_num} 评论 </View>
                    </View>
                    <View className='follow-it'>
                        <View>关注问题</View>
                    </View>
                </View>
            </View>
        </View>
      </View>
    )
  }
}

在使用的组件的时候,想使用{...item}的方式来解构赋值,发现也暂时不支持,内心有点奔溃。

不过所幸的是,Taro的错误提醒很人性化,没有让我在这里浪费很多时间调试,于是我就老老实实一个一个赋值啦。

this.state.list.map((item,index) => {
    return <Feed 
        feed_source_img={item.feed_source_img}
        feed_source_txt={item.feed_source_txt}
        question={item.question}
        answer_ctnt={item.answer_ctnt} 
        good_num={item.good_num} 
        comment_num={item.comment_num} 
        key={index} />
})

6.4 “发现页面”的 tab 切换功能

tab切换原理: 在组件上绑定 onClick 事件 改变 this.state.currentNavtab 值,再通过判断来实现 tab 切换,函数参数传递方式为 this.switchTab.bind(this,index),具体代码如下:

export default class Discovery extends Component {
    constructor() {
    super(...arguments)
    this.state = {
        currentNavtab: 0,
        navTab: ['推荐', '圆桌', '热门', '收藏'],
    }
  }
  switchTab(index,e) {
    this.setState({
      currentNavtab: index
    });
  }
  render () {
    return (
      <View>
        <View className='top-tab flex-wrp flex-tab' >
        {
          this.state.navTab.map((item,index) => {
            return (<View className={this.state.currentNavtab === index ? 'toptab flex-item active' : 'toptab flex-item' } key={index} onClick={this.switchTab.bind(this,index)}>
              {item}
            </View>)
          })
        }
        </View>
        <ScrollView scroll-y className='container discovery withtab'>
          <View className='ctnt0' hidden={this.state.currentNavtab==0 ? false : true}>
              ...
          </View>
            <View className='txcenter' hidden={this.state.currentNavtab==1 ? false : true}>
              <Text>圆桌</Text>
            </View>
            <View className='txcenter' hidden={this.state.currentNavtab==2 ? false : true}>
              <Text>热门</Text>
            </View>
            <View className='txcenter' hidden={this.state.currentNavtab==3 ? false : true}>
              <Text>收藏</Text>
            </View>
        </ScrollView>
      </View> 
    )
  }
}

6.5 轮播功能

轮播页使用了 Swiper 组件,参数跟小程序都是一一对应,具体可以查看详细文档,在重构过程也主要是把 wxml 换成 jsx 的形式。

<Swiper className='activity' indicatorDots='true'
    autoplay='true' interval='5000' duration='500'>
    {this.state.imgUrls.map((item,index) => {
        return (<SwiperItem key={index}>
            <Image src={item} className='slide-image' width='355' height='375' />
        </SwiperItem>)
    })}
</Swiper>

7. 使用中发现的问题总结

  • Taro 的组件转化为微信小程序跟web后,标签是不一样的,比如Image组件会变成imageimg标签,所以不建议使用标签名命名css,建议直接用 class
  • 目前的事件绑定不支持匿名函数
// 不支持
<View onClick={()=> this.navigateTo('/pages/answer/answer')} >  </View>
  • Taro 组件在 web 端有许多属性和事件暂不支持
  • 运行环境不同,某些 api 需要根据环境不同处理,比如 wx.getUserInfo 在 web 端是不存在的,此时我们需要判断环境来执行代码
if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP) {
    // 小程序环境
} else if (Taro.getEnv() === Taro.ENV_TYPE.WEB ) {
    // WEB(H5)环境
}
  • Taro 目前不支持 SVG(毕竟小程序不支持) 目前的第三方库还比较少(毕竟 Taro 刚出来不久,希望接下来社区能出来各种 ui 库)
  • Taro 组件暂时不支持纯函数,不支持解构赋值 {...item}

8. 总结

将该项目迁移到 Taro 成本并不高,绝大多数工作是做语法的变换。Taro采用 React 的写法写微信小程序,总体体验是非常不错的,有 React 开发经验的小伙伴相信能很快上手。同一份代码就能同时支持web端跟小程序端,确实很让人惊艳,虽然目前 web 端的支持还有待完善,但方向是没有错的,接下来只是时间问题。期待 Taro 在接下来的表现。最后对该项目有兴趣的同学可以下载代码下来跑一跑 GitHub地址