保姆级教程:猫眼电影搜索小组件——react实战入门

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

前言

React 是一个用于构建用户界面的 JAVASCRIPT 库。React 主要用于构建 UIReact起源于Facebook 的内部项目,用来架设 Instagram 的网站,react作为前端大三框架之一,近几年热度很高,受到很多大厂青睐,那么新手怎么写好一个react项目呢?

新手必备技能

在学习react之前,你肯定对html,css,javascript有一定了解,然而这是不够的,最好去看看es6中的promise和async,对你理解接下来写的项目帮助很大的。

常用的react开源组件库

本项目需要用到的React开源组件库

  • weui, react-weui——weui是一套同微信原生视觉体验一致的基础样式库,可以阅读官方文档学习如何使用,而react-weui就是将这些样式封装成我们可以直接使用的组件。
  • axios——axios是一个用于发送Ajax请求的http库,本质上时对Ajax的封装,而且支持Promise操作,让我们无需再使用传统的callback方式进行异步编程。
  • antd, antd-mobile——antd 是基于 Ant Design 设计体系的 React UI 组件库,而antd-mobile是Ant Design的移动规范的React实现,是一个基于Preact/React/React Native的UI组件库
  • styled-components——styled-components 是一个常用的 css in js 类库。和所有同类型的类库一样,通过 js 赋能解决了原生 css 所不具备的能力,比如变量、循环、函数等。

另外,我们还用到在线接口工具 faskmock 模拟ajax请求。它更加真实的模拟了前端开发中后端提供数据的方式。

正文

对照一波

QQ图片20220627114346.jpg

让我们先看看这个组件的亮点吧~

1. 轮播图

(蹭一波 梦华录 的热度,嘻嘻~)

chrome-capture-2022-5-25.gif

轮播图下面的卡片,用两层flex布局,轻松实现

  .card {
        display: flex;
        flex-wrap: wrap;
        justify-content:space-around;
    }
    .card-title {
        display:flex;
        flex-direction: column;
        margin: 0.1rem 0.1rem;
    }

2. tab切换

Spin loading状态

chrome-capture-2022-5-27.gif

3. 电竞赛事城市定位功能

按照手动选择的城市,筛选赛事信息

chrome-capture-2022-5-25 (1).gif

4. 搜索功能

可以根据标题和定位的城市,搜索对应的赛事

  • 按赛事地点搜索

chrome-capture-2022-5-25 (6).gif

  • 按赛事标题搜索

chrome-capture-2022-5-25 (5).gif

1. 组件设计思路

  • 首页焦点图 用了antd组件库中的swiper,根据需要调样式,实现起来比较简单,这里就不讲解了
  • tab导航栏切换 用了antd-mobile组件库中的Tabs, activeLineMode='fixed',可以自己修改样式, 也可以用antd组件库中的Tab,但是切换需要自己手写实现
  • 城市定位功能实现 useSearchParams获取参数,search.get(name)获取定位的城市带到events页面
  • 搜索功能的实现 在当前tab下输入含标题或城市的内容
  • 暂无赛事状态 匹配不到搜索内容,显示暂无赛事

根据我们的需求,先初始化一个react项目 npm init @vitejs/app,再建立好文件夹

  • 根目录 public 静态资源目录 不需要在src 里面引入

  • src文件下

    • api 封装axios接口
    • assets 放置静态资源 如图片/字体图标/全局样式
      • 在assets 下创建font、image

      • 字体图标就在iconfont上去下载自己想要的或者使用font-awesome组件库

      • image 可以下载需要的图标或者等会在fastmock传入数据(可要可不要)

      • reset 就是对页面进行样式重置(这里代码太长,推荐随便到掘金或者github上找一个比较全的样式重置就可

    • components 放置通用组件
    • pages 单页面存放
    • routers 独立配置文件 把组件封装到一起

QQ图片20220625171848.png

2 配置工作

2.1 配置vite.config

为了避免太长的路径,引入文件时不方便,

  import { defineConfig } from 'vite'
  import react from '@vitejs/plugin-react`
  import path from 'path'
 // https://vitejs.dev/config
 export default defineConfig({
 plugins: [react()],
 resolve: {
 alias: {
   "@":path.resolve(__dirname,'src')
     }
   }
 })

2.2 移动端适配

不同型号手机端适应页面

  • 在public文件夹下创建js子文件夹创建adapter.js文件

  • 这里设置20px为1rem 后面所有的大小将不再使用px,使用rem代替px确保能够适应所有的手机型号

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@":path.resolve(__dirname,'src')
    }
  }
})

2.3 请求数据的接口

import axios from 'axios'
export const getCities = () => 
   axios.get('https://www.fastmock.site/mock/e7ebe2913686274082d1afac39c04d20/beers/cities')

3. 功能实现

3.1 tabbar实现切换效果

这里讲下antd中Tab实现的吧,因为需要自己实现。 activeKey是激活状态,给每个tab绑定一个点击事件setactiveKey,每次点击不同的tab的时候,useState(activeKey),改变activeKey的值,从而达到切换tab的效果

     // result 存放获取赛事信息的数据
    if(tab) {
            switch(tab) {
                case "全部":
                    result = result.filter(todo => todo.pos.includes(cityName))
                    // 数组filter方法,过滤,筛选得到符合条件的信息
                    break;
                case "电竞赛事":
                    result = result.filter(todo => todo.type == '电竞赛事')
                    break;
                case "体育赛事":
                    result = result.filter(todo => todo.type == '体育赛事' && todo.pos.includes(cityName))

                default:
                    break;
            }
    }

3.2 城市定位功能

点击“城市”按钮,跳转到/cities页面,search.get(name)把选择的城市cityName传给之前的页面,然后跳转到events页面,定位成功

QQ图片20220625194044.png

先测试一下,能不能得到数据

    const [search] = useSearchParams()
    console.log('我是search',search)
    const cityName = search.get('name') || ''
    console.log(cityName)
   

控制台打印输出

QQ图片20220625193203.png

具体实现

export default function Cities() {
    const [cities,setCities] = useState([])
    useEffect(() => {
        (async () => {
            let {data} = await getCities()
            // console.log(data)
            setCities(data)
        })()
    })

    const renderCities = () => {
        return cities.map(({id, nm}) => {
            return <Link
                className="city_name"
                to={{
                    pathname: '/events',
                    search: `name=${nm}`
                }}
                key={id}>
                {nm}
            </Link>
        })
    }

    return (
        <CityWrapper>
        {renderCities()}
        </CityWrapper>
    )
}

3.3 搜索功能实现

filter+includes 实现搜索

为了避免因为输入操作太频繁,搜索功能卡顿,这里用了onPressEnter事件, 按回车的时候,才触发搜索功能, 撤销搜索,清空输入框内容,再按回车即可。

export const fetchTodos = withDelay(params => {
    console.log(params)
    const { query, tab,cityName} = params;  // 把需要搜索的条件解构出来,
    // console.log(query, tab);
    let result = todos; // result 存放获取赛事信息的数据
    if(tab) {
            switch(tab) {
                case "全部":
                    result = result.filter(todo => todo.pos.includes(cityName))
                    // 数组filter方法,过滤,筛选得到符合条件的信息
                    break;
                case "电竞赛事":
                    result = result.filter(todo => todo.type == '电竞赛事')
                    break;
                case "体育赛事":
                    result = result.filter(todo => todo.type == '体育赛事' &&todo.pos.includes(cityName))

                default:S
                    break;
            }
    }
    if(query) {
        result = result.filter(todo => todo.text.includes(query)||todo.pos.includes(query))
    }
    // Promise 类 resolve 静态方法
    // Promise.all 返回一个fullfiled 的 promise 实例
    return Promise.resolve({
        tab, result
    })
})

3.4 暂无赛事 loading 状态。

因为在数据请求过程之,页面会空白,为了提升视觉上的效果,在这个时间段我们就设置一个loading样式,这个样式组件我们直接使用weuiSpin组件。 当没有匹配的赛事,显示暂无赛事,小猫咪就出现啦~

  <Spin spinning = {loading} tip ="加载中~">
      { todos.length== 0 ?
       <div className='nodata'>
          <img src={No}></img>
          <p>暂无代办事项</p> 
       </div>:
       <TodoList todos = {todos}  />
         }
</Spin>

暂无赛事显示的内容,根据喜好,自己设计,我是模仿猫眼电影,所以用的是小猫咪。

QQ图片20220625195817.png

4 组件实现

4.1 footer组件,固定在页面底部,并且每个页面都显示,放在components文件夹

这里补充一下classnames的用处,动态添加className,当有多个类名时这样写 <li className={classnames('app-title',{active:tab==="全部"})} onClick={changeTab.bind(null,'全部')}>全部</li>

export default function Footer(props) {
    const {pathname} = useLocation()
    if (['/cities'].indexOf(pathname) != -1) return
    //console.log(pathname)
    return (
        <FooterWrapper>
            <Link to = '/home' className={classnames({active:pathname == '/home'})}>
                <i className='iconfont icon-shouye'></i>
                <span>首页</span>
            </Link>
            <Link to = '/movie' className={classnames({active:pathname == '/movie'})}>
                <i className='iconfont icon-dianying'></i>
                <span>电影/影院</span>
            </Link>
            <Link to = '/yanchu' className={classnames({active:pathname == '/yanchu'})}>
                <i className='iconfont icon-yanchu'></i>
                <span>演出</span>
            </Link>
            <Link to = '/events' className={classnames({active:pathname == '/events'})}>
                <i className='iconfont icon-saishi'></i>
                <span>体育/赛事</span>
            </Link>
            <Link to = '/mine' className={classnames({active:pathname == '/mine'})}>
                <i className='iconfont icon-wode'></i>
                <span>我的</span>
            </Link>
        </FooterWrapper>
    )
}

QQ图片20220625200919.jpg

QQ图片20220625201015.jpg

4C8074123E71934426F148B3386409EB.jpg

但是这里还有一个小细节,手动定位城市的时候,跳出来的页面,不需要底部导航栏

if (['/cities'].indexOf(pathname) != -1) return

4.2 TodoInput组件

输入框组件,react是单向绑定,给输入框的值设置一个状态,当触发onChange事件的时候,改变输入框的值,从而实现输入框的值动态改变,当触发onPressEnter事件,搜索功能开启

const TodoInput = ({placeholder,onSetQuery}) => {
    const [value, setValue] = useState("")
    const onAdd = () => {
        // console.log('......')
        onSetQuery(value);
    }
    return (
        <>
        <section className="input-wrap">
            <Input className="Input"
            onPressEnter={onAdd}
            placeholder = {placeholder}
            value={value}
            onChange={e => setValue(e.target.value)}
            />
        </section>
        </>
    )
}
export default TodoInput

4.3 TodoList组件

赛事列表组件,渲染列表每一项

const TodoList = ({todos}) => {
    // const onDelect = () => {

    // }
    return (
        <div className="list-wrap">
            { todos[0] !== 1 &&
                // List组件来自antd  
                // 逻辑 配置数据源 
                // renderItem 每一个jsx输出
                <List 
                    itemLayout='horizontal'
                    dataSource={todos}
                    renderItem={({ id,url,dec, price,text,pos}) => {
                        const className = classNames({
                            "list-item": true,
                        })
                        return (
                            <Item className= {className}>
                                <div className='event-list'>
                                    <span>
                                        <img src = {url}></img>
                                    </span>
                                    <span className='desc'>
                                        <p className='p1'>{text}</p>
                                        <p className='p2'>{dec}</p>
                                        <p className='p2'>{pos}</p>
                                        <p className='p3'> 
                                            <span>售票中:</span>
                                            {price}
                                      </p>
                                    </span>
                                </div>
                            </Item>
                        )
                    }}
                />
            }
        </div>
    )
}

export default TodoList

4.4 轮播图组件

效果预览 引入antd组件库 Swiper

chrome-capture-2022-5-25 (4).gif

export default function Banners() {
    let swiper = null;
    useEffect(() => {
        // swiper 不能多次实例化 
        if (swiper) {
            return 
        }
        new Swiper('.btn-banners', {
            loop: true,
            autoplay: {
                delay: 2000
            },
            pagination: {
                el: '.swiper-pagination'
            }
        })
    }, [])
    return (
    <BannersWrapper>
        <div className="btn-banners swiper-container">
            <div className="swiper-wrapper">
                <div className="swiper-slide">
                    <p>
                        <img  src="https://puui.qpic.cn/media_img/lena/PICw0n5js_580_1680/0" /> 
                    </p>
                </div>
                <div className="swiper-slide">
                    <p>
                        <img src="https://vc.qpic.cn/tpic/mtviw4vsGBMdU/4ney5967pvoxn686/1680" />
                    </p>
                </div>
                <div className="swiper-slide">
                    <p>
                        <img src="https://puui.qpic.cn/media_img/lena/PICv1znd5_580_1680/0" /> 
                    </p>
                </div>
            </div>
            <div className="swiper-pagination"></div>
        </div>
    </BannersWrapper>
    )
}

4.5 配置路由组件,把路由放到MyRoutes文件夹,避免App.jsx文件太繁杂,页面不整洁

<Routes>
      <Route path="/" element={<Home/>}></Route>
      <Route path="/home" element={<Home/>}></Route>
      <Route path="/movie" element={<Movie/>}></Route>
      <Route path="/yanchu" element={<Yanchu/>}></Route>
      <Route path="/events" element={<Events/>}></Route>
      <Route path="/mine" element={<Mine/>}></Route>
      <Route path="/cities" element={<Cities/>}></Route>
</Routes>

5 项目优化

1. 在写项目的过程中发现,首页打开较慢。

react是单页应用,在单页应用中,如果没有设置懒加载,webpack打包后的文件就会很大;从而造成进入首页时,需要加载的资源过多,时间过长,不利于用户体验。运用懒加载,就则可以将页面进行划分,在需要的时候加载对应的页面。可以有效的分担首页所承担的加载压力,减少首页加载用时。

2. 解决办法

React.lazy() 和 Suspense

React.lazy 功能允许将动态导入呈现为常规组件

Suspense:用于在因网络延迟等而导致的组件不能快速的加载到页面时出现的提示。

MyRoutes.jsx 引入lazy

// MyRoutes
import React,{lazy} from 'react'
import { Route,Routes} from 'react-router'
const Cities = lazy(() => import('@/pages/Cities'))
const Events = lazy(() => import('@/pages/Events'))
const Movie = lazy(() => import('@/pages/Movie'))
const Yanchu = lazy(() => import('@/pages/Yanchu'))
const Mine = lazy(() => import('@/pages/Mine'))

App.jsx 中引入 Suspense

import { useEffect, useState ,Suspense} from 'react'
import './App.css'
import 'antd/dist/antd.css' // UI 框架的样式
import Footer from './components/Footer'
import 'font-awesome/css/font-awesome.min.css'
import MyRoutes from './MyRoutes'
function App() {
  return (
    <>
      <Suspense fallback={<div>loading...</div>}>
        <MyRoutes/>
      </Suspense>
      <Footer/>
    </>
  )
}

export default App

6. 总结

因为时间很紧,部分功能还没实现,用户体验上还需要继续优化。后续会继续跟进,希望这篇文章对新手入门react实战有帮助呀。小可爱们,你们的点赞就是我最大的支持哦!

项目地址

gitpage晚点上线哦! 先上源码地址

gitee.com/gchhch12/gc…

猜你喜欢

转载自juejin.im/post/7114134613634580511
今日推荐