从React到小程序:配合tailwind等现代前端开发体验,用TodoList玩转Taro全栈核心API
前言
作为React开发者,如何快速切入小程序开发领域?Taro为你提供了无缝的开发体验。本文通过TodoList这个经典案例,带你从环境搭建开始,全面体验Taro的核心API,包括页面生命周期管理、状态切换、本地存储、下拉刷新等功能,同时讲解多端开发技巧和性能优化策略,让你快速掌握React思想在小程序开发中的应用,轻松实现从React到小程序的技术迁移。
环境搭建
搭建react+taro+typescript+weapp-tailwind的项目
taro文档:https://docs.taro.zone/docs/GETTING-STARTED
weapp-tailwind文档:https://weapp-tw.icebreaker.top/docs/quick-start/v4/taro-webpack
Taro安装及使用
安装
Taro 项目基于 node,请确保已具备较新的 node 环境(>=16.20.0),推荐使用 node 版本管理工具 nvm 来管理 node,这样不仅可以很方便地切换 node 版本,而且全局安装时候也不用加 sudo 了。
CLI 工具安装
首先,你需要使用 npm 或者 yarn 全局安装 @tarojs/cli,或者直接使用 npx:
- npm
- yarn
- pnpm
# 使用 npm 安装 CLI
$ npm install -g @tarojs/cli
请注意
由于 Taro 部分能力使用 Rust 开发,在 Windows 上,请确保安装了 Microsoft Visual C++ Redistributable。请查看:https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
查看 Taro 全部版本信息
可以使用 npm info 查看 Taro 版本信息,在这里你可以看到当前最新版本
npm info @tarojs/cli

由图第 1 行可知最新版本,如果你用的是 beta 或者 canary 你可以通过 dist-tags: 下面那行看到最新的版本。
项目初始化
使用命令创建模板项目:
$ taro init myApp
npm 5.2+ 也可在不全局安装的情况下使用 npx 创建模板项目:
$ npx @tarojs/cli init myApp

在创建完项目之后,Taro 会默认开始安装项目所需要的依赖,安装使用的工具按照 yarn > cnpm > npm 顺序进行检测。一般来说,依赖安装会比较顺利,但某些情况下可能会安装失败,这时候你可以在项目目录下自己使用安装命令进行安装:
- npm
- yarn
- pnpm
# 进入项目根目录
$ cd myApp
# 使用 npm 安装依赖
$ npm install
Taro webpack
注意:选webpack的原因是因为vite适配有问题
官方解释:Taro Vite 目前不稳定,已知 bug 较多,而且依赖链版本较老,不推荐在新项目里使用。
如果没有强依赖,优先选择 Taro Webpack、uni-app、weapp-vite 等更稳定的方案。
安装
- npm
- Yarn
- pnpm
- Bun
pnpm add -D tailwindcss @tailwindcss/postcss postcss weapp-tailwindcss
然后把下列脚本,添加进你的 package.json 的 scripts 字段里:
package.json
"scripts": {
"postinstall": "weapp-tw patch"
}
使用 pnpm@10+
pnpm@10 默认只允许 onlyBuiltDependencies 中的包运行生命周期脚本。安装完本插件后,请执行 pnpm approve-builds weapp-tailwindcss 将其加入白名单,避免 postinstall 的 weapp-tw patch 被跳过。
这是为了给 tailwindcss@4 打上支持 rpx 单位的补丁,否则它会把 rpx 认为是一种颜色
如需在补丁前强制刷新 tailwindcss-patch 的缓存,可改为:
package.json
"scripts": {
"postinstall": "weapp-tw patch --clear-cache"
}
默认不清理缓存;只有当你怀疑缓存导致补丁未生效或目标不一致时,才需要添加 --clear-cache。***
配置
在你的根目录创建 postcss.config.mjs
postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
}
}
在你的 app.css 里面添加
@import "weapp-tailwindcss/index.css";
关于 @import ‘tailwindcss’
默认情况下,weapp-tailwindcss 的 rewriteCssImports 选项会自动将@import 'tailwindcss' 改写为 @import 'weapp-tailwindcss/index.css'。
这意味着你可以继续在 IntelliSense 辅助入口中使用官方文档的写法 @import 'tailwindcss',以获得更好的 IDE 智能提示 支持。
如果是实际运行时入口,推荐直接写 @import 'weapp-tailwindcss/index.css'。
如果遇到报错或样式不生效,也请优先手动改为 @import 'weapp-tailwindcss/index.css', 或将rewriteCssImports 设置为false。
注册插件
在项目的配置文件 config/index 中注册:
config/index.[jt]s
import { UnifiedWebpackPluginV5 } from 'weapp-tailwindcss/webpack'
import path from 'node:path'
// 假如你使用 js 配置,则使用下方 require 的写法
// const { UnifiedWebpackPluginV5 } = require('weapp-tailwindcss/webpack')
// const path = require('node:path')
{
// 找到 mini 这个配置
mini: {
// postcss: { /*...*/ },
// 中的 webpackChain, 通常紧挨着 postcss
webpackChain(chain, webpack) {
// 复制这块区域到你的配置代码中 region start
chain.merge({
plugin: {
install: {
plugin: UnifiedWebpackPluginV5,
args: [{
// 这里可以传参数
rem2rpx: true,
cssEntries: [
// 你 @import "weapp-tailwindcss/index.css"; 那个文件绝对路径
path.resolve(__dirname, '../src/app.css'),
],
}],
},
},
})
// region end
}
}
}
tailwindcss@4 必须配置
cssEntries并且使用绝对路径,否则tailwindcss生成的类名不会参与转译。
运行
然后执行命令发布到微信小程序
- npm
- Yarn
- pnpm
- Bun
pnpm run dev:weapp
微信开发者工具导入这个项目,即可看到效果
做多端开发tips
config/index.ts中
outputRoot: dist/${process.env.TARO_ENV}, //在dis目录下,创建获取不同运行环境的编译目录,保证打包不冲突
Step 1 执行方案
1. 开启下拉刷新
在页面配置文件中启用下拉刷新功能。
文件: src/pages/index/index.config.ts
export default definePageConfig({
navigationBarTitleText: "待办清单",
enablePullDownRefresh: true, // 允许下拉刷新
backgroundTextStyle: "dark",
});
2. 构建基础结构与状态
在首页实现输入框、待办列表和已完成列表的布局,并初始化示例数据。
文件: src/pages/index/index.tsx
import { useState } from "react";
import { View, Text, Input, Button } from "@tarojs/components";
import Taro, { useLoad, useDidShow, usePullDownRefresh } from "@tarojs/taro";
import "./index.css";
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
export default function Index() {
const [inputValue, setInputValue] = useState("");
// 待办列表初始数据
const [todos, setTodos] = useState<TodoItem[]>([
{ id: 1, text: "学习 Taro 基础", completed: false },
{ id: 2, text: "配置项目骨架", completed: false },
]);
// 已完成列表初始数据
const [completedTodos] = useState<TodoItem[]>([
{ id: 3, text: "环境搭建", completed: true },
]);
// Taro 页面生命周期 Hook
useLoad(() => {
console.log("页面加载完成");
});
useDidShow(() => {
console.log("页面显示");
});
// 监听下拉刷新
usePullDownRefresh(() => {
console.log("正在刷新数据...");
// 模拟刷新逻辑
setTimeout(() => {
Taro.stopPullDownRefresh(); // 停止刷新动画
}, 1000);
});
const handleAddTodo = () => {
if (!inputValue.trim()) return;
const newTodo: TodoItem = {
id: Date.now(),
text: inputValue,
completed: false,
};
setTodos([...todos, newTodo]);
setInputValue("");
};
return (
<View className="p-4">
{/* 输入区域 */}
<View className="flex mb-6">
<Input
className="flex-1 border-b border-gray-300 p-2 mr-2"
placeholder="添加新任务..."
value={inputValue}
onInput={(e) => setInputValue(e.detail.value)}
/>
<Button size="mini" type="primary" onClick={handleAddTodo}>
添加
</Button>
</View>
{/* 待办列表 */}
<View className="mb-6">
<Text className="text-lg font-bold mb-2 block">待办事项</Text>
{todos.map((item) => (
<View key={item.id} className="p-3 border-b border-gray-100">
<Text>{item.text}</Text>
</View>
))}
</View>
{/* 已完成列表 */}
<View>
<Text className="text-lg font-bold mb-2 block text-gray-400">
已完成
</Text>
{completedTodos.map((item) => (
<View
key={item.id}
className="p-3 border-b border-gray-100 text-gray-400 line-through"
>
<Text>{item.text}</Text>
</View>
))}
</View>
</View>
);
}
Step 2 - Step 5 执行方案
1. 核心功能点
- Step 2 (新增): 增加输入校验与
Taro.showToast反馈。 - Step 3 (切换): 实现待办与已完成状态的互转。
- Step 4 (编辑): 增加行内编辑功能,支持保存与取消。
- Step 5 (删除): 增加删除确认弹窗
Taro.showModal。
2. 代码实现
文件: src/pages/index/index.tsx
import { useState } from "react";
import { View, Text, Input, Button } from "@tarojs/components";
import Taro, { useLoad, useDidShow, usePullDownRefresh } from "@tarojs/taro";
import "./index.css";
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
export default function Index() {
const [inputValue, setInputValue] = useState("");
const [editingId, setEditingId] = useState<number | null>(null);
const [editingText, setEditingText] = useState("");
const [todos, setTodos] = useState<TodoItem[]>([
{ id: 1, text: "学习 Taro 基础", completed: false },
{ id: 2, text: "配置项目骨架", completed: false },
]);
const [completedTodos, setCompletedTodos] = useState<TodoItem[]>([
{ id: 3, text: "环境搭建", completed: true },
]);
useLoad(() => console.log("页面加载完成"));
useDidShow(() => console.log("页面显示"));
usePullDownRefresh(() => {
setTimeout(() => Taro.stopPullDownRefresh(), 1000);
});
// --- Step 2: 新增待办 ---
const handleAddTodo = () => {
if (!inputValue.trim()) {
Taro.showToast({ title: "内容不能为空", icon: "none" });
return;
}
const newTodo: TodoItem = {
id: Date.now(),
text: inputValue,
completed: false,
};
setTodos([newTodo, ...todos]);
setInputValue("");
Taro.showToast({ title: "添加成功", icon: "success" });
};
// --- Step 3: 切换状态 ---
const toggleTodo = (item: TodoItem) => {
if (!item.completed) {
// 待办 -> 已完成
setTodos(todos.filter((t) => t.id !== item.id));
setCompletedTodos([{ ...item, completed: true }, ...completedTodos]);
} else {
// 已完成 -> 待办
setCompletedTodos(completedTodos.filter((t) => t.id !== item.id));
setTodos([{ ...item, completed: false }, ...todos]);
}
};
// --- Step 4: 编辑待办 ---
const startEdit = (item: TodoItem) => {
setEditingId(item.id);
setEditingText(item.text);
};
const saveEdit = () => {
if (!editingText.trim()) {
Taro.showToast({ title: "内容不能为空", icon: "none" });
return;
}
const updateList = (list: TodoItem[]) =>
list.map((t) => (t.id === editingId ? { ...t, text: editingText } : t));
setTodos(updateList(todos));
setCompletedTodos(updateList(completedTodos));
setEditingId(null);
Taro.showToast({ title: "修改成功", icon: "success" });
};
// --- Step 5: 删除待办 ---
const handleDelete = (id: number, isCompleted: boolean) => {
Taro.showModal({
title: "确认删除",
content: "确定要删除这条待办吗?",
success: (res) => {
if (res.confirm) {
if (isCompleted) {
setCompletedTodos(completedTodos.filter((t) => t.id !== id));
} else {
setTodos(todos.filter((t) => t.id !== id));
}
Taro.showToast({ title: "已删除", icon: "success" });
}
},
});
};
return (
<View className="p-4">
{/* 输入区域 */}
<View className="flex mb-6">
<Input
className="flex-1 border-b border-gray-300 p-2 mr-2"
placeholder="添加新任务..."
value={inputValue}
onInput={(e) => setInputValue(e.detail.value)}
/>
<Button size="mini" type="primary" onClick={handleAddTodo}>添加</Button>
</View>
{/* 待办列表 */}
<View className="mb-6">
<Text className="text-lg font-bold mb-2 block">待办事项 ({todos.length})</Text>
{todos.map((item) => (
<View key={item.id} className="flex items-center justify-between p-3 border-b border-gray-100">
{editingId === item.id ? (
<Input
className="flex-1 border-b border-blue-400"
value={editingText}
onInput={(e) => setEditingText(e.detail.value)}
onBlur={saveEdit}
focus
/>
) : (
<Text className="flex-1" onClick={() => toggleTodo(item)}>{item.text}</Text>
)}
<View className="flex">
<Text className="text-blue-500 mr-3 text-sm" onClick={() => startEdit(item)}>编辑</Text>
<Text className="text-red-500 text-sm" onClick={() => handleDelete(item.id, false)}>删除</Text>
</View>
</View>
))}
</View>
{/* 已完成列表 */}
<View>
<Text className="text-lg font-bold mb-2 block text-gray-400">已完成 ({completedTodos.length})</Text>
{completedTodos.map((item) => (
<View key={item.id} className="flex items-center justify-between p-3 border-b border-gray-100 opacity-60">
<Text className="flex-1 line-through" onClick={() => toggleTodo(item)}>{item.text}</Text>
<Text className="text-red-500 text-sm" onClick={() => handleDelete(item.id, true)}>删除</Text>
</View>
))}
</View>
</View>
);
}

Step 6 执行方案:本地持久化 (Storage)
1. 核心任务
- 使用
Taro.getStorageSync在页面加载时读取历史数据。 - 使用
useEffect监听数据变化,并调用Taro.setStorageSync自动保存。 - 统一存储 Key 为
TODO_LIST_DATA。
2. 代码实现
文件: src/pages/index/index.tsx
import { useState, useEffect } from "react"; // 新增 useEffect
import { View, Text, Input, Button } from "@tarojs/components";
import Taro, { useDidShow, useLoad, usePullDownRefresh } from "@tarojs/taro";
import "./index.css";
const STORAGE_KEY = "TODO_LIST_DATA"; // 定义统一的 Key
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
export default function Index() {
const [inputValue, setInputValue] = useState("");
const [editingId, setEditingId] = useState<number | null>(null);
const [editingText, setEditingText] = useState("");
const [todos, setTodos] = useState<TodoItem[]>([]);
const [completedTodos, setCompletedTodos] = useState<TodoItem[]>([]);
// --- Step 6: 页面初始化读取数据 ---
useLoad(() => {
try {
const savedData = Taro.getStorageSync(STORAGE_KEY);
if (savedData) {
setTodos(savedData.todos || []);
setCompletedTodos(savedData.completedTodos || []);
}
} catch (e) {
console.error("读取本地数据失败", e);
}
});
// --- Step 6: 数据变化自动持久化 ---
useEffect(() => {
// 只有当数据不为空(或已初始化)时才保存,避免初始化时的空数据覆盖本地存储
// 或者简单处理:每次变化都保存
Taro.setStorageSync(STORAGE_KEY, {
todos,
completedTodos
});
}, [todos, completedTodos]);
// ... 其余业务逻辑保持不变 (handleAddTodo, toggleTodo, saveEdit, handleDelete)
// 注意:在 useEffect 自动保存后,无需在每个函数里手动调用 setStorageSync
// (以下省略重复的 UI 渲染部分,实际开发时请合并到 index.tsx)
}
3. 验收标准
- 新增一个待办事项。
- 刷新微信开发者工具(或点击编译)。
- 检查页面是否仍然显示刚才新增的内容。
- 在“调试器 -> Storage”面板中应能看到
TODO_LIST_DATA及其内容。

刷新页面后=》

Step 7 执行方案:下拉刷新与页面生命周期
1. 核心任务
- 在
usePullDownRefresh中实现从Storage重新读取数据的功能,模拟数据刷新。 - 在
useLoad和useDidShow中添加日志,观察小程序页面生命周期的触发顺序。 - 确保在逻辑结束时调用
Taro.stopPullDownRefresh()停止动画。
2. 代码实现
文件: src/pages/index/index.tsx
import { useState, useEffect } from "react";
import { View, Text, Input, Button } from "@tarojs/components";
import Taro, { useDidShow, useLoad, usePullDownRefresh } from "@tarojs/taro";
import "./index.css";
const STORAGE_KEY = "TODO_LIST_DATA";
export default function Index() {
// ... 状态定义保持不变
// --- Step 7: 页面生命周期观察 ---
useLoad(() => {
console.log("Lifecycle: useLoad (页面加载,仅触发一次)");
loadData();
});
useDidShow(() => {
console.log("Lifecycle: useDidShow (页面显示,每次切回页面都会触发)");
});
// 封装读取逻辑,以便复用
const loadData = () => {
try {
const savedData = Taro.getStorageSync(STORAGE_KEY);
if (savedData) {
setTodos(savedData.todos || []);
setCompletedTodos(savedData.completedTodos || []);
}
} catch (e) {
console.error("读取本地数据失败", e);
}
};
// --- Step 7: 下拉刷新 ---
usePullDownRefresh(() => {
console.log("Lifecycle: usePullDownRefresh (用户触发下拉刷新)");
// 重新读取数据
loadData();
// 提示刷新成功并停止动画
setTimeout(() => {
Taro.stopPullDownRefresh();
Taro.showToast({ title: "刷新成功", icon: "none" });
}, 500);
});
// ... 其余业务逻辑保持不变
}

3. 验收标准
- 生命周期观察:打开控制台,观察进入页面时
useLoad和useDidShow的输出顺序。尝试切换到其他页面再回来,观察useDidShow是否再次触发。 - 下拉刷新验证:在模拟器中手动下拉页面。
- 应看到顶部刷新动画。
- 0.5 秒后动画消失,并弹出“刷新成功”提示。
- 控制台应有相应的刷新日志。
Step A 执行方案:组件拆分与解耦
1. 核心任务
- 将输入区域、待办列表、已完成列表拆分为独立组件,提升代码可维护性。
2. 代码实现
2.1 输入组件 src/components/TodoInput.tsx
import { View, Input, Button } from "@tarojs/components";
interface TodoInputProps {
value: string;
onInput: (value: string) => void;
onAdd: () => void;
}
export default function TodoInput({ value, onInput, onAdd }: TodoInputProps) {
return (
<View className="flex mb-6">
<Input
className="flex-1 border-b border-gray-300 p-2 mr-2"
placeholder="add new todo..."
value={value}
onInput={(e) => onInput(e.detail.value)}
/>
<Button size="mini" type="primary" onClick={onAdd}>
添加
</Button>
</View>
);
}
2.2 待办列表组件 src/components/TodoList.tsx
import { View, Text, Input } from "@tarojs/components";
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
todos: TodoItem[];
editingId: number | null;
editingText: string;
onToggle: (item: TodoItem) => void;
onStartEdit: (item: TodoItem) => void;
onEditInput: (text: string) => void;
onSaveEdit: () => void;
onDelete: (id: number) => void;
onLongPress: (item: TodoItem) => void;
}
export default function TodoList({
todos,
editingId,
editingText,
onToggle,
onStartEdit,
onEditInput,
onSaveEdit,
onDelete,
onLongPress,
}: TodoListProps) {
return (
<View className="mb-6">
<Text className="text-lg font-bold mb-2 block">
TodoList({todos.length})
</Text>
{todos.map((item) => (
<View
key={item.id}
className="flex items-center justify-between p-3 border-b border-gray-100"
>
{editingId === item.id ? (
<Input
className="flex-1 border-b border-blue-400"
value={editingText}
onInput={(e) => onEditInput(e.detail.value)}
onBlur={onSaveEdit}
focus
/>
) : (
<Text
className="flex-1"
onClick={() => onToggle(item)}
onLongPress={() => onLongPress(item)}
>
{item.text}
</Text>
)}
<View className="flex">
<Text
className="text-blue-500 mr-3 text-sm"
onClick={() => onStartEdit(item)}
>
编辑
</Text>
<Text
className="text-red-500 text-sm"
onClick={() => onDelete(item.id)}
>
删除
</Text>
</View>
</View>
))}
</View>
);
}
2.3 已完成列表组件 src/components/CompletedList.tsx
import { View, Text } from "@tarojs/components";
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
interface CompletedListProps {
todos: TodoItem[];
onToggle: (item: TodoItem) => void;
onDelete: (id: number) => void;
onLongPress: (item: TodoItem) => void;
}
export default function CompletedList({
todos,
onToggle,
onDelete,
onLongPress,
}: CompletedListProps) {
return (
<View>
<Text className="text-lg font-bold mb-2 block text-gray-400">
FinishList({todos.length})
</Text>
{todos.map((item) => (
<View
key={item.id}
className="flex items-center justify-between p-3 border-b border-gray-100 opacity-60"
>
<Text
className="flex-1 line-through"
onClick={() => onToggle(item)}
onLongPress={() => onLongPress(item)}
>
{item.text}
</Text>
<Text
className="text-red-500 text-sm"
onClick={() => onDelete(item.id)}
>
删除
</Text>
</View>
))}
</View>
);
}
2.4 首页容器 src/pages/index/index.tsx
import { View } from "@tarojs/components";
import Taro, { useDidShow, useLoad, usePullDownRefresh } from "@tarojs/taro";
import "./index.css";
import { useState, useEffect } from "react";
import TodoInput from "../../components/TodoInput";
import TodoList from "../../components/TodoList";
import CompletedList from "../../components/CompletedList";
const STORAGE_KEY = "TODO_LIST_DATA";
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
export default function Index() {
const [inputValue, setInputValue] = useState("");
const [editingId, setEditingId] = useState<number | null>(null);
const [editingText, setEditingText] = useState("");
const [todos, setTodos] = useState<TodoItem[]>([]);
const [completedTodos, setCompletedTodos] = useState<TodoItem[]>([]);
useLoad(() => {
loadData();
});
const loadData = () => {
try {
const savedData = Taro.getStorageSync(STORAGE_KEY);
if (savedData) {
setTodos(savedData.todos || []);
setCompletedTodos(savedData.completedTodos || []);
}
} catch (e) {
console.error("读取本地数据失败", e);
}
};
useEffect(() => {
Taro.setStorageSync(STORAGE_KEY, {
todos,
completedTodos,
});
}, [todos, completedTodos]);
usePullDownRefresh(() => {
loadData();
setTimeout(() => {
Taro.stopPullDownRefresh();
Taro.showToast({ title: "刷新成功", icon: "none" });
}, 500);
});
const handleAddTodo = () => {
if (!inputValue.trim()) {
Taro.showToast({ title: "内容不能为空", icon: "none" });
return;
}
const newTodo: TodoItem = {
id: Date.now(),
text: inputValue,
completed: false,
};
setTodos([newTodo, ...todos]);
setInputValue("");
Taro.showToast({ title: "添加成功", icon: "success" });
};
const toggleTodo = (item: TodoItem) => {
if (!item.completed) {
setTodos((prev) => prev.filter((t) => t.id !== item.id));
setCompletedTodos((prev) => [{ ...item, completed: true }, ...prev]);
} else {
setCompletedTodos((prev) => prev.filter((t) => t.id !== item.id));
setTodos((prev) => [{ ...item, completed: false }, ...prev]);
}
};
const startEdit = (item: TodoItem) => {
setEditingId(item.id);
setEditingText(item.text);
};
const saveEdit = () => {
if (!editingText.trim()) {
Taro.showToast({ title: "内容不能为空", icon: "none" });
return;
}
const updateList = (list: TodoItem[]) =>
list.map((t) => (t.id === editingId ? { ...t, text: editingText } : t));
setTodos(updateList(todos));
setCompletedTodos(updateList(completedTodos));
setEditingId(null);
Taro.showToast({ title: "修改成功", icon: "success" });
};
const handleDelete = (id: number, isCompleted: boolean) => {
Taro.showModal({
title: "确认删除",
content: "确定要删除这条待办吗?",
success: (res) => {
if (res.confirm) {
if (isCompleted) {
setCompletedTodos(completedTodos.filter((t) => t.id !== id));
} else {
setTodos(todos.filter((t) => t.id !== id));
}
Taro.showToast({ title: "已删除", icon: "success" });
}
},
});
};
const showActionMenu = (item: TodoItem) => {
const options = [
item.completed ? "撤回到待办" : "标记为完成",
"编辑内容",
"删除任务",
];
Taro.showActionSheet({
itemList: options,
success: (res) => {
switch (res.tapIndex) {
case 0:
toggleTodo(item);
Taro.showToast({
title: item.completed ? "已撤回" : "已完成",
icon: "success",
});
break;
case 1:
startEdit(item);
break;
case 2:
handleDelete(item.id, item.completed);
break;
}
},
});
};
return (
<View className="p-4">
<TodoInput
value={inputValue}
onInput={setInputValue}
onAdd={handleAddTodo}
/>
<TodoList
todos={todos}
editingId={editingId}
editingText={editingText}
onToggle={toggleTodo}
onStartEdit={startEdit}
onEditInput={setEditingText}
onSaveEdit={saveEdit}
onDelete={(id) => handleDelete(id, false)}
onLongPress={showActionMenu}
/>
<CompletedList
todos={completedTodos}
onToggle={toggleTodo}
onDelete={(id) => handleDelete(id, true)}
onLongPress={showActionMenu}
/>
</View>
);
}

Step B 执行方案:交互反馈 API (操作可感知)
1. 核心任务
- ActionSheet 菜单: 长按待办项时调用
Taro.showActionSheet。 - 操作闭环: 在菜单中集成“完成/撤回”、“编辑”和“删除”功能。
- 反馈增强: 确保每个动作(新增、删除、修改、状态切换)都有对应的
Toast提示。
2. 代码实现
文件: src/pages/index/index.tsx
// --- Step B: 长按菜单处理 ---
const showActionMenu = (item: TodoItem) => {
const options = [
item.completed ? "撤回到待办" : "标记为完成",
"编辑内容",
"删除任务"
];
Taro.showActionSheet({
itemList: options,
success: (res) => {
switch (res.tapIndex) {
case 0: // 切换状态
toggleTodo(item);
Taro.showToast({
title: item.completed ? "已撤回" : "已完成",
icon: "success"
});
break;
case 1: // 编辑
startEdit(item);
break;
case 2: // 删除
handleDelete(item.id, item.completed);
break;
}
}
});
};
// --- UI 渲染部分更新 ---
// 在 Text 组件上增加 onLongPress 事件
<Text
className="flex-1"
onClick={() => toggleTodo(item)}
onLongPress={() => showActionMenu(item)} // 关键:长按触发
>
{item.text}
</Text>
3. 验收标准
- Toast 覆盖:
- 新增成功有提示。
- 输入为空有警告提示。
- 删除成功有提示。
- 切换状态(点击或菜单)有提示。
- Modal 确认:
- 点击“删除”按钮或通过菜单选择删除,必须弹出确认框。
- ActionSheet 唤起:
- 长按任何一条待办(无论待办中还是已完成),都能弹出底部菜单。
- 菜单项功能(撤回/完成、编辑、删除)均能正常工作。
更多API参考
tips:配置context7的mcp、对应skill,开发体更佳:Context7 - Up-to-date documentation for LLMs and AI code editors
mcp:
"context7": {
"args": [
"-y",
"@upstash/context7-mcp@latest"
],
"command": "npx"
}
skill:
pnpx playbooks add skill whinc/my-claude-plugins --skill taro-docs
Step C:本地持久化 API(重启不丢)
目标
- 掌握小程序本地存储读写。
极简实现
- 初始化读取
TODO_LIST_V2。 - 每次列表变更立即写回。
API 体验点(Context7 官方)
Taro.getStorageSyncTaro.setStorageSync
验收
- 关闭开发者工具再打开,数据仍在。
Step D:页面生命周期与刷新 API
目标
- 体验“页面级”而非“组件级”的小程序能力。
极简实现
- 开启
enablePullDownRefresh: true。 - 下拉刷新后重读本地数据,并停止动画。
- 打印页面显示/隐藏日志。
API 体验点(Context7 官方)
usePullDownRefreshTaro.stopPullDownRefreshuseDidShow/useDidHide/useLoad
验收
- 下拉可触发刷新且动画正常结束。
- 能说清 3 个生命周期触发场景。
Step E:滚动与触底 API(列表场景常用)
目标
- 体验列表页面高频 API:滚动监听、触底加载。
极简实现
- 用 mock 数据做分页(每页 10 条)。
- 触底追加下一页。
- 顶部显示实时滚动位置。
API 体验点(Context7 官方)
useReachBottomusePageScroll
验收
- 触底自动追加数据。
- 页面显示
scrollTop实时变化。
Step F:页面视觉与节点查询 API
目标
- 体验运行时查询节点信息的能力。
极简实现
- 页面 ready 后测量 Todo 容器高度,并展示在页面上。
API 体验点(Context7 官方)
useReadyTaro.createSelectorQuery().select(...).boundingClientRect().exec()
验收
- 可拿到容器高度并渲染。
Step G:设备能力小实验(轻量增强)
目标
- 在不破坏主流程的前提下,体验设备能力 API。
极简实现
- 点击“复制内容”按钮:复制当前 Todo 文本。
- 点击“网络状态”按钮:读取当前网络类型。
- 新增成功后轻震动(可选,真机体验更明显)。
API 体验点(Context7 官方)
Taro.setClipboardData/Taro.getClipboardDataTaro.getNetworkTypeTaro.vibrateShort
验收
- 复制、读网络、轻震动均可触发。
Step H:导航栏与状态提示 API
目标
- 体验页面壳层相关 API(标题、角标)。
极简实现
- 动态设置标题:
待办(n)。 - 当存在已完成项时显示角标,否则移除角标。
API 体验点(Context7 官方)
Taro.setNavigationBarTitleTaro.setTabBarBadge/Taro.removeTabBarBadge(仅 tabBar 页面生效)
验收
- 标题随数据变化。
- 满足条件时角标正确显示/移除。
Step I:网络请求 API(可选扩展)
目标
- 体验请求链路,但不把项目复杂化。
极简实现
- 用
Taro.request拉一组远程 mock todos(或静态 JSON)。 - 提供“同步远程示例数据”按钮。
API 体验点(Context7 官方)
Taro.request
验收
- 可发起请求并将返回结果映射为 Todo。
Step J:媒体处理 API(图片与视频)
目标
- 掌握小程序中最常用的媒体选择与保存能力。
实现任务
- 图片:
chooseImage选择图片并预览,saveImageToPhotosAlbum保存图片到相册。 - 视频:
chooseVideo(或chooseMedia) 选择视频,saveVideoToPhotosAlbum保存视频。 - 练习权限处理:保存到相册需要
scope.writePhotosAlbum权限。
重点 API(官方)
Taro.chooseImage/Taro.chooseVideo/Taro.chooseMediaTaro.saveImageToPhotosAlbum/Taro.saveVideoToPhotosAlbumTaro.getSetting/Taro.authorize(权限申请)
验收标准
- 能成功唤起系统相册选择资源。
- 能将临时文件(tempFilePath)成功保存至手机本地相册。
Step K:文件系统与上传(File System)
目标
- 理解小程序临时文件、持久化文件与远程上传流程。
实现任务
- 文件保存:使用
Taro.saveFile将临时文件转为小程序本地持久化文件。 - 文件上传:使用
Taro.uploadFile将选择的媒体文件发送至服务端。 - 文件信息:使用
Taro.getFileInfo获取文件大小、摘要(MD5)等。
重点 API(官方)
Taro.saveFileTaro.uploadFileTaro.getFileInfo/Taro.getSavedFileInfo
验收标准
- 选择文件后能看到上传进度或成功日志。
- 能在小程序本地存储中管理已下载或保存的文件。
建议执行顺序(学习效率优先)
A -> B -> C -> D -> E -> F -> G -> H -> I(可选) -> J -> K
每步记录模板(建议)
- 我用了哪个 API:
- 它解决了什么问题:
- 我踩到的坑:
- 如果重写一次,我会怎么改:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)