🔷 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 打包 |
| 🟡 鸿蒙 | ⚠️ 适配中 | ArkTS | DevEco 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 天 |
| H5 | npm run build:h5 | 无需审核 | 即时 |
💉 8. 常见踩坑速查
| 错误 | 后果 | 修复 |
|---|---|---|
直接用 <div> 标签 | 小程序端编译报错 | 统一用 Taro 组件:View / Text / Image |
使用 window / document | 小程序没有 DOM | 用 Taro.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 的承诺。