Taro跨端开发各阶段具体实现(React版)
以下是基于React+Taro 3.x的全阶段可落地实现方案,包含完整代码示例、操作命令、验证步骤,所有代码均可直接复制运行,适配微信小程序+H5双端。
前置环境验证
先确认环境符合要求,执行以下命令:
bash
# 验证Node版本(v14+)
node -v # 输出如 v16.18.0
# 验证npm版本
npm -v # 输出如 8.19.2
# 验证Taro CLI版本(v3+)
taro -v # 输出如 taro/cli: 3.6.18
# 配置淘宝镜像(加速依赖安装)
npm config set registry https://registry.npmmirror.com
阶段一:基础准备 - 具体实现
步骤1:初始化项目
bash
# 创建项目(命名myFirstTaro)
taro init myFirstTaro
# 初始化时的交互选择(按回车确认):
# ? 请输入项目介绍!→ (直接回车)
# ? 请选择框架 React
# ? 请选择CSS预处理器 Sass
# ? 请选择模板源 github
# ? 请选择模板 default
# ? 请选择包管理器 npm
# 进入项目目录
cd myFirstTaro
# 安装依赖
npm install
步骤2:解读初始项目结构(核心文件)
| 文件/目录 | 作用 |
|---|---|
src/app.ts |
项目入口文件,全局配置/生命周期 |
src/app.config.ts |
全局配置(对应小程序app.json),配置页面路由、窗口样式等 |
src/pages/index |
首页目录,包含页面逻辑(index.tsx)、样式(index.scss)、配置(index.config.ts) |
config/index.ts |
Taro编译配置,自定义多端打包规则 |
步骤3:实现"显示当前时间"的自定义页面
3.1 添加新页面路由
修改src/app.config.ts,添加time页面路由:
typescript
export default defineAppConfig({
pages: [
'pages/index/index', // 原有首页
'pages/time/time' // 新增时间页面
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'Taro学习',
navigationBarTextStyle: 'black'
}
})
3.2 创建时间页面文件
在src/pages下新建time目录,创建3个文件:
time.config.ts(页面配置)time.tsx(页面逻辑)time.scss(页面样式)
文件1:time.config.ts
typescript
export default definePageConfig({
navigationBarTitleText: '当前时间'
})
文件2:time.tsx(核心逻辑)
typescript
import { useState, useEffect } from 'react'
import { View, Text } from '@tarojs/components'
import './time.scss'
export default function TimePage() {
// 定义状态存储当前时间
const [currentTime, setCurrentTime] = useState('')
// 生命周期:页面加载时启动定时器
useEffect(() => {
// 更新当前时间
const updateTime = () => {
const now = new Date()
setCurrentTime(now.toLocaleString('zh-CN'))
}
// 初始化执行一次
updateTime()
// 每秒更新一次
const timer = setInterval(updateTime, 1000)
// 页面卸载时清除定时器(防止内存泄漏)
return () => clearInterval(timer)
}, [])
return (
<View className="time-page">
<Text className="title">当前时间</Text>
<Text className="time-text">{currentTime}</Text>
</View>
)
}
文件3:time.scss(样式)
scss
.time-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
.title {
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
.time-text {
font-size: 28rpx;
color: #666;
}
}
步骤4:运行项目验证
4.1 微信小程序端
bash
# 启动微信小程序开发服务(实时编译)
taro build --type weapp --watch
- 打开微信开发者工具 → 导入项目 → 选择
myFirstTaro/dist目录 → 进入"当前时间"页面,能看到每秒刷新的时间。
4.2 H5端
bash
# 启动H5开发服务(新开终端)
taro build --type h5 --watch
- 浏览器访问
http://localhost:10086→ 点击底部导航(或手动输入路由/pages/time/time),验证时间显示正常。
阶段二:项目实战 - 具体实现
目标:实现"简易待办事项"demo,包含组件封装、状态管理、多页面通信
步骤1:封装通用UI组件
在src下新建components目录,创建3个通用组件:
1.1 按钮组件(Button/index.tsx)
typescript
import { FC, ReactNode } from 'react'
import { Button as TaroButton } from '@tarojs/components'
import './index.scss'
// 定义组件Props类型
interface MyButtonProps {
text: ReactNode; // 按钮文字
type?: 'primary' | 'default' | 'danger'; // 按钮类型
size?: 'large' | 'middle' | 'small'; // 按钮尺寸
onClick?: () => void; // 点击事件
}
// 通用按钮组件
const MyButton: FC<MyButtonProps> = ({
text,
type = 'default',
size = 'middle',
onClick
}) => {
return (
<TaroButton
className={`my-button my-button--${type} my-button--${size}`}
onClick={onClick}
>
{text}
</TaroButton>
)
}
export default MyButton
1.2 按钮样式(Button/index.scss)
scss
.my-button {
border-radius: 8rpx;
border: none;
// 类型样式
&--primary {
background-color: #1677ff;
color: #fff;
}
&--default {
background-color: #f5f5f5;
color: #333;
}
&--danger {
background-color: #ff4d4f;
color: #fff;
}
// 尺寸样式
&--large {
width: 100%;
height: 88rpx;
font-size: 32rpx;
}
&--middle {
width: 200rpx;
height: 68rpx;
font-size: 28rpx;
}
&--small {
width: 120rpx;
height: 48rpx;
font-size: 24rpx;
}
}
1.3 输入组件(Input/index.tsx)
typescript
import { FC, useState } from 'react'
import { View, Input as TaroInput, Text } from '@tarojs/components'
import './index.scss'
interface MyInputProps {
placeholder?: string;
value: string;
onChange: (val: string) => void;
label?: string; // 输入框标签
required?: boolean; // 是否必填
}
const MyInput: FC<MyInputProps> = ({
placeholder = '请输入内容',
value,
onChange,
label,
required = false
}) => {
return (
<View className="my-input-wrapper">
{label && (
<Text className="my-input-label">
{label}
{required && <Text className="required">*</Text>}
</Text>
)}
<TaroInput
className="my-input"
placeholder={placeholder}
value={value}
onInput={(e) => onChange(e.detail.value)}
/>
</View>
)
}
export default MyInput
1.4 输入组件样式(Input/index.scss)
scss
.my-input-wrapper {
width: 100%;
margin-bottom: 20rpx;
.my-input-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
.required {
color: #ff4d4f;
margin-left: 4rpx;
}
}
.my-input {
width: 100%;
height: 72rpx;
padding: 0 20rpx;
border: 1px solid #e5e5e5;
border-radius: 8rpx;
font-size: 28rpx;
}
}
1.5 卡片组件(Card/index.tsx)
typescript
import { FC, ReactNode } from 'react'
import { View, Text } from '@tarojs/components'
import './index.scss'
interface MyCardProps {
title?: ReactNode;
content: ReactNode;
extra?: ReactNode; // 右侧额外内容
}
const MyCard: FC<MyCardProps> = ({
title,
content,
extra
}) => {
return (
<View className="my-card">
{title && <Text className="my-card__title">{title}</Text>}
<View className="my-card__content">{content}</View>
{extra && <Text className="my-card__extra">{extra}</Text>}
</View>
)
}
export default MyCard
1.6 卡片样式(Card/index.scss)
scss
.my-card {
background-color: #fff;
border-radius: 8rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
&__title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
&__content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
&__extra {
font-size: 26rpx;
color: #1677ff;
margin-top: 12rpx;
text-align: right;
}
}
步骤2:状态管理(内置provide/inject)
2.1 创建全局状态上下文(src/contexts/TodoContext.tsx)
typescript
import { createContext, useContext, useState, ReactNode } from 'react'
// 定义待办项类型
export interface TodoItem {
id: string;
content: string;
createTime: string;
completed: boolean;
}
// 定义上下文类型
interface TodoContextType {
todos: TodoItem[];
addTodo: (content: string) => void;
deleteTodo: (id: string) => void;
toggleTodo: (id: string) => void;
}
// 创建上下文(默认值为null,使用时需判断)
const TodoContext = createContext<TodoContextType | null>(null)
// 自定义Provider组件
export const TodoProvider = ({ children }: { children: ReactNode }) => {
// 初始化待办列表(从本地存储读取)
const [todos, setTodos] = useState<TodoItem[]>(() => {
const storedTodos = Taro.getStorageSync('todos') || []
return storedTodos
})
// 添加待办
const addTodo = (content: string) => {
if (!content.trim()) return
const newTodo: TodoItem = {
id: Date.now().toString(), // 用时间戳作为唯一ID
content,
createTime: new Date().toLocaleString('zh-CN'),
completed: false
}
const newTodos = [...todos, newTodo]
setTodos(newTodos)
// 持久化到本地存储
Taro.setStorageSync('todos', newTodos)
}
// 删除待办
const deleteTodo = (id: string) => {
const newTodos = todos.filter(todo => todo.id !== id)
setTodos(newTodos)
Taro.setStorageSync('todos', newTodos)
}
// 切换待办完成状态
const toggleTodo = (id: string) => {
const newTodos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
setTodos(newTodos)
Taro.setStorageSync('todos', newTodos)
}
return (
<TodoContext.Provider value={{ todos, addTodo, deleteTodo, toggleTodo }}>
{children}
</TodoContext.Provider>
)
}
// 自定义Hook,简化上下文调用
export const useTodo = () => {
const context = useContext(TodoContext)
if (!context) {
throw new Error('useTodo must be used within a TodoProvider')
}
return context
}
2.2 全局注入Provider(修改src/app.tsx)
typescript
import { Component } from 'react'
import { TodoProvider } from './contexts/TodoContext'
import './app.scss'
class App extends Component {
componentDidMount() {}
componentDidShow() {}
componentDidHide() {}
// 在App根组件中注入TodoProvider,让所有页面都能访问状态
render() {
return (
<TodoProvider>
{this.props.children}
</TodoProvider>
)
}
}
export default App
步骤3:实现待办列表页面(src/pages/todo/list.tsx)
typescript
import { View, Text, ScrollView } from '@tarojs/components'
import MyCard from '@/components/Card'
import MyButton from '@/components/Button'
import { useTodo } from '@/contexts/TodoContext'
import Taro from '@tarojs/taro'
import './list.scss'
export default function TodoList() {
const { todos, deleteTodo, toggleTodo } = useTodo()
// 跳转到添加待办页面
const goToAdd = () => {
Taro.navigateTo({ url: '/pages/todo/add' })
}
// 渲染待办列表
const renderTodos = () => {
if (todos.length === 0) {
return <Text className="empty-tip">暂无待办事项,点击添加吧~</Text>
}
return todos.map(todo => (
<MyCard
key={todo.id}
title={todo.content}
content={`创建时间:${todo.createTime}`}
extra={
<View className="todo-actions">
<MyButton
text={todo.completed ? '未完成' : '已完成'}
type={todo.completed ? 'default' : 'primary'}
size="small"
onClick={() => toggleTodo(todo.id)}
/>
<MyButton
text="删除"
type="danger"
size="small"
onClick={() => deleteTodo(todo.id)}
style={{ marginLeft: 10 }}
/>
</View>
}
/>
))
}
return (
<View className="todo-list-page">
<MyButton
text="添加待办"
type="primary"
size="large"
onClick={goToAdd}
style={{ marginBottom: 20 }}
/>
<ScrollView className="todo-list" scrollY>
{renderTodos()}
</ScrollView>
</View>
)
}
列表页面样式(list.scss)
scss
.todo-list-page {
padding: 20rpx;
min-height: 100vh;
background-color: #f8f8f8;
.todo-list {
height: calc(100vh - 120rpx);
}
.empty-tip {
display: block;
text-align: center;
font-size: 28rpx;
color: #999;
margin-top: 100rpx;
}
.todo-actions {
display: flex;
justify-content: flex-end;
}
}
步骤4:实现添加待办页面(src/pages/todo/add.tsx)
typescript
import { useState } from 'react'
import { View } from '@tarojs/components'
import MyInput from '@/components/Input'
import MyButton from '@/components/Button'
import { useTodo } from '@/contexts/TodoContext'
import Taro from '@tarojs/taro'
import './add.scss'
export default function AddTodo() {
const [content, setContent] = useState('')
const { addTodo } = useTodo()
// 提交待办
const handleSubmit = () => {
addTodo(content)
// 清空输入框
setContent('')
// 返回列表页
Taro.navigateBack()
// 提示成功
Taro.showToast({
title: '添加成功',
icon: 'success'
})
}
return (
<View className="add-todo-page">
<MyInput
label="待办内容"
required
placeholder="请输入待办事项"
value={content}
onChange={setContent}
/>
<MyButton
text="提交"
type="primary"
size="large"
onClick={handleSubmit}
/>
</View>
)
}
添加页面样式(add.scss)
scss
.add-todo-page {
padding: 20rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
步骤5:配置待办页面路由(修改src/app.config.ts)
typescript
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/time/time',
'pages/todo/list', // 待办列表
'pages/todo/add' // 添加待办
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'Taro待办',
navigationBarTextStyle: 'black'
}
})
步骤6:验证待办功能
- 重启开发服务:
taro build --type weapp --watch - 微信开发者工具中进入
/pages/todo/list页面:- 点击"添加待办"→ 输入内容→ 提交 → 列表显示新待办;
- 点击"已完成/未完成"→ 状态切换;
- 点击"删除"→ 待办项移除;
- 刷新页面 → 待办数据从本地存储加载,不丢失。
阶段三:进阶能力 - 具体实现
步骤1:调用Taro原生API(添加"上传图片"功能)
1.1 修改添加待办页面(add.tsx),增加图片上传
typescript
import { useState } from 'react'
import { View, Image } from '@tarojs/components'
import MyInput from '@/components/Input'
import MyButton from '@/components/Button'
import { useTodo } from '@/contexts/TodoContext'
import Taro from '@tarojs/taro'
import './add.scss'
export default function AddTodo() {
const [content, setContent] = useState('')
const [imageUrl, setImageUrl] = useState('') // 存储图片路径
const { addTodo } = useTodo()
// 选择图片
const chooseImage = () => {
Taro.chooseImage({
count: 1, // 最多选1张
sizeType: ['original', 'compressed'], // 原图/压缩图
sourceType: ['album', 'camera'], // 相册/相机
success: (res) => {
// 小程序端返回临时文件路径
setImageUrl(res.tempFilePaths[0])
},
fail: (err) => {
Taro.showToast({
title: '选择图片失败',
icon: 'none'
})
console.error('选择图片失败:', err)
}
})
}
// 提交待办(含图片)
const handleSubmit = () => {
// 扩展待办项,增加图片字段
addTodo(`${content} ${imageUrl ? '[图片]' : ''}`)
// 实际项目中可存储图片路径,这里简化显示
setContent('')
setImageUrl('')
Taro.navigateBack()
Taro.showToast({
title: '添加成功',
icon: 'success'
})
}
return (
<View className="add-todo-page">
<MyInput
label="待办内容"
required
placeholder="请输入待办事项"
value={content}
onChange={setContent}
/>
{/* 图片上传区域 */}
<View className="image-upload">
<MyButton
text="选择图片"
type="default"
size="middle"
onClick={chooseImage}
style={{ marginBottom: 10 }}
/>
{imageUrl && (
<Image
src={imageUrl}
className="preview-image"
mode="widthFix"
/>
)}
</View>
<MyButton
text="提交"
type="primary"
size="large"
onClick={handleSubmit}
/>
</View>
)
}
1.2 添加图片样式(add.scss)
scss
.add-todo-page {
padding: 20rpx;
background-color: #f8f8f8;
min-height: 100vh;
.image-upload {
margin-bottom: 20rpx;
}
.preview-image {
width: 200rpx;
height: auto;
border-radius: 8rpx;
margin-top: 10rpx;
}
}
步骤2:多端条件编译(区分小程序/H5显示不同按钮)
修改待办列表页面(list.tsx),添加"分享"按钮(仅小程序显示):
typescript
// 新增:引入环境变量
import { ENV_TYPE, getEnv } from '@tarojs/taro'
// 在renderTodos中修改extra部分:
extra={
<View className="todo-actions">
<MyButton
text={todo.completed ? '未完成' : '已完成'}
type={todo.completed ? 'default' : 'primary'}
size="small"
onClick={() => toggleTodo(todo.id)}
/>
<MyButton
text="删除"
type="danger"
size="small"
onClick={() => deleteTodo(todo.id)}
style={{ marginLeft: 10 }}
/>
{/* 条件编译:仅小程序显示分享按钮 */}
{getEnv() === ENV_TYPE.WEAPP && (
<MyButton
text="分享"
type="default"
size="small"
onClick={() => {
Taro.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
}}
style={{ marginLeft: 10 }}
/>
)}
</View>
}
步骤3:性能优化
3.1 包体积分析
-
安装分析插件:
bashnpm install webpack-bundle-analyzer --save-dev -
修改
config/index.ts,添加打包分析配置:typescriptimport { defineConfig } from '@tarojs/cli' import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' export default defineConfig({ // 原有配置... mini: { webpackChain(chain, webpack) { // 仅生产环境分析包体积 if (process.env.NODE_ENV === 'production') { chain.plugin('bundle-analyzer').use(BundleAnalyzerPlugin, [ { analyzerMode: 'static', // 生成静态html文件 openAnalyzer: false, // 不自动打开浏览器 reportFilename: 'bundle-report.html' // 报告文件名 } ]) } } }, h5: { webpackChain(chain, webpack) { if (process.env.NODE_ENV === 'production') { chain.plugin('bundle-analyzer').use(BundleAnalyzerPlugin, [ { analyzerMode: 'static', openAnalyzer: false, reportFilename: 'h5-bundle-report.html' } ]) } } } }) -
执行生产打包,生成分析报告:
bashtaro build --type weapp -
打开
dist/bundle-report.html,查看体积过大的依赖,例如:- 移除无用依赖:删除
package.json中未使用的包,执行npm uninstall 包名; - 图片压缩:使用tinypng压缩项目中的图片资源。
- 移除无用依赖:删除
3.2 按需加载(路由懒加载)
修改src/app.config.ts,开启路由懒加载:
typescript
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/time/time',
'pages/todo/list',
'pages/todo/add'
],
window: {
// 原有配置...
},
// 新增:开启路由懒加载
lazyCodeLoading: 'requiredComponents'
})
3.3 渲染优化(避免列表重复渲染)
修改待办列表页面(list.tsx),使用React.memo优化组件:
typescript
import { memo } from 'react' // 新增
// 提取待办项为独立组件,并使用memo包裹
const TodoItem = memo(({ todo, toggleTodo, deleteTodo }: {
todo: TodoItem;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
}) => {
return (
<MyCard
key={todo.id}
title={todo.content}
content={`创建时间:${todo.createTime}`}
extra={
<View className="todo-actions">
<MyButton
text={todo.completed ? '未完成' : '已完成'}
type={todo.completed ? 'default' : 'primary'}
size="small"
onClick={() => toggleTodo(todo.id)}
/>
<MyButton
text="删除"
type="danger"
size="small"
onClick={() => deleteTodo(todo.id)}
style={{ marginLeft: 10 }}
/>
{getEnv() === ENV_TYPE.WEAPP && (
<MyButton
text="分享"
type="default"
size="small"
onClick={() => {
Taro.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
}}
style={{ marginLeft: 10 }}
/>
)}
</View>
}
/>
)
})
// 渲染列表时使用TodoItem组件
const renderTodos = () => {
if (todos.length === 0) {
return <Text className="empty-tip">暂无待办事项,点击添加吧~</Text>
}
return todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
/>
))
}
阶段四:工程化 - 具体实现
步骤1:深度定制config/index.ts
typescript
import { defineConfig } from '@tarojs/cli'
import path from 'path'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
export default defineConfig({
projectName: 'myFirstTaro',
date: '2025-12-18',
designWidth: 750, // 设计稿宽度(750rpx = 屏幕宽度)
deviceRatio: {
640: 2.34 / 2,
750: 1,
828: 1.81 / 2
},
sourceRoot: 'src',
outputRoot: `dist/${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'}`, // 区分开发/生产输出目录
plugins: [],
alias: {
// 配置路径别名,简化导入
'@': path.resolve(__dirname, '..', 'src'),
'@components': path.resolve(__dirname, '..', 'src/components'),
'@contexts': path.resolve(__dirname, '..', 'src/contexts')
},
mini: {
postcss: {
pxtransform: {
enable: true,
config: {}
},
url: {
enable: true,
config: {
limit: 1024 // 小于1kb的图片转base64
}
},
cssModules: {
enable: false, // 关闭CSS Modules(如需开启,需配置命名规则)
config: {
namingPattern: 'module',
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
},
webpackChain(chain, webpack) {
// 生产环境包体积分析
if (process.env.NODE_ENV === 'production') {
chain.plugin('bundle-analyzer').use(BundleAnalyzerPlugin, [
{
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
}
])
}
// 代码分割
chain.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
}
}
})
}
},
h5: {
publicPath: '/',
staticDirectory: 'static',
postcss: {
autoprefixer: {
enable: true,
config: {}
},
cssModules: {
enable: false,
config: {
namingPattern: 'module',
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
},
webpackChain(chain, webpack) {
if (process.env.NODE_ENV === 'production') {
chain.plugin('bundle-analyzer').use(BundleAnalyzerPlugin, [
{
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'h5-bundle-report.html'
}
])
}
}
}
})
步骤2:多环境配置
2.1 创建环境变量文件
-
根目录新建
.env.development(开发环境):envTARO_ENV=development REACT_APP_API_BASE_URL=https://test-api.example.com -
根目录新建
.env.production(生产环境):envTARO_ENV=production REACT_APP_API_BASE_URL=https://api.example.com
2.2 封装环境变量工具(src/utils/env.ts)
typescript
// 环境变量工具类
export const env = {
// 当前环境
mode: process.env.NODE_ENV || 'development',
// API基础地址
apiBaseUrl: process.env.REACT_APP_API_BASE_URL || '',
// 是否为开发环境
isDev: process.env.NODE_ENV === 'development',
// 是否为生产环境
isProd: process.env.NODE_ENV === 'production'
}
// 示例:请求封装
export const request = (url: string, options = {}) => {
const fullUrl = `${env.apiBaseUrl}${url}`
return Taro.request({
url: fullUrl,
...options,
header: {
'Content-Type': 'application/json',
...options.header
}
})
}
2.3 使用环境变量(示例:待办列表页面)
typescript
import { env } from '@/utils/env'
// 在组件中打印环境变量
console.log('当前环境:', env.mode)
console.log('API地址:', env.apiBaseUrl)
步骤3:代码规范配置
3.1 安装ESLint/Prettier依赖
bash
npm install eslint prettier eslint-config-prettier eslint-plugin-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser --save-dev
3.2 根目录新建.eslintrc.js
javascript
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'prettier'
],
rules: {
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn'
},
env: {
browser: true,
node: true,
es6: true
}
}
3.3 根目录新建.prettierrc.js
javascript
module.exports = {
printWidth: 100, // 每行代码长度
tabWidth: 2, // 缩进位数
useTabs: false, // 使用空格而非tab缩进
semi: true, // 语句末尾添加分号
singleQuote: true, // 使用单引号
quoteProps: 'as-needed', // 仅在必要时为对象属性添加引号
jsxSingleQuote: true, // JSX中使用单引号
trailingComma: 'es5', // 尾随逗号
bracketSpacing: true, // 对象字面量的大括号之间添加空格
bracketSameLine: false, // 标签的闭合括号不换行
arrowParens: 'avoid', // 箭头函数参数仅在必要时添加括号
endOfLine: 'lf' // 行结束符
}
步骤4:单元测试示例
4.1 安装测试依赖
bash
npm install jest @tarojs/test-utils @testing-library/react react-test-renderer --save-dev
4.2 测试待办添加功能(src/contexts/tests/TodoContext.test.tsx)
typescript
import { render, act } from '@testing-library/react'
import { TodoProvider, useTodo } from '../TodoContext'
// 测试组件
const TestComponent = () => {
const { addTodo, todos } = useTodo()
return (
<div>
<button onClick={() => addTodo('测试待办')}>添加</button>
<span>{todos.length}</span>
</div>
)
}
describe('TodoContext', () => {
// 测试添加待办
it('should add todo correctly', () => {
const { getByText } = render(
<TodoProvider>
<TestComponent />
</TodoProvider>
)
// 初始待办数量为0
expect(getByText('0')).toBeTruthy()
// 点击添加按钮
act(() => {
getByText('添加').click()
})
// 待办数量变为1
expect(getByText('1')).toBeTruthy()
})
})
4.3 运行测试
bash
npx jest
最终验证与交付
- 运行所有环境验证:
- 开发环境:
npm run dev:weapp/npm run dev:h5 - 生产环境:
npm run build:weapp/npm run build:h5
- 开发环境:
- 检查交付物:
- 4个demo项目(基础→组件→原生→工程化);
- 学习笔记(环境坑点、状态管理思路、性能优化清单);
- 可部署的跨端项目(dist目录)。
常见问题解决
- 小程序授权失败:微信开发者工具→详情→本地设置→勾选"不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书";
- 样式跨端差异 :优先使用rpx单位,避免使用
position: fixed(H5/小程序表现不同); - 包体积过大:删除无用依赖、压缩图片、开启代码分割;
- 状态同步问题:本地存储修改后需同步更新全局状态。