从零到一:用 Taro + React 搭建数据采集小程序

从零到一:用 Taro + React 搭建数据采集小程序

本文用最白痴化的方式,带你一步步完成一个可用于面试的小程序项目


一、项目简介

这是一个数据采集工具小程序,功能包括:

  • 图片采集(拍照/相册选择)
  • 数据管理(查看/删除)
  • 本地存储

二、什么是 Taro?为什么选它?

2.1 Taro 是什么?

一句话理解:Taro 是一个让你用 React/Vue 写小程序的框架

复制代码
以前(原生小程序):
├── index.js      Page({ ... })
├── index.wxml    <view>...</view>
├── index.wxss    样式
└── index.json    配置

现在(Taro + React):
└── index.tsx     组件,在一个文件里写逻辑 + JSX

2.2 为什么面试喜欢 Taro?

  • ✅ 大厂都在用(京东、腾讯等)
  • ✅ React 开发者零学习成本
  • ✅ 一套代码多端运行(微信/支付宝/H5等)
  • ✅ 现代化工程化开发体验

三、从零开始:一键搭建项目

3.1 准备工作

你需要先安装:

  • Node.js:去官网下载安装即可
  • pnpm/npm:包管理器

检查安装:

bash 复制代码
node -v
npm -v

3.2 初始化 Taro 项目

注意: 这里有个坑!别自己手动创建,一定要用官方脚手架!

bash 复制代码
# 进入项目目录
cd /Users/lichenyang/项目/小程序面试开发项目

# 用 Taro 脚手架初始化
npx @tarojs/cli init taro-demo

然后会问你一堆问题,按如下选择:

复制代码
? 请输入项目名称 taro-demo
? 请输入项目介绍 数据采集工具小程序
? 请选择框架 React
? 请选择语言 TypeScript
? 请选择 CSS 预处理器 Sass
? 请选择模板 默认模板

3.3 进入目录安装依赖

bash 复制代码
cd taro-demo
npm install

3.4 启动项目!

bash 复制代码
npm run dev:weapp

注意点!

  • ❌ 不要直接在微信开发者工具打开 taro-demo 目录
  • ✅ 打开的是编译后的 taro-demo/dist 目录

四、项目目录结构分析

先看一眼生成的目录:

复制代码
taro-demo/
├── src/
│   ├── pages/          # 页面文件夹(重要!)
│   │   └── index/      # 首页
│   ├── app.tsx         # 入口组件
│   ├── app.config.ts   # 全局配置
│   └── app.scss        # 全局样式
├── config/             # Taro 配置
├── project.config.json # 微信开发者工具配置
└── package.json        # 依赖包

重点讲解:

文件/目录 作用
app.config.ts 小程序全局配置(页面路由、TabBar等)
pages/ 放各个页面
src/utils/ 放工具函数(我们自己建的)

五、第一步:配置页面路由和 TabBar

5.1 理解 app.config.ts

这个文件相当于原生小程序的 app.json,我们在里面配置三个页面:

typescript 复制代码
// src/app.config.ts
export default defineAppConfig({
  pages: [
    'pages/index/index',     // 首页
    'pages/media/index',     // 多媒体采集
    'pages/data/index'       // 数据管理
  ],
  window: {
    backgroundTextStyle: 'light',
    navigationBarBackgroundColor: '#1890ff',
    navigationBarTitleText: '数据采集工具',
    navigationBarTextStyle: 'white',
    backgroundColor: '#f5f5f5'
  },
  tabBar: {
    color: '#999',
    selectedColor: '#1890ff',
    backgroundColor: '#fff',
    borderStyle: 'black',
    list: [
      { pagePath: 'pages/index/index', text: '首页' },
      { pagePath: 'pages/media/index', text: '多媒体' },
      { pagePath: 'pages/data/index', text: '数据' }
    ]
  }
})

5.2 创建页面文件夹

src/pages/ 下创建两个新文件夹:

复制代码
src/pages/
├── index/        # 已存在
├── media/        # 新建
└── data/         # 新建

每个页面需要三个文件:

  • index.tsx - 页面逻辑和结构
  • index.scss - 页面样式
  • index.config.ts - 页面配置

六、第二步:本地存储工具类

6.1 为什么需要单独封装存储?

  • ✅ 统一管理,方便维护
  • ✅ 加上类型安全(TypeScript)
  • ✅ 后续加上传逻辑时方便修改

6.2 创建存储工具类

新建 src/utils/storage.ts

typescript 复制代码
import Taro from '@tarojs/taro'

// 定义数据类型(TypeScript 的好处)
interface ImageItem {
  id: string
  path: string
  createTime: string
}

const STORAGE_KEY = 'collectedImages'

export const StorageManager = {
  // 获取所有图片
  getImages(): ImageItem[] {
    try {
      const data = Taro.getStorageSync(STORAGE_KEY)
      return data || []
    } catch (e) {
      console.error('获取图片失败', e)
      return []
    }
  },

  // 添加一张图片
  addImage(imagePath: string): ImageItem | null {
    const images = this.getImages()
    const newImage: ImageItem = {
      id: Date.now() + Math.random().toString(36).substr(2, 9),
      path: imagePath,
      createTime: new Date().toISOString()
    }
    images.unshift(newImage)
    try {
      Taro.setStorageSync(STORAGE_KEY, images)
      return newImage
    } catch (e) {
      console.error('保存图片失败', e)
      return null
    }
  },

  // 删除图片
  deleteImage(id: string): boolean {
    let images = this.getImages()
    images = images.filter(img => img.id !== id)
    try {
      Taro.setStorageSync(STORAGE_KEY, images)
      return true
    } catch (e) {
      console.error('删除图片失败', e)
      return false
    }
  },

  // 清空所有
  clearAll(): boolean {
    try {
      Taro.setStorageSync(STORAGE_KEY, [])
      return true
    } catch (e) {
      console.error('清空失败', e)
      return false
    }
  }
}

注意点:

  • 这里用了 unshift 而不是 push,让新添加的图片在最前面
  • 每个方法都加了 try-catch,防止报错崩溃

七、第三步:图片采集页(多媒体页)

7.1 页面代码拆解

新建 src/pages/media/index.tsx

tsx 复制代码
import { useState, useEffect } from 'react'
import { View, Button, Image, Text } from '@tarojs/components'
import { useDidShow } from '@tarojs/taro'
import Taro from '@tarojs/taro'
import { StorageManager } from '../../utils/storage'
import './index.scss'

interface ImageItem {
  id: string
  path: string
  createTime: string
}

export default function Media() {
  const [images, setImages] = useState<ImageItem[]>([])

  // 加载图片的函数
  const loadImages = () => {
    const savedImages = StorageManager.getImages()
    setImages([...savedImages])  // ... 是为了创建新数组,确保 React 检测到变化
  }

  // 页面加载时执行一次
  useEffect(() => {
    loadImages()
  }, [])

  // ⭐ 重点!每次页面显示时都刷新!
  // 切换 Tab 回来时,能看到新添加的数据
  useDidShow(() => {
    loadImages()
  })

  // 选择图片
  const chooseImage = () => {
    Taro.chooseImage({
      count: 9,
      sizeType: ['compressed'],
      sourceType: ['album', 'camera'],
      success: (res) => {
        const tempPaths = res.tempFilePaths
        tempPaths.forEach(path => {
          StorageManager.addImage(path)
        })
        loadImages()
        Taro.showToast({
          title: `已添加 ${tempPaths.length} 张图片`,
          icon: 'success'
        })
      },
      // ⭐ 必须加!用户取消选择时不会报错
      fail: (err) => {
        console.log('取消选择', err)
      }
    })
  }

  // 删除图片
  const deleteImage = (id: string) => {
    Taro.showModal({
      title: '确认删除',
      content: '确定要删除这张图片吗?',
      success: (res) => {
        if (res.confirm) {
          StorageManager.deleteImage(id)
          loadImages()
          Taro.showToast({ title: '已删除', icon: 'success' })
        }
      }
    })
  }

  // 预览大图
  const previewImage = (path: string) => {
    const allPaths = images.map(img => img.path)
    Taro.previewImage({
      current: path,
      urls: allPaths
    })
  }

  return (
    <View className='container'>
      <View className='header'>
        <Text className='title'>图片采集</Text>
        <Button className='add-btn' onClick={chooseImage}>+ 添加图片</Button>
      </View>

      {images.length > 0 ? (
        <View className='image-grid'>
          {images.map((item) => (
            <View key={item.id} className='image-item'>
              <Image
                className='image'
                src={item.path}
                mode='aspectFill'
                onClick={() => previewImage(item.path)}
              />
              <View className='delete-btn' onClick={() => deleteImage(item.id)}>×</View>
            </View>
          ))}
        </View>
      ) : (
        <View className='empty'>
          <Text className='empty-text'>还没有采集图片</Text>
          <Text className='empty-desc'>点击上方按钮添加图片</Text>
        </View>
      )}
    </View>
  )
}

7.2 重点注意点!

问题 解决方案
切换 Tab 后数据不更新 useDidShow,每次页面显示时刷新
React 列表不重新渲染 [...savedImages] 创建新数组,而不是直接赋值
取消选择时报红 chooseImagefail 回调
删除时需要二次确认 Taro.showModal

7.3 页面配置

新建 src/pages/media/index.config.ts

typescript 复制代码
export default definePageConfig({
  navigationBarTitleText: '多媒体采集'
})

7.4 样式代码

新建 src/pages/media/index.scss

scss 复制代码
.container {
  padding: 32px;
  min-height: 100vh;
  background: #f7f8fa;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 32px;
  padding: 0 8px;
}

.title {
  font-size: 40px;
  font-weight: 700;
  color: #1f2937;
}

.add-btn {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 20px;
  padding: 20px 40px;
  font-size: 28px;
  font-weight: 600;
  margin: 0;
  box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);

  &::after {
    border: none;
  }
}

.image-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}

.image-item {
  position: relative;
  width: 100%;
  padding-top: 100%;
  border-radius: 20px;
  overflow: hidden;
  background: #fff;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}

.image {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.delete-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 44px;
  height: 44px;
  background: rgba(255, 59, 48, 0.95);
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 36px;
  line-height: 1;
  font-weight: bold;
  box-shadow: 0 4px 12px rgba(255, 59, 48, 0.4);
  backdrop-filter: blur(10px);
}

.empty {
  text-align: center;
  padding: 160px 40px;
}

.empty-text {
  display: block;
  font-size: 32px;
  color: #9ca3af;
  margin-bottom: 16px;
  font-weight: 500;
}

.empty-desc {
  display: block;
  font-size: 26px;
  color: #d1d5db;
}

八、第四步:数据管理页

8.1 页面代码

新建 src/pages/data/index.tsx

tsx 复制代码
import { useState, useEffect } from 'react'
import { View, Button, Image, Text } from '@tarojs/components'
import { useDidShow } from '@tarojs/taro'
import Taro from '@tarojs/taro'
import { StorageManager } from '../../utils/storage'
import './index.scss'

interface ImageItem {
  id: string
  path: string
  createTime: string
}

export default function Data() {
  const [images, setImages] = useState<ImageItem[]>([])

  const loadImages = () => {
    const savedImages = StorageManager.getImages()
    console.log('Data 加载图片:', savedImages)
    setImages([...savedImages])
  }

  useEffect(() => {
    loadImages()
  }, [])

  useDidShow(() => {
    console.log('Data 页面显示')
    loadImages()
  })

  // 格式化时间显示
  const formatTime = (isoString: string) => {
    const date = new Date(isoString)
    return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
  }

  const previewImage = (path: string) => {
    const allPaths = images.map(img => img.path)
    Taro.previewImage({
      current: path,
      urls: allPaths
    })
  }

  const deleteImage = (id: string) => {
    Taro.showModal({
      title: '确认删除',
      content: '确定要删除这张图片吗?',
      success: (res) => {
        if (res.confirm) {
          StorageManager.deleteImage(id)
          loadImages()
          Taro.showToast({ title: '已删除', icon: 'success' })
        }
      }
    })
  }

  const clearAll = () => {
    if (images.length === 0) {
      Taro.showToast({ title: '没有数据', icon: 'none' })
      return
    }
    Taro.showModal({
      title: '确认清空',
      content: '确定要清空所有图片吗?此操作不可恢复!',
      confirmColor: '#ff4d4f',
      success: (res) => {
        if (res.confirm) {
          StorageManager.clearAll()
          loadImages()
          Taro.showToast({ title: '已清空', icon: 'success' })
        }
      }
    })
  }

  return (
    <View className='container'>
      <View className='header'>
        <Text className='title'>数据管理 ({images.length})</Text>
        {images.length > 0 && (
          <Button className='clear-btn' onClick={clearAll}>清空</Button>
        )}
      </View>

      {images.length > 0 ? (
        <View className='image-list'>
          {images.map((item) => (
            <View key={item.id} className='image-card'>
              <Image
                className='preview-img'
                src={item.path}
                mode='aspectFill'
                onClick={() => previewImage(item.path)}
              />
              <View className='card-info'>
                <Text className='time'>{formatTime(item.createTime)}</Text>
                <Button
                  className='delete-btn'
                  size='mini'
                  onClick={() => deleteImage(item.id)}
                >
                  删除
                </Button>
              </View>
            </View>
          ))}
        </View>
      ) : (
        <View className='empty'>
          <Text className='empty-text'>暂无采集数据</Text>
          <Text className='empty-desc'>去「多媒体」页面添加图片吧</Text>
        </View>
      )}
    </View>
  )
}

8.2 同样的,页面配置和样式

配置 src/pages/data/index.config.ts

typescript 复制代码
export default definePageConfig({
  navigationBarTitleText: '数据管理'
})

样式 src/pages/data/index.scss 参考前面的样式代码。


九、第五步:首页美化

修改 src/pages/index/index.tsx

tsx 复制代码
import { View, Text } from '@tarojs/components'
import './index.scss'

export default function Index() {
  return (
    <View className='container'>
      <View className='card'>
        <Text className='title'>数据采集工具</Text>
        <Text className='desc'>Taro + React + TypeScript</Text>
      </View>
    </View>
  )
}

样式 src/pages/index/index.scss 参考前面的美化版代码。


十、常见坑点总结(面试会问!)

10.1 关于 Taro 单位

写法 说明
px Taro 推荐,编译时自动转成 rpx
px * 2 ❌ 不要这样!

10.2 关于页面刷新

场景 用什么钩子
第一次加载 useEffect
每次页面显示 useDidShow

面试话术:

小程序切换 Tab 时页面不会重新加载,所以需要用 useDidShow 在每次页面显示时刷新数据。

10.3 关于 React 列表渲染

typescript 复制代码
// ❌ 这样可能不会重新渲染
setImages(savedImages)

// ✅ 这样创建新数组,React 会检测到变化
setImages([...savedImages])

10.4 关于微信开发者工具导入目录

目录 是否正确
taro-demo ❌ 源目录,不能直接打开
taro-demo/dist ✅ 编译后的目录

十一、面试时如何介绍这个项目?

11.1 项目介绍话术

这是一个数据采集工具小程序,用于采集图片数据并本地管理。

技术栈:Taro + React + TypeScript

核心功能:

  1. 图片采集(拍照/相册选择)
  2. 数据管理(查看/删除)
  3. 本地持久化存储

11.2 面试官可能会问的点

问题 回答方向
为什么选 Taro? 多端复用、React 生态、大厂在用
数据为什么要存在本地? 模拟上传队列、网络异常时不丢数据
切换 Tab 数据为什么不更新? useDidShow 钩子解决
还有什么可以优化的? 图片压缩、断点续传、云存储上传等

11.3 进阶优化方向(可选)

如果面试官问你还可以怎么加功能,你可以说:

  1. 图片压缩:上传前压缩图片大小
  2. 断点续传:大文件分片上传
  3. 图片元数据提取:获取时间、地点等信息
  4. 批量上传队列:管理上传状态
  5. 数据质量校验:检测图片是否模糊

十二、完整运行流程回顾

  1. cd taro-demo
  2. npm install
  3. npm run dev:weapp
  4. ✅ 微信开发者工具打开 taro-demo/dist
  5. ✅ 测试功能!

十三、总结

恭喜你!现在你已经有了一个可以用于面试的小程序项目了。

这个项目展示了什么能力?

  • ✅ Taro/React 基础开发
  • ✅ 小程序常用 API 使用
  • ✅ 状态管理(React Hooks)
  • ✅ 本地数据存储
  • ✅ 工程化项目搭建
相关推荐
黄华SJ520it13 小时前
139小程序商城模式开发
小程序·软件需求·系统开发
Greg_Zhong14 小时前
详细说下小程序中使用canvas的体验
小程序·canvas绘制·canvas绘制内容溢出·绘制内容模拟器不正常·绘制内容真机正常
小羊Yveesss17 小时前
2026 多门店小程序如何提升效率?连锁门店降本增效实操指南,数字化转型必看
大数据·小程序
2501_9419820517 小时前
提高私域转化率:如何通过 API 自动发送小程序卡片?
小程序·机器人·自动化·企业微信·rpa
暗不需求19 小时前
React项目架构深度解析:从0到1理解现代前端工程化
前端·javascript·react.js
码视野20 小时前
完全开源-支持二开-可做毕业论文-家政服务预约小程序
小程序
码视野20 小时前
全开源-健身运动预约小程序 — 从需求到原型的全栈实践
小程序
游戏开发爱好者821 小时前
深入理解iOSTime Profiler:提升iOS应用性能的关键工具
android·ios·小程序·https·uni-app·iphone·webview
tianxiaxue121 小时前
微信小程序如何跟企微互通
微信小程序·小程序·企业微信