假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

lxf2023-04-20 16:56:01

我正在参加「创意开发 投稿大赛」详情请看:AdminJS创意开发大赛来了!

一 . 前言呀~

前段时间,Cavan在AdminJS上分享了一篇 【构建】react打造你的第一个Bilibili首页开发项目 - AdminJS (Admin.net)的文章,收到了很多反馈和鼓励,感谢各位支持,和大佬们的指正,不以至于我有了成长的方向和动力。正好最近也学习Redux,于是Cavan在上个初级项目构建的基础上进行完善并增加新页面,使用React-Hooks + Redux完善了更为完整的Bilibili项目。

在这篇(更新)文章中,Cavan将会分享如何完整的实现一个React-Hooks + Redux项目,主要介绍如何使用Redux实现数据流管理,以及项目优化和Cavan遇到的小坑。

持续更新,持续学习,欢迎关注收藏,希望对你所有帮助~

在 线 体 验 地 址 : BilibiliLike

二 . 你将学到这些

1. 主要开发业务

  • react 18.0.0 + react-dom :时下最流行的 MVVM框架 React开发流程
  • redux :时下大厂必备的 全局数据流管理功能
  • react-hooks :各种 Hooks,实操使用小技巧
  • antd-mobile :移动端最好用的,来自于阿里的,封装套用组件的使用指南
  • react-router路由配置,和二级路由实现
  • styled-components :React 开发常用样式搭建方法
  • axios前端界面,拉取后端数据的,最新异步Promise网络请求方法 + api工程化封装流程
  • fastmock :穷学生必备免费后端小接口
  • 项目架构搭建标准 :开发一个项目,可以这么分项目解构和资源层级 ...

2. 实用小技巧

  • classnames : 动态添加类名,实现可操控的样式方法
  • prop-types : 严格控制父子组件传值的类型合理性
  • react-lazyload :懒加载 优化用户第一次进站体验
  • memo : React自带 页面渲染性能优化 让你的网页选择性渲染需要更渲染资源的组件
  • vite.config.js : 配置小tips 端口配置 路径配置
  • 初始化相对单位 :自适应手机像素比例 配置方法
  • 新手简单取数据小技巧 :非爬虫,爬虫爬的好,牢饭少不了(bushi
  • 细节开发数据处理小函数 :正则匹配应用 + 时间戳转换具体时间 + 数字数据格式化 ...

三 . 为你展示项目

1. 主页展示

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

1.1. 加载过程 + 二级路由 步骤分解

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

1.2. 懒加载 步骤分解

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

2. 视频详情页展示

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

2.1. 文字轮播效果 + 视频信息展开收起 步骤分解

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

2.2. Swiper+Tabs可滑动可选菜单栏 + 分评论点赞取消点赞 步骤分解

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

3. 个人主页展示

3.1. 区域可选切换Tabs + 文字展开收起 步骤分解

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

四 . 带你实现代码

1. Redux 是这样子配置的

1.1. redux 架构思路

  1. 分仓库
    1. 数据管理和组件,在有了 redux 后,变成了平级关系 /store /page
    2. 模块化数据管理,每个模块 reducer+action 下放到页面级路由模块中,方便管理
    3. 每个模块都提供 index.js , 方便统一管理 store, 所有的 reducer,action,constans 都一起 export,作为清单文件
  2. 主仓库
    1. 用于统一管理各个分仓数据,并给根组件提供Provider功能的store,和state树根

1.2. 主仓库配置

  • index.js
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const composeEnhancers =
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ // 引入Redux可视化插件
    || compose;
const store = createStore(
    reducer, // 仓库数据
    composeEnhancers( // 组合中间件
        applyMiddleware(thunk) // 异步用中间件
    )
)
export default store;
  • reducer.js
import { combineReducers } from 'redux'
import { reducer as RecommendPart } from
    '@/pages/VideoDetail/RecommendPart/store'
import ...
// 引入并合并分仓
export default combineReducers({
    donghuatuijian: DonghuaTuijianReducer,
    shouye: ShouyeReducer,
    space: SpaceReducer,
    recommend: RecommendPart,
    comments: CommentsReducer
})
  • main.js(根组件配置:Provider声明式开发,提供给子组件数据管理功能)
import {BrowserRouter} from 'react-router-dom' 
import { Provider } from 'react-redux' 
import store from './store' 
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}> 
    <BrowserRouter> 
        <App /> 
    </BrowserRouter> 
</Provider> )

1.3. 分仓库配置 + 点赞/取消点赞功能介绍

  1. 在评论区列表中,建立仓库文件夹store
  2. store 分为四个文件 分别是:
  • index.js(负责整理仓库功能,并统一向外输出)
import reducer from './reducer'
import * as actionCreators from './actionCreators'
import * as constants from './constants'
export {
    reducer,
    actionCreators,
    constants
}
  • actionCreators.js(负责统一管理数据状态改变的函数执行,给reducer分配相应的action:状态类型,数据)
import * as actionTypes from './constants'
import { getCommentsListRequest } from '@/api/request'
const changeCommentList = (data) => ({
    type: actionTypes.SET_COMMENTLIST,
    data
})
export const getCommentList = () => {
    return (dispatch) => {
        getCommentsListRequest() // 异步请求 axios 外部数据
            .then(data => {
                dispatch(changeCommentList(data.data.replies))
            })
    }
}
export const changeDianzan = (id) => {
    return ({ // 通知点赞模块调整点赞状态
        type: actionTypes.SET_DIANZAN,
        id
    })
}

  • reducer.js(负责根据action值,做相应操作,以实现数据流管理)
import * as actionTypes from './constants'
const defaultState = {
    commentList: [],
    enterLoading: true
}
export default (state = defaultState, action) => {
    switch (action.type) {
        case actionTypes.SET_DIANZAN:
            return {
                ...state,
                commentList: state.commentList.map(item => {
                    // 当 匹配到 相应评论数据的id值 ,在该数据上就行like数调整
                    // item.action 初始值为 0 ,用于做是否已点赞判断
                    // 0 未点赞(可点赞) ;1 已点赞(可取消赞)
                    if (item.rpid == action.id) {
                        if (!item.action) {
                            item.like++;
                            item.action++;
                        }
                        else {
                            item.like--;
                            item.action--;
                        }
                    }
                    return item
                }),
            }
        case actionTypes.SET_COMMENTLIST:
            return {
                ...state,
                commentList: action.data
            }
        default:
            return state;
    }
}
  • 需求store组件:index.js
const CommentsPart = (props) => {
  const { commentList } = props;
  const { getCommentListDispatch, setDianzanDispatch } = props;
  const ChangeDianzan = (id) => {
    setDianzanDispatch(id)
  }
  useEffect(() => {
    getCommentListDispatch();
  }, [])
  return (
    <ListWrapper>
      <div className="list">
        <ul>
          {
            commentList.map(comment => {
              return (
                <CommentItem
                  comment={comment}
                  key={comment.rpid}
                  ChangeDianzan={ChangeDianzan}
                />
              )
            })
          }
        </ul>
      </div>
    </ListWrapper>
  )
}
const mapStateToProps = (state) => {
  return {
    commentList: state.comments.commentList,
    idtest: state.comments.idtest,
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    getCommentListDispatch() {
      dispatch(getCommentList())
    },
    setDianzanDispatch(id) {
      dispatch(changeDianzan(id))
    }
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(memo(CommentsPart))

// 子组件 点击控制 点赞Redux执行 传回给父组件 setDianzanDispatch(id)
<span className="like"
     onClick={() => ChangeDianzan(rpid)}
>

2. 二级路由巧实现

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

  • 二级路由实现思路:
    • 首先,利用antd-mobile的Tabs组件,实现每一层菜单样式
    • 其次,把菜单数据封装在一个对象数组中。结构如:
      const CannelData = [
         {
         "cannelname": "/donghua",
         "ctitle": "动画",
         "children": [
             {
                 "cannelname": "/donghua/1",
                 "ctitle": "推荐"
             },...]
         }
       ]
       ```
      
    • 因为 Tabs 需要 activeKey 来设置选中的分区。所以要动态读取path,更新activeKey值。
    let fenqu = pathname.match(/^\/[^\/]*/);
    <Tabs activeKey={fenqu}>
    
    // fenqu 还能用于 navigate 跳转每个分区的推荐页
    const { pathname } = useLocation();
    const navigate = useNavigate();
    if (/^\/\w+$/.test(pathname) || /^\/\w+\/$/.test(pathname)) {
    // 找到第一个斜杆后的路由参数,如果有,就做二级子路由跳转
            let fenqu = pathname.match(/^\/[^\/]*/)
            navigate(`${fenqu}/1`)
    }
    
    // 路由实现是通过解套数组,map出数据
    // 优点是:可以把通过改变api数据,去增删改查分区路由
    CannelData.map(
       (item) => {
           return (
            <Tabs.Tab
               title={
               // 注意title  装菜单详细html标签离谱)
               // NavLink 会给所选路由加个active以便所选路由可视化
                 <NavLink to={item.cannelname} className={classnames({ active: pathname == item.cannelname })}>
                 <span>{item.ctitle}</span>
                 </NavLink>
                 }
               key={item.cannelname}
            >
            </Tabs.Tab>
            )
         }
      )
      // 注意:这段代码要实时监听pathname,实现重新选中activeKey!
      // 可以用 useEffect(()=>{...},[pathname]) 实现
    
    • 二级路由实现
    // 简单聊一下吧:
    const CannelItems = () => {
            const res = CannelData.filter(
                ({ children }) =>
                    children.length > 0
            )
            const items = res.filter(
                ({ cannelname }) =>
                    pathname.includes(cannelname)
            )
    }
    // 首先 二级路由也要读取pathname以便加载出需要的二级子路由数据
    // 其次 这边map前,要做两次数据筛选:
    // 1. 有孩子(二级菜单)的才要加载二级路由,没有的就把二级路由栏隐藏掉,不能影响布局
        // if (isPathPartlyExisted(pathname)) return; 
        // 可以写个工具函数隐藏没有二级路由的菜单栏
    // 2. 再通过pathname取到所在分区的数据,再去给map输出子分区数据
        // 子路由数据 map 逻辑和父路由一样 items.map... 就好
    
  • 下拉分区实现
    • 假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)
    • antd-mobile 魔改 Dropdown 实现
    • 最主要是 pathname 监听,实现classNames的active改变,与 二级路由activeKey 实现 共同跳转路由 + active 响应
    • ref useRef 用于绑定dom值,这里用于恢复下拉框标签
<DropdownWrapper ActiveKey={pathname}>
   <Dropdown arrow={<DownOutline />} ref={ref}>
      <Dropdown.Item key='sorter' title=''>
          <DrawerWrapper>
              <div>
                  {
                    CannelData.map(
                       (item) => {
                          return (
                            <NavLink key={item.cannelname}
                                     to={item.cannelname}
                                     className={classnames({ active: pathname == item.cannelname })}
                                     onClick={() => {
                                           ref.current?.close()
                                     }}
                            >
                            <span>{item.ctitle}</span>
                            </NavLink>
                                  )
                              }
                          )
                    }
              </div>
              <i className="iconfont general_pullup_s" onClick={() => {
                      ref.current?.close()
              }}></i>
          </DrawerWrapper>
        </Dropdown.Item>
     </Dropdown>
</DropdownWrapper>

3. 轮播文字妙实现

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

  • 参考文章:文字轮播与图片轮播?CSS 不在话下 - AdminJS (Admin.net)
  • @chokcoco 膜拜大佬的css 简单易懂!!!

4. 视频信息下拉框小动画爽实现

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

  • antd-mobile 魔改 Collapse组件 实现下拉和收起
  • 上拉收起小细节:
    • 小头像的消失,点赞、收藏、缓存图标的显示
    // 用一个变量show,实现display是否消失小头像和播放量数据
    let show = display ? { "display": "" } : { "display": "none" };
    <div className="left" style={show}>
        <a className="avatar" href='/space'>
            <img src="xxx" className="bfs-img face"/>
        </a>
        <a className="name" href='/space'>CAVAN咔叽</a>
        <span className="view-stat">4万观看</span>
    </div>
    <div className="right">
        ...
    </div>
    // 因为 left 和 right 都是inline-block,当 left 被 "display": "none";right 就会并入行首。从而实现,小头像收起,点赞数据向左并入
    

5. Tabs与Swiper的绑定 实现菜单和数据 双向绑定

假期里,把B站变成你的入职项目吧!(React-Hooks+Redux 打造让面试官眼前一亮的Bilibili~)

  • 代码很清晰,可以看一下
import React, { useRef, useState } from 'react'
import { Tabs, Swiper } from 'antd-mobile'
import { TabsWrapper } from './style'
import RecommendPart from '../RecommendPart'
import CommentsPart from '../CommentsPart'

const tabItems = [
    { key: 'recommendPart', title: '相关推荐' },
    { key: 'commentsPart', title: '评论 145' },
]

const TabPart = () => {
    const swiperRef = useRef(null)
    const [activeIndex, setActiveIndex] = useState(0)

    return (
        <TabsWrapper>
            <div className='v-switcher__header'>
                <Tabs
                    activeKey={tabItems[activeIndex].key}
                    onChange={key => {
                        const index = tabItems.findIndex(item => item.key === key)
                        setActiveIndex(index)
                        swiperRef.current?.swipeTo(index)
                    }}
                >
                    {tabItems.map(item => (
                        <Tabs.Tab title={item.title} key={item.key} />
                    ))}
                </Tabs>
                <Swiper
                    direction='horizontal'
                    loop
                    indicator={() => null}
                    ref={swiperRef}
                    defaultIndex={activeIndex}
                    onIndexChange={index => {
                        setActiveIndex(index)
                    }}
                >
                    <Swiper.Item>
                        <RecommendPart />
                    </Swiper.Item>
                    <Swiper.Item>
                        <CommentsPart />
                    </Swiper.Item>
                </Swiper>
            </div>
        </TabsWrapper>
    )
}

export default TabPart 
  • 实在不会的话,可以参考antd-mobile Tabs 标签页 - Ant Design Mobile

6. 性能优化Part~:

  • memo
    • import { memo } from 'react'
    • export default memo(xxxx)
    • 就可以实现减少渲染重复未变数据
  • lazyLoad
<LazyLoad
     // 占位图片
     placeholder={<img
                  src={placeholderImg} 
                  className='m-bfs-pic pic'
                  />}
     >
     <img src={pic} 
          className={classnames("m-bfs-pic pic", { notfond: !pic })} />
</LazyLoad>
  • 路由懒加载
    • import { lazy, Suspense } from "react"
    • const XXX = lazy(() => import('@/pages/XXX'))
    • 类似于下方