多端开发之Taro

概述

Taro 是京东凹凸实验室开源的一套遵循 React 语法规范的多端统一开发解决方案。它支持用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/QQ 小程序/H5/React Native 等应用,实现了真正的一套代码多端运行。

相关资料

官方文档

  • Taro 文档 - Taro官方文档,包含完整的API参考和开发指南

案例与资源

技术原理

框架支持

插件生态

核心问题解析

Taro 如何实现多端适配?

Taro 通过编译时和运行时的双重机制实现多端适配:

编译时适配机制:

小程序
H5
RN
源代码
AST解析
语法转换
目标平台
小程序语法
H5语法
RN语法
小程序代码
H5代码
RN代码

关键实现原理:

  1. AST语法树转换 - 将React/Vue语法转换为目标平台语法
  2. 组件映射 - 统一组件API映射到平台特定组件
  3. API适配 - 统一API调用转换为平台特定API
  4. 样式处理 - CSS样式转换为平台支持的样式格式

Taro的多端开发架构

Taro架构由以下主要组成部分:

目标平台
运行时层
编译层
开发层
微信小程序
支付宝小程序
H5页面
React Native App
小程序运行时
H5运行时
RN运行时
Babel转换器
Webpack构建
平台适配器
React/Vue代码
Taro组件库
Taro API

各组成部分作用:

  • 开发层:提供统一的开发接口和组件库
  • 编译层:负责语法转换和平台适配
  • 运行时层:提供平台特定的运行时支持
  • 目标平台:最终的应用运行环境

Taro 项目初始化

Taro 4.0.7 安装

环境要求

在开始之前,请确保开发环境满足以下要求:

  • Node.js 版本:>= 16.0.0
  • npm 版本:>= 8.0.0
  • Python 版本:>= 3.7.0(用于编译native模块)
安装 Taro CLI

Taro CLI是Taro开发的核心工具,负责项目初始化、编译构建等功能。

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

// 或使用 yarn
yarn global add @tarojs/cli@latest

// 验证安装
taro --version

项目搭建

创建新项目

使用Taro CLI创建新项目:

// 创建项目
taro init myApp

// 进入项目目录
cd myApp

// 安装依赖
npm install

项目模板选择

初始化时可选择不同的项目模板:

// 选择项目模板的交互流程
? 请选择模板
  default(默认模板)
  mobx(支持 mobx 的模板)
  redux(支持 redux 的模板)
  wxplugin(微信小程序插件开发模板)
  wxcloud(微信小程序云开发模板)
启动项目

根据目标平台启动开发服务器:

网页端开发

// 启动H5开发服务器
npm run dev:h5

// 访问地址:http://localhost:10086

小程序开发

// 微信小程序
npm run dev:weapp

// 支付宝小程序
npm run dev:alipay

// 百度小程序
npm run dev:swan

// 字节跳动小程序
npm run dev:tt

基础内容

项目结构

典型的Taro项目结构如下:

myApp/
├── dist/                   // 编译结果目录
├── config/                 // 项目配置目录
│   ├── dev.js             // 开发环境配置
│   ├── prod.js            // 生产环境配置
│   └── index.js           // 通用配置
├── src/                   // 源码目录
│   ├── pages/             // 页面文件目录
│   │   └── index/         // 首页目录
│   │       ├── index.jsx  // 页面组件
│   │       ├── index.scss // 页面样式
│   │       └── index.config.js // 页面配置
│   ├── components/        // 组件目录
│   ├── utils/            // 工具函数目录
│   ├── app.jsx           // 应用入口组件
│   ├── app.scss          // 全局样式文件
│   └── app.config.js     // 应用配置文件
├── package.json          // 项目包管理文件
└── project.config.json   // 小程序项目配置
重要文件说明

app.config.js - 应用配置文件

export default {
  pages: [
    'pages/index/index',
    'pages/user/index'
  ],
  window: {
    backgroundTextStyle: 'light',
    navigationBarBackgroundColor: '#fff',
    navigationBarTitleText: 'Taro应用',
    navigationBarTextStyle: 'black'
  },
  tabBar: {
    list: [{
      pagePath: 'pages/index/index',
      text: '首页',
      iconPath: './images/tab1.png',
      selectedIconPath: './images/tab1-active.png'
    }]
  }
}

页面配置文件示例

// pages/index/index.config.js
export default {
  navigationBarTitleText: '首页',
  enablePullDownRefresh: true,
  backgroundTextStyle: 'dark'
}

Taro 常用组件与 API

Taro 常用组件

View 容器组件

View是最基础的容器组件,类似于HTML中的div:

import { View } from '@tarojs/components'

function MyComponent() {
  return (
    <View className='container'>
      <View className='content'>
        这是一个基础容器
      </View>
    </View>
  )
}
Text 文本组件

Text组件用于显示文本内容:

import { Text } from '@tarojs/components'

function TextExample() {
  return (
    <View>
      <Text className='title'>标题文本</Text>
      <Text selectable>可选择的文本内容</Text>
      <Text decode>&lt;p&gt;支持实体解码&lt;/p&gt;</Text>
    </View>
  )
}
Image 图片组件

Image组件用于显示图片:

import { Image } from '@tarojs/components'

function ImageExample() {
  const handleImageLoad = (e) => {
    console.log('图片加载完成:', e.detail)
  }

  return (
    <View>
      <Image
        src='https://example.com/image.jpg'
        mode='aspectFit'
        lazyLoad
        onLoad={handleImageLoad}
        className='image'
      />
    </View>
  )
}
Button 按钮组件

Button组件提供各种交互按钮功能:

import { Button } from '@tarojs/components'

function ButtonExample() {
  const handleClick = () => {
    console.log('按钮被点击')
  }

  return (
    <View>
      <Button
        type='primary'
        size='normal'
        onClick={handleClick}
      >
        主要按钮
      </Button>

      <Button
        type='default'
        plain
        openType='share'
      >
        分享按钮
      </Button>
    </View>
  )
}
Input 输入组件

Input组件用于用户输入:

import { Input } from '@tarojs/components'
import { useState } from 'react'

function InputExample() {
  const [value, setValue] = useState('')

  const handleInput = (e) => {
    setValue(e.detail.value)
  }

  return (
    <View>
      <Input
        type='text'
        placeholder='请输入内容'
        value={value}
        onInput={handleInput}
        maxlength={50}
        className='input'
      />
      <Input
        type='password'
        placeholder='请输入密码'
        password
      />
    </View>
  )
}
Form 表单组件

Form组件用于数据收集和提交:

import { Form, Button, Input, Switch } from '@tarojs/components'

function FormExample() {
  const handleSubmit = (e) => {
    console.log('表单数据:', e.detail.value)
  }

  const handleReset = () => {
    console.log('表单重置')
  }

  return (
    <Form onSubmit={handleSubmit} onReset={handleReset}>
      <View className='form-item'>
        <Input name='username' placeholder='用户名' />
      </View>

      <View className='form-item'>
        <Input name='password' placeholder='密码' password />
      </View>

      <View className='form-item'>
        <Switch name='remember' />
        <Text>记住我</Text>
      </View>

      <Button formType='submit' type='primary'>提交</Button>
      <Button formType='reset' type='default'>重置</Button>
    </Form>
  )
}

Taro API

网络请求

Taro提供了统一的网络请求API:

import Taro from '@tarojs/taro'

// GET 请求
async function fetchData() {
  try {
    const response = await Taro.request({
      url: 'https://api.example.com/data',
      method: 'GET',
      data: {
        page: 1,
        limit: 10
      },
      header: {
        'Content-Type': 'application/json'
      }
    })
    console.log('请求成功:', response.data)
    return response.data
  } catch (error) {
    console.error('请求失败:', error)
    throw error
  }
}

// POST 请求
async function postData(formData) {
  return Taro.request({
    url: 'https://api.example.com/submit',
    method: 'POST',
    data: formData,
    header: {
      'Content-Type': 'application/json'
    }
  })
}
Toast 提示

显示消息提示框:

import Taro from '@tarojs/taro'

// 成功提示
function showSuccess() {
  Taro.showToast({
    title: '操作成功',
    icon: 'success',
    duration: 2000
  })
}

// 加载提示
function showLoading() {
  Taro.showLoading({
    title: '加载中...',
    mask: true
  })

  // 3秒后隐藏
  setTimeout(() => {
    Taro.hideLoading()
  }, 3000)
}

// 错误提示
function showError() {
  Taro.showToast({
    title: '操作失败',
    icon: 'error',
    duration: 2000
  })
}
获取设备信息

获取系统信息和设备信息:

import Taro from '@tarojs/taro'

async function getDeviceInfo() {
  try {
    const systemInfo = await Taro.getSystemInfo()
    console.log('系统信息:', {
      platform: systemInfo.platform,
      system: systemInfo.system,
      version: systemInfo.version,
      screenWidth: systemInfo.screenWidth,
      screenHeight: systemInfo.screenHeight,
      windowWidth: systemInfo.windowWidth,
      windowHeight: systemInfo.windowHeight
    })
    return systemInfo
  } catch (error) {
    console.error('获取系统信息失败:', error)
  }
}

// 获取网络状态
async function getNetworkInfo() {
  const networkType = await Taro.getNetworkType()
  console.log('网络类型:', networkType.networkType)
  return networkType
}
路由跳转

Taro提供了多种页面跳转方式:

import Taro from '@tarojs/taro'

// 保留当前页面,跳转到应用内的某个页面
function navigateTo(url) {
  Taro.navigateTo({
    url: `/pages/detail/index?id=123`
  })
}

// 关闭当前页面,跳转到应用内的某个页面
function redirectTo() {
  Taro.redirectTo({
    url: '/pages/home/index'
  })
}

// 跳转到 tabBar 页面
function switchTab() {
  Taro.switchTab({
    url: '/pages/index/index'
  })
}

// 返回上一页面
function navigateBack() {
  Taro.navigateBack({
    delta: 1 // 返回的页面数
  })
}
存储

本地数据存储和获取:

import Taro from '@tarojs/taro'

// 同步存储
function setStorageSync() {
  try {
    Taro.setStorageSync('userInfo', {
      id: 123,
      name: 'John',
      email: 'john@example.com'
    })
    console.log('数据存储成功')
  } catch (error) {
    console.error('存储失败:', error)
  }
}

// 异步存储
async function setStorage() {
  try {
    await Taro.setStorage({
      key: 'token',
      data: 'abc123'
    })
    console.log('Token存储成功')
  } catch (error) {
    console.error('Token存储失败:', error)
  }
}

// 获取存储数据
function getStorageSync() {
  try {
    const userInfo = Taro.getStorageSync('userInfo')
    if (userInfo) {
      console.log('用户信息:', userInfo)
      return userInfo
    }
  } catch (error) {
    console.error('获取数据失败:', error)
  }
}

// 清除存储
function clearStorage() {
  Taro.clearStorageSync()
  console.log('存储已清空')
}

Taro 多端开发方案详解

内置环境变量

process.env.TARO_ENV

Taro在编译时会注入环境变量,用于识别当前的编译平台:

// 根据平台执行不同逻辑
function getPlatformSpecificConfig() {
  switch (process.env.TARO_ENV) {
    case 'weapp':
      // 微信小程序特有逻辑
      return {
        appId: 'wx123456',
        apiBase: 'https://api.weapp.com'
      }
    case 'alipay':
      // 支付宝小程序特有逻辑
      return {
        appId: 'alipay123',
        apiBase: 'https://api.alipay.com'
      }
    case 'h5':
      // H5特有逻辑
      return {
        apiBase: 'https://api.h5.com'
      }
    case 'rn':
      // React Native特有逻辑
      return {
        apiBase: 'https://api.rn.com'
      }
    default:
      return {
        apiBase: 'https://api.default.com'
      }
  }
}

// 平台判断函数
export const isWeapp = process.env.TARO_ENV === 'weapp'
export const isH5 = process.env.TARO_ENV === 'h5'
export const isRN = process.env.TARO_ENV === 'rn'

引用不同资源

可以根据不同平台引用不同的资源文件:

// 根据平台引用不同的图片资源
function getImageResource() {
  if (process.env.TARO_ENV === 'weapp') {
    return require('../images/logo.weapp.png')
  } else if (process.env.TARO_ENV === 'h5') {
    return require('../images/logo.h5.png')
  } else {
    return require('../images/logo.default.png')
  }
}

// 在组件中使用
function LogoComponent() {
  const logoSrc = getImageResource()

  return (
    <Image
      src={logoSrc}
      className='logo'
      mode='aspectFit'
    />
  )
}

加载不同组件

Taro支持根据编译平台自动选择对应的组件文件:

文件命名规则

  • component.js - 默认组件
  • component.weapp.js - 微信小程序专用
  • component.alipay.js - 支付宝小程序专用
  • component.h5.js - H5专用
  • component.rn.js - React Native专用
// components/CustomButton/index.js (默认实现)
import { Button } from '@tarojs/components'

export default function CustomButton(props) {
  return (
    <Button {...props}>
      {props.children}
    </Button>
  )
}
// components/CustomButton/index.h5.js (H5专用实现)
import { Button } from '@tarojs/components'

export default function CustomButton(props) {
  return (
    <Button
      {...props}
      className={`${props.className} h5-button`}
      onClick={(e) => {
        // H5特有的点击处理
        console.log('H5 button clicked')
        props.onClick && props.onClick(e)
      }}
    >
      {props.children}
    </Button>
  )
}
// 在页面中使用 - Taro会自动选择对应平台的组件
import CustomButton from '../../components/CustomButton'

function PageComponent() {
  return (
    <View>
      <CustomButton onClick={() => console.log('clicked')}>
        点击我
      </CustomButton>
    </View>
  )
}

组件文件中的跨平台支持

Vue文件支持

对于Vue项目,同样支持多端文件:

<!-- components/UserCard/index.vue (默认实现) -->
<template>
  <view class="user-card">
    <image :src="avatar" class="avatar" />
    <text class="name">{{ name }}</text>
  </view>
</template>

<script>
export default {
  name: 'UserCard',
  props: {
    avatar: String,
    name: String
  }
}
</script>

<style lang="scss">
.user-card {
  display: flex;
  align-items: center;

  .avatar {
    width: 60px;
    height: 60px;
    border-radius: 30px;
  }

  .name {
    margin-left: 20px;
    font-size: 16px;
  }
}
</style>
<!-- components/UserCard/index.h5.vue (H5专用实现) -->
<template>
  <div class="user-card h5-card" @click="handleClick">
    <img :src="avatar" class="avatar" alt="用户头像" />
    <span class="name">{{ name }}</span>
  </div>
</template>

<script>
export default {
  name: 'UserCard',
  props: {
    avatar: String,
    name: String
  },
  methods: {
    handleClick() {
      // H5专有的点击事件处理
      this.$emit('cardClick', { name: this.name })
    }
  }
}
</script>

<style lang="scss">
.user-card {
  cursor: pointer;
  transition: all 0.3s ease;

  &:hover {
    transform: scale(1.05);
  }

  &.h5-card {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    padding: 10px;
    border-radius: 8px;
  }
}
</style>

指定平台保留/剔除样式

使用条件编译注释来控制特定平台的样式:

// styles/common.scss
.container {
  padding: 20px;

  /* #ifdef H5 */
  max-width: 1200px;
  margin: 0 auto;
  /* #endif */

  /* #ifdef MP-WEAPP */
  padding-top: 44px; // 微信小程序状态栏高度
  /* #endif */

  /* #ifndef H5 */
  // 非H5平台的样式
  background: #f5f5f5;
  /* #endif */
}

// 按钮样式的跨平台适配
.custom-button {
  border: none;
  border-radius: 4px;
  padding: 12px 24px;

  /* #ifdef H5 */
  cursor: pointer;
  transition: all 0.3s ease;

  &:hover {
    opacity: 0.8;
  }
  /* #endif */

  /* #ifdef MP */
  // 所有小程序平台
  border-radius: 8px;
  /* #endif */
}

统一接口的多端文件

创建统一接口,内部根据平台调用不同实现:

// utils/storage/index.js (统一接口)
export default {
  setItem: (key, value) => {
    // 这里会根据编译平台自动选择对应的实现
    return setItem(key, value)
  },

  getItem: (key) => {
    return getItem(key)
  },

  removeItem: (key) => {
    return removeItem(key)
  },

  clear: () => {
    return clear()
  }
}
// utils/storage/index.h5.js (H5实现)
export function setItem(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value))
    return Promise.resolve()
  } catch (error) {
    return Promise.reject(error)
  }
}

export function getItem(key) {
  try {
    const value = localStorage.getItem(key)
    return Promise.resolve(value ? JSON.parse(value) : null)
  } catch (error) {
    return Promise.reject(error)
  }
}

export function removeItem(key) {
  try {
    localStorage.removeItem(key)
    return Promise.resolve()
  } catch (error) {
    return Promise.reject(error)
  }
}

export function clear() {
  try {
    localStorage.clear()
    return Promise.resolve()
  } catch (error) {
    return Promise.reject(error)
  }
}
// utils/storage/index.weapp.js (微信小程序实现)
import Taro from '@tarojs/taro'

export function setItem(key, value) {
  return Taro.setStorage({
    key,
    data: value
  })
}

export function getItem(key) {
  return Taro.getStorage({ key })
    .then(res => res.data)
    .catch(() => null)
}

export function removeItem(key) {
  return Taro.removeStorage({ key })
}

export function clear() {
  return Taro.clearStorage()
}

统一接口文件的命名规则

Taro的多端文件命名遵循以下规则:

基础规则

  • filename.js - 通用文件,所有平台默认使用
  • filename.平台.js - 平台专用文件,优先级高于通用文件

平台标识

  • .h5 - H5 Web端
  • .weapp - 微信小程序
  • .alipay - 支付宝小程序
  • .swan - 百度小程序
  • .tt - 字节跳动小程序
  • .qq - QQ小程序
  • .rn - React Native

优先级顺序

导入文件
是否存在平台专用文件?
使用平台专用文件
使用通用文件
加载完成

使用要点

  1. 按需创建 - 只为有差异的平台创建专用文件
  2. 接口统一 - 保持所有平台文件的导出接口一致
  3. 回退机制 - 确保通用文件能在所有平台正常工作

使用场景

  • API差异处理 - 不同平台的API调用方式不同
  • UI适配 - 不同平台的交互或展示需求不同
  • 功能特性 - 利用平台特有功能增强用户体验
  • 性能优化 - 针对平台特性进行性能优化

多端组件开发

创建一个支持多端的用户头像组件:

// components/Avatar/index.js (通用实现)
import { View, Image } from '@tarojs/components'
import { useState } from 'react'

export default function Avatar({
  src,
  size = 60,
  defaultSrc = '/images/default-avatar.png',
  onClick
}) {
  const [imageSrc, setImageSrc] = useState(src || defaultSrc)

  const handleError = () => {
    setImageSrc(defaultSrc)
  }

  return (
    <View
      className='avatar-container'
      onClick={onClick}
      style={{ width: `${size}px`, height: `${size}px` }}
    >
      <Image
        src={imageSrc}
        className='avatar-image'
        mode='aspectFill'
        onError={handleError}
        style={{
          width: `${size}px`,
          height: `${size}px`,
          borderRadius: `${size/2}px`
        }}
      />
    </View>
  )
}
// components/Avatar/index.h5.js (H5专用实现,支持更多交互)
import { View, Image } from '@tarojs/components'
import { useState } from 'react'

export default function Avatar({
  src,
  size = 60,
  defaultSrc = '/images/default-avatar.png',
  onClick,
  showTooltip = false,
  tooltipText = '用户头像'
}) {
  const [imageSrc, setImageSrc] = useState(src || defaultSrc)
  const [showTooltipState, setShowTooltip] = useState(false)

  const handleError = () => {
    setImageSrc(defaultSrc)
  }

  const handleMouseEnter = () => {
    if (showTooltip) {
      setShowTooltip(true)
    }
  }

  const handleMouseLeave = () => {
    setShowTooltip(false)
  }

  return (
    <View
      className='avatar-container h5-avatar'
      onClick={onClick}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      style={{
        width: `${size}px`,
        height: `${size}px`,
        position: 'relative'
      }}
    >
      <Image
        src={imageSrc}
        className='avatar-image'
        mode='aspectFill'
        onError={handleError}
        style={{
          width: `${size}px`,
          height: `${size}px`,
          borderRadius: `${size/2}px`,
          cursor: onClick ? 'pointer' : 'default',
          transition: 'all 0.3s ease'
        }}
      />

      {showTooltip && showTooltipState && (
        <View className='avatar-tooltip'>
          {tooltipText}
        </View>
      )}
    </View>
  )
}

多端脚本逻辑

处理不同平台的业务逻辑差异:

// utils/platform/share.js (通用分享逻辑)
export function shareContent(content) {
  console.log('默认分享逻辑', content)
  return Promise.resolve()
}
// utils/platform/share.weapp.js (微信小程序分享)
import Taro from '@tarojs/taro'

export function shareContent(content) {
  return new Promise((resolve) => {
    // 微信小程序的分享逻辑
    Taro.showShareMenu({
      withShareTicket: true,
      success: () => {
        console.log('微信分享菜单开启成功')
        resolve()
      }
    })
  })
}

// 页面分享配置
export function onShareAppMessage(content) {
  return {
    title: content.title,
    path: content.path,
    imageUrl: content.imageUrl
  }
}
// utils/platform/share.h5.js (H5分享)
export function shareContent(content) {
  if (navigator.share) {
    // 使用Web Share API
    return navigator.share({
      title: content.title,
      text: content.description,
      url: content.url
    })
  } else {
    // 降级到复制链接
    return copyToClipboard(content.url).then(() => {
      alert('链接已复制到剪贴板')
    })
  }
}

function copyToClipboard(text) {
  return navigator.clipboard.writeText(text)
}

多端页面路由

不同平台的路由跳转处理:

// utils/router/index.js
export class Router {
  static navigateTo(url, params = {}) {
    const fullUrl = this.buildUrl(url, params)

    if (process.env.TARO_ENV === 'h5') {
      return this.h5Navigate(fullUrl)
    } else {
      return this.miniProgramNavigate(fullUrl)
    }
  }

  static buildUrl(url, params) {
    if (!params || Object.keys(params).length === 0) {
      return url
    }

    const queryString = Object.keys(params)
      .map(key => `${key}=${encodeURIComponent(params[key])}`)
      .join('&')

    return `${url}${url.includes('?') ? '&' : '?'}${queryString}`
  }

  static h5Navigate(url) {
    // H5的路由跳转
    window.location.href = url
    return Promise.resolve()
  }

  static miniProgramNavigate(url) {
    // 小程序的路由跳转
    return Taro.navigateTo({ url })
  }

  static back(delta = 1) {
    if (process.env.TARO_ENV === 'h5') {
      window.history.go(-delta)
      return Promise.resolve()
    } else {
      return Taro.navigateBack({ delta })
    }
  }
}

// 使用示例
// Router.navigateTo('/pages/detail/index', { id: 123, type: 'product' })

端平台插件

Taro 内置的端平台插件

Taro官方提供了多个平台插件支持:

// config/index.js 中配置内置插件
const config = {
  plugins: [
    // 微信小程序插件 (内置,无需手动添加)
    // '@tarojs/plugin-platform-weapp',

    // 支付宝小程序插件 (内置,无需手动添加)
    // '@tarojs/plugin-platform-alipay',

    // H5插件 (内置,无需手动添加)
    // '@tarojs/plugin-platform-h5',

    // React Native插件 (内置,无需手动添加)
    // '@tarojs/plugin-platform-rn'
  ]
}
其它端平台插件

第三方和扩展平台插件:

// 安装第三方平台插件
npm install @tarojs/plugin-platform-tt      // 字节跳动小程序
npm install @tarojs/plugin-platform-qq      // QQ小程序
npm install @tarojs/plugin-platform-kwai    // 快手小程序
npm install @tarojs/plugin-platform-jd      // 京东小程序
使用方法

在项目配置中添加插件:

// config/index.js
const config = {
  plugins: [
    '@tarojs/plugin-platform-tt',    // 字节跳动
    '@tarojs/plugin-platform-qq',    // QQ小程序
    '@tarojs/plugin-platform-kwai',  // 快手小程序
  ],

  // 针对不同平台的特殊配置
  mini: {
    // 小程序通用配置
    compile: {
      exclude: ['src/utils/h5-only.js']
    }
  },

  h5: {
    // H5特有配置
    webpackChain(chain) {
      chain.plugin('analyzer')
        .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [{
          analyzerMode: 'static'
        }])
    }
  }
}

对应的编译命令:

// package.json
{
  "scripts": {
    "dev:weapp": "taro build --type weapp --watch",
    "dev:alipay": "taro build --type alipay --watch",
    "dev:tt": "taro build --type tt --watch",
    "dev:qq": "taro build --type qq --watch",
    "dev:kwai": "taro build --type kwai --watch",
    "dev:h5": "taro build --type h5 --watch",

    "build:weapp": "taro build --type weapp",
    "build:alipay": "taro build --type alipay",
    "build:tt": "taro build --type tt",
    "build:qq": "taro build --type qq",
    "build:kwai": "taro build --type kwai",
    "build:h5": "taro build --type h5"
  }
}

端平台的插件化设计思想

插件化设计的理念

Taro的插件化设计遵循以下核心理念:

输出目标
平台插件
核心架构
微信小程序
H5应用
React Native
支付宝小程序
自定义平台
WeApp Plugin
H5 Plugin
RN Plugin
Alipay Plugin
Custom Plugin
Taro Core
插件系统
平台抽象层
解耦合

通过插件系统实现平台逻辑与核心框架的解耦:

// 平台插件接口定义
interface IPlatformPlugin {
  // 平台名称
  name: string

  // 文件类型映射
  fileType: {
    templ: string    // 模板文件扩展名
    style: string    // 样式文件扩展名
    script: string   // 脚本文件扩展名
    config: string   // 配置文件扩展名
  }

  // 编译处理函数
  fn: (options: CompileOptions) => Promise<void>

  // 平台特有的组件映射
  components?: ComponentsMap

  // 平台特有的API映射
  apis?: ApisMap
}
开放性

支持第三方开发者扩展新平台:

// 自定义平台插件示例
class CustomPlatformPlugin {
  constructor() {
    this.name = 'custom-platform'
    this.fileType = {
      templ: '.ctml',
      style: '.css',
      script: '.js',
      config: '.json'
    }
  }

  // 注册插件
  apply(ctx) {
    ctx.registerPlatform({
      name: this.name,
      fileType: this.fileType,
      fn: async (options) => {
        await this.compile(options)
      }
    })
  }

  // 编译逻辑
  async compile(options) {
    const { config, appPath, outputPath } = options

    // 1. 处理模板文件
    await this.processTemplates(appPath, outputPath)

    // 2. 处理样式文件
    await this.processStyles(appPath, outputPath)

    // 3. 处理脚本文件
    await this.processScripts(appPath, outputPath, config)

    // 4. 生成配置文件
    await this.generateConfig(appPath, outputPath, config)
  }

  async processTemplates(appPath, outputPath) {
    // 模板处理逻辑
    console.log('Processing templates for custom platform...')
  }

  async processStyles(appPath, outputPath) {
    // 样式处理逻辑
    console.log('Processing styles for custom platform...')
  }

  async processScripts(appPath, outputPath, config) {
    // 脚本处理逻辑
    console.log('Processing scripts for custom platform...')
  }

  async generateConfig(appPath, outputPath, config) {
    // 配置文件生成逻辑
    console.log('Generating config for custom platform...')
  }
}

module.exports = CustomPlatformPlugin
可复用性

插件可以复用通用的编译逻辑和工具:

// 插件工具函数
class PlatformUtils {
  // 通用的文件处理工具
  static async processFiles(inputDir, outputDir, processor) {
    const files = await this.getFiles(inputDir)

    for (const file of files) {
      const content = await fs.readFile(file, 'utf8')
      const processedContent = await processor(content, file)
      const outputFile = path.join(outputDir, path.relative(inputDir, file))

      await this.ensureDir(path.dirname(outputFile))
      await fs.writeFile(outputFile, processedContent)
    }
  }

  // AST转换工具
  static transformAST(code, transformers) {
    const ast = parser.parse(code)

    transformers.forEach(transformer => {
      traverse(ast, transformer)
    })

    return generator(ast).code
  }

  // 样式处理工具
  static async processStyles(styleCode, platform) {
    let processed = styleCode

    // 移除平台特定的条件编译代码
    processed = this.removeConditionalCode(processed, platform)

    // 转换样式单位
    processed = this.transformStyleUnits(processed, platform)

    return processed
  }
}

插件的结构

标准的Taro平台插件应该包含以下结构:

my-taro-plugin/
├── package.json          // 插件包配置
├── index.js             // 插件入口文件
├── lib/                 // 编译逻辑
│   ├── compiler.js      // 编译器主逻辑
│   ├── template.js      // 模板处理
│   ├── style.js         // 样式处理
│   └── script.js        // 脚本处理
├── templates/           // 模板文件
│   ├── app.json         // 应用配置模板
│   ├── page.json        // 页面配置模板
│   └── component.json   // 组件配置模板
└── README.md           // 插件说明文档

插件的封装示例

创建插件目录
mkdir taro-plugin-custom-platform
cd taro-plugin-custom-platform
npm init -y
编写 package.json
{
  "name": "taro-plugin-custom-platform",
  "version": "1.0.0",
  "description": "Taro custom platform plugin",
  "main": "index.js",
  "keywords": ["taro", "plugin", "custom-platform"],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "@tarojs/shared": "^3.0.0",
    "@tarojs/runner-utils": "^3.0.0"
  },
  "peerDependencies": {
    "@tarojs/cli": "^3.0.0"
  }
}
编写 index.js
const { processApis } = require('@tarojs/shared')
const path = require('path')
const fs = require('fs-extra')

// 平台特有的API列表
const CUSTOM_APIS = {
  // 无需Promise化的API
  noPromiseApis: [
    'getSystemInfo',
    'createCanvas'
  ],

  // 需要Promise化的API
  needPromiseApis: [
    'request',
    'uploadFile',
    'downloadFile'
  ]
}

class CustomPlatformPlugin {
  constructor() {
    this.name = 'custom-platform'
    this.fileType = {
      templ: '.ctml',
      style: '.ccss',
      script: '.js',
      config: '.json'
    }
  }

  apply(ctx) {
    // 注册平台
    ctx.registerPlatform({
      name: this.name,
      useConfigName: 'mini', // 使用mini程序配置
      fileType: this.fileType,
      fn: async (options) => {
        const { config, appPath, outputPath, nodeModulesPath } = options
        const { emptyDirectory } = ctx.helper

        // 清空输出目录
        emptyDirectory(outputPath)

        // 初始化运行时API
        await this.initApis(outputPath)

        // 编译应用
        await this.compile(options)
      }
    })
  }

  // 初始化平台API
  async initApis(outputPath) {
    const apisPath = path.join(outputPath, 'apis.js')
    const apiContent = this.generateApiContent()

    await fs.writeFile(apisPath, apiContent)
  }

  generateApiContent() {
    return `
// 自定义平台API初始化
import { processApis } from '@tarojs/shared'

// 模拟的全局API对象
const customGlobal = {
  request: (options) => {
    return fetch(options.url, {
      method: options.method || 'GET',
      headers: options.headers,
      body: options.data ? JSON.stringify(options.data) : undefined
    }).then(response => response.json())
  },

  getSystemInfo: () => {
    return {
      platform: 'custom',
      version: '1.0.0',
      screenWidth: window.innerWidth,
      screenHeight: window.innerHeight
    }
  },

  showToast: (options) => {
    alert(options.title)
    return Promise.resolve()
  }
}

export function initNativeApi(taro) {
  processApis(taro, customGlobal, ${JSON.stringify(CUSTOM_APIS)})
}
`
  }

  // 编译主逻辑
  async compile(options) {
    const { config, appPath, outputPath } = options

    console.log(\`开始编译自定义平台应用...\`)

    // 处理应用配置
    await this.processAppConfig(appPath, outputPath, config)

    // 处理页面文件
    await this.processPages(appPath, outputPath, config)

    // 处理组件文件
    await this.processComponents(appPath, outputPath)

    console.log(\`自定义平台应用编译完成!\`)
  }

  async processAppConfig(appPath, outputPath, config) {
    const appConfigPath = path.join(appPath, 'src', 'app.config.js')

    if (await fs.pathExists(appConfigPath)) {
      const appConfig = require(appConfigPath).default

      // 转换为自定义平台的应用配置格式
      const customAppConfig = {
        name: appConfig.name || 'CustomApp',
        pages: appConfig.pages || [],
        window: {
          title: appConfig.window?.navigationBarTitleText || 'App',
          backgroundColor: appConfig.window?.backgroundColor || '#ffffff'
        }
      }

      await fs.writeFile(
        path.join(outputPath, 'app.json'),
        JSON.stringify(customAppConfig, null, 2)
      )
    }
  }

  async processPages(appPath, outputPath, config) {
    const pagesDir = path.join(appPath, 'src', 'pages')

    if (await fs.pathExists(pagesDir)) {
      const pages = await fs.readdir(pagesDir)

      for (const page of pages) {
        await this.processPage(appPath, outputPath, page)
      }
    }
  }

  async processPage(appPath, outputPath, pageName) {
    const pageDir = path.join(appPath, 'src', 'pages', pageName)
    const outputPageDir = path.join(outputPath, 'pages', pageName)

    await fs.ensureDir(outputPageDir)

    // 处理页面脚本文件
    const jsFile = path.join(pageDir, 'index.jsx')
    if (await fs.pathExists(jsFile)) {
      let content = await fs.readFile(jsFile, 'utf8')
      content = this.transformPageScript(content)
      await fs.writeFile(path.join(outputPageDir, 'index.js'), content)
    }

    // 处理页面样式文件
    const scssFile = path.join(pageDir, 'index.scss')
    if (await fs.pathExists(scssFile)) {
      let content = await fs.readFile(scssFile, 'utf8')
      content = this.transformPageStyle(content)
      await fs.writeFile(path.join(outputPageDir, 'index.ccss'), content)
    }

    // 处理页面配置文件
    const configFile = path.join(pageDir, 'index.config.js')
    if (await fs.pathExists(configFile)) {
      const config = require(configFile).default
      const customConfig = this.transformPageConfig(config)
      await fs.writeFile(
        path.join(outputPageDir, 'index.json'),
        JSON.stringify(customConfig, null, 2)
      )
    }
  }

  transformPageScript(content) {
    // 简单的JSX到自定义平台语法转换
    return content
      .replace(/from '@tarojs\/components'/g, "from 'custom-components'")
      .replace(/from '@tarojs\/taro'/g, "from 'custom-taro'")
      .replace(/<View/g, '<custom-view')
      .replace(/<\/View>/g, '</custom-view>')
      .replace(/<Text/g, '<custom-text')
      .replace(/<\/Text>/g, '</custom-text>')
  }

  transformPageStyle(content) {
    // 样式转换逻辑
    return content
      .replace(/rpx/g, 'rem') // 假设自定义平台使用rem单位
  }

  transformPageConfig(config) {
    // 页面配置转换
    return {
      title: config.navigationBarTitleText || '',
      pullRefresh: config.enablePullDownRefresh || false
    }
  }

  async processComponents(appPath, outputPath) {
    const componentsDir = path.join(appPath, 'src', 'components')

    if (await fs.pathExists(componentsDir)) {
      const outputComponentsDir = path.join(outputPath, 'components')
      await fs.copy(componentsDir, outputComponentsDir)
    }
  }
}

module.exports = CustomPlatformPlugin
module.exports.default = CustomPlatformPlugin

使用自定义插件

安装插件
// 本地安装
npm install ./path/to/taro-plugin-custom-platform

// 或者从npm安装(如果已发布)
npm install taro-plugin-custom-platform
配置插件
// config/index.js
const config = {
  plugins: [
    'taro-plugin-custom-platform'
  ],

  // 如果插件需要特殊配置
  pluginOptions: {
    'taro-plugin-custom-platform': {
      // 插件配置选项
      customOption: 'value'
    }
  }
}

module.exports = config
编译命令
// package.json
{
  "scripts": {
    "dev:custom": "taro build --type custom-platform --watch",
    "build:custom": "taro build --type custom-platform"
  }
}

多端应用构建与发布

微信小程序构建与发布

构建微信小程序

微信小程序的构建流程如下:

开发者 Taro CLI 编译器 微信开发者工具 微信服务器 taro build --type weapp 启动编译流程 JSX转换为WXML SCSS转换为WXSS 生成配置文件 编译完成 构建产物输出到dist目录 导入项目 上传代码 返回上传结果 开发者 Taro CLI 编译器 微信开发者工具 微信服务器

开发环境构建

// 启动微信小程序开发模式
npm run dev:weapp

// 或使用Taro CLI直接运行
taro build --type weapp --watch

// 构建完成后,使用微信开发者工具打开 dist 目录

生产环境构建

// 生产环境构建
npm run build:weapp

// 或带环境变量
NODE_ENV=production taro build --type weapp

// 开启代码压缩
NODE_ENV=production taro build --type weapp --uglify

构建优化配置

// config/index.js
const config = {
  mini: {
    // 微信小程序特有配置
    compile: {
      // 排除不需要的文件
      exclude: ['src/utils/h5-only.js']
    },

    // 代码分包配置
    subPackages: [
      {
        root: 'pages/user/',
        pages: [
          'profile/index',
          'settings/index'
        ]
      }
    ],

    // 预加载配置
    preloadRule: {
      'pages/index/index': {
        network: 'all',
        packages: ['pages/user/']
      }
    },

    // 优化配置
    optimization: {
      usedExports: true,
      sideEffects: false
    }
  }
}
发布微信小程序

版本管理流程

// 1. 更新版本号
// package.json
{
  "version": "1.2.0"
}

// 2. 构建生产版本
npm run build:weapp

// 3. 提交代码审核
// 在微信开发者工具中:
// - 点击"上传"按钮
// - 填写版本号和项目备注
// - 上传代码包

自动化发布脚本

// scripts/deploy-weapp.js
const { execSync } = require('child_process')
const fs = require('fs-extra')
const path = require('path')

async function deployWeapp() {
  try {
    console.log('🚀 开始构建微信小程序...')

    // 1. 清理旧的构建产物
    const distPath = path.resolve(__dirname, '../dist')
    if (await fs.pathExists(distPath)) {
      await fs.remove(distPath)
    }

    // 2. 构建生产版本
    execSync('NODE_ENV=production taro build --type weapp', {
      stdio: 'inherit'
    })

    // 3. 检查关键文件
    const appJsonPath = path.join(distPath, 'app.json')
    if (!(await fs.pathExists(appJsonPath))) {
      throw new Error('app.json 文件不存在')
    }

    // 4. 验证包大小
    const stats = await fs.stat(distPath)
    const sizeInMB = stats.size / (1024 * 1024)
    if (sizeInMB > 20) {
      console.warn(`⚠️ 包大小警告: ${sizeInMB.toFixed(2)}MB,接近20MB限制`)
    }

    console.log('✅ 微信小程序构建完成')
    console.log(`📦 请使用微信开发者工具打开: ${distPath}`)

  } catch (error) {
    console.error('❌ 构建失败:', error.message)
    process.exit(1)
  }
}

deployWeapp()

使用CI/CD自动发布

// .github/workflows/deploy-weapp.yml
name: Deploy WeChat MiniProgram

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'

    - name: Install dependencies
      run: npm ci

    - name: Build WeChat MiniProgram
      run: npm run build:weapp

    - name: Upload to WeChat
      run: |
        # 使用微信开发者工具命令行
        # 需要提前配置好开发者工具的CLI
        cli -u dist/ --upload-desc "$(git log -1 --pretty=%B)"

注意事项

微信小程序发布注意事项

  1. 代码包大小限制

    • 主包不超过2MB
    • 总包大小不超过20MB
    • 单个分包不超过2MB
  2. API使用规范

    • 遵循微信小程序API使用规范
    • 注意隐私API的使用声明
    • 及时更新基础库版本要求
  3. 审核要点

    • 功能完整性检查
    • 用户体验合规性
    • 内容合规性审核

H5 应用构建与发布

构建 H5 应用

H5应用构建流程:

源代码
Webpack编译
代码分割
资源优化
生成静态文件
部署到CDN

开发环境启动

// 启动H5开发服务器
npm run dev:h5

// 自定义端口和host
PORT=3000 HOST=0.0.0.0 npm run dev:h5

// 开启HTTPS开发服务器
HTTPS=true npm run dev:h5

生产环境构建

// 构建H5生产版本
npm run build:h5

// 分析包大小
npm run build:h5 -- --analyzer

// 构建并预览
npm run build:h5 && npx serve dist -s

H5专用配置

// config/index.js
const config = {
  h5: {
    // 静态资源CDN配置
    publicPath: process.env.NODE_ENV === 'production'
      ? 'https://cdn.example.com/taro-app/'
      : '/',

    // 路由配置
    router: {
      mode: 'browser', // 或 'hash'
      basename: '/taro-app'
    },

    // Webpack配置定制
    webpackChain(chain) {
      // 添加别名
      chain.resolve.alias
        .set('@', path.resolve(__dirname, '..', 'src'))

      // 优化分包
      chain.optimization.splitChunks({
        chunks: 'all',
        cacheGroups: {
          vendor: {
            name: 'vendors',
            test: /[\\/]node_modules[\\/]/,
            chunks: 'all',
            priority: 10
          },
          taro: {
            name: 'taro',
            test: /[\\/]node_modules[\\/]@tarojs[\\/]/,
            chunks: 'all',
            priority: 15
          }
        }
      })

      // 添加PWA支持
      if (process.env.NODE_ENV === 'production') {
        chain.plugin('workbox')
          .use(require('workbox-webpack-plugin').GenerateSW, [{
            skipWaiting: true,
            clientsClaim: true,
            runtimeCaching: [{
              urlPattern: /^https:\/\/api\.example\.com/,
              handler: 'NetworkFirst',
              options: {
                cacheName: 'api-cache'
              }
            }]
          }])
      }
    },

    // HTML模板配置
    template: path.join(process.cwd(), 'src/index.template.html'),

    // 开发服务器配置
    devServer: {
      port: 10086,
      host: '0.0.0.0',
      https: false,
      open: true,
      proxy: {
        '/api': {
          target: 'https://api.example.com',
          changeOrigin: true,
          pathRewrite: {
            '^/api': ''
          }
        }
      }
    }
  }
}
发布 H5 应用

静态文件部署

// 部署到Nginx
// 构建应用
npm run build:h5

// 将dist目录内容复制到Nginx服务器
scp -r dist/* user@server:/var/www/html/taro-app/

使用Docker部署

创建 Dockerfile
# Dockerfile
# 多阶段构建,优化镜像大小
FROM node:16-alpine as builder

# 设置工作目录
WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build:h5

# 生产阶段
FROM nginx:alpine

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 暴露端口
EXPOSE 80

# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

Nginx配置文件

# nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # 日志格式
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    # 基础配置
    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;
    keepalive_timeout  65;
    types_hash_max_size 2048;

    # Gzip压缩
    gzip  on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript
               application/javascript application/xml+rss
               application/json image/svg+xml;

    server {
        listen       80;
        server_name  localhost;
        root         /usr/share/nginx/html;
        index        index.html;

        # 处理SPA路由
        location / {
            try_files $uri $uri/ /index.html;
        }

        # 静态资源缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, no-transform";
        }

        # 安全头
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;

        # 错误页面
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }
    }
}
构建 Docker 镜像
// 构建镜像
docker build -t taro-h5-app:latest .

// 查看镜像
docker images | grep taro-h5-app
运行 Docker 容器
// 运行容器
docker run -d \
  --name taro-h5-app \
  -p 80:80 \
  --restart unless-stopped \
  taro-h5-app:latest

// 查看容器状态
docker ps | grep taro-h5-app

// 查看容器日志
docker logs taro-h5-app

参数说明

  • -d: 后台运行
  • --name: 容器名称
  • -p 80:80: 端口映射
  • --restart unless-stopped: 自动重启策略
访问应用

构建和部署完成后,可以通过以下方式访问应用:

// 本地访问
http://localhost

// 服务器访问
http://your-domain.com

// 健康检查
curl -I http://localhost

配置 HTTPS (可选)

创建 Caddyfile

如果需要HTTPS支持,可以使用Caddy作为反向代理:

# Caddyfile
your-domain.com {
    reverse_proxy taro-h5-app:80

    # 自动HTTPS
    tls your-email@example.com

    # 静态资源缓存
    @static {
        path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg
    }
    header @static Cache-Control "public, max-age=31536000"

    # 安全头
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}
使用 Caddyfile 运行 Docker
# docker-compose.yml
version: '3.8'

services:
  taro-app:
    build: .
    container_name: taro-h5-app
    restart: unless-stopped
    networks:
      - app-network

  caddy:
    image: caddy:alpine
    container_name: caddy-proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  caddy_data:
  caddy_config:

启动服务

// 启动所有服务
docker-compose up -d

// 查看服务状态
docker-compose ps

// 查看日志
docker-compose logs -f caddy

通过这样的配置,您的Taro H5应用就可以通过HTTPS安全访问,并且具备了自动续期SSL证书的能力。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐