Header 动画
第一章:不同页面 fixed 切换
通过观察页面,Home 与 entire 页面的 header 是固定定位,而 detail 页面不是固定定位。简单实现是给每个页面单独写一个组件,不共用。这里采用共用 header 组件。
为了能动态改变定位模式,把这部分信息存到 redux 中,header 组件在一加载的时候从 store 中取数据,看是否要固定定位。拿到数据后通过 css in js (props) 把信息传到 css 中。
一、数据存到 redux
1. 新建 store 模块
@\store\features\main.js
import { createSlice } from "@reduxjs/toolkit"
const mainSlice = createSlice({
name: "main",
initialState: {
headerConfig: {
isFixed: false,
}
},
reducers: {
changeHeaderConfigAction(state, { payload }) {
state.headerConfig = payload
}
}
})
export const { changeHeaderConfigAction } = mainSlice.actions
export default mainSlice.reducer2. 引入 store
@\store\index.js
import { configureStore } from "@reduxjs/toolkit"
import mainReducer from "./features/main"
const store = configureStore({
reducer: {
// ......
main: mainReducer,
},
})
export default store二、分发配置
在 Home、entire、detail 页面分发 css 配置。
/* 以 Home 组件为例,其他无非是把 isFixed: true 改为 false */
const dispatch = useDispatch()
useEffect(() => {
dispatch(changeHeaderConfigAction({ isFixed: true }))
}, [dispatch])特别注意,@\index.js 中 Suspense 标签与 Provider 标签顺序。一定要 Provider 标签在外,Suspense 标签在里面。为什么?
在 @\components\app-header\index.jsx 中,使用 useSelector 从 store 中拿到数据,进而取出 isFixed,打印 isFixed 值。
/* 从redux中获取数据 */
const { headerConfig } = useSelector((state) => ({
headerConfig: state.main.headerConfig
}), shallowEqual)
const { isFixed } = headerConfig
console.log(isFixed)当进入 Home、entire、detail 页面,会分发 action,但是 header 组件的 isFixed 打印语句没有打印(store 中的数据改变了)。
因为组件是异步加载的,subscribe 不监听异步组件中 action 的。解决办法跟简单,Suspense 被 Provider 包裹即可。
<Provider store={store}>
<Suspense fallback={<div>Loading...</div>}>
<BrowserRouter>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</BrowserRouter>
</Suspense>
</Provider>说明
今后开发中,必须 <Provider store={store}> 标签置为最外层。
三、header 使用配置
@\components\app-header\index.jsx
<HeaderWrapper className={classNames({fixed: isFixed})}>
{/* ...... */}
</HeaderWrapper>@\components\app-header\style.jsx
&.fixed {
position: fixed;
z-index: 99;
top: 0;
left: 0;
right: 0;
}第二章:页面切换滚动到顶部
1)编写 hooks
@\hooks\useScrollTop.js
import { useEffect } from "react"
import { useLocation } from "react-router-dom"
export default function useScrollTop() {
const location = useLocation()
useEffect(() => {
window.scrollTo(0, 0)
}, [location.pathname])
}2)使用 hook,在 @\App.js 文件中
import useScrollTop from './hooks/useScrollTop'
useScrollTop()第三章:搜索状态下的布局
一、留出布局位置

@\components\app-header\index.jsx
<HeaderWrapper className={classNames({ fixed: isFixed })}>
<div className='content'>
<div className='top'>
<HeaderLeft />
<HeaderCenter />
<HeaderRight />
</div>
<div className="search-area"></div>
</div>
<div className='cover'></div>
</HeaderWrapper>二、封装选项卡组件
@\components\app-header\c-cpns\header-center\c-cpns 下面封装 SearchSections 与 SearchTabs 组件。
三、使用组件
@\components\app-header\c-cpns\header-center\index.jsx
const [tabIndex, setTabIndex] = useState(0)
const titles = SearchTitles.map(item => item.title)
<CenterWrapper>
{/* <div className='search-bar'>
<div className='text'>搜索房源和体验</div>
<div className='icon'>
<IconSearchBar />
</div>
</div> */}
<div className='search-detail'>
<SearchTabs titles={titles} />
<div className='infos'>
<SearchSections searchInfos={SearchTitles[tabIndex].searchInfos} />
</div>
</div>
</CenterWrapper>
第四章:搜索状态的切换动画效果
什么时候搜索状态显示或隐藏?单击下面的控件来切换。

一、SearchArea 封装为组件
SearchArea 区域要隐藏或展示,当单击【搜索房源和体验】的搜索框时。还要动画。所以封装为组件。
@\components\app-header\style.jsx
export const SearchAreaWrapper = styled.div`
transition: height 250ms ease;
height: ${props => props.isSearch ? "100px": "0"};
`@\components\app-header\index.jsx
/* 定义组件内部的状态 */
const [isSearch, setIsSearch] = useState(false)
/* 为组件添加事件与传入状态 */
<HeaderCenter isSearch={isSearch} searchBarClick={e => setIsSearch(true)} />
<SearchAreaWrapper isSearch={isSearch}/>
{ isSearch && <div className='cover' onClick={e => setIsSearch(false)}></div> }二、HeaderCenter 编写逻辑
1)isSearch 控制【搜索房源和体验】和【搜索状态】的显示隐藏。
const { isSearch } = props
<CenterWrapper>
{!isSearch ? (
{/* 搜索房源和体验 */}
) : (
{/* 搜索状态 */}
)}
</CenterWrapper>2)给【搜索房源和体验】绑定单击事件,来改变 isSearch 的值。
@\components\app-header\c-cpns\header-center\index.jsx
const { searchBarClick } = props
function searchBarClickHandle() {
if (searchBarClick) searchBarClick()
}
<div className='search-bar' onClick={searchBarClickHandle}>
<div className='text'>搜索房源和体验</div>
<div className='icon'>
<IconSearchBar />
</div>
</div>三、添加动画效果
使用 CSSTransition 来实现。
第五章:监听滚动滚动效果的消失
1)@\hooks\useScrollPosition.js
import { useEffect, useState } from 'react'
import { throttle } from 'underscore'
export default function useScrollPosition () {
// 状态来记录位置
const [scrollX, setScrollX] = useState(0)
const [scrollY, setScrollY] = useState(0)
// 监听window滚动
useEffect(() => {
const handleScroll = throttle(function () {
setScrollX(window.scrollX)
setScrollY(window.scrollY)
}, 100)
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
// 返回
return { scrollX, scrollY }
}2)只要【搜索状态】没激活,就一直记录滚动的位置。当【搜索状态】激活时,判断新的滚动位置与记录的滚动位置是否大于 30 px,如果大于,就隐藏【搜索状态】。
@\components\app-header\index.jsx
/* 监听滚动的监听 */
const { scrollY } = useScrollPosition()
const prevY = useRef(0)
// 在正常情况的情况下 (搜索框没有弹出来) , 不断记录值
if (!isSearch) prevY.current = scrollY
// 在弹出搜索功能的情况, 滚动的距离大于之前记录的距离的 30
if (isSearch && Math.abs(scrollY - prevY.current) > 30) setIsSearch(false)第六章:首页的顶部透明度的效果
1)顶部透明这个也要用 store 控制,每个使用 header 的组件在一加载的时候就派发配置。
topAlpha 这个状态在 home、detail、entire 页面都用得到,因为 header 组件是共用的。所以,topAlpha 添加到 store 中。
@\store\features\main.js
const mainSlice = createSlice({
name: "main",
initialState: {
headerConfig: {
isFixed: false,
topAlpha: false,
}
},
reducers: { /* ...... */ }
})2)home、entire、detail 页面一加载就派发 action。以 home 页面为例。
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchHomeDataAction())
dispatch(changeHeaderConfigAction({ isFixed: true, topAlpha: true }))
}, [dispatch])3)在 header 中,根据派发的配置,判断是否需要透明(只有当页面在顶部且派发的配置是透明才背景透明)。
@\components\app-header\index.jsx
const { topAlpha } = headerConfig
/* 透明度的逻辑 */
const isAlpha = topAlpha && scrollY === 0
<ThemeProvider theme={{ isAlpha }}>
<HeaderWrapper className={classNames({ fixed: isFixed })}>
<div className='content'>
<div className='top'>
<HeaderLeft />
<HeaderCenter
isSearch={isAlpha || isSearch}
searchBarClick={e => setIsSearch(true)}
/>
<HeaderRight />
</div>
<SearchAreaWrapper isSearch={isAlpha || isSearch} />
</div>
{isSearch && (
<div className='cover' onClick={e => setIsSearch(false)}></div>
)}
</HeaderWrapper>
</ThemeProvider>为什么要用 ThemeProvider 标签?因为不光 header 子组件需要使用 isAlpha,CSS 中也需要根据 isAlpha 判断字体、背景等颜色。干脆直接了当,使用一个主题,这样在 header 后代组件都可以使用啦。