Developing Mini Programs with Taro
9月 9, 2023
·
5 分钟阅读时长
·
2350
字
·
-阅读
-评论
Introduction
Taro is a cross-platform development framework that allows you to write code once and compile it to run on multiple platforms including WeChat Mini Programs, Alipay Mini Programs, Baidu Smart Programs, ByteDance Mini Programs, QQ Mini Programs, and H5, React Native, and other platforms.
Why Choose Taro
Advantages
- Write Once, Run Everywhere: Single codebase for multiple platforms
- React-like Syntax: Familiar development experience for React developers
- Component-based Development: Reusable components across platforms
- Rich Ecosystem: Comprehensive toolchain and plugin system
- TypeScript Support: Built-in TypeScript support
- Performance Optimization: Optimized compilation and runtime
Supported Platforms
- Mini Programs: WeChat, Alipay, Baidu, ByteDance, QQ, Kuaishou
- Mobile Apps: React Native (iOS/Android)
- Web: H5, Progressive Web Apps
- Desktop: Electron (experimental)
Getting Started
Installation
# Install Taro CLI globally
npm install -g @tarojs/cli
# Create new project
taro init myApp
# Choose template
# ❯ TypeScript
# JavaScript
# Vue
# Vue3
Project Structure
myApp/
├── src/
│ ├── pages/ # Page components
│ │ └── index/
│ │ ├── index.tsx
│ │ ├── index.config.ts
│ │ └── index.scss
│ ├── components/ # Reusable components
│ ├── utils/ # Utility functions
│ ├── services/ # API services
│ ├── app.tsx # App entry
│ ├── app.config.ts # App configuration
│ └── app.scss # Global styles
├── config/ # Build configuration
├── package.json
└── project.config.json
Basic Configuration
// app.config.ts
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/profile/profile'
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'My Taro App',
navigationBarTextStyle: 'black'
},
tabBar: {
color: '#666',
selectedColor: '#b4282d',
backgroundColor: '#fafafa',
borderStyle: 'black',
list: [
{
pagePath: 'pages/index/index',
text: 'Home',
iconPath: 'assets/home.png',
selectedIconPath: 'assets/home-active.png'
},
{
pagePath: 'pages/profile/profile',
text: 'Profile',
iconPath: 'assets/profile.png',
selectedIconPath: 'assets/profile-active.png'
}
]
}
});
Component Development
Functional Components
import { View, Text, Button } from '@tarojs/components';
import { useState } from 'react';
import Taro from '@tarojs/taro';
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const showToast = () => {
Taro.showToast({
title: `Count: ${count}`,
icon: 'none'
});
};
return (
<View className="counter">
<Text className="count-text">Count: {count}</Text>
<Button onClick={increment}>Increment</Button>
<Button onClick={showToast}>Show Toast</Button>
</View>
);
};
export default Counter;
Class Components
import { Component } from 'react';
import { View, Text, Input } from '@tarojs/components';
import Taro from '@tarojs/taro';
interface State {
inputValue: string;
todos: string[];
}
class TodoList extends Component<{}, State> {
state: State = {
inputValue: '',
todos: []
};
componentDidMount() {
console.log('Component mounted');
}
handleInputChange = (e) => {
this.setState({
inputValue: e.detail.value
});
};
addTodo = () => {
const { inputValue, todos } = this.state;
if (inputValue.trim()) {
this.setState({
todos: [...todos, inputValue],
inputValue: ''
});
}
};
render() {
const { inputValue, todos } = this.state;
return (
<View className="todo-list">
<View className="input-section">
<Input
value={inputValue}
onInput={this.handleInputChange}
placeholder="Enter todo..."
/>
<Button onClick={this.addTodo}>Add</Button>
</View>
<View className="todos">
{todos.map((todo, index) => (
<View key={index} className="todo-item">
<Text>{todo}</Text>
</View>
))}
</View>
</View>
);
}
}
export default TodoList;
Platform APIs
Navigation
import Taro from '@tarojs/taro';
// Navigate to new page
const navigateToDetail = () => {
Taro.navigateTo({
url: '/pages/detail/detail?id=123'
});
};
// Redirect to page
const redirectToLogin = () => {
Taro.redirectTo({
url: '/pages/login/login'
});
};
// Navigate back
const goBack = () => {
Taro.navigateBack({
delta: 1
});
};
// Switch tab
const switchToProfile = () => {
Taro.switchTab({
url: '/pages/profile/profile'
});
};
Storage
import Taro from '@tarojs/taro';
// Set storage
const saveUserData = async (userData) => {
try {
await Taro.setStorage({
key: 'userData',
data: userData
});
console.log('Data saved successfully');
} catch (error) {
console.error('Failed to save data:', error);
}
};
// Get storage
const getUserData = async () => {
try {
const result = await Taro.getStorage({
key: 'userData'
});
return result.data;
} catch (error) {
console.error('Failed to get data:', error);
return null;
}
};
// Remove storage
const clearUserData = async () => {
try {
await Taro.removeStorage({
key: 'userData'
});
console.log('Data cleared');
} catch (error) {
console.error('Failed to clear data:', error);
}
};
Network Requests
import Taro from '@tarojs/taro';
// GET request
const fetchUserList = async () => {
try {
const response = await Taro.request({
url: 'https://api.example.com/users',
method: 'GET',
header: {
'Content-Type': 'application/json'
}
});
if (response.statusCode === 200) {
return response.data;
} else {
throw new Error('Request failed');
}
} catch (error) {
console.error('Network error:', error);
Taro.showToast({
title: 'Network error',
icon: 'none'
});
}
};
// POST request
const createUser = async (userData) => {
try {
const response = await Taro.request({
url: 'https://api.example.com/users',
method: 'POST',
data: userData,
header: {
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('Failed to create user:', error);
throw error;
}
};
// File upload
const uploadImage = async (filePath) => {
try {
const response = await Taro.uploadFile({
url: 'https://api.example.com/upload',
filePath: filePath,
name: 'file',
formData: {
userId: '123'
}
});
return JSON.parse(response.data);
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
};
State Management
Using Redux
npm install @reduxjs/toolkit react-redux
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import counterSlice from './counterSlice';
const store = configureStore({
reducer: {
counter: counterSlice,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
// store/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// app.tsx
import { Component, PropsWithChildren } from 'react';
import { Provider } from 'react-redux';
import store from './store';
class App extends Component<PropsWithChildren> {
render() {
return (
<Provider store={store}>
{this.props.children}
</Provider>
);
}
}
export default App;
// Using Redux in components
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store';
import { increment, decrement } from '../store/counterSlice';
const Counter: React.FC = () => {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<View>
<Text>Count: {count}</Text>
<Button onClick={() => dispatch(increment())}>+</Button>
<Button onClick={() => dispatch(decrement())}>-</Button>
</View>
);
};
Platform-Specific Development
Conditional Compilation
import Taro from '@tarojs/taro';
const MyComponent: React.FC = () => {
const handleClick = () => {
// #ifdef WEAPP
// WeChat Mini Program specific code
wx.showModal({
title: 'WeChat',
content: 'This is WeChat Mini Program'
});
// #endif
// #ifdef ALIPAY
// Alipay Mini Program specific code
my.alert({
title: 'Alipay',
content: 'This is Alipay Mini Program'
});
// #endif
// #ifdef H5
// H5 specific code
alert('This is H5');
// #endif
};
return (
<View>
<Button onClick={handleClick}>Platform Test</Button>
{/* Conditional rendering */}
{process.env.TARO_ENV === 'weapp' && (
<Text>WeChat Mini Program only</Text>
)}
{process.env.TARO_ENV === 'h5' && (
<Text>H5 only</Text>
)}
</View>
);
};
Platform-Specific Styles
// Common styles
.container {
padding: 20px;
// WeChat Mini Program
/* #ifdef WEAPP */
background-color: #f0f0f0;
/* #endif */
// H5
/* #ifdef H5 */
background-color: #ffffff;
max-width: 750px;
margin: 0 auto;
/* #endif */
// Alipay Mini Program
/* #ifdef ALIPAY */
background-color: #1677ff;
/* #endif */
}
Custom Components
Creating Reusable Components
// components/Card/index.tsx
import { View, Text } from '@tarojs/components';
import { ReactNode } from 'react';
import './index.scss';
interface CardProps {
title?: string;
children: ReactNode;
onClick?: () => void;
}
const Card: React.FC<CardProps> = ({ title, children, onClick }) => {
return (
<View className="card" onClick={onClick}>
{title && <View className="card-title">{title}</View>}
<View className="card-content">{children}</View>
</View>
);
};
export default Card;
// components/Card/index.scss
.card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
overflow: hidden;
&-title {
padding: 16px;
font-size: 18px;
font-weight: 600;
border-bottom: 1px solid #f0f0f0;
}
&-content {
padding: 16px;
}
}
Component with Hooks
import { useState, useEffect } from 'react';
import { View, Text } from '@tarojs/components';
import Taro from '@tarojs/taro';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await Taro.request({
url: `https://api.example.com/users/${userId}`,
method: 'GET'
});
if (response.statusCode === 200) {
setUser(response.data);
}
} catch (error) {
console.error('Failed to fetch user:', error);
Taro.showToast({
title: 'Failed to load user',
icon: 'none'
});
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]);
if (loading) {
return <Text>Loading...</Text>;
}
if (!user) {
return <Text>User not found</Text>;
}
return (
<View className="user-profile">
<Text className="user-name">{user.name}</Text>
<Text className="user-email">{user.email}</Text>
</View>
);
};
export default UserProfile;
Performance Optimization
Code Splitting
import { lazy, Suspense } from 'react';
import { View, Text } from '@tarojs/components';
// Lazy load heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const MyPage: React.FC = () => {
return (
<View>
<Text>Page Content</Text>
<Suspense fallback={<Text>Loading...</Text>}>
<HeavyComponent />
</Suspense>
</View>
);
};
Memoization
import { memo, useMemo, useCallback } from 'react';
import { View, Text, Button } from '@tarojs/components';
interface ItemProps {
item: {
id: number;
name: string;
price: number;
};
onSelect: (id: number) => void;
}
const ExpensiveItem = memo<ItemProps>(({ item, onSelect }) => {
const formattedPrice = useMemo(() => {
// Expensive calculation
return `$${item.price.toFixed(2)}`;
}, [item.price]);
const handleSelect = useCallback(() => {
onSelect(item.id);
}, [item.id, onSelect]);
return (
<View className="item">
<Text>{item.name}</Text>
<Text>{formattedPrice}</Text>
<Button onClick={handleSelect}>Select</Button>
</View>
);
});
export default ExpensiveItem;
Testing
Unit Testing with Jest
npm install --save-dev @types/jest jest ts-jest
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1'
},
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts']
};
// __tests__/Counter.test.tsx
import { render, fireEvent } from '@testing-library/react';
import Counter from '../Counter';
describe('Counter Component', () => {
test('renders initial count', () => {
const { getByText } = render(<Counter />);
expect(getByText('Count: 0')).toBeInTheDocument();
});
test('increments count when button clicked', () => {
const { getByText } = render(<Counter />);
const button = getByText('Increment');
fireEvent.click(button);
expect(getByText('Count: 1')).toBeInTheDocument();
});
});
Deployment
Build for Different Platforms
# Build for WeChat Mini Program
npm run build:weapp
# Build for Alipay Mini Program
npm run build:alipay
# Build for H5
npm run build:h5
# Build for React Native
npm run build:rn
WeChat Mini Program Deployment
# Development build
npm run dev:weapp
# Production build
npm run build:weapp
# Open WeChat Developer Tools
# Import the dist folder as project
H5 Deployment
// config/prod.js
module.exports = {
env: {
NODE_ENV: '"production"'
},
defineConstants: {
API_BASE_URL: '"https://api.production.com"'
},
h5: {
publicPath: '/myapp/',
staticDirectory: 'static',
router: {
mode: 'browser' // or 'hash'
}
}
};
Best Practices
1. Component Design
// Good - Single responsibility
const UserAvatar: React.FC<{
src: string;
size?: 'small' | 'medium' | 'large';
onClick?: () => void;
}> = ({ src, size = 'medium', onClick }) => {
const className = `avatar avatar-${size}`;
return (
<Image
className={className}
src={src}
onClick={onClick}
/>
);
};
// Good - Composition over inheritance
const UserCard: React.FC<{ user: User }> = ({ user }) => {
return (
<Card title="User Profile">
<UserAvatar src={user.avatar} size="large" />
<Text>{user.name}</Text>
<Text>{user.email}</Text>
</Card>
);
};
2. Error Handling
import { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';
const useApiData = <T>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await Taro.request({ url });
if (response.statusCode === 200) {
setData(response.data);
} else {
throw new Error(`HTTP ${response.statusCode}`);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
3. Type Safety
// Define types for better development experience
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
interface PageProps {
userId: string;
}
const UserDetailPage: React.FC = () => {
const { userId } = Taro.getCurrentInstance().router?.params as PageProps;
const { data: user, loading, error } = useApiData<User>(
`/api/users/${userId}`
);
// Type-safe component logic
};
Conclusion
Taro provides a powerful solution for cross-platform mini program development. Key benefits include:
- Code Reusability: Write once, deploy everywhere
- Familiar Syntax: React-like development experience
- Rich Ecosystem: Comprehensive tooling and community support
- Performance: Optimized compilation and runtime
- Type Safety: Built-in TypeScript support
While Taro handles most platform differences automatically, understanding platform-specific features and limitations is crucial for building robust applications. Start with simple components and gradually explore advanced features as your application grows.
最近有个项目小程序开发,因为过去的N年使用的技术主要是react,所以经过权衡,决定使用taro,毕竟taro支持react进行编写。这里积攒了一些经验,总结下,以备之后查。
Get Started
npm i -g @tarojs/cli
taro init
# 如果未来计划使用taro-ui,则需要选择sass,因为taro-ui使用的sass。
UI组件库
- taro-ui 也调研了下其它的,考虑到维护和使用的便捷性,最终选择taro-ui。
一些类库
- 表单状态管理,可以使用
react-hook-form - 状态管理,可以使用
redux - 网络请求,可以使用
axios - 二维码生成,可以使用
taro-code

