05-Taro4.x多端小程序开发实战

3 阅读5分钟

🔷 Taro 4.x 多端小程序开发实战:一套代码征服微信/支付宝/抖音

微信小程序、支付宝小程序、抖音小程序、H5……每个平台写一遍? Taro 让你一套 React/Vue 代码编译到所有主流小程序平台 + Web。 本文以 Taro 4.x + React + TypeScript 为主线,从项目搭建到多端发布全流程。

核心价值:前端工程师零学习成本(React/Vue 原生写法),一次开发覆盖 6+ 个平台, 节省 70% 的多端维护成本。


📊 Taro 能力总览

目标平台支持状态编译产物发布方式
🟢 微信小程序✅ 生产级WXML + WXSS + JS微信开发者工具上传
🟢 支付宝小程序✅ 生产级AXML + ACSS + JS支付宝开发者工具
🟢 抖音小程序✅ 生产级TTML + TTSS + JS抖音开发者工具
🟢 百度小程序✅ 可用SWAN + CSS + JS百度开发者工具
🟢 H5 / Web✅ 生产级HTML + CSS + JS常规 Web 部署
🟡 React Native⚠️ 实验性RN 组件App 打包
🟡 鸿蒙⚠️ 适配中ArkTSDevEco Studio

🛠 1. 项目搭建:5 分钟启动

创建项目

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

# 创建项目(选择 React + TypeScript + SCSS)
taro init my-mini-app
# 选项:
# - React
# - TypeScript
# - SCSS
# - 默认模板

cd my-mini-app
npm install

项目结构

my-mini-app/
├── config/                  # 构建配置
│   ├── index.ts            # 通用配置
│   ├── dev.ts              # 开发环境
│   └── prod.ts             # 生产环境
├── src/
│   ├── app.ts              # 应用入口
│   ├── app.config.ts       # 全局配置(页面路由、TabBar)
│   ├── app.scss            # 全局样式
│   ├── pages/              # 页面目录
│   │   └── index/
│   │       ├── index.tsx   # 页面组件
│   │       ├── index.config.ts  # 页面配置
│   │       └── index.module.scss
│   ├── components/         # 公共组件
│   ├── stores/             # 状态管理
│   ├── api/                # 接口层
│   ├── utils/              # 工具函数
│   └── styles/             # 全局样式 / Token
├── package.json
└── tsconfig.json

开发与编译命令

# 开发模式(实时编译 + 监听)
npm run dev:weapp      # 微信小程序
npm run dev:alipay     # 支付宝小程序
npm run dev:tt         # 抖音小程序
npm run dev:h5         # H5 浏览器

# 生产构建
npm run build:weapp
npm run build:alipay
npm run build:tt
npm run build:h5

📐 2. 页面开发:React 写法,小程序能力

全局配置

// src/app.config.ts
export default defineAppConfig({
  pages: [
    'pages/index/index',
    'pages/category/index',
    'pages/cart/index',
    'pages/profile/index',
  ],
  window: {
    backgroundTextStyle: 'dark',
    navigationBarBackgroundColor: '#0F172A',
    navigationBarTitleText: 'MyApp',
    navigationBarTextStyle: 'white',
  },
  tabBar: {
    color: '#94A3B8',
    selectedColor: '#6366F1',
    backgroundColor: '#1E293B',
    list: [
      { pagePath: 'pages/index/index', text: '首页', iconPath: 'assets/home.png', selectedIconPath: 'assets/home-active.png' },
      { pagePath: 'pages/category/index', text: '分类', iconPath: 'assets/category.png', selectedIconPath: 'assets/category-active.png' },
      { pagePath: 'pages/cart/index', text: '购物车', iconPath: 'assets/cart.png', selectedIconPath: 'assets/cart-active.png' },
      { pagePath: 'pages/profile/index', text: '我的', iconPath: 'assets/profile.png', selectedIconPath: 'assets/profile-active.png' },
    ],
  },
})

首页开发

// src/pages/index/index.tsx
import { View, Text, Image, ScrollView } from '@tarojs/components'
import { useLoad, useShareAppMessage } from '@tarojs/taro'
import { useBannerStore } from '@/stores/banner'
import { useProductStore } from '@/stores/product'
import { SearchBar } from '@/components/SearchBar'
import { BannerSwiper } from '@/components/BannerSwiper'
import { ProductCard } from '@/components/ProductCard'
import styles from './index.module.scss'

export default function IndexPage() {
  const { banners, fetchBanners } = useBannerStore()
  const { products, loading, fetchProducts, loadMore } = useProductStore()

  // 页面加载生命周期
  useLoad(() => {
    fetchBanners()
    fetchProducts()
  })

  // 分享配置
  useShareAppMessage(() => ({
    title: '发现好物,尽在 MyApp',
    path: '/pages/index/index',
  }))

  return (
    <View className={styles.container}>
      {/* 搜索栏 */}
      <SearchBar placeholder="搜索你想要的商品" />

      <ScrollView
        scrollY
        className={styles.scrollContent}
        onScrollToLower={loadMore}
      >
        {/* Banner 轮播 */}
        <BannerSwiper banners={banners} />

        {/* 商品网格 */}
        <View className={styles.sectionTitle}>
          <Text className={styles.titleText}>🔥 热门推荐</Text>
        </View>

        <View className={styles.productGrid}>
          {products.map((product) => (
            <ProductCard key={product.id} product={product} />
          ))}
        </View>

        {loading && (
          <View className={styles.loadingTip}>
            <Text>加载中...</Text>
          </View>
        )}
      </ScrollView>
    </View>
  )
}
// src/pages/index/index.module.scss
@import '@/styles/tokens.scss';

.container {
  min-height: 100vh;
  background-color: $color-bg-primary;
}

.scrollContent {
  height: calc(100vh - 88rpx);
}

.sectionTitle {
  padding: $spacing-md $spacing-md $spacing-sm;
}

.titleText {
  font-size: $font-size-lg;
  color: $color-text-primary;
  font-weight: 700;
}

.productGrid {
  display: flex;
  flex-wrap: wrap;
  padding: 0 $spacing-sm;
  gap: $spacing-sm;
}

.loadingTip {
  text-align: center;
  padding: $spacing-lg;
  color: $color-text-secondary;
  font-size: $font-size-sm;
}

🧠 3. 状态管理:Zustand 轻量方案

npm install zustand
// src/stores/product.ts
import { create } from 'zustand'
import { getProducts, type Product } from '@/api/product'

interface ProductState {
  products: Product[]
  loading: boolean
  page: number
  hasMore: boolean
  fetchProducts: () => Promise<void>
  loadMore: () => Promise<void>
  refresh: () => Promise<void>
}

export const useProductStore = create<ProductState>((set, get) => ({
  products: [],
  loading: false,
  page: 1,
  hasMore: true,

  fetchProducts: async () => {
    set({ loading: true })
    try {
      const { list, total } = await getProducts({ page: 1, pageSize: 20 })
      set({ products: list, page: 1, hasMore: list.length < total })
    } finally {
      set({ loading: false })
    }
  },

  loadMore: async () => {
    const { loading, hasMore, page, products } = get()
    if (loading || !hasMore) return

    set({ loading: true })
    try {
      const nextPage = page + 1
      const { list, total } = await getProducts({ page: nextPage, pageSize: 20 })
      const allProducts = [...products, ...list]
      set({
        products: allProducts,
        page: nextPage,
        hasMore: allProducts.length < total,
      })
    } finally {
      set({ loading: false })
    }
  },

  refresh: async () => {
    set({ products: [], page: 1, hasMore: true })
    await get().fetchProducts()
  },
}))

📡 4. 网络请求封装

// src/utils/request.ts
import Taro from '@tarojs/taro'

interface RequestConfig {
  url: string
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: any
  header?: Record<string, string>
}

interface ApiResponse<T = any> {
  code: number
  data: T
  message: string
}

const BASE_URL = process.env.TARO_APP_API_URL || 'https://api.example.com'

export async function request<T>(config: RequestConfig): Promise<T> {
  const token = Taro.getStorageSync('token')

  try {
    const response = await Taro.request({
      url: `${BASE_URL}${config.url}`,
      method: config.method || 'GET',
      data: config.data,
      header: {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
        ...config.header,
      },
    })

    const result = response.data as ApiResponse<T>

    if (result.code !== 0) {
      // Token 过期
      if (result.code === 401) {
        Taro.removeStorageSync('token')
        Taro.redirectTo({ url: '/pages/login/index' })
      }
      throw new Error(result.message || '请求失败')
    }

    return result.data
  } catch (error) {
    Taro.showToast({ title: '网络异常,请重试', icon: 'none' })
    throw error
  }
}

// src/api/product.ts
import { request } from '@/utils/request'

export interface Product {
  id: string
  name: string
  price: number
  image: string
  desc: string
}

interface ProductListResponse {
  list: Product[]
  total: number
}

export function getProducts(params: { page: number; pageSize: number }) {
  return request<ProductListResponse>({
    url: '/api/products',
    data: params,
  })
}

export function getProductDetail(id: string) {
  return request<Product>({
    url: `/api/products/${id}`,
  })
}

🔀 5. 多端差异处理:条件编译

环境判断

// 运行时判断平台
import Taro from '@tarojs/taro'

const env = Taro.getEnv()
// WEAPP | ALIPAY | TT | SWAN | WEB | RN

if (env === Taro.ENV_TYPE.WEAPP) {
  // 微信小程序特有逻辑
} else if (env === Taro.ENV_TYPE.WEB) {
  // H5 特有逻辑
}

编译时条件编译(推荐)

// 使用 process.env.TARO_ENV 编译时剔除无关代码

// 微信小程序特有的分享
{/* #ifdef weapp */}
<Button openType="share">分享给朋友</Button>
{/* #endif */}

// H5 特有的
{/* #ifdef h5 */}
<Button onClick={copyLink}>复制链接</Button>
{/* #endif */}

多端样式差异

// src/styles/platform.scss

// 微信小程序安全区域适配
/* #ifdef weapp */
.safe-bottom {
  padding-bottom: env(safe-area-inset-bottom);
}
/* #endif */

// H5 适配
/* #ifdef h5 */
.safe-bottom {
  padding-bottom: 16px;
}
/* #endif */

多端 API 差异封装

// src/utils/platform.ts

// 支付封装 — 各平台调用不同
export async function pay(orderInfo: OrderInfo) {
  const env = Taro.getEnv()

  switch (env) {
    case Taro.ENV_TYPE.WEAPP:
      return Taro.requestPayment({
        timeStamp: orderInfo.timeStamp,
        nonceStr: orderInfo.nonceStr,
        package: orderInfo.package,
        signType: orderInfo.signType,
        paySign: orderInfo.paySign,
      })

    case Taro.ENV_TYPE.ALIPAY:
      return Taro.tradePay({ tradeNO: orderInfo.tradeNo })

    case Taro.ENV_TYPE.WEB:
      window.location.href = orderInfo.payUrl
      break

    default:
      throw new Error(`不支持的支付平台: ${env}`)
  }
}

🧩 6. 组件开发:NutUI 搭配自定义组件

安装 NutUI

npm install @nutui/nutui-react-taro
// config/index.ts — 按需引入配置
export default {
  plugins: ['@tarojs/plugin-html'],
  designWidth: 750,
  deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    828: 1.81 / 2,
  },
}

自定义商品卡片组件

// src/components/ProductCard/index.tsx
import { View, Text, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { Price } from '@nutui/nutui-react-taro'
import type { Product } from '@/api/product'
import styles from './index.module.scss'

interface Props {
  product: Product
}

export function ProductCard({ product }: Props) {
  const handleClick = () => {
    Taro.navigateTo({
      url: `/pages/product-detail/index?id=${product.id}`,
    })
  }

  return (
    <View className={styles.card} onClick={handleClick}>
      <Image
        className={styles.image}
        src={product.image}
        mode="aspectFill"
        lazyLoad
      />
      <View className={styles.info}>
        <Text className={styles.name} numberOfLines={2}>
          {product.name}
        </Text>
        <View className={styles.priceRow}>
          <Price price={product.price} size="normal" thousands />
        </View>
      </View>
    </View>
  )
}
// src/components/ProductCard/index.module.scss
@import '@/styles/tokens.scss';

.card {
  width: calc(50% - #{$spacing-xs});
  background-color: $color-bg-secondary;
  border-radius: $radius-lg;
  overflow: hidden;
}

.image {
  width: 100%;
  height: 340rpx;
}

.info {
  padding: $spacing-sm;
}

.name {
  font-size: $font-size-sm;
  color: $color-text-primary;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 72rpx;
}

.priceRow {
  margin-top: $spacing-xs;
}

📦 7. 发布上线:多端提交流程

微信小程序发布

# 1. 构建生产版本
npm run build:weapp

# 2. 自动上传(CI/CD 推荐)
npm install -g miniprogram-ci

# upload.js
const ci = require('miniprogram-ci')
const project = new ci.Project({
  appid: 'your-appid',
  type: 'miniProgram',
  projectPath: './dist/weapp',
  privateKeyPath: './private.key',
})

ci.upload({
  project,
  version: '1.0.0',
  desc: '首次发布',
})

多端发布 Checklist

平台构建命令开发者工具审核周期
微信小程序npm run build:weapp微信开发者工具1-7 天
支付宝小程序npm run build:alipay支付宝小程序开发者工具1-3 天
抖音小程序npm run build:tt抖音开发者工具1-3 天
H5npm run build:h5无需审核即时

💉 8. 常见踩坑速查

错误后果修复
直接用 <div> 标签小程序端编译报错统一用 Taro 组件:View / Text / Image
使用 window / document小程序没有 DOMTaro.getSystemInfoSync() 等 API 替代
CSS 用 px 单位各设备显示不一致统一用 rpx(750 设计稿)或配置 postcss-pxtransform
忘记配置分包主包超过 2MB 限制subPackages 拆分非首页页面
H5 和小程序路由 API 不同跳转失败统一用 Taro.navigateTo / Taro.redirectTo
直接引入 npm 包的 CSS小程序不支持部分 CSS 特性使用 NutUI 等 Taro 适配的组件库

✅ Taro 开发 Checklist

项目启动

  • 安装 Taro CLI 并创建项目
  • 配置全局样式 Token(SCSS 变量)
  • 配置 app.config.ts 页面路由和 TabBar
  • 封装 request.ts 网络请求

核心开发

  • 使用 Zustand 搭建状态管理
  • 用 Taro 组件(View/Text/Image)替代 HTML 标签
  • 处理多端差异(条件编译 / 平台 API 封装)
  • 集成 NutUI 组件库

发布前

  • 检查主包体积(< 2MB)
  • 配置分包加载
  • 各平台开发者工具预览测试
  • 配置分享卡片和页面元信息

小程序的本质是"受限的 Web" — 没有 DOM、没有 Window、包体有限制。 Taro 的价值不是消除这些限制,而是让你用熟悉的 React/Vue 写法优雅地适应它们会写 React,就会写小程序 — 这就是 Taro 的承诺。