Skip to content

Taro 开发指南

简介

Taro 是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/QQ 小程序/H5/React Native 等应用。本文将介绍 Taro 的核心概念、开发流程和最佳实践。

项目创建

1. 环境准备

bash
# 安装 Taro CLI
npm install -g @tarojs/cli

# 创建项目
taro init my-app

2. 项目结构

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 提供了跨平台开发的能力,通过一套代码可以同时开发多个平台的应用。通过合理使用框架特性和遵循最佳实践,可以构建出高性能、可维护的跨平台应用。

参考资源