超强React项目实战,一学就会,附源码和在线链接
前言
React是Facebook开源的一个用来快速动态构建用户界面的js库,React通过操作虚拟DOM(不总是直接操作DOM)来减少更新的次数,React的高效使其非常受欢迎,大厂都在用,赶紧学起来!希望对跟我一样初学react的同学有所帮助。
页面展示
- 猫眼电影小程序原页面
- 实现后效果?优化后!(添加搜索列表功能,优化城市选择)在线访问地址在文章末尾
设计师直呼:我让你模仿没让你超越啊
分析
react中一切皆组件,开发一个功能(页面),相当于开发一个组件。把这一个大的组件划分成一个个组件,然后逐步搭建起来。从这个页面可以看出
- 头部 电竞赛事
- 热门赛事 中 导航切换
- 城市选择
- 赛事列表
- 无赛事时显示的nothing
- 底部导航
主要功能:
- loading状态
- 标签栏切换赛事
- 搜索框根据列表信息搜索赛事
- 城市选择,可筛选城市赛事
- 路由切换页面
- ...
划分的一个个组件及父子组件,在下图的文件目录架构中清晰可见。
简单说明一下
- src:源码文件,开发目录
- assets:是专门用于保存各种外部文件的,比如音频、图片、字体等
- api:存放接口文件
- components:组件文件夹
- pages:页面级别组件
- unit: 存放一些js组件,封装的一些js方法
- routes: 路由配置
准备工作
- 初始化脚手架(vitejs 当前最快的脚手架),npm init @vitejs/app -> 输入项目名 -> 选择react 开发框架 -> 确定用react -> 得到了一个模板项目
- 安装相关依赖和UI组件库,本文所使用的是antd-mobile 的UI组件库
- 创建目录
- npm run dev 启动
实现
在分析完如何写以及搭建好我们的目录架构之后,开始进入正题。
图3
路由配置
使用了路由懒加载,将路由组件分包,在进入其他目标页面时再加载,可提升首页加载速度。为了方便,我这里设置了主界面是体育/赛事Contest,因为要实现的主要页面是在体育/赛事,其他页面后续完善
import { lazy } from 'react'
import { Routes, Route} from 'react-router-dom'
const Contest = lazy(() => import('../pages/Contest'))
const Home = lazy(() => import('../pages/Home'))
const Movie = lazy(() => import('../pages/Movie'))
const Yanchu = lazy(() => import('../pages/Yanchu'))
const Mine = lazy(() => import('../pages/Mine'))
const RoutesConfig = () => {
return (
<Routes>
<Route path="/home" element={<Home />}></Route>
<Route path="/movie" element={<Movie />}></Route>
<Route path="/yanchu" element={<Yanchu />}></Route>
<Route path="/contest" element={<Contest />}></Route>
<Route path="/" element={<Contest />}></Route>
<Route path="/mine" element={<Mine />}></Route>
</Routes>
)
}
export default RoutesConfig
底部导航栏
录制所用的工具不太好,区域限制,见谅
路由跳转实现
<FooterWrapper>
<Link to="./home" className={classnames({active:pathname == '/home'})}>
<i className='iconfont icon-shouye'></i>
<span>首页</span>
</Link>
<Link to="./movie" className={classnames({active:pathname == '/movie'})}>
<i className='iconfont icon-dianyingpiaoiocn'></i>
<span>电影/影院</span>
</Link>
<Link to="./yanchu" className={classnames({active:pathname == '/yanchu'})}>
<i className='iconfont icon-yanchu'></i>
<span>演出</span>
</Link>
<Link to="./contest" className={classnames({active:pathname == '/contest'})}>
<i className='iconfont icon-saishi'></i>
<span>体育赛事</span>
</Link>
<Link to="./mine" className={classnames({active:pathname == '/mine'})}>
<i className='iconfont icon-wode'></i>
<span>我的</span>
</Link>
</FooterWrapper>
头部标题和布局
移动端flex布局YYDS,多用flex弹性布局,这个很简单就不细说了,具体的可下载代码查看
标签栏
使用antd-mobile的Tabs,activeKey是当前点击的值,后续获取用来实现切换列表功能,定义一个tabs数组存放标签栏数据,然后map加入每一个
<Tabs activeKey={activeKey} onChange={setActiveKey}
activeLineMode='fixed'
style={{'--fixed-active-line-width': '25px', display: 'flex',justifyContent: 'flex-start', '--title-font-size': '0.1'}}
>
{tabs.map(item => (
<Tabs.Tab key={item.key} title={item.title} />
))}
</Tabs>
因为数据不多,也可以这样写比较方便
<Tabs>
<Tabs.Tab title='全部' key='全部' />
<Tabs.Tab title='电竞赛事' key='电竞赛事' />
<Tabs.Tab title='体育赛事' key='体育赛事' />
</Tabs>
城市选择
使用多个小组件拼成,点击城市,弹出层(Popup组件)出现,内容包括城市搜索框(SearchBar)、城市选择列表(CheckList)。
- Popup:visible控制弹出层的开关,初始状态设置一开始为flase(不弹出),城市选择被点击时改变状态为true(弹出层出现)。点击非弹出层区域外时onMaskClick={() => {setVisible(false)}}或者CheckList的列表被选择时弹出层关闭。
- useMemo函数:使用来做缓存的,只有当依赖项改变时(SearchBar的内容)才会发生变化,否则就拿缓存的值,这样就不用在每次渲染的时候再做计算
- 子组件通过调用父组件传来的函数,来通知父组件城市的改变,用以通过选择后城市来筛选列表
标签栏下的搜索框跟城市选择的搜索框是类似的,这里是直接稍作更改后复用的
export default ({getCity}) => {
const [visible, setVisible] = useState(false)
const [selected, setSelected] = useState('城市')
const [searchText, setSearchText] = useState('')
const pushCity = () => {
// console.log('1111111111111111')
getCity(selected)
}
const filteredItems = useMemo(() => { // 根据搜索框内容更新城市列表
if (searchText) {
return items.filter(item => item.includes(searchText))
} else {
return items
}
}, [searchText])
useEffect(() => {
pushCity(selected)
}, [selected])
return (
<>
<div className='city-space'
onClick={() => {
setVisible(true)
setSearchText('') // 为了再次点击时,搜索框被清空
}}
>
<div className='city-choose'>
<span>{selected?selected:'城市'}</span>
<i className="iconfont icon-down" />
</div>
</div>
<Popup // 弹出层
visible={visible}
onMaskClick={() => {
setVisible(false)
}}
destroyOnClose
>
<div className='searchBarContainer'>
<SearchBar // 搜索框
placeholder='搜索城市'
value={searchText}
onChange={v => {
setSearchText(v)
}}
/>
</div>
<div className='checkListContainer'>
<CheckList // 列表选择
className='myCheckList'
defaultValue={selected ? [selected] : []}
onChange={val => {
setSelected(val[0])
setVisible(false) // 选择后,弹出层关闭
}}
>
{filteredItems.map(item => (
<CheckList.Item key={item} value={item}>
{item}
</CheckList.Item>
))}
</CheckList>
</div>
</Popup>
</>
)
}
搜索功能的实现部分
这里说的搜索包括标签栏选择筛选赛事列表、城市选择筛选固定城市赛事、搜索框搜索列表信息关键字。三者的筛选会互相影响但又独立分开,比如选了城市北京,那其他两个搜索只会搜索到该城市的赛事,但又也不会影响其他组件已定状态的改变。
通过useEffect来做赛事数据的更新,把从3个的子组件中接收到的筛选信息activeKey、inputContent、 city交给另一个子组件函数fetchcontents去完成下一步工作。
从这里可以发现,子组件并没有自己去做接口请求数据来筛选列表,而是统一交给父组件,然后由父组件去递交给工具函数,我们要知道这些开发套路
- 接口都放在api目录下
- 接口请求在路由级别组件发生, 子组件不要去做
- 子组件不做数据请求, 由父组件统一并传过来,子组件不做复杂状态
useEffect(() => {
setLoading(true)
setPlaceholder(`在${activeKey}里搜索`)
setContents([7])
fetchcontents({inputContent, activeKey, city})
.then(data => {
setContents([...data.result])
setLoading(false)
})
}, [activeKey, inputContent, city])
筛选数据的函数,为了下载后更易观看,我把赛事列表写在本地,用delay函数模拟数据请求的延迟,使loading加载中更好显示出来。
const delay = time => new Promise(resolve => setTimeout(resolve, time));
const withDelay = fn => async (...args) => {
await delay(1000);
return fn(...args)
}
export const fetchcontents = withDelay(params => {
const { inputContent, activeKey, city='' } = params
let result = contents;
if (activeKey) {
switch (activeKey) {
case "电竞赛事":
result = result.filter(content => content.type === '电竞赛事')
break;
case "体育赛事":
result = result.filter(content => content.type === '体育赛事')
break;
default:
break
}
}
if (inputContent) {
result = result.filter(content => (
content.text+content.date+content.price+content.pos).includes(inputContent))
}
if (city && city !== '所有城市'&& city !== '城市') {
result = result.filter(content => content.pos.includes(city))
}
return Promise.resolve({
activeKey, result
})
})
loading加载
当数据获取到时,将loading状态设为false,使SpinLoading不显示,Space和SpinLoading来自antd-mobile
const [loading, setLoading] = useState(true);
{
loading &&
<Space direction='horizontal' wrap block style={{ '--gap': '16px' }}>
<SpinLoading className='list-loading' style={{ '--size': '40px' }} />
</Space>
}
赛事列表&&loading
将contents数据从父组件中解构出来,使用antd-mobile的List组件快速完成赛事列表
const ContentList = ({contents}) => {
return (
<div>
{ contents[0] !== 7 &&
<List style={{"marginBottom": "2.3rem"}}>
{contents.map(({ img, text, date, pos, price,id }) => {
return (
<List.Item key={id}>
<ListWrapper>
<img src={img} />
<div className='list-content'>
<p>{text}</p>
<p>{date}</p>
<p>{pos}</p>
<div><span>售票中</span>{price}</div>
</div>
</ListWrapper>
</List.Item>
)
})}
</List>
}
</div>
)
}
虽然看上去写的判断很奇怪,这是我在改完bug后最终代码,为了能控制3个画面(加载中,无赛事,赛事列表)在不同状态的显示与否,点击切换标签栏时,赛事列表先消失,加载该标签栏列表或者显示暂无赛事,关键部分就是在下方代码的setContents([7])
useEffect(() => {
setLoading(true)
setPlaceholder(`在${activeKey}里搜索`)
setContents([7])
fetchcontents({inputContent, activeKey, city})
.then(data => {
setContents([...data.result])
setLoading(false)
})
}, [activeKey, inputContent, city])
css 方面
reset
很多同学喜欢使用*通配符来做reset,但* 性能不好,不是每一个标签都需要reset设置,推荐使用下面的做法(部分代码)
html,body,div,span,applet,object,iframe,
h1,h2,h3(省略了很多){
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
}
body {
line-height: 1;
}
...
移动端不要用px用rem
移动端为了能适配不同屏幕大小的手机,我们的样式不能用固定的px,要用相对单位rem,引入下面的js文件解决适配和屏幕切换问题
var init = function () {
var clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
if (clientWidth >= 640) {
clientWidth = 640;
}
var fontSize = 20 / 375 * clientWidth;
document.documentElement.style.fontSize = fontSize + "px";
}
init();
window.addEventListener("resize", init);
css in js
css放哪,可以写在css文件然后引入,我更倾向与使用css in js的方式,styled-components 会生成一个hash类名,绝对不重名,还能嵌套写
import styled from 'styled-components'
export const Wrapper = styled.div`
里面写样式
`
图标哪里找
- font-awesome: 方便 缺点是 没有定制性
- iconfont 网站 可以去找,下载一个代码包
结语
为了缩减篇幅(不是我偷懒),还有很多东西没有拿出来说,可以下载源码后再慢慢看,后续也会持续优化和增加其他页面,点个赞支持一下吧!
线上浏览地址&&项目地址
线上浏览地址(切为手机显示): Vite App (576711977.github.io)
转载自:https://juejin.cn/post/7114138865702535199