Taro 4.x 小程序开发技能
面向 Taro 4.3 + React 18 + TypeScript + @tarojs/components(截至本次核对,npm 上 @tarojs/cli 最新稳定版就是 4.3.0)。
三条版本底线,先记住再写代码:
- Node.js ≥ 18(
@tarojs/cli@4.3.0的engines.node就是>= 18)。 - React 只能是 18:
@tarojs/react@4.3.0与@tarojs/plugin-framework-react@4.3.0的 peer 都是react ^18,上 React 19 会在安装期或构建期炸。 - 不要再加
NODE_OPTIONS=--openssl-legacy-provider:那是 webpack4/md4 时代的历史包袱,Taro 4 + webpack5 在 Node 24 下直接构建通过。
初始化
# 装 CLI(npm / yarn / pnpm 任选,下面以 pnpm 为例)
pnpm add -g @tarojs/cli
# 用官方模板创建(交互式:框架 / TS / CSS 预处理器 / 包管理器)
taro init myApp
npx @tarojs/cli init myApp # 不想全局安装时
# 或用本技能自带的 4.3 模板骨架(含 services 层、Mock 开关、示例页面)
bash scripts/init_project.sh my-miniprogram
taro init 会按 yarn > cnpm > npm 探测并自动装依赖;装失败再进目录手动 pnpm install。
pnpm 10+ 的硬门槛:默认拒绝执行依赖的 postinstall,pnpm 12 直接让
pnpm install以ERR_PNPM_IGNORED_BUILDS失败。Taro 的@tarojs/cli/@tarojs/binding/@swc/core/esbuild/core-js都要跑 postinstall,必须放行:pnpm approve-builds(交互式,自动写回配置),或在pnpm-workspace.yaml写allowBuilds: { '@tarojs/cli': true, ... }—— pnpm 12 的键名是allowBuilds,pnpm 10 那个onlyBuiltDependencies已改名。模板已带这份白名单(实测pnpm install退出码 0);npm / yarn 项目不需要这个文件。
命令
| 命令 | 说明 |
|------|------|
| pnpm dev:weapp | = taro build --type weapp --watch,微信开发者工具导入 dist/ |
| pnpm build:weapp | 微信小程序生产构建 |
| pnpm dev:h5 / build:h5 | 同一份代码跑浏览器 |
| taro build --type <t> | 目标:weapp swan alipay tt qq jd h5 rn |
| taro build --type weapp --pages pages/a/index | 只编指定页面,大项目调试提速 |
| taro build --type weapp --blended / --new-blended | 混编:Taro 页面嵌进原生小程序工程 |
| taro build --plugin weapp | 小程序插件工程 |
| taro create --name=xxx --type=page | 由 @tarojs/plugin-generator 生成页面骨架 |
| taro info / taro doctor / taro inspect | 环境信息 / 项目体检 / 导出 webpack 配置 |
目录结构
config/index.ts # defineConfig<'webpack5'>,designWidth / mini / h5
types/global.d.ts # /// <reference types="@tarojs/taro" /> + 图片模块声明
babel.config.js # 仅 webpack 编译器需要;babel-preset-taro { framework: 'react', ts: true }
src/
├── app.config.ts # defineAppConfig:pages / window / tabBar
├── app.tsx # 函数组件入口,返回 children
├── index.html # H5 端模板
├── styles/global.scss # 设计系统 token
├── pages/{module}/{page}/index.tsx + index.scss + index.config.ts
└── services/{api,utils}/ + mock.ts
编译配置(config/index.ts)
import { defineConfig, type UserConfigExport } from '@tarojs/cli';
export default defineConfig<'webpack5'>(async () => ({
sourceRoot: 'src',
outputRoot: 'dist',
framework: 'react',
compiler: 'webpack5', // 'webpack4' | 'webpack5' | 'vite'(vite 自 Taro 4.0.0 起支持)
designWidth: 750, // 见下方「px → rpx」
deviceRatio: { 375: 2, 640: 2.34 / 2, 750: 1, 828: 1.81 / 2 },
mini: { postcss: { pxtransform: { enable: true, config: {} } } }, // 小程序端配置键是 mini,早期按平台写 weapp 的写法已不再使用
h5: { publicPath: '/', devServer: { port: 10086 } },
}));
px → rpx:designWidth: 750 时源码 1px → 1rpx(按 750 设计稿直接量);designWidth: 375 时 1px → 2rpx(按 CSS 像素写)。两种都不能混用——本技能模板走 375,仓库里的 apps/miniprogram 走 750,改基线要连带改所有尺寸。
页面写法
新代码一律函数组件 + hooks(类组件的生命周期仍被支持,但别在新增页面里用):
import { Text, View } from '@tarojs/components';
import Taro, { useRouter, useDidShow, usePullDownRefresh } from '@tarojs/taro';
import { useState } from 'react';
import './index.scss';
export default function TaskDetailPage() {
const { params } = useRouter(); // 替代 3.x 的 this.$router.params
const [loading, setLoading] = useState(true);
useDidShow(() => {
// 每次页面显示刷新
});
usePullDownRefresh(async () => {
// TODO: 换成真实请求,例如 await getTaskDetail(params.taskId)
setLoading(false);
Taro.stopPullDownRefresh(); // 必须手动收起
});
return (
<View className="page">
<Text>{loading ? '加载中...' : `任务 ${params.taskId ?? ''}`}</Text>
</View>
);
}
// pages/task/detail/index.config.ts —— 页面配置是 .config.ts + definePageConfig
export default definePageConfig({
navigationBarTitleText: '任务详情',
enablePullDownRefresh: true,
});
细节(路由 API、TabBar 片段、分包、生命周期对照表)见 references/pages.md。
API 一律走 Taro.*
不要出现裸 wx.:H5/RN 端直接失效,且绕开 Taro 的类型。这是硬约束,不是风格问题。
import Taro from '@tarojs/taro';
Taro.setStorageSync('token', token);
const token = Taro.getStorageSync<string>('token');
await Taro.navigateTo({ url: '/pages/home/index' });
需要平台分支时用 process.env.TARO_ENV('weapp' | 'h5' | 'rn' | ...),别用 typeof wx !== 'undefined'。
请求层与 Mock
// services/utils/request.ts —— 切真实后端只改这两行
const IS_MOCK = true;
const BASE_URL = 'https://api.example.com';
// services/api/index.ts
import { get, post } from '../utils/request';
export const getUserInfo = () => get<UserInfo>('/api/auth/user-info');
响应约定、鉴权头、模块划分见 references/api-design.md。
流式接口(SSE)在小程序端要 Taro.request({ enableChunked: true }) + onChunkReceived,回调给的是 ArrayBuffer 且可能在多字节字符中间切断,必须增量解码:
decoder ??= new TextDecoder('utf-8');
buffer += decoder.decode(res.data as ArrayBuffer, { stream: true }); // 不带 stream:true 中文必乱码
设计系统
见 references/design-system.md。里面那套粉色是示例品牌,换项目请整组替换 token,不要只改一两个色值。
编译与预览
微信开发者工具导入 dist/,AppID 先用测试号 touristappid,开发期勾选「不校验合法域名」。
project.config.json的miniprogramRoot指向dist/,别改到根目录。src/sitemap.json不会被自动拷进dist/,配了sitemapLocation就必须用copy.patterns显式拷贝(from以src/开头、to以dist/开头,from不支持 glob),否则开发者工具会报文件不存在。tabBar的iconPath必须是真实存在的图片文件,缺文件开发者工具直接报错——这也是本技能模板默认不配 tabBar 的原因,需要时按 references/pages.md 补。dist/与node_modules/要进.gitignore;dist/里的project.config.json每次构建会被覆盖。
真机相关坑(跨端不一致的高发区)
- 自定义导航栏(
navigationStyle: 'custom')要自己算状态栏与胶囊按钮:
const { statusBarHeight = 0, windowWidth } = Taro.getWindowInfo();
const menu = Taro.getMenuButtonBoundingClientRect();
const navbarHeight = (menu.top - statusBarHeight) * 2 + menu.height;
const safeRight = windowWidth - menu.left + 8;
ScrollView的scrollTop是受控属性:不跟onScroll里回写真实值,重渲染时会把上一次的旧目标值重新应用,表现为「点开折叠区就跳回底部」。- H5 端 Taro 组件的真实类名带前缀(
Textarea渲染成taro-textarea、Button成taro-button-core,已在@tarojs/components产物里核对),样式要打到这一层,只写「自定义类名 + 原生标签」的组合会被它自己的默认样式压掉。 disabled={false}在 H5 会渲染成disabled="false"属性,CSS 的[disabled]选择器照样命中,置灰态请用显式的 class 判断,别用属性选择器。Button有默认背景/边框/字号,重写时同时清button与[role="button"],并补::after { border: none }。useShareAppMessage/useShareTimeline只在微信端生效,H5 分支要给降级入口,别把分享按钮做成永久高亮却什么都不发生。- 4.3 的
TARO_ENV类型里出现了harmony/jdrn,但taro build --type的可选项仍是weapp/swan/alipay/tt/qq/jd/h5/rn,别在代码里假定它们能构建。
从 Taro 3 迁过来的破坏性改动
| 3.x 写法 | 4.x 写法 |
|----------|----------|
| NODE_OPTIONS=--openssl-legacy-provider | 删掉 |
| 配置里的 weapp: {} | mini: {} |
| index.config.js / app.config.js | index.config.ts + definePageConfig / defineAppConfig |
| class Page extends Component + componentDidShow | 函数组件 + useDidShow / useLoad |
| (this as any).$router.params | useRouter().params |
| wx.request / wx.setStorageSync | Taro.request / Taro.setStorageSync |
| @tarojs/cli init . --framework react | taro init myApp(交互式,含 CSS 预处理器与包管理器选择) |
Scan to join WeChat group