React 项目复盘:旅游 App 开发中的项目亮点

164 阅读15分钟

前言

最近,我完成了一个基于React的小型练手项目。虽然这个项目在页面结构和路由设计上可能略显凌乱,但它却为我提供了宝贵的学习机会和实践经验。作为一个纯粹用于技术探索的副项目,我并不打算将它写进简历,但它确实让我深入理解了React的诸多核心概念和实用技巧。

好吧,相信你们也等不及了,让我们一起来看看到底有什么值得一起学习的,如果有什么要优化或者改善的地方,欢迎到评论区交流讨论🚀🚀

项目结构

虽然项目比较乱,但是基础的项目结构还是不能乱,这样也方便了别的小伙伴来一起学习我们完成的项目,也方便日后自己再去看或者完善整个项目,便于大家阅读和理解。

trip/
├── mock/                    # 模拟数据接口 (Mock API)
├── node_modules/            # 依赖库
├── public/                  # 静态资源(直接复制到构建目录)
├── src/
│   ├── api/                 # API 请求封装
│   │   ├── config.js        # 请求基础配置
│   │   ├── detail.js        # 图片相关API
│   │   ├── home.js          # 首页相关API
│   │   ├── login.js         # 登录相关API
│   │   └── search.js        # 搜索相关API
│   ├── assets/              # 静态资源(图片/字体等)
│   ├── components/          # 通用组件
│   │   ├── BlankLayout/     # 空白布局
│   │   ├── ImageCard/       # 图片卡片组件
│   │   ├── Loading/         # 加载状态组件
│   │   ├── MainLayout/      # 主布局
│   │   ├── SearchBox/       # 搜索框组件
│   │   ├── Toast/           # 消息提示组件
│   │   └── Waterfall/       # 瀑布流布局组件
│   ├── hooks/               # 自定义 Hooks
│   │   └── useTitle.js      # 动态修改页面标题的Hook
│   ├── llm/                 # 大语言模型集成
│   │   └── index.js         # LLM 相关逻辑
│   ├── pages/              # 页面组件
│   │   ├── Account/        # 用户账户页
│   │   ├── Collection/      # 收藏页
│   │   ├── Detail/         # 详情页(关联detail.js API)
│   │   ├── Discount/       # 折扣页
│   │   ├── Home/           # 首页(关联home.js API)
│   │   ├── Login/          # 登录页(关联login.js API)
│   │   ├── Search/         # 搜索页(关联search.js API)
│   │   └── Trip/           # 旅行计划页
│   ├── store/               # 状态管理(Zustand/Jotai)
│   │   ├── useDetailStore.js  # 详情页状态
│   │   ├── useImagesStore.js  # 图片相关状态
│   │   └── useSearchStore.js  # 搜索状态
│   ├── utils/               # 工具函数
│   │   └── index.js         # 通用工具方法
│   ├── App.css              # 全局样式
│   ├── App.jsx              # 根组件(💶表示核心文件)
│   ├── index.css            # 基础样式
│   └── main.jsx             # 应用入口(💶表示核心文件)
├── .env.local               # 本地环境变量($表示敏感文件)
├── .gitignore               # Git忽略规则(📌表示重要配置)
├── eslint.config.js         # ESLint配置(🔺表示代码规范)
├── index.html               # 主HTML文件(<>表示模板)
├── package.json             # 项目配置({ }表示核心配置)
├── pnpm-lock.yaml           # pnpm依赖锁文件(!表示锁定版本)
├── postcss.config.js        # PostCSS配置(JS标识配置文件)
├── README.md                # 项目说明文档
└── vite.config.js           # Vite构建配置(🔺表示构建工具)

当然上面写的那些界面并不一定是实现了对应的功能,所以说比较乱,但是里面的知识点确实真的,那就让我们一起来一个一个慢慢讨论吧

配置和依赖

配置信息

  • .env.loacl: 主要是用来存储一些敏感信息的,比如:数据库密码、API的密钥(key)...不同的环境有不一样的文件类型
  • .gitignore:主要用于告诉 Git 哪些文件或目录不应该被版本控制,就是不用被提交到远程仓库里面(比如:GitHub...),常见的就有上面 .env.loacl文件和node_modules依赖文件

对于这两个配置信息想要详细了解的可以看:理解 .env 文件和 .gitignore 文件的作用与最佳实践

  • postcss.config.js:是 PostCSS 的配置文件,主要用于配置移动端(或任何前端项目)的 CSS 处理工具链。不了解也没关系,之后我会详细讲到,这也是一个亮点。
  • vite.config.js:是 Vite 构建工具的配置文件,用于定义项目的构建、开发服务器、插件、路径别名等个性化设置。通过这个文件,开发者可以自定义项目结构、优化构建流程、集成第三方工具(如 Vue/React 插件),从而提升开发体验和构建效率。

下面就是我的vite.config.js文件中的配置

// 导入必要的模块和插件
import { defineConfig } from 'vite' // Vite的配置函数
import react from '@vitejs/plugin-react' // Vite官方提供的React插件
import { viteMockServe } from 'vite-plugin-mock' // 用于开发时模拟API的插件
import path from 'path' // Node.js路径模块,用于处理文件路径

// 导出Vite配置
export default defineConfig({
  // 配置插件
  plugins: [
    react(), // 启用React支持
    viteMockServe({ // 配置模拟服务器
      mockPath: 'mock', // mock文件存放的目录
      localEnabled: true, // 本地开发时启用mock服务
    })
  ],
  // 配置模块解析
  resolve: {
    // 设置路径别名
    alias: {
      '@': path.resolve(__dirname, 'src') // 将@符号映射到src目录
    }
  }
})

总共就写了两个配置,一个是mock的,因为这个项目并没有完整的后端接口,我就用mock代替后端来返回数据;第二个就是路径配置,不然对于大型项目来说,每次都要"../../"之类的特别容易搞混,而且换一个位置路径就会出现问题,所以就直接把src目录替换成@,这样之后导入文件只需要写import name from '@/api/..'就可以了,直接从src目录下找。

对于其他的配置信息就是构建项目时自带的,我也没有详细的去了解,有懂的小伙伴也可以在评论区交流。

依赖信息

先让我们来看看有哪些依赖吧,依赖信息主要写在package.json文件中

{
  // 项目基本信息
  "name": "trip",                  // 项目名称
  "private": true,                // 私有项目,不会被发布到npm
  "version": "0.0.0",             // 项目版本号(初始版本)
  "type": "module",               // 使用ES模块规范

  // 脚本命令
  "scripts": {
    "dev": "vite",                // 启动开发服务器
    "build": "vite build",        // 生产环境构建
    "lint": "eslint .",           // 执行代码检查
    "preview": "vite preview"     // 预览生产构建
  },

  // 生产依赖(项目运行时需要的依赖)
  "dependencies": {
    "@react-vant/icons": "^0.1.0", // React Vant的图标库
    "axios": "^1.11.0",           // HTTP客户端库
    "lib-flexible": "^0.3.2",     // 移动端自适应解决方案
    "mitt": "^3.0.1",            // 轻量事件发射器
    "mockjs": "^1.1.0",          // 数据模拟库
    "react": "^18.2.0",          // React核心库
    "react-dom": "^18.2.0",      // React DOM渲染
    "react-router-dom": "^7.7.0", // React路由库
    "react-vant": "^3.3.5",       // 移动端React组件库
    "zustand": "^5.0.6"          // 状态管理库
  },

  // 开发依赖(仅开发时需要的工具)
  "devDependencies": {
    // ESLint相关
    "@eslint/js": "^9.22.0",                // ESLint核心规则
    "eslint": "^9.22.0",                    // JavaScript代码检查工具
    "eslint-plugin-react-hooks": "^5.2.0",   // React Hooks的ESLint规则
    "eslint-plugin-react-refresh": "^0.4.19",// React Fast Refresh支持
    "globals": "^16.0.0",                   // 全局变量配置
    
    // TypeScript类型定义
    "@types/react": "^19.0.10",             // React类型定义
    "@types/react-dom": "^19.0.4",          // ReactDOM类型定义
    
    // Vite相关
    "@vitejs/plugin-react": "^4.3.4",       // Vite的React插件
    "vite": "^6.3.1",                       // 构建工具
    "vite-plugin-mock": "^3.0.2",           // Mock数据插件
    
    // PostCSS相关
    "postcss": "^8.5.6",                    // CSS处理工具
    "postcss-pxtorem": "^6.1.0",            // px转rem插件
    
    // 其他工具
    "jwt": "^0.2.0"                         // JSON Web Token实现
  }
}

上面的注释都大概解释了一下每个依赖是用来干什么的,如果想要详细了解对应的相关内容的可以查看下面的博客进行了解(或问ai搜索相关的内容):

我还是简单介绍一下上面依赖的大概作用吧

react-vant它是React的一个第三方库,现在好像因为React的版本问题好像有一些组件不是很兼容;@react-vant/iconsreact-vant库中的图标库,可以在Icon 图标 - react vant里面查找对应的图标,但是我比较喜欢用阿里的iconfont-阿里巴巴矢量图标库的这个图库,里面真的很多图标和背景图片,而且也挺好使用的,有对应的文档。

lib-flexiblepostcss是用来处理移动端设备的适配问题,因为每个手机的尺寸可能不一样,但是我们写的App要适配所有的移动端,它的左右就显示出来了,它会自动调整字体大小、边距等问题,不然你要一个个手换得到猴年马月,具体的可以看上面的博客。

mitt这个是一个适配React观察者模式(发布-订阅模式) 的集成库,这样我们就不用手敲这个JS的设计模式了,对于一些信息的提示和主题切换有一些比较好的优势。

vite-plugin-mock这个是用来写后端的接口的,我们前端开发不可能一直等着后端完成然后返回数据,这个插件就可以很好的模拟后端返回一些数据。测试接口能不能返回数据我推荐一个软件叫Apifox ,它可以帮你测试对应的接口是否能返回数据

mockjs这个也是后端方面的,可以随机生成一些数据,这样就不用我们在上面自定义的后端接口里面编辑固定的数据了,包括数字、文段、图片等等。

zustand这个是一个轻量级别的状态管理库,主要是用来管理数据,用于组件之间的通信管理。它对于我这种中小型的项目简直是绝配,它的使用简单轻便,相对于Redux更简单上手,Redux适用于那种大型的、复杂的项目。

react-router-dom路由我就不多说了,它基本是所有项目的标配

jwt它主要用于用户登录管理,对每个登录的用户发一个令牌,这样下次你来的时候出示这个令牌就知道你是谁了。比如:一个网站一开始你都要注册,但是注册之后,后面就不用了,它会自动登录你的账号,因为你登录这个网站的时候,它会发送一个请求里面携带了你之前注册的令牌,这样它就知道你是谁了。(不过好像这个项目里面我并没有实现这个功能,这个功能好像是在另一个项目里面单独测试了,感兴趣的可以看看xwj_ai/react/jwt-demo项目)

瀑布流的实现

这个功能大家在很多地方也可以看到,比如小红书、番茄小说等App中可以看到它们的首页就是通过这个实现的,这种布局高效利用屏幕空间,减少浏览时的割裂感,用户无需频繁翻页即可沉浸式探索海量内容,同时智能算法会根据停留时间实时调整推荐,增强个性化体验。

它的实现是在@/pages/Collection中定义了,里面引用了@/components/Waterfall组件

import styles from './waterfall.module.css'
import {
    useEffect,
    useRef,
} from 'react'
import ImageCard from '@/components/ImageCard'

const Waterfall = (props) => {
    const loader = useRef(null);
    const { images, fetchMore, loading } = props;
    useEffect(() => {
        // ref 出现在窗口 intersectionObserver
        // 观察者模式
        const observer = new IntersectionObserver(([entry]) => {
            // console.log(entry)
            if(entry.isIntersecting) {
                fetchMore();
            }
        }) 
        if(loader.current) observer.observe(loader.current);
        return () => observer.disconnect();
    }, [])
    return (
        <div className={styles.wrapper}>
            <div className={styles.column}>
                {
                    images.filter((_, index) => index % 2 === 0).map(img => (
                        <ImageCard key={img.id} {...img}/>
                    ))
                }
            </div>
            <div className={styles.column}>
                {
                    images.filter((_, index) => index % 2 !== 0).map(img => (
                        <ImageCard key={img.id} {...img}/>
                    ))
                }
            </div>
            <div ref={loader} className={styles.loader}>加载中...</div>
        </div>
    )
}

export default Waterfall

它是两列式布局左边是index为偶数的,右边为奇数,这么写会有一个问题,下面会讲,大家也可以先想想。然后创建了一个IntersectionObserver对象用来观察加载中这个盒子的,当它出现在屏幕里面,那么说明已经到了底部我们就应该继续发送请求,返回新的数据。entry.isIntersecting是用来判断我们要观察的元素是否出现在页面中,在就发送请求。

这里还引入了另一个组件@/components/ImageCard,它主要是用来实现图片的设计的。

import styles from './imagecard.module.css'
import {
    useRef,
    useEffect,
} from 'react'

const ImageCard = (props) => {
    const { url , height } = props;
    const imgRef = useRef(null);
    useEffect(() => {
        const observer = new IntersectionObserver(([entry], obs) => {
            if(entry.isIntersecting) {
                const img = entry.target;
                const oImg = document.createElement('img');
                oImg.src = img.dataset.src;
                oImg.onload = () => {
                    img.src = img.dataset.src;
                }
                obs.unobserve(img);
            }
        })
        if(imgRef.current) observer.observe(imgRef.current);
    }, [])
    return (
        <div className={styles.card} style={{height}}>
            <img ref={imgRef} data-src={url} className={styles.img} />
        </div>
    )
}

export default ImageCard

在这个页面中我们也使用了IntersectionObserver对象,这个是用来观察图片的,来实现图片懒加载的功能(相关知识可以看图片懒加载到底有多少种玩法?这些方案你真的都了解吗?)。当图片出现在屏幕里面就将data-src上面的url赋值给img的ref,这样就可以显示出图片了。obs是指IntersectionObserver对象本身,不了解的可以去网上搜搜。

基本就是这样实现的,但是我们可以看到两个IntersectionObserver对象取消观察对象的方式不一样。在Collection取消观察用的是disconnect,它表示完全停止所有观察或连接。而在ImageCard中用的是unobserve,它表示停止观察某个特定目标,所以不同的场合要用不同的方法,要注意区分。

OK呀,基本的实现大家应该都懂了,那么让我们看看前面遇到的问题。主要它会出现一边长一边短的情况,因为有一种极端情况,当左边的都是短的,而右边都是长的,那么IntersectionObserver没有监听到加载中,就不会出现新的数据也就会有一半是白屏的状态,下面就是这种情况。

image.png

那能不能解决呢?能的兄弟。我们可以用两个数组分别存储左右两边对应的数据,然后这样就可以计算出左边对应的总高和右边的,进行比较,当加载出新数据时,如果左边的总高比右边的要高,那么数据就放在右边的数组里面,反之放在左边,依次遍历。

移动端适配

其实这个完全可以去看我上面写的那个博客,因为那里面写的比较详细,但是我还是决定在这里也简单说一说,大家了解一下就ok了。

当设计师拿出的设计稿是375x750的,让我们去设计417x820的手机时,该怎么办呢,不会一个一个去计算然后在设置对应的px吧。

首先我们要先了解一个叫rem的单位,它是一个相对单位,相对于htmlfont-size的一个单位,当你的页面中htmlfont-size为20px时,那么2rem就相当于是40px,也就是根页面的两倍,当然一般浏览器默认的font-size16px。好的,那么我们现在了解了相对单位的概念。

那么lib-flexible就该登场了,它是在我们的@/main.jsx中引入的,之后就不用管了。

image.png

它的作用很简单就是,将你htmlfont-size进行动态的修改,当你的设备是375x750时,它会自动的将你的htmlfont-size设置成37.5px也就是你设备的十分之一,如果你的设备是400x800,那么htmlfont-size就会变成40px,这么做就实现了动态修改单位。

如果你每个属性的单位都是rem的话,那么对于2rem而言,在375x750设备中,就是75px了,而在400x800设备中就变成了80px了,这样就可以动态的适配所有的移动端了。但是如果设计师给的设计稿上面写的都是px的单位,那么就要让我们自己去换算成rem单位,还是浪费时间,那有没有什么插件或者依赖可以自动的将我们写的px单位转为rem单位呢??有的,兄弟有的,下面就让我们来了解一下吧。

这个时候postcss-pxtorem就登场了,它的作用就是根据你的配置文件中的rootValue的值自动的将px转为rempostcss.config.js配置文件如下所示:

export default {
    plugins: {
      "postcss-pxtorem": {
        rootValue: 75, // 以 iPhone6 为参考,1rem = 75px
        propList: ['*'], // 所有属性都转换
        exclude: /node_modules/i, // 排除 node_modules 中的文件
      },
    },
  }

上面的rootValue值是75,就表示1rem等于75px,也就是说当你在页面上写了150px时,它会自动的将你的150px转为2rem。这个属性值就根据设计师给你的上面写就好了。

总体的流程就是:当你页面中写了一个属性是150px时,它会根据你的postcss.config.js配置文件中的rootValue的值进行换算,如果你的rootValue等于75px,那么你这个属性就会变成2rem,当你使用375x750设备时,由于htmlfont-sizelib-flexible修改成了37.5px,那么你的2rem就会变成75px,在不同的设备上它会进行对应的转换,这样就会自动的缩放来适配所有的移动端了。

总结

讲到这里就有点讲不下去了,项目有点乱,其实也没啥了,就剩下一些用户体验中的骨架屏、搜索建议、搜索框的自动聚焦等,然后性能优化就是防抖、memo、useMemo的使用,还有CSS的原子化和模块化,最后就是HTML5和es6的新特性了。

这些东西基本都是在我主页的文章里面有讲,大家可以去看看,那里梳理的比较好一点,对于里面出现的Coze的工作流的引入我了解的也不是很透彻,所以也就没写在文章里面了,这里还有DeepSeek的对话API的引入和文生图的大模型引入,当然这里没有实现文生图,文生图在我下面提到的项目中有。

这个项目我放在xwj_ai/react/trip at main · Acscanf/xwj_ai里面了,感兴趣的小伙伴可以下载玩玩,了解一下。

然后就是基于上面的一些亮点写的一个比较完善的小说APP的项目了,这个项目就比较完善,没什么乱的,估计就会放在简历里面了,大家也可以看看xwj_BookStack,这个就有文生图,还有上面我说的瀑布流的优化里面都有,连接DeepSeek大模型对话也有,不过就没有连接Coze工作流的实现了。

ok呀,祝大家学业顺利,找到好的工作,秋招努力,加油🚀🚀🚀