我正在参与「构思开发 投稿大赛」详情请看:构思开发大赛来了!
一 . 前语呀~
前段时刻,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. 主页展示
1.1. 加载过程 + 二级路由 过程分化
1.2. 懒加载 过程分化
2. 视频详情页展示
2.1. 文字轮播效果 + 视频信息打开收起 过程分化
2.2. Swiper+Tabs可滑动可选菜单栏 + 分评观点赞撤销点赞 过程分化
3. 个人主页展示
3.1. 区域可选切换Tabs + 文字打开收起 过程分化
四 . 带你完成代码
1. Redux 是这姿态装备的
1.1. redux 架构思路
- 分库房
- 数据办理和组件,在有了 redux 后,变成了平级联系 /store /page
- 模块化数据办理,每个模块 reducer+action 下放到页面级路由模块中,便利办理
- 每个模块都供给 index.js , 便利一致办理 store, 一切的 reducer,action,constans 都一起 export,作为清单文件
- 主库房
- 用于一致办理各个分仓数据,并给根组件供给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. 分库房装备 + 点赞/撤销点赞功用介绍
- 在谈论区列表中,建立库房文件夹store
- 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. 二级路由巧完成
- 二级路由完成思路:
- 首要,使用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... 就好
- 下拉分区完成
- 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. 轮播文字妙完成
- 参阅文章:文字轮播与图片轮播?CSS 不在话下 – ()
- @chokcoco 崇拜大佬的css 简略易懂!!!
4. 视频信息下拉框小动画爽完成
- 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的绑定 完成菜单和数据 双向绑定
- 代码很明晰,能够看一下
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站 !!!
-
继续更新,继续学习,期望对你一切帮助~