Taro+React跨端开发实战指南

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:验证待办功能

  1. 重启开发服务:taro build --type weapp --watch
  2. 微信开发者工具中进入/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 包体积分析
  1. 安装分析插件:

    bash 复制代码
    npm install webpack-bundle-analyzer --save-dev
  2. 修改config/index.ts,添加打包分析配置:

    typescript 复制代码
    import { 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'
              }
            ])
          }
        }
      }
    })
  3. 执行生产打包,生成分析报告:

    bash 复制代码
    taro build --type weapp
  4. 打开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(开发环境):

    env 复制代码
    TARO_ENV=development
    REACT_APP_API_BASE_URL=https://test-api.example.com
  • 根目录新建.env.production(生产环境):

    env 复制代码
    TARO_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

最终验证与交付

  1. 运行所有环境验证:
    • 开发环境:npm run dev:weapp/npm run dev:h5
    • 生产环境:npm run build:weapp/npm run build:h5
  2. 检查交付物:
    • 4个demo项目(基础→组件→原生→工程化);
    • 学习笔记(环境坑点、状态管理思路、性能优化清单);
    • 可部署的跨端项目(dist目录)。

常见问题解决

  1. 小程序授权失败:微信开发者工具→详情→本地设置→勾选"不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书";
  2. 样式跨端差异 :优先使用rpx单位,避免使用position: fixed(H5/小程序表现不同);
  3. 包体积过大:删除无用依赖、压缩图片、开启代码分割;
  4. 状态同步问题:本地存储修改后需同步更新全局状态。
相关推荐
阿里巴啦13 小时前
React + Three.js + R3F + Vite 实战:可交互的三维粒子化展厅
react.js·three.js·粒子化·drei·postprocessing·三维粒子化
[seven]13 小时前
React Router TypeScript 路由详解:嵌套路由与导航钩子进阶指南
前端·react.js·typescript
San3015 小时前
现代前端工程化实战:从 Vite 到 React Router demo的构建之旅
react.js·前端框架·vite
Qinana17 小时前
从零开始实现 GitHub 仓库导航器(Windows 实操版)
react.js·前端框架·vite
南山安17 小时前
React学习:Vite+React 基础架构分析
javascript·react.js·面试
一只叫煤球的猫17 小时前
我做了一个“慢慢来”的开源任务管理工具:蜗牛待办(React + Supabase + Tauri)
前端·react.js·程序员
智航GIS18 小时前
ArcGIS大师之路500技---032山体阴影
arcgis
前端无涯18 小时前
react组件(4)---高阶使用及闭坑指南
前端·react.js