Taro 开发指南
简介
Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/QQ 小程序/H5/React Native 等应用。本文将介绍 Taro 的核心概念、开发流程和最佳实践。
项目创建
1. 环境准备
bash
# 安装 Taro CLI
npm install -g @tarojs/cli
# 创建项目
taro init my-app2. 项目结构
bash
my-app/
├── config/ # 项目编译配置目录
├── src/ # 源码目录
│ ├── pages/ # 页面文件目录
│ ├── components/ # 组件文件目录
│ ├── assets/ # 静态资源目录
│ ├── app.config.js # 全局配置
│ ├── app.js # 项目入口文件
│ └── app.scss # 项目入口样式
├── package.json
└── README.md页面开发
1. 页面配置
javascript
// app.config.js
export default {
pages: [
'pages/index/index',
'pages/user/user'
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black'
},
tabBar: {
color: '#999',
selectedColor: '#333',
backgroundColor: '#fff',
list: [
{
pagePath: 'pages/index/index',
text: '首页',
iconPath: './assets/tabbar/home.png',
selectedIconPath: './assets/tabbar/home-active.png'
},
{
pagePath: 'pages/user/user',
text: '我的',
iconPath: './assets/tabbar/user.png',
selectedIconPath: './assets/tabbar/user-active.png'
}
]
}
}2. 页面开发
jsx
// pages/index/index.jsx
import { Component } from 'react'
import { View, Text, Image } from '@tarojs/components'
import { AtList, AtListItem } from 'taro-ui'
import './index.scss'
export default class Index extends Component {
state = {
list: [],
loading: false
}
componentDidMount() {
this.loadData()
}
async loadData() {
try {
this.setState({ loading: true })
const res = await Taro.request({
url: 'https://api.example.com/list',
method: 'GET'
})
this.setState({ list: res.data })
} catch (error) {
Taro.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.setState({ loading: false })
}
}
render() {
const { list, loading } = this.state
return (
<View className='index'>
<View className='header'>
<Text className='title'>首页</Text>
</View>
{loading ? (
<View className='loading'>加载中...</View>
) : (
<AtList>
{list.map(item => (
<AtListItem
key={item.id}
title={item.title}
note={item.description}
arrow='right'
onClick={() => this.handleItemClick(item)}
/>
))}
</AtList>
)}
</View>
)
}
handleItemClick(item) {
Taro.navigateTo({
url: `/pages/detail/detail?id=${item.id}`
})
}
}组件开发
1. 自定义组件
jsx
// components/custom-card/index.jsx
import { Component } from 'react'
import { View, Text, Image } from '@tarojs/components'
import './index.scss'
export default class CustomCard extends Component {
static defaultProps = {
image: '',
title: '',
description: '',
onClick: () => {}
}
render() {
const { image, title, description, onClick } = this.props
return (
<View className='custom-card' onClick={onClick}>
<Image className='image' src={image} mode='aspectFill' />
<View className='content'>
<Text className='title'>{title}</Text>
<Text className='description'>{description}</Text>
</View>
</View>
)
}
}2. 组件样式
scss
// components/custom-card/index.scss
.custom-card {
background: #fff;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.image {
width: 100%;
height: 200px;
}
.content {
padding: 16px;
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.description {
font-size: 14px;
color: #666;
}
}
}3. 组件使用
jsx
// pages/index/index.jsx
import { Component } from 'react'
import { View } from '@tarojs/components'
import CustomCard from '../../components/custom-card'
import './index.scss'
export default class Index extends Component {
state = {
list: []
}
render() {
const { list } = this.state
return (
<View className='index'>
{list.map(item => (
<CustomCard
key={item.id}
image={item.image}
title={item.title}
description={item.description}
onClick={() => this.handleCardClick(item)}
/>
))}
</View>
)
}
handleCardClick(item) {
Taro.navigateTo({
url: `/pages/detail/detail?id=${item.id}`
})
}
}状态管理
1. Redux 配置
javascript
// store/index.js
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from './reducers'
const store = createStore(
rootReducer,
applyMiddleware(thunkMiddleware)
)
export default store
// store/reducers/index.js
import { combineReducers } from 'redux'
import user from './user'
import cart from './cart'
export default combineReducers({
user,
cart
})
// store/reducers/user.js
const INITIAL_STATE = {
userInfo: null,
isLoggedIn: false
}
export default function user(state = INITIAL_STATE, action) {
switch (action.type) {
case 'SET_USER_INFO':
return {
...state,
userInfo: action.payload,
isLoggedIn: true
}
case 'CLEAR_USER_INFO':
return {
...state,
userInfo: null,
isLoggedIn: false
}
default:
return state
}
}2. 使用 Redux
jsx
// pages/user/user.jsx
import { Component } from 'react'
import { View, Button } from '@tarojs/components'
import { connect } from 'react-redux'
import { getUserInfo, clearUserInfo } from '../../store/actions/user'
import './user.scss'
@connect(
({ user }) => ({
userInfo: user.userInfo,
isLoggedIn: user.isLoggedIn
}),
dispatch => ({
getUserInfo: () => dispatch(getUserInfo()),
clearUserInfo: () => dispatch(clearUserInfo())
})
)
export default class User extends Component {
handleLogin = async () => {
try {
const { userInfo } = await Taro.getUserProfile({
desc: '用于完善用户资料'
})
this.props.getUserInfo(userInfo)
} catch (error) {
Taro.showToast({
title: '登录失败',
icon: 'none'
})
}
}
handleLogout = () => {
this.props.clearUserInfo()
}
render() {
const { userInfo, isLoggedIn } = this.props
return (
<View className='user'>
{isLoggedIn ? (
<View>
<View className='user-info'>
<Image className='avatar' src={userInfo.avatarUrl} />
<Text className='nickname'>{userInfo.nickName}</Text>
</View>
<Button onClick={this.handleLogout}>退出登录</Button>
</View>
) : (
<Button onClick={this.handleLogin}>登录</Button>
)}
</View>
)
}
}网络请求
1. 请求封装
javascript
// utils/request.js
const baseURL = 'https://api.example.com'
export const request = (options) => {
return new Promise((resolve, reject) => {
Taro.request({
url: baseURL + options.url,
method: options.method || 'GET',
data: options.data,
header: {
'Content-Type': 'application/json',
...options.header
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(res)
}
},
fail: (err) => {
reject(err)
}
})
})
}
// api/index.js
import { request } from '../utils/request'
export const api = {
// 获取列表
getList(params) {
return request({
url: '/list',
method: 'GET',
data: params
})
},
// 获取详情
getDetail(id) {
return request({
url: `/detail/${id}`,
method: 'GET'
})
},
// 提交表单
submitForm(data) {
return request({
url: '/submit',
method: 'POST',
data
})
}
}2. 使用请求
jsx
// pages/detail/detail.jsx
import { Component } from 'react'
import { View, Text, Image } from '@tarojs/components'
import { api } from '../../api'
import './detail.scss'
export default class Detail extends Component {
state = {
loading: true,
error: null,
detail: null
}
componentDidMount() {
const { id } = this.$router.params
this.loadDetail(id)
}
async loadDetail(id) {
try {
this.setState({ loading: true })
const res = await api.getDetail(id)
this.setState({ detail: res.data })
} catch (error) {
this.setState({ error: '加载失败' })
} finally {
this.setState({ loading: false })
}
}
render() {
const { loading, error, detail } = this.state
if (loading) {
return <View className='loading'>加载中...</View>
}
if (error) {
return <View className='error'>{error}</View>
}
return (
<View className='detail'>
<Image className='image' src={detail.image} mode='aspectFill' />
<View className='content'>
<Text className='title'>{detail.title}</Text>
<Text className='price'>¥{detail.price}</Text>
<Text className='description'>{detail.description}</Text>
</View>
</View>
)
}
}条件编译
jsx
// 使用条件编译
import { Component } from 'react'
import { View, Text } from '@tarojs/components'
export default class Platform extends Component {
render() {
return (
<View>
{/* 微信小程序 */}
{process.env.TARO_ENV === 'weapp' && (
<View>微信小程序特有内容</View>
)}
{/* H5 */}
{process.env.TARO_ENV === 'h5' && (
<View>H5 特有内容</View>
)}
{/* React Native */}
{process.env.TARO_ENV === 'rn' && (
<View>React Native 特有内容</View>
)}
</View>
)
}
}性能优化
1. 图片优化
jsx
// 使用图片组件
import { Image } from '@tarojs/components'
// 使用 webp 格式
<Image src='/assets/image.webp' mode='aspectFill' />
// 使用懒加载
<Image lazyLoad src='/assets/image.jpg' mode='aspectFill' />
// 使用图片预加载
componentDidMount() {
const images = [
'/assets/image1.jpg',
'/assets/image2.jpg',
'/assets/image3.jpg'
]
images.forEach(src => {
Taro.getImageInfo({
src,
success: () => {
console.log('图片预加载成功:', src)
}
})
})
}2. 列表优化
jsx
// 使用虚拟列表
import { VirtualList } from '@tarojs/components'
export default class List extends Component {
state = {
list: []
}
render() {
const { list } = this.state
return (
<VirtualList
height={800}
width='100%'
itemData={list}
itemCount={list.length}
itemSize={100}
>
{({ index, data }) => (
<View className='list-item'>
<Text>{data[index].title}</Text>
</View>
)}
</VirtualList>
)
}
}
// 使用分页加载
export default class PaginationList extends Component {
state = {
list: [],
page: 1,
pageSize: 10,
hasMore: true
}
async loadMore() {
if (!this.state.hasMore) return
try {
const { page, pageSize } = this.state
const res = await api.getList({ page, pageSize })
this.setState({
list: [...this.state.list, ...res.data],
page: page + 1,
hasMore: res.data.length === pageSize
})
} catch (error) {
Taro.showToast({
title: '加载失败',
icon: 'none'
})
}
}
render() {
const { list, hasMore } = this.state
return (
<View>
<View className='list'>
{list.map(item => (
<View key={item.id} className='list-item'>
<Text>{item.title}</Text>
</View>
))}
</View>
{hasMore && (
<View className='load-more' onClick={this.loadMore}>
加载更多
</View>
)}
</View>
)
}
}最佳实践
1. 项目结构
bash
src/
├── api/ # API 接口
├── components/ # 公共组件
├── pages/ # 页面
├── assets/ # 静态资源
├── store/ # Redux 状态管理
├── utils/ # 工具函数
├── app.config.js # 全局配置
├── app.js # 入口文件
└── app.scss # 全局样式2. 代码规范
javascript
// 使用 ESLint 配置
// .eslintrc.js
module.exports = {
root: true,
extends: [
'taro/react'
],
rules: {
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off'
}
}
// 使用 Prettier 配置
// .prettierrc
{
"semi": false,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "none"
}工具链
1. 开发工具
json
{
"devDependencies": {
"@tarojs/cli": "^3.0.0",
"@tarojs/components": "^3.0.0",
"@tarojs/taro": "^3.0.0",
"@tarojs/runtime": "^3.0.0",
"@tarojs/plugin-framework-react": "^3.0.0",
"@tarojs/plugin-platform-weapp": "^3.0.0",
"@tarojs/plugin-platform-h5": "^3.0.0",
"@tarojs/plugin-platform-rn": "^3.0.0",
"react": "^17.0.0",
"react-dom": "^17.0.0"
}
}2. 发布工具
json
{
"scripts": {
"build:weapp": "taro build --type weapp",
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"dev:weapp": "npm run build:weapp -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch"
}
}总结
Taro 提供了跨平台开发的能力,通过一套代码可以同时开发多个平台的应用。通过合理使用框架特性和遵循最佳实践,可以构建出高性能、可维护的跨平台应用。