Skip to content

React-项目:爱彼迎

[TOC]

项目介绍

爱彼迎团队:

  • 开发团队国内200多个人;

  • 经历过数年版本迭代最终完成的一个产品;

爱彼迎目前的项目

  • 已经不针对国内房东、订房进行维护,依然可以订阅国外的房屋;

  • 爱彼迎已经退出中国市场,不排除有一天国内的网站不能继续访问;

  • 项目本身确实非常美观、好看;

项目核心学习、掌握的知识点:

  • 对于第一个React项目,我们的核心是对前面所学知识进行练习、实战;

  • 掌握React开发的流程、模式、项目架构,项目中会有很多组件、工具等封装、抽取、复用思想;

  • 最重要的是学习React开发的模式和编程的思想,而不是局限于我上课期间所讲的内容,并且大部分样式和布局内容需要大家课程自行完成;

  • 在这个项目过程中,我会尽量将之前所学的所有知识都运用起来,但是我们不会为了用某个知识而用某个知识;

  • 课程中会使用我服务器已经获取到的数据,一是国内的数据更好看,二是担心它数据有一天不再维护,三是我对数据已经进行了大量的整理;

  • 后续我们还会专门学习React+TypeScript项目实战的内容,React本身非常的灵活,对JavaScript本身要求也较高,但是最重要的还是练习;

项目规范

  1. 文件夹、文件名称统一小写、多个单词以连接符(-)连接;
  2. JavaScript变量名称采用小驼峰标识,常量全部使用大写字母,组件采用大驼峰;
  3. CSS采用普通CSS和styled-components结合来编写(全局采用普通CSS、局部采用styled-components);
  4. 整个项目不再使用class组件,统一使用函数式组件,并且全面拥抱Hooks;
  5. 所有的函数式组件,为了避免不必要的渲染,全部使用memo进行包裹;
  6. 组件内部的状态,使用useState、useReducer;业务数据全部放在redux中管理;
  7. 函数组件内部基本按照如下顺序编写代码:
    • 组件内部state管理;
    • redux的hooks代码;
    • 其他hooks相关代码(比如自定义hooks);
    • 其他逻辑代码;
    • 返回JSX代码;
  8. redux代码规范如下:
    • redux目前我们学习了两种模式,在项目实战中尽量两个都用起来,都需要掌握;
    • 每个模块有自己独立的reducer或者slice,之后合并在一起;
    • redux中会保存共享的状态、从服务器获取到的数据状态;
  9. 网络请求采用axios
    • 对axios进行二次封装;
    • 所有的模块请求会放到一个请求文件中单独管理;
  10. 项目使用AntDesign、MUI(Material UI)
    • 爱彼迎本身的设计风格更多偏向于Material UI,但是课程中也会尽量讲到AntDesign的使用方法;
    • 项目中某些AntDesign、MUI中的组件会被拿过来使用;
    • 但是大部分组件还是自己进行编写、封装、实现;
  11. 其他规范在项目中根据实际情况决定和编写;

创建react项目

1、创建项目:

sh
create-react-app mr-airbnb

2、项目配置:

  • 配置项目的icon

  • 配置项目的标题

  • 配置jsconfig.json

3、通过craco配置别名和less文件:

  • 安装:

    安装craco:npm i @craco/craco@alpha -D

    安装less:npm i craco-less@2.1.0-alpha.0 -D

  • 创建文件:craco.config.js

  • 配置别名

    js
    const path = require('path')
    
    function resolve(pathname) {
      return path.resolve(__dirname, pathname)
    }
    
    module.exports = {
      webpack: {
        // 配置别名
        alias: {
          '@': resolve('src')
        }
      }
    }
  • 配置less

    js
      const path = require('path')
    +  const CracoLessPlugin = require('craco-less')
    
      function resolve(pathname) {
        return path.resolve(__dirname, pathname)
      }
    
      module.exports = {
        // 配置Less
    +    plugins: [
    +      {
    +        plugin: CracoLessPlugin
    +      }
    +    ],
        webpack: {
          // 配置别名
          alias: {
            '@': resolve('src')
          }
        }
      }
  • 修改启动脚本

    js
      "scripts": {
    +    "start": "craco start",
    +    "build": "craco build",
    +    "test": "craco test",
        "eject": "react-scripts eject"
      },

目录结构

对项目进行目录结构的划分:

image-20230420164459186

CSS样式的重置

对默认CSS样式进行重置:

  • normalize.css

    安装:npm i normalize.css

  • reset.css

    js
    @mainColor: #484848;
    
    body, button, dd, dl, dt, form, h1, h2, h3, h4, h5, h6, hr, input, li, ol, p, td, textarea, th, ul {
      padding: 0;
      margin: 0;
    }
    
    a {
      color: @mainColor;
      text-decoration: none;
    }
    
    img {
      vertical-align: top;
    }
    
    ul, ol, li {
      list-style-type: none;
    }

全家桶–Router配置

1、安装:npm i react-router-dom

2、导入HashRouter

js
import { HashRouter } from 'react-router-dom';
root.render(
  <HashRouter>
    <App />
  </HashRouter>
);

3、配置路由

js
import { lazy } from "react"
import { Navigate } from "react-router-dom"

import NotFound from "@/views/not-found"
// 路由懒加载
const Home = lazy(() => import('@/views/home'))
const Detail = lazy(() => import('@/views/detail'))
const Entire = lazy(() => import('@/views/entire'))

const routes = [
  { path: '/', element: <Navigate to='/home'/> },
  { path: '/home', element: <Home /> },
  { path: '/entire', element: <Entire /> },
  { path: '/detail', element: <Detail /> },
  { path: '*', element: <NotFound /> }

]

export default routes

4、异步加载时使用Suspense包裹App

js
root.render(
  <Suspense fallback={<div>Loading...</div>}>
    <HashRouter>
      <App />
    </HashRouter>
  </Suspense>
)

5、挂载路由

js
+ import { useRoutes } from 'react-router-dom'
+ import routes from './router'

const App = memo(() => {
  return (
    <div>
      <div className="header">Header</div>
      <div className="pages">
        {/* 挂载路由 */}
+        {useRoutes(routes)}
      </div>
      <div className="footer">Footer</div>
    </div>
  )
})

全家桶–Redux状态管理

Redux状态管理的选择:

  • 普通方式:目前项目中依然使用率非常高;
  • @reduxjs/toolkit方式:推荐方式, 未来的趋势;

1、安装:

  • 安装RTK:npm i @reduxjs/toolkit
  • 安装react-redux:npm i react-redux

2、创建store

js
import { configureStore } from "@reduxjs/toolkit";
import homeReducer from './features/home'

const store = configureStore({
  reducer: {
    home: homeReducer
  }
})

export default store

3、创建reducer

RTK方式:

js
import { createSlice } from "@reduxjs/toolkit";

const homeSlice = createSlice({
  name: 'home',
  initialState: {
    count: 100,
    banners: []
  },
  reducers: {

  }
})

export default homeSlice.reducer

普通方式:

  • constants.js

    js
    export const CHANGE_MSG = 'change_msg'
  • createAction.js

    js
    import { CHANGE_MSG } from "./constants";
    
    export function changeMsg(msg) {
      return {
        type: CHANGE_MSG,
        msg: msg
      }
    }
  • reducer.js

    js
    import { CHANGE_MSG } from "./constants"
    
    const initialState = {
      msg: 'hi'
    }
    
    function reducer(state = initialState, action) {
      switch(action.type) {
        case CHANGE_MSG:
          return { ...state, msg: action.msg }
        default:
          return state
      }
    }
    
    export default reducer
  • index.js

    js
    import detailReducer from "./reducer";
    
    import { changeMsg } from "./createAction";
    
    export {
      detailReducer,
      changeMsg
    }

4、导入store到组件树

js
root.render(
+  <Provider store={store}>
    <Suspense fallback={<div>Loading...</div>}>
      <HashRouter>
+        <App />
      </HashRouter>
    </Suspense>
+  </Provider>
)

网络请求-axios

1、安装:npm i axios

2、封装axios

js
import axios from "axios";

import { BASE_URL, TIMEOUT } from "./config";

class MrRequest {
  constructor(baseURL, timeout = 10000) {
    this.instance = axios.create({baseURL, timeout})
    this.instance.interceptors.response.use(res => {
      return res.data
    }, err => {
      return err
    })
  }
  request(config) {
    return this.instance.request(config)
  }
  get(config) {
    return this.request({...config, method: 'GET'})
  }
  post(config) {
    return this.request({...config, method: 'POST'})
  }
}

const mrRequest = new MrRequest(BASE_URL, TIMEOUT)
export default mrRequest

3、配置

js
export const BASE_URL = 'http://codercba.com:1888/airbnb/api'
export const TIMEOUT = 10000

4、测试网络请求

js
const Home = memo(() => {
  const [highScore, setHighScore] = useState({}) 

  // 测试网络请求
  useEffect(() => {
+    mrRequest.request({
+      url: '/home/highScore'
+    }).then(res => {
+      setHighScore(res)
+    })
+  }, [])

  return (
    <div>
      <h3>Home</h3>
      <div className="high-score">
        <div className="title">{highScore.title}</div>
        <div className="subtitle">{highScore.subtitle}</div>
        <ul className="list">
          {
            highScore.list?.map(item => {
              return (
                <li key={item.id}>{item.name}</li>
              )
            })
          }
        </ul>
      </div>
    </div>
  )
})

集成UI组件库

MUI

安装

MUI + styled-components

sh
npm i @mui/material @mui/styled-engine-sc styled-components

配置styled-components

js
 module.exports = {
   //...
+  resolve: {
+    alias: {
+      '@mui/styled-engine': '@mui/styled-engine-sc'
+    },
+  },
 };

使用MUI

image-20230427165022132

image-20230427165006788

错误:

使用button时报错:

image-20230427165927946

解决:

安装@emotion/react @emotion/styled

sh
npm i @mui/material @emotion/react @emotion/styled

Antd

安装

sh
npm i antd

配置antd + less

1、引入样式

js
@import "~antd/dist/antd.less"
// 或者
import 'antd/dist/antd.less'

2、配置webpack

image-20230427171148167

使用antd

image-20230427172653632

image-20230427171422434

组件

AppHeader

image-20230422162232248

页面布局

js
const AppHeader = memo((props) => {
  return (
    <AppHeaderWrapper>
      <HeaderLeft />
      <HeaderCenter />
      <HeaderRight />
    </AppHeaderWrapper>
  )
})

HeaderLeft

image-20230422155126079

js
const HeaderLeft = memo(() => {
  return (
    <HeaderLeftWrapper>
+      <img className='logo' src={require("@/assets/img/logo.png")} alt="" />
    </HeaderLeftWrapper>
  )
})

HeaderCenter

image-20230422161530465

js
const HeaderCenter = memo((props) => {
  return (
    <HeaderCenterWrapper>
      <div className="text">搜索房源和体验</div>
      <div className="search">
        <IconSearch />
      </div>
    </HeaderCenterWrapper>
  )
})

HeaderRight

image-20230422154943125

js
const HeaderRight = memo((props) => {
  return (
    <HeaderRightWrapper>
      <div className="btns">
        <span className="btn">登录</span>
        <span className="btn">注册</span>
        <span className="btn">
          <IconGlobal />
        </span>
      </div>
      <div className="profile">
        <span className='icon icon-list'>
          <IconList />
        </span>
        <span className='icon icon-profile'>
          <IconProfile />
        </span>
      </div>
    </HeaderRightWrapper>
  )
})

阴影动画效果

image-20230422160336416

基本写法

css
.profile {
  transition: box-shadow 200ms ease;
  &:hover {
    box-shadow: 0 2px 4px rgba(0,0,0,.18);
  }
}

抽取动画效果

js
const theme = {
  mixin: {
    boxShadow: `
      transition: box-shadow 200ms ease;
      &:hover {
        box-shadow: 0 2px 4px rgba(0,0,0,.18);
      }
    `
  }
}

使用动画效果

css
  .profile {
    width: 280px;
    padding-left: 15px;
    font-weight: 600;
+    ${props => props.theme.mixin.boxShadow}
  }

Profile点击面板切换效果

image-20230422162409825

1、布局

html
      <div className="profile" onClick={e => setIsPanelShow(true)}>
        <span className='icon icon-list'>
          <IconList />
        </span>
        <span className='icon icon-profile'>
          <IconProfile />
        </span>

+        {/* 面板 */}
+        <div className="panel">
          <div className="top">
            <div className="item register">注册</div>
            <div className="item login">登录</div>
          </div>
          <div className="bottom">
            <div className="item">出租房源</div>
            <div className="item">开展体验</div>
            <div className="item">帮助</div>
          </div>
        </div>
      </div>

2、点击切换显示和隐藏

js
const HeaderRight = memo((props) => {
  const [isPanelShow, setIsPanelShow] =  useState(false)
        {/* 面板 */}
+        {isPanelShow && (
          <div className="panel">
            ...
          </div>
        )}
})

3、点击Profile显示

js
<div className="profile" onClick={(e) => setIsPanelShow(true)}>

4、监听window点击,隐藏面板

注意: 需要在addEventListener最后的参数设置true,表示捕获模式,这样点击window时才会隐藏面板

js
  useEffect(() => {
    function windowClickHandle() {
      setIsPanelShow(false)
    }
    window.addEventListener('click', windowClickHandle, true)
    return () => {
      window.removeEventListener('click', windowClickHandle, true)
    }
  }, [])

在网页中嵌入SVG图片

1、将svg保存到组件中,并导出

js
import React, { memo } from 'react'
import styleStrToObj from '../utils'

const IconMenu = memo(() => {
  return (
    <svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="presentation" focusable="false" style={styleStrToObj("display: block; fill: none; height: 100%; width: 100%; stroke: currentcolor; stroke-width: 3; overflow: visible;")}><g fill="none" fillRule="nonzero"><path d="m2 16h28"></path><path d="m2 24h28"></path><path d="m2 8h28"></path></g></svg>
  )
})

export default IconMenu

2、修改svg中的style为jsx格式的style对象写法

js
style={styleStrToObj("display: block; fill: none; height: 100%; width: 100%; stroke: currentcolor; stroke-width: 3; overflow: visible;")

3、styleStrToObj方法

js
function styleStrToObj(styleStr) {
  const obj = {} 
  const s = styleStr
              .toLowerCase()
              .replace(/-(.)/g, function(m, g) {
                return g.toUpperCase()
              })
              .replace(/;\s?$/g, '')
              .split(/:|;/g)
  for(let i=0; i<s.length; i+=2) {
    obj[s[i].replace(/\s/g, '')] = s[i+1].replace(/^\s+|\s+$/g, '')
  }            
  return obj
}
  
export default styleStrToObj

4、修改svg颜色:修改svg的父元素的颜色

js
    .icon{
      display: inline-block;
+      color: ${props => props.theme.color.second};
    }

5、在页面中使用该组件

js
  <span className='icon icon-profile'>
+    <IconProfile />
  </span>

设置主题色

方式一:CSS原生的方法

image-20230422131754974

image-20230422131744506

方式二:styled-components的方式

1、定义theme

js
const theme = {
  color: {
    main: '#484848',
    second: '#767676'
  }
}

export default theme

2、使用styled-components 中的ThemeProvider挂载theme到组件树中

js
root.render(
  <Suspense fallback={<div>Loading...</div>}>
    <Provider store={store}>
+      <ThemeProvider theme={theme}>
        <HashRouter>
          <App />
        </HashRouter>
+      </ThemeProvider>
    </Provider>
  </Suspense>
)

3、在组件中使用theme

css
  .icon{
    display: inline-block;
+    color: ${props => props.theme.color.second};
  }

AppFooter

image-20230422181119842

页面布局

html
const AppFooter = memo(() => {
  return (
    <AppFooterWrapper>
      <div className="container">
        <div className="links">
+          {footerData.map((item, index) => {
            return (
              <dl className="item-group" key={index}>
                <dt className="item-title">{item.name}</dt>
+                {item.list.map((iten, indey) => {
                  return (
+                    <dd className="item-cell" key={indey}>
+                      {iten}
+                    </dd>
                  )
                })}
              </dl>
            )
          })}
        </div>
        <div className="info">
          <div className="left">
            <div className="logo">
              <IconLogo />
            </div>
            <div className="text">
              <div>
                <a href="#">京ICP备16017121号</a>
                <a href="#">京ICP证 160773号</a>
                <img className="icon" src={require('@/assets/img/public_security_bureau_logo.png')} alt="" />
                <a href="#">京公网安备 11010502032345号</a>
                <span>安彼迎网络(北京)有限公司</span>
                <img className="icon" src={require('@/assets/img/electronic_business_license.png')} alt="" />
                <a href="#">营业执照</a>
              </div>
              <div>
                <span>© 2023 Airbnb, Inc. All rights reserved.</span>
                <a href="#">条款</a>·<a href="#">隐私政策</a>·<a href="#">网站地图</a
                <span>· 全国旅游投诉渠道 12301</span>
              </div>
            </div>
          </div>
          <div className="right">
            <a className="tag" href="">
              <IconSina />
            </a>
            <a className="tag" href="">
              <IconWeixin />
            </a>
          </div>
        </div>
      </div>
    </AppFooterWrapper>
  )
})

模拟数据

json
[
  {
    "name": "爱彼迎",
    "list": ["工作机会", "爱彼迎新闻", "政策", "无障碍设施"]
  },
  {
    "name": "发现",
    "list": ["信任与安全", "旅行基金", "商务差旅", "爱彼迎杂志", "Airbnb.org"]
  },
  {
    "name": "出租",
    "list": ["为什么要出租?", "房东义务", "开展体验", "资源中心"]
  },
  {
    "name": "客服支持",
    "list": ["帮助", "邻里支持"]
  }
]

SectionHeader

js
import PropTypes from 'prop-types'
import React, { memo } from 'react'
import { SectionHeaderWrapper } from './style'

const SectionHeader = memo((props) => {
+  const { title, subtitle } = props
  return (
    <SectionHeaderWrapper>
      <h2 className="title">{title}</h2>
+      {subtitle && <div className="subtitle">{subtitle}</div>}
    </SectionHeaderWrapper>
  )
})

SectionHeader.propTypes = {
+  title: PropTypes.string,
+  subtitle: PropTypes.string
}

export default SectionHeader

使用组件

js
  return (
    <HomeSectionWrapper>
+      <SectionHeader title={infoData?.title} subtitle={infoData?.subtitle}/>
      <SectionRooms roomList={infoData?.list?.slice(0, 8)}/>
    </HomeSectionWrapper>
  )

SectionRooms

页面布局

js
import PropTypes from 'prop-types'
import React, { memo } from 'react'
import { SectionRoomsWrapper } from './style'
import RoomItem from '../RoomItem'

const SectionRooms = memo((props) => {
+  const { roomList } = props
  return (
    <SectionRoomsWrapper>
      {
+        roomList?.map(item => {
+          return <RoomItem key={item.id} itemData={item}/>
+        })
      }
    </SectionRoomsWrapper>
  )
})

SectionRooms.propTypes = {
+  roomList: PropTypes.array
}

export default SectionRooms

使用组件

js
  return (
    <HomeSectionWrapper>
      <SectionHeader title={infoData?.title} subtitle={infoData?.subtitle}/>
+      <SectionRooms roomList={infoData?.list?.slice(0, 8)}/>
    </HomeSectionWrapper>
  )

RoomItem

image-20230422181436084

页面布局

js
import PropTypes from 'prop-types'
import React, { memo } from 'react'
import { RoomItemWrapper } from './style'
import { Rate } from 'antd'

const RoomItem = memo((props) => {
  const { itemData } = props
  return (
    <RoomItemWrapper>
      <div className="inner">
+        {/* 1. 设置长款一样的图片 */}
        <div className="cover">
          <img src={itemData.picture_url} alt="" />
        </div>
        <div className="tag">
          <span className="text">{itemData.verify_info.messages.join(' · ')}</span>
        </div>
        <div className="title">{itemData.name}</div>
        <div className="price">
          <div className="bottom">
+            {/* 2. 引入Antd组件 */}
            <Rate
              allowHalf
              value={itemData.star_rating ?? 5}
              style={{ fontSize: '12px', color: itemData.star_rating_color }}
            />
            <span className="reviews-count">{itemData.reviews_count}</span>
+            {/* 3. 只有当 bottom_info 有值时才显示comment */}
            {itemData.bottom_info && <span className="comment"> · {itemData.bottom_info?.content}</span>}
          </div>
        </div>
      </div>
    </RoomItemWrapper>
  )
})

RoomItem.propTypes = {
  itemData: PropTypes.object
}

export default RoomItem

设置长款一样的图片

html
        <div className="cover">
          <img src={itemData.picture_url} alt="" />
        </div>
css
  .cover {
    position: relative;
+    padding: 66.6666% 5px 5px 5px;
    img {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  }

传递样式变量到css

js
const theme = {
  color: {
    main: '#484848',
    second: '#767676',
+    third: '#222222'
  }
}
export default theme
css
  .title {
    font-weight: 600;
+    color: ${props => props.theme.color.third};
    margin-top: 8px;
  }

调整rating的间距

css

    .ant-rate {
      li {
+        margin-inline-end: 0;
      }
    }

使用组件

js
const SectionRooms = memo((props) => {
  const { roomList } = props
  return (
    <SectionRoomsWrapper>
      {
        roomList?.map(item => {
+          return <RoomItem key={item.id} itemData={item}/>
        })
      }
    </SectionRoomsWrapper>
  )
})

Section-tabs

使用组件

image-20230503154417177

image-20230503154443649

页面布局

image-20230503154619128

点击item,切换rooms

image-20230503155508227

1、点击事件

image-20230503154938757

2、导入classnames

sh
npm i classnames

3、高亮点击的item

image-20230503155338856

image-20230503155447173

4、暴露内部切换事件

组件外部

image-20230503155711229

image-20230503160637209

image-20230503160553679

组件内部

image-20230503155959734

image-20230503160017918

5、抽取带tabs的HomeSectionV2

见:组件:HomeSectionV2

tabs滚动效果

见:组件:ScrollView

SectionFooter

使用组件

image-20230503165726431

image-20230503170636322

页面布局

1、显示全部

image-20230503170334554

image-20230503165939212

2、查看更多xxx房源

image-20230503170339471

image-20230503170805197

ScrollView

image-20230503172428046

image-20230503172452193

image-20230503172441017

使用组件

image-20230503172910828

页面布局

image-20230503172939349

右边按钮

当scrollWidth - clientWidth的值大于0时,显示右边按钮

image-20230503174217738

点击[右边按钮],向左滚动1个item的距离

image-20230503180120374

image-20230503180435570

添加滚动动画

image-20230503180344485

当totalDistance - newOffsetLeft的值大于0时,显示右边按钮,否则隐藏右边按钮

image-20230503181459567

image-20230503181513407

image-20230503181045201

左边按钮

1、显示、隐藏左边按钮

当newOffsetLeft > 0 时显示左边按钮

点击右边按钮时

image-20230504110250842

点击左边按钮时

image-20230504110727405

2、抽取点击按钮的处理函数

image-20230504110900955

按钮样式

image-20230504111447755

Home

组件:HomeSection

页面布局

js
import PropTypes from 'prop-types'
import React, { memo } from 'react'
import { HomeSectionWrapper } from './style'
import SectionHeader from '../SectionHeader'
import SectionRooms from '../SectionRooms'

const HomeSection = memo((props) => {
+  const { infoData } = props
  return (
    <HomeSectionWrapper>
+      <SectionHeader title={infoData?.title} subtitle={infoData?.subtitle}/>
+      <SectionRooms roomList={infoData?.list?.slice(0, 8)}/>
    </HomeSectionWrapper>
  )
})

+ HomeSection.propTypes = {
+  infoData: PropTypes.object
+ }

export default HomeSection

使用组件

js
  return (
    <HomeWrapper>
      <HomeBanner />
      <div className="content">
+        <HomeSection infoData={goodPrice}/>
+        <HomeSection infoData={highScore}/>
+        <HomeSection infoData={plus}/>
      </div>
    </HomeWrapper>
  )

组件:HomeSectionV2

使用组件

image-20230503161253991

页面布局

image-20230503161619086

image-20230503161616284

useState默认值

由于useState(initial)的initial只有在组件第一次渲染时才有效,但是此时第一次传递的是空字符串''

image-20230503162907096

上面的方法无法提供默认值

解决方法:控制第一次discountInfo没有值时,不能渲染

image-20230503163042661

image-20230503163333397

组件:HomeLongfor

使用组件

image-20230504113125647

image-20230504113219308

页面布局

image-20230504120835406

image-20230504113235182

组件:LongforItem

image-20230504114056454

image-20230504113651126

见:webpack中引入图片

webpack中引入图片

1、在img的src中引入图片

image-20230422165416485

2、在background的url中引入图片

image-20230422165648851

或者直接在模板字符串中使用require()函数导入

js
export const HomeBannerWrapper = styled.div`
  height: 529px;
  background: url(${require('@/assets/img/cover_01.jpeg')}) center/cover;
`

高性价比

API:http://codercba.com:1888/airbnb/api/home/goodprice

image-20230422172519575

页面布局

js
const Home = memo(() => {
  const dispatch = useDispatch()

  // 网络请求
  useEffect(() => {
    dispatch(fetechHomeDataAction())
  }, [dispatch])

  // 获取redux store
  const { goodPrice } = useSelector(state => ({
    goodPrice: state.home.goodPrice
  }))

  return (
    <HomeWrapper>
      <HomeBanner />
      <div className="content">
        <HomeSection infoData={goodPrice}/>
      </div>
    </HomeWrapper>
  )
})

HomeSection见:组件:HomeSection

获取数据

1、在service中发送网络请求

js
// 高性价比
export const getHomeGoodprice = () => {
  return mrRequest.get({ url: '/home/goodprice' })
}

2、redux

js
export const fetechHomeDataAction = createAsyncThunk('homedata', (payload, { dispatch }) => {
  // 高性价比
+  getHomeGoodprice().then((res) => {
+    dispatch(changeGoodPriceAction(res))
  })
})
const homeSlice = createSlice({
  name: 'home',
  initialState: {
    goodPrice: {}
  },
  reducers: {
+    changeGoodPriceAction(state, { payload }) {
+      state.goodPrice = payload
+    }
  }
})

3、在组件中派发异步事件

js
const Home = memo(() => {
  const dispatch = useDispatch()

  // 网络请求
  useEffect(() => {
+    dispatch(fetechHomeDataAction())
  }, [dispatch])
})

4、从redux中获取数据到组件中

js
const Home = memo(() => {
  // 获取redux store
+  const { goodPrice } = useSelector(state => ({
+    goodPrice: state.home.goodPrice
+  }), shallowEqual)

  return (
    <HomeWrapper>
      <div className="content">
+        <HomeSection infoData={goodPrice}/>
      </div>
    </HomeWrapper>
  )
})

高分房源

API: http://codercba.com:1888/airbnb/api/home/highscore

image-20230503163941400

获取数据

1、services

js
// 高分房源
export const getHomeHighScore = () => {
  return mrRequest.get({ url: '/home/highscore' })
}

2、store/modules/home.js

createAsyncThunk 中同时发送多个异步请求,可以通过dispath 将获得的res保存到state中

js
export const fetechHomeDataAction = createAsyncThunk('homedata', (payload, { dispatch }) => {
  // 高性价比
  getHomeGoodprice().then((res) => {
    dispatch(changeGoodPriceAction(res))
  })

  // 高分房源
+  getHomeHighScore().then((res) => {
+    dispatch(changeHighScoreAction(res))
+  })
})
const homeSlice = createSlice({
  name: 'home',
  initialState: {
    goodPrice: {},
    highScore: {}
  },
  reducers: {
    changeGoodPriceAction(state, { payload }) {
      state.goodPrice = payload
    },
+    changeHighScoreAction(state, { payload }) {
+      state.highScore = payload
+    }
  },
})
+ export const { changeGoodPriceAction, changeHighScoreAction } = homeSlice.actions

3、在组件中获取highScore数据

js
const Home = memo(() => {
  // 获取redux store
  const { goodPrice, highScore, plus } = useSelector(state => ({
    goodPrice: state.home.goodPrice,
+    highScore: state.home.highScore
  }), shallowEqual)
})

4、在组件中展示数据

js
const Home = memo(() => {
  return (
    <HomeWrapper>
      <HomeBanner />
      <div className="content">
        <HomeSection infoData={goodPrice}/>
+        <HomeSection infoData={highScore}/>
      </div>
    </HomeWrapper>
  )
})

抽取组件

见:Home / 组件:HomeSection

折扣优惠

API: http://codercba.com:1888/airbnb/api/home/discount

image-20230422171815580

获取数据

1、service

image-20230427211312645

2、store

image-20230427211450467

image-20230427211423326

展示页面

1、组件中获取store数据

image-20230427212042880

2、展示discount数据(先写死)

image-20230427212522177

3、修改item宽度

动态传递item宽度

HomeSection

image-20230427213429691

SectionRooms

image-20230427213213742

RoomItem

image-20230427213026714

RoomItem的style文件

image-20230427212940455

封装tabs

见:Section-tabs

热门推荐

API:http://codercba.com:1888/airbnb/api/home/hotrecommenddest

image-20230503164105362

image-20230503164137150

获取数据

1、services

image-20230503164456955

2、redux

image-20230503164538977

image-20230503164621419

3、Home组件

image-20230503164722816

image-20230503164746936

4、封装SectionFooter

见:组件:SectionFooter

向往城市

API: http://codercba.com:1888/airbnb/api/home/longfor

image-20230503164154724

获取数据

1、services

image-20230504112734621

2、redux

image-20230504112758562

image-20230504112843645

3、Home组件

见:Home/组件:HomeLongfor

plus房源

API: http://codercba.com:1888/airbnb/api/home/plus

image-20230503164212975

获取数据

1、services

image-20230504120947187

2、redux

image-20230504121028541

image-20230504121042465

封装:HomeSectionV3

使用组件

页面布局

image-20230504121953236

Entire

image-20230504123330408

image-20230504123453247

跳转Entire

image-20230504123211344

点击logo,回到首页

image-20230505173248679

页面布局

image-20230504173253460

组件:EntireFilter

image-20230504173333480

模拟数据

image-20230504173453116

页面布局

image-20230504173836962

点击选中过滤项

点击过滤项,如果已经存在selectItems中,则移除该项,否则就添加该项到selectItems中

image-20230504174547778

如果当前项存在于selectItems中,则添加active类

image-20230504174641125

组件:EntireRooms

获取数据

1、services

image-20230504180828675

2、redux

constants

image-20230504180242575

reducer

image-20230504180434154

actionCreators

image-20230504180542605

在actionCreators中发送异步网络请求

image-20230504181506908

index

image-20230504180607255

3、在组件中调用redux中的异步请求

image-20230504181335137

4、通过currentPage动态获取数据

image-20230504181736904

页面布局

组件:EntirePagination

js

image-20230505181837216

自定义UI组件分页样式

image-20230505181419163

方法一:修改mui的主题色

image-20240719173900798

方法二:覆盖mui的样式

css
  /* 覆盖MUI样式 */
  .MuiPaginationItem-page.Mui-selected {
    background-color: #222;
    color: #fff;
  }
  .MuiPaginationItem-page {
    margin: 0 10px;
    &:hover {
      text-decoration: underline;
    }
  }

点击分页,实现分页

image-20230509145546011

image-20230509145750469

优化

优化: 合并2次dispatch

优化: 点击页码后,回到顶部,并添加加载蒙版

1、redux/actionCreators 中生成changeIsLoadingAction

image-20230509162203820

2、redux/reducer中修改isLoading

image-20230509162244922

image-20230509162435587

3、在组件中通过isLoading显示/隐藏蒙版

image-20230509162739651

image-20230509162757874

image-20230509162803775

轮播图

页面布局

1、基础使用

image-20230509165145289

2、不显示指示器

image-20230509165403947

3、显示左右控制箭头

image-20230509165746885

4、修改svg图片的大小

image-20230509165959520

image-20230509170113843

5、修改svg图片的颜色

方法:可以通过修改当前元素父元素的颜色来实现

image-20230509170355377

6、控制箭头样式实现

image-20230509170715422

点击控制箭头

1、点击控制箭头,实现图片切换

image-20230509171047147

image-20230509171217442

image-20230509171306085

指示器

1、结构搭建

jsx
const Indicator = memo((props) => {
  return (
+    <IndicatorCss>
+      <div className="content">
+        {props.children}
+      </div>
+    </IndicatorCss>
  )
})

Indicator.propTypes = {}

export default Indicator

2、使用Indicator

jsx
const Test = memo(() => {
  const list = ['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff', 'ggg', 'hhh', 'iii', 'jjj', 'kkk']
  
  return (
    <TestCss>
      <div className="control">
        <button>上一页</button>
        <button>下一页</button>
      </div>
      <div className="list">
+        <Indicator>
  +        {
  +          list.map((item, index) => {
  +            return <button key={item}>{item}</button>
  +          })
  +        }
+        </Indicator>
      </div>
    </TestCss>
  )
})

3、记录当前选中index

jsx
const Test = memo(() => {
+  const [currIndex, setCurrIndex] = useState(0)
  // 事件处理函数
  function btnClickHandle(isNext) {
    // 计算新索引
+    let newIndex = isNext ? currIndex + 1 : currIndex - 1
+    if(newIndex < 0) newIndex = list.length - 1
+    if(newIndex >  list.length - 1) newIndex = 0

 +   setCurrIndex(newIndex)
  }
    <TestCss>
      <div className="control">
 +       <button onClick={e => btnClickHandle(false)}>上一页</button>
 +       <button onClick={e => btnClickHandle(true)}>下一页</button>
		...
      </div>
    </TestCss>
})

4、分析滚动位置

image-20230509181848967

5、在组件外部传递选中index到组件内部

jsx
+        <Indicator currIndex={currIndex}>
          {
            list.map((item, index) => {
              return <button key={item}>{item}</button>
            })
          }
        </Indicator>

6、在组件内部,根据index计算滚动距离,保证每个当前项都在中间位置

jsx
const Indicator = memo((props) => {
+  const { currIndex } = props
+  const contentRef = useRef()

  // 根据currIndex计算滚动的距离
  useEffect(() => {
+    const itemEl = contentRef.current.children[currIndex]
+    const itemLeft = itemEl.offsetLeft
+    const itemWidth = itemEl.clientWidth
+    const contentWidth = contentRef.current.clientWidth
+    const maxDistance = contentRef.current.scrollWidth - contentWidth
    
    // console.log(itemLeft, itemWidth, contentWidth, maxDistance)
     // 当前项要滚动的距离
+    let distance = itemLeft + itemWidth * 0.5 - contentWidth * 0.5
     // 当前项不移动的特殊情况 
+    if(distance < 0) distance = 0
+    if(distance > maxDistance) distance = maxDistance
     // 执行移动动画
+    contentRef.current.style.transform = `translateX(${-distance}px)`
+  }, [currIndex])

  return (
    <IndicatorCss>
+      <div className="content" ref={contentRef}>
        {props.children}
      </div>
    </IndicatorCss>
  )
})

样式

css
export const IndicatorCss = styled.div`
  overflow: hidden;

  .content {
    display: flex;
    flex-wrap: nowrap;
+    transition: transform 200ms ease;
  }
`

7、不移动的特殊情况

左边:当distance为负时,不需要移动到中间位置

右边:当distance > scrollWidth - clientWidth时,不需要移动

jsx
     // 当前项不移动的特殊情况 
+    if(distance < 0) distance = 0
+    if(distance > maxDistance) distance = maxDistance

项目中使用指示器

1、页面布局

image-20230510102318028

2、设置样式

image-20230510102632075

3、修改选中索引index

image-20230510102524307

是否显示轮播图

image-20230510103421401

image-20230510103326218

Detail

image-20230510111131802

跳转Detail

1、组件内部点击item

image-20230510103515574

2、传递内部itemClick事件到外部

image-20230510103713901

image-20230510103704948

3、在外部EntireRooms中定义itemClick的事件处理函数

image-20230510103854427

4、跳转时传递参数数据

image-20230510104614349

5、由于itemData数据较大,使用redux共享的方式传递数据

image-20230510105108253

image-20230510105132702

6、在EntireRooms组件中,保存item到redux的detailInfo

image-20230510105221458

7、在Detail页面中,获取detailInfo

image-20230510105735479

DetailPictures

Detail页面

image-20230510110417981

DetailPictures组件

image-20230510111630725

样式

image-20230510112051760

image-20230510112153766

image-20230510112456092

图片浏览器

image-20230510115351694

页面布局

1、基础使用PictureBrowser组件

image-20230510120902058

2、让滚动功能消失

image-20230510121603189

3、点击按钮,显示PictureBrowser组件

image-20230510121836549

image-20230510121915160

关闭图片浏览器

1、页面布局

image-20230510122932109

image-20230510122952383

2、监听关闭事件

组件内

image-20230510123052362

image-20230510123334138

组件外

image-20230510123146336

图片展示区

1、页面布局

image-20230510124945554

2、上一个、下一个

image-20230510154812152

image-20230510125613134

3、图片切换动画

第三方包:npm i react-transition-group

image-20230510155030917

image-20230510155851596

动画样式

image-20230510155717894

image-20230510155808171

通过isNext,判断动画方向

image-20230510160151933

image-20230510160209406

image-20230510160216881

image-20230510160244558

图片预览指示器

1、页面布局

image-20230510161659317

image-20230510162045958

2、svg图片组件

image-20230510161433586

3、添加选中时的样式active

image-20230510162158698

4、动态显示desc中的内容

image-20230510162545508

5、点击item指示器,切换图片

image-20230510162654557

6、显示/隐藏照片列表,并添加动画

这里不要使用display:block/none,因为它不 支持动画。可以使用height 切换

image-20230510162959121

image-20230510163040142

image-20230510163232166

image-20230510163252137

image-20230510163408095

解决点击时动画方向的问题

image-20230510163653833

image-20230510163817513

Extra

点击item阻止冒泡

image-20230510164322897

image-20230510164547406

页面切换滚动到顶部

方法: 在App页面监听页面是否发生路由跳转,如果跳转了则滚动到顶部

image-20230511115419912

封装成Hook

image-20230511115544064

image-20230511115534859

Header效果

不同页面的fixed效果切换

1、方法一:每个页面都有一个自己的<AppHeader /> 实例

2、方法二:3个页面共享同一个<AppHeader /> 实例【当前使用】

3、redux公共模块main

image-20230511112533252

image-20230511111911690

4、设置headerConfig

home页面

image-20230511112559127

entire页面

image-20230511112657174

detail页面

image-20230511112756705

5、在AppHeader中根据headerConfig设置样式

image-20230511114558583

image-20230511114708948

6、设置entire页面Filter组件也为fixed定位

问题: AppHeader没有监听headerConfig的改变

原因: 这是由于home,entire,detail页面是通过懒加载的方式导入的,在加载的时候Suspense 是在Provider 外部,没有被Provider 包裹,所以就不会监听懒加载页面的更新

jsx
root.render(
+  <Suspense fallback={<div>Loading...</div>}>
+    <Provider store={store}>
      <ThemeProvider theme={theme}>
        <HashRouter>
          <App />
        </HashRouter>
      </ThemeProvider>
+    </Provider>
+  </Suspense>
)

解决:Suspense 放在Provider 内部

jsx
root.render(
+  <Provider store={store}>
+    <Suspense fallback={<div>Loading...</div>}>
      <ThemeProvider theme={theme}>
        <HashRouter>
          <App />
        </HashRouter>
      </ThemeProvider>
+    </Suspense>
+  </Provider>
)

Header动画效果

image-20230511120109680

1、页面布局

image-20230511121947107

image-20230511120740643

2、HeaderCenter

image-20230511121237477

image-20230511121755800

样式

image-20230511121803312

3、模拟数据

image-20230511121142824

4、搜索状态的切换动画效果

AppHeader组件中

image-20230511122512938

image-20230511122515663

image-20230511122518542

HeaderCenter组件中

image-20230511122630371

image-20230511122853991

5、点击HeaderCenter,修改isSearch状态为true

image-20230511123005279

image-20230511123057426

组件外修改isSearch

image-20230511123126731

6、点击蒙版,修改isSearch状态为false

image-20230511123208384

7、动画效果

image-20230511124134138

detail动画样式

image-20230511124420179

bar动画样式

image-20230511124431788

8、监听滚动,search消失

useScrollPosition的hook

image-20230511125429373

为了防止监听滚动太频繁,对事件函数进行节流

  • 安装:npm i underscore

image-20230511125856429

image-20230511125936388

使用useScrollPosition

image-20230511130751777

Header顶部透明效果

image-20230511131158192

1、

image-20230511131640752

home页面

image-20230511131716591

其他页面为false

2、AppHeader组件中通过topAlpha修改透明度

image-20230511131821770

image-20230511131914707

image-20230511132232123

设置样式

image-20230511132505021

其他样式类似

3、当isAlpha=true时,isSearch一定为true

image-20230511133232848

解决suspense时AppHeader会打印2次的问题

解决: suspense不要包裹App,而是包裹useRoutes(routes)

image-20230511161849197

项目部署

购买服务器

安装Nginx