噜噜旅游App(4)——搜索页面开发教程:热门推荐与关键词搜索模块

引言

效果图

在这个教程中,我们将介绍如何使用 React、Zustand 状态管理库和 Mock.js 来构建一个包含热门推荐和关键词搜索功能的搜索页面。我们将逐步讲解每一个组件的功能以及它们之间的交互。

完整代码

1. 创建 Zustand Store 实现全局状态管理

store/useSearchStore.js

src/store/useSearchStore.js 文件中创建一个 Zustand Store 来管理搜索建议列表 (suggestList) 和热门搜索列表 (hotList) 的状态。

js 复制代码
// search 全局状态共享
import {
    create
} from 'Zustand'
import {
    getSuggestList,
    getHotList
} from '@/api/search'

const useSearchStore = create((set,get) => {
    // get 读操作
    const searchHistory = JSON.parse(localStorage.getItem('searchHistory')) || []
    return {
        searchHistory,
        suggestList: [], // 返回list
        hotList:[], // 热门搜索
        setSuggestList: async(keyword)=>{
            const res = await getSuggestList(keyword);
            console.log(res);
            set({
                suggestList: res.data
            })
        },
        setHotList: async () => {
            const res = await getHotList();
            console.log(res);
            set({
                hotList: res.data
            })
        }
    }
})

export default useSearchStore

2. API 请求封装与模拟数据配置

src/api/search.js 中定义获取搜索建议和热门搜索列表的方法:

api/search.js

js 复制代码
// search模块
import axios from './config'

export const getSuggestList = (keyword) => {
    return axios.get(`/search?keyword=${keyword}`);
}

export const getHotList = async () => {
    return axios.get('/hotList');
}

3. 模拟后端接口响应

为了便于本地开发,我们可以使用 Mock.js 来模拟后端接口响应。在 src/mock/index.js 中添加如下代码:

mock/data.js

js 复制代码
import Mock from 'mockjs';

Mock.mock('/api/search', 'get', (req, res) => {
    const keyword = req.query.keyword;
    let num = Math.floor(Math.random() * 10);
    let list = [];
    for (let i = 0; i < num; i++) {
        const randomData = Mock.mock({
            title: '@ctitle'
        });
        list.push(`${randomData.title} ${keyword}`);
    }
    return {
        code: 0,
        data: list
    };
});

Mock.mock('/api/hotlist', 'get', {
    code: 0,
    data: [
        { id: '101', city: "北京" },
        { id: '102', city: "上海" },
        { id: '103', city: "福州" }
    ]
});

不要忘记配置 Axios 基础 URL 和拦截器:

api/config.js

js 复制代码
import axios from 'axios'

axios.defaults.baseURL = 'http://localhost:5173/api'

axios.interceptors.request.use((config) => {
    return config
})
// 响应拦截
axios.interceptors.response.use((data) => {
    return data.data
})

export default axios

接下来,在 src/components/Search.js 中创建 Search 组件,它将负责渲染整个搜索界面,包括搜索框、热门推荐和搜索建议列表。

Pages/Search/index.jsx

jsx 复制代码
import SearchBox from '@/components/SearchBox'
import useTitle from '@/hooks/useTitle'
import useSearchStore from '@/store/useSearchStore'
import styles from './search.module.css'
import { useState, useEffect, memo } from 'react';

// 性能优化
const HotListItems = memo((props) => {
  console.log('___________',props)
  const {hotList} = props;
  return (
    <div className={styles.hot}>
      <h1>热门推荐</h1>
      {
        hotList.map((item) => (
          <div key={item.id} className={styles.item}>
            {item.city}
          </div>
        ))
      }
    </div> 
  )
})

const Search = () => {
  const [query, setQuery] = useState('');
  const {
    hotList,
    setHotList,
    suggestList,
    setSuggestList,
  } = useSearchStore()

  useEffect(() => {
    setHotList();
  }, [])

  const handleQuery = (query) => {
    setQuery(query);
    if(!query.trim()) return
    setSuggestList(query);
  }

  const suggestListStyle = {
    display: query.trim() == '' ? 'none' : 'block'
  }
  
  useTitle('搜索')
  return (
    <div className={styles.container}>
      <div className={styles.wrapper}>
        <SearchBox handleQuery={handleQuery} />
        {/* 组件化,维护性更好 */}
        <HotListItems hotList={hotList} />
        <div className={styles.suggestList} style={suggestListStyle}>
          {
            suggestList.map( item => (
              <div key={item} className={styles.suggestItem}>
                {item}
              </div>
            ))
          }
        </div>
      </div>
    </div>
  )
}

export default Search

src/components/SearchBox.js 中创建 SearchBox 组件,它是一个输入框,允许用户输入搜索关键词,并触发搜索操作。

components/SearchBox/index.jsx

jsx 复制代码
import { memo, useEffect, useRef, useState, useMemo } from 'react';
import { ArrowLeft, Close } from '@react-vant/icons';
import styles from './search.module.css';
import { debounce } from '@/utils';

const SearchBox = ({ handleQuery }) => {
    const [query, setQuery] = useState('');
    const queryRef = useRef(null);

    const handleChange = (e) => {
        if (e.target.value.trim() === '') {
            setQuery('');
            return;
        }
        setQuery(e.target.value.trim());
    };

    const clearQuery = () => {
        setQuery('');
        queryRef.current.value = '';
        queryRef.current.focus();
    };

    const handleQueryDebounce = useMemo(() => debounce(handleQuery, 300), []);
    const displayStyle = query.trim() ? { display: 'block' } : { display: 'none' };

    useEffect(() => {
        handleQueryDebounce(query);
    }, [query]);

    return (
        <div className={`${styles.wrapper}`}>
            <ArrowLeft onClick={() => history.go(-1)} />
            <input type="text" className={styles.ipt} placeholder="搜索旅游相关" ref={queryRef} onChange={handleChange} />
            <Close onClick={clearQuery} style={displayStyle} />
        </div>
    );
};

export default memo(SearchBox);

关键词搜索整体流程梳理

从用户输入到数据展示

首先,用户在SearchBox组件中输入框中输入关键词(如:"北京"),组件使用useEffect和防抖debounce,调用通过props传入的handleQuery函数,作为防抖函数的参数传入,在用户停止输入约300ms后执行,将"北京"作为参数传递。

子组件searchBox.jsx

jsx 复制代码
<input 
    type="text"
    className={styles.ipt} 
    placeholder="搜索旅游相关"
    ref={queryRef}
    onChange={handleChange}
/>
jsx 复制代码
const handleQueryDebounce = useMemo(() => {
        return debounce(handleQuery, 300)
    }, [])

// 非受控组件
useEffect(() => {
    handleQueryDebounce(query);
}, [query])

父组件的handleQuery("北京")被调用,更新输入框query的状态,随后调用 Zustand Store 中的 setSuggestList("北京") 方法。 父组件search.jsx

jsx 复制代码
 const handleQuery = (query) => {
    setQuery(query);
    if(!query.trim()) return
    setSuggestList(query);
  }
  /*....其他*/
  <SearchBox handleQuery={handleQuery} />

setSuggestList 方法是一个异步函数,接收一个参数,即关键词"北京",在它内部会调用 getSuggestList("北京")。 而getSuggestList方法会进行API请求 ,函数执行 axios.get('/search?keyword=北京')

状态管理:useSearchStore.js

js 复制代码
setSuggestList: async(keyword)=>{
    const res = await getSuggestList(keyword);
    console.log(res);
    set({
        suggestList: res.data
    })
}

api请求

js 复制代码
export const getSuggestList = (keyword) => {
    return axios.get(`/search?keyword=${keyword}`);
}

找到匹配的配置 (url) 后,Mock.js 会执行该配置项中的 response 函数,并将请求信息(req)和一个响应对象(res,虽然这里没用到)作为参数传递进去。

response 函数内部,const keyword = req.query.keyword; 这行代码就是用来从拦截到的 HTTP 请求中提取查询参数 keyword 的值

mock.js

js 复制代码
{
    url: '/api/search',
    method: 'get',
    timeout: 1000,
    response:(req, res)=>{
        // ?keyword=释小龙
        const keyword = req.query.keyword;
        let num = Math.floor(Math.random() * 10); //生成0-9的随机数
        let list = [];
        for(let i = 0; i < num; i++) {
            const randomData = Mock.mock({
                title: '@ctitle'
            }) //Mock.mock 返回一个对象
            console.log(randomData)
            list.push(`${randomData.title} ${keyword}`)
        }
        return {
            code: 0,
            data: list
        }
    }
}

使用 Mock.js 生成随机数据,并将提取到的 keyword 值("北京")拼接到生成的标题后面,形成建议列表。

最后将生成的列表包装成 { code: 0, data: list } 的格式返回给 axios

axios 收到 Mock.js 返回的模拟数据,getSuggestList 函数的 Promise 被 resolve。
Search 组件因为订阅了 suggestList 状态,所以重新渲染,将新的建议列表展示在页面上。

总结

通过这个教程,我们学习了如何使用 React 结合 Zustand 状态管理库来实现一个简单的搜索页面,该页面支持热门推荐和关键词搜索功能。同时我们也了解了如何利用 Mock.js 来为前端开发提供稳定的模拟数据环境。希望这篇教程对你有所帮助!

相关推荐
Java手札7 分钟前
安装如下依赖(package.json 未包含):vueelement-plusecharts@element-plus/icons-vue
前端·javascript·vue.js
EndingCoder12 分钟前
Three.js + AI:结合 Stable Diffusion 生成纹理贴图
开发语言·前端·javascript·人工智能·stable diffusion·ecmascript·three.js
haruma sen16 分钟前
VUE前端
前端
汪叽家的兔子羡17 分钟前
vue模块化导入
前端·javascript·vue.js·typescript·vue3·vue2·vite
植物系青年23 分钟前
300 行代码!手把手教你写一个简版 Vue3 框架 📣
前端·vue.js
秉承初心24 分钟前
Vue3与ES6+的现代化开发实践(AI)
前端·vue.js·es6
Spirited_Away29 分钟前
脚手架开发之多包管理(npm, yarn, pnpm workspaces)
前端·面试
iaku31 分钟前
🔥React高级特性实战:错误边界、Portals与Refs进阶
前端·react.js·trae
薛定谔的猫234 分钟前
type-challenges系列(番外):技巧与知识点
前端·typescript
tiantian_cool40 分钟前
Xcode 导入与使用 SVG 文件矢量图适配全流程
前端