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. 状态同步问题:本地存储修改后需同步更新全局状态。
相关推荐
YFF菲菲兔12 小时前
其他 Hooks 解析
react.js
想你依然心痛20 小时前
AtomCode 在前端开发中的实战体验:React + TypeScript 项目开发实录
前端·react.js·typescript
前端炒粉20 小时前
个人简历面经总结二
前端·网络·vue.js·react.js·面试
非科班Java出身GISer2 天前
ArcGIS JS API 5.0 ESM 双模块系统冲突解决方案
arcgis·arcgis js 引入问题·arcgis js amd·arcgis esm引入问题·arcgis js 资源冲突
谢尔登2 天前
【React】 状态管理方案
前端·react.js·前端框架
Eiceblue2 天前
使用 JavaScript 在 React 中实现 Word 转 PDF
javascript·react.js·word
光影少年3 天前
react navite 跨端核心原理
前端·react native·react.js
用户298698530143 天前
在 React 中使用 JavaScript 合并 Excel 文件
前端·javascript·react.js
CaffeinePro4 天前
告别知识点零散!React零基础通关,从环境搭建到Ant Design页面实战
前端·react.js