多端开发之Taro
多端开发之Taro
概述
Taro 是京东凹凸实验室开源的一套遵循 React 语法规范的多端统一开发解决方案。它支持用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/QQ 小程序/H5/React Native 等应用,实现了真正的一套代码多端运行。
相关资料
官方文档
- Taro 文档 - Taro官方文档,包含完整的API参考和开发指南
案例与资源
技术原理
- 编译原理相关 - 深入解析Taro的编译时处理机制
框架支持
- Taro React Hooks - Taro中React Hooks的使用指南
- Taro Vue - Taro对Vue框架的支持文档
插件生态
- Taro plugin - Taro插件开发示例,如mock插件
核心问题解析
Taro 如何实现多端适配?
Taro 通过编译时和运行时的双重机制实现多端适配:
编译时适配机制:
关键实现原理:
- AST语法树转换 - 将React/Vue语法转换为目标平台语法
- 组件映射 - 统一组件API映射到平台特定组件
- API适配 - 统一API调用转换为平台特定API
- 样式处理 - CSS样式转换为平台支持的样式格式
Taro的多端开发架构
Taro架构由以下主要组成部分:
各组成部分作用:
- 开发层:提供统一的开发接口和组件库
- 编译层:负责语法转换和平台适配
- 运行时层:提供平台特定的运行时支持
- 目标平台:最终的应用运行环境
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><p>支持实体解码</p></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
优先级顺序:
使用要点:
- 按需创建 - 只为有差异的平台创建专用文件
- 接口统一 - 保持所有平台文件的导出接口一致
- 回退机制 - 确保通用文件能在所有平台正常工作
使用场景:
- 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的插件化设计遵循以下核心理念:
解耦合
通过插件系统实现平台逻辑与核心框架的解耦:
// 平台插件接口定义
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"
}
}
多端应用构建与发布
微信小程序构建与发布
构建微信小程序
微信小程序的构建流程如下:
开发环境构建:
// 启动微信小程序开发模式
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)"
注意事项
微信小程序发布注意事项:
-
代码包大小限制:
- 主包不超过2MB
- 总包大小不超过20MB
- 单个分包不超过2MB
-
API使用规范:
- 遵循微信小程序API使用规范
- 注意隐私API的使用声明
- 及时更新基础库版本要求
-
审核要点:
- 功能完整性检查
- 用户体验合规性
- 内容合规性审核
H5 应用构建与发布
构建 H5 应用
H5应用构建流程:
开发环境启动:
// 启动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证书的能力。
更多推荐



所有评论(0)