我正在参与「构思开发 投稿大赛」详情请看:构思开发大赛来了!

一 . 前语呀~

前段时刻,Cavan在上共享了一篇 【构建】react打造你的第一个Bilibili主页开发项目 – ()的文章,收到了很多反应和鼓舞,感谢各位支撑,和大佬们的纠正,不以至于我有了生长的方向和动力。正好最近也学习了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. 视频详情页展示

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 不在话下 – ()
  • @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’))
    • 类似于下方
      <Suspense fallback={null}>
            <Routes path="/" element={<Navigate to="/recommend" replace={true} />}>
                <Route path="/recommend" element={<Recommend />} />
                <Route path="/singers" element={<Singers />} />
                <Route path="/rank" element={<Rank />} />
                <Route path="/search" element={<Search />} />
            </Routes>
        </Suspense>
    

五 . 来吧!着手实操起来!

1. 想学更多 ? 这边请 !

  • 项目地址:project_Developing/bilibiliLike CavanOu/Cavans_JSwarehouse1 – 码云 – 开源我国 (gitee.com)

  • 在线体会地址: BilibiliLike

2. 下期预告 :

  • 最Super的TypeScript实战项目开发

  • 最新版本的Redux数据流办理流程

  • 最火爆的Hooks函数式开发流程

  • 最爱你的 up主 在 Bilibili 大楼里 等你~

  • 所以,不要吝啬你的 Star 点赞 保藏 重视 呀 ! ~

3. 写在最终嗷~

  • 有问题和可优化点,欢迎大佬谈论区谈论纠正

  • 求大佬合作和谈论技能,加微信:CaVaN_9

  • 点赞 保藏 谈论纠正! 爱你们 ~ 期望你能提前入职 B站 !!!

  • 继续更新,继续学习,期望对你一切帮助~