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
组件,并在组件上绑定 onScrolltoupper
和 onScrolltolower
来绑定滚动到顶部及底部所触发的事件, 同时 upperThreshold
和 lowerThreshold
能够调整触发时距边界的距离。
数据请求 api 使用 Taro.request
, 跟wx.request
使用方法基本一致,不同的是 Taro.request
天然支持 promise
化,mock 数据使用 easy-mock 来提供 。
点击跳转使用 Taro.navigateTo
,跟 wx.navigateTo
也基本一致,在写跳转的时候,一开始想用匿名函数的形式写,发现 Taro
目前还不支持,据说就要支持了,Taro
的迭代速度还是很快的。
将首页进行页面重构的时候,遇到最费时间的问题,应该是wxss
转化为scss
,Taro
的组件转化为微信小程序和 web 后,标签是不一样的,比如Image
组件会变成image
或img
标签,然而原来的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.children
;props
不能传递jsx
;
在抽象组件的时候,主要有以下注意点
- 在写法上,
Taro
组件首字母要大写并采用驼峰命名法,比如在wxml
里面的标签是view、scroll-view、image
,在Taro
要写成View、ScrollView、Image
。Taro
的事件绑定事件绑定都以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 切换功能
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
组件会变成image
或img
标签,所以不建议使用标签名命名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 端的支持还有待完善,但方向是没有错的,接下来只是时间问题。
本文来源: 掘金 如需转载请联系原作者