持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
前言
作为一名前段学习者,react是我们不可缺少的web开发工具。但对一名react初学者来说,要想开发一个比较完成的移动端组件,还是有点难度的,这时候我们就需要去参照一个模板,而我这个项目思路清晰,逻辑简单,可以作为一个参照(我也是参考别的项目写的,小声哔哔...)。只要你电脑安装了node.js,就能让项目跑起来!
项目简介
预期效果
第一张为首页,也就是默认显示页;第二张为我的大学详情页,通过首页顶部下拉菜单进行路由跳转。
实际效果
目录介绍
合理的目录结构有利于项目管理,功能分配,每个文件夹都有明确的分工,这样子不仅开发时思路清晰,后期项目的更改也更方便,下面介绍一个src下每个文件的具体分工。
- api--获取fastmock接口数据,所有的数据请求都在这个文件完成
- assets--存放一些静态资源,如iconfont图片库
- components--顾名思义,放置大大小小的组件,其中的子文件夹common放置一些可复用组件,如轮播组件比较常用的
- config--js文件,一些自定义配置,如别名配置
- pages--放置页面级别组件
- routes--路由配置文件,所有的路由跳转放在这里
依赖包
- antd-mobile:蚂蚁金服组件库,有很多常用组件,非常好用-文档
- axios: 数据请求工具
- react-router,react-router-dom: 路由跳转功能,二级路由可实现页面局部刷新
- styled-components: css in js的样式编写风格
- swiper: 轮播图插件
详细组件介绍
底部Tab导航栏
移动端组件开发中底部Tab导航栏是必不可少的,实现非常简单,将Footer组件用position:fixed固定挂载在页面底部,四个Tab盒子用flex布局平均分配空间,每个Tab用Link进行一级路由跳转,从而实现主体内容刷新,达到Tab切换效果。
import React from 'react'
import { FooterWrapper } from './style'
import { Link } from 'react-router-dom'
export default function Footer() {
return (
<FooterWrapper>
<Link to="/home">
<i className='iconfont icon-31shouye'></i>
<span>首页</span>
</Link>
<Link to="/message">
<i className='iconfont icon-xiaoxi1'></i>
<span>消息</span>
</Link>
<Link to="/note">
<i className='iconfont icon-biji1'></i>
<span>笔记</span>
</Link>
<Link to="/mine">
<i className='iconfont icon-31wode'></i>
<span>我</span>
</Link>
</FooterWrapper>
)
}
Footer组件放在其父组件,也就是根组件App.jsx中,路由跳转内容要放在父组件相应位置中占位,所占的页面位置就是路由区域。
import { Suspense } from 'react'
import './App.css'
import RoutesConfig from './routes'
import Header from './components/Header'
import Footer from './components/Footer'
function App() {
return (
<div className="App">
<Header></Header>
<Suspense fallback={<div>loading...</div>}>
<RoutesConfig></RoutesConfig>
</Suspense>
<Footer></Footer>
</div>
)
}
export default App
所有路由配置全都封装在RoutesConfig中,除了首页默认显示路由,其余路由都用lazy懒加载引入,配合Suspense组件包裹路由,达到缓加载的效果,可加快首页渲染速度。Suspense的fallback属性接受任何在组件加载过程中你想展示的React元素,在加载完成之前显示。
import React from 'react'
import { lazy } from 'react'
import { Routes,Route } from 'react-router-dom'
import Home from '../pages/Home'
import HomeCommon from '../components/HomeCommon'
import HomeRead from '../components/HomeRead/inde'
import HomeFind from '../components/HomeFind'
import HomeConcern from '../components/HomeConcern'
import HomeVideo from '../components/HomeVideo'
const Mine =lazy(()=>import('../pages/Mine'))
const Message =lazy(()=>import('../pages/Message'))
const Note =lazy(()=>import('../pages/Note'))
const MyUniversity =lazy(()=>import('../pages/MyUniversity'))
export default function RoutesConfig() {
return (
<div>
<Routes>
<Route path='/' element={<Home />}></Route>
<Route path='/home' element={<Home />}>
<Route path='/home/common' element={<HomeCommon />}></Route>
<Route path='/home/find' element={<HomeFind />}></Route>
<Route path='/home/concern' element={<HomeConcern />}></Route>
<Route path='/home/read' element={<HomeRead />}></Route>
<Route path='/home/video' element={<HomeVideo />}></Route>
</Route>
<Route path='/message' element={<Message />}></Route>
<Route path='/mine' element={<Mine />}></Route>
<Route path='/note' element={<Note />}></Route>
<Route path='/myuniversity' element={<MyUniversity />}></Route>
</Routes>
</div>
)
}
效果
二级路由Nav导航栏
二级路由在移动端也是非常的常见,我这里的二级路由NavBar使用ul列表做的,列表水平排列,通过overflow-x: auto设置水平方向滚动,white-space:nowrap设置不换行,从而达到超出屏幕宽度滚动的效果;路由跳转使用字符串模板${}跳转到相应路径,二级路由主体内容要在其父组件用Outlet进行占位,这样才能在切换NavBar时达到页面局部刷新的效果。
export default function HomeNav() {
let homenavs=[
{ id:1,desc:'常用',path:'/common'},
{ id:2,desc:'发现',path:'/find'},
{ id:3,desc:'关注',path:'/concern'},
{ id:4,desc:'微读书',path:'/read'},
{ id:5,desc:'微视频',path:'/video'},
]
return (
<Wrapper>
<div className="nav-box">
<ul id='list'>
{
homenavs.map((item,index)=>{
return(
<li key={item.id}>
<NavLink
index={index}
to={`/home${item.path}`}
className="nav-item"
>
{item.desc}
</NavLink>
</li>
)
})
}
</ul>
</div>
</Wrapper>
)
}
父组件Outlet占位
return (
<Wrapper>
<HomeSearchBar></HomeSearchBar>
<HomeNav></HomeNav>
<Outlet></Outlet>
</Wrapper>
)
NavBar选中时添加底部下划线,NavLink路由的css属性有个active属性,选中时才会添加,所以使用伪元素active::after就可以在其底部添加下划线。
&.active::after
{
content: "";
background-color: blue;
width:1rem;
height: 0.1rem;
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
}
效果
常用课程列表
课程列表分为“常用”和“最近使用”,都放在一个数据请求中,其中有个checked属性,如果为true则添加到常用列表,为false则在最近使用列表中;数据请求在其父组件完成,然后传给子组件,子组件通过map循环输出;css样式用flex布局,一般flex布局可以解决绝大部分布局设计。
export default function CommonCourse({course, addToCommon}) {
// console.log(course);
return (
<Wrapper>
<div className="list">
{
course.map((item, index) => {
return (
item.checked &&
<div className="list-item" key={item.id}>
<div className="list-left">
<img src={item.img} alt="" />
</div>
<div className="list-center">{item.desc}</div>
<div className="list-right">
<i>
<a href="#">></a>
</i>
</div>
</div>
);
})
}
</div>
<div className="interval"></div>
<div className="recent">最近使用</div>
<div className="list">
{course.map((item, index) => {
return (
!item.checked &&
<div className="list-item" key={item.id}>
<div className="list-left">
<img src={item.img} alt="" />
</div>
<div className="list-center">{item.desc}</div>
<div className="list-right">
<button onClick={() => addToCommon(item)}>+常用</button>
</div>
</div>
);
})}
</div>
</Wrapper>
);
}
添加功能及模态框的实现
添加功能及模态框的实现在父组件完成
前面说到数据对象有个checked属性可以判断是否为常用课程,所以可以通过给button点击事件添加一个立即执行函数,返回值为父组件传过来的添加函数,该函数接受一个参数item,该参数为点击的当前列表项,所以我们可以通过let idx = course.findIndex(data => e.id === data.id)拿到该项列表id,然后修改该项checked的boolean值,就能实现将课程添加到常用了。最后还要通过useEffect修改拿到的数据状态,useEffect会在每次页面更新后执行。
modal模态框的实现,我给模态框组件设置了一个loading状态,初始值为false,在触发添加函数时,改变其状态为true,并且触发定时器函数,定时为2s,2s后loading状态变回false。
export default function HomeCommon() {
const [course, setCourse] = useState([]);
const [loading, setLoading] = useState(false);
// 提示模态框
const modal = () => {
return (
loading && (
<Modal>
<span>添加成功!</span>
</Modal>
)
);
};
// 定时让模态框消失
const setState = () => {
setTimeout(() => {
setLoading(false);
}, 2000);
};
// 改变数据checked,将数据添加到常用
const addToCommon = e => {
let idx = course.findIndex(data => e.id === data.id);
course[idx].checked = !course[idx].checked;
setCourse([...course]);
setLoading(true);
setState();
};
useEffect(() => {
(async () => {
let { data } = await getCourse();
setCourse(data);
})();
}, []);
return (
<>
{modal()}
<Wrapper>
<div className="commonWrapper">
<CommonCourse course={course} addToCommon={addToCommon}></CommonCourse>
</div>
</Wrapper>
</>
);
效果
(说明一下,这里添加后样式没有任何问题,可能是生成gif动图的原因导致看上去列表样式不一样)
顶部下拉菜单
下拉菜单我这里引用了antd-mobile组件库中的Dropdown组件,然后在下拉内容中添加了一个路由跳转;顶部的标题内容使用useLocation拿到当前地址,然后根据地址对应的标题使用字符串模板显示顶部区域的标题,当切换不同路由时,顶部标题会相应变化,这里要注意的是,在useEffect中不能带第二个参数,否则不能在页面渲染后一直更改标题,只会在首页渲染后执行一次。
在顶部我还写了一个弹出层组件,也可跳转路由,但由于其比下拉菜单复杂,并且在学习通中也是下拉菜单,所以就不介绍弹出层Popup组件了。
import React, { useEffect,useState } from 'react'
import { HeaderWrapper } from './style'
import { useLocation } from 'react-router-dom'
import { pageTitle } from '../../config'
import HeaderPopup from '../common/HeaderPopup'
import { Popup,Dropdown } from 'antd-mobile'
export default function Header() {
const currentpathname='/myuniversity'
const [title,setTitle]=useState('首页')
const {pathname='/'}=useLocation()
// console.log(pathname)
useEffect(()=>{
let _title=pageTitle[pathname] || ''
// console.log(_title)
setTitle(_title)
})
const [visible2, setVisible2] = useState(false)
const onCloseVis=()=>{
setVisible2(false)
}
if(pathname==currentpathname) {
return
}
return (
<HeaderWrapper>
<Dropdown className='dropdown'>
<Dropdown.Item key='sorter' title={title} >
<HeaderPopup title={title} visible2={visible2} onClose={onCloseVis} ></HeaderPopup>
</Dropdown.Item>
</Dropdown>
<i className='header-right'>></i>
// 弹窗层注释代码
{/* <header onClick={()=>{
setVisible2(true)
}}>{title}<i className='iconfont icon-xiangxia'></i></header>
<i className='header-right'>></i>
<Popup
visible={visible2}
onMaskClick={()=>{
setVisible2(false)
}}
position='top'
bodyStyle={{height:'20vh'}}
>
<HeaderPopup title={title} visible2={visible2} onClose={onCloseVis} ></HeaderPopup>
</Popup> */}
</HeaderWrapper>
)
}
效果
大学详情组件
顶部返回主页
点击“<”返回到首页,引用了react-router-dom中的useNavigate,在其实例化对象中加入需要跳转的路径即可。顶部的导航引用的是antd-mobie中的NavBar组件,其属性onBack是点击返回区域的回调。
const navigate=useNavigate()
<NavBar
right={right()}
onBack={()=>navigate('/home')}
>
搜索框组件
antd-mobile是真的好用,基本上常用的组件都有。
<div className='search'>
<div className='search-item'>
<SearchBar placeholder='超星发现' showCancelButton />
</div>
</div>
轮播图组件
轮播图可以用swiper插件,但个人觉得antd-mobile中的走马灯Swiper组件比较好用,且简单。只需要将几个图片资源放在Swiper.item中map循环即可。其中Swiper的参数autoplay为自动播放,loop为循环播放。
import React from 'react'
import { Wrapper } from './style'
import { Swiper } from 'antd-mobile'
const imgs = [
'https://ts1.cn.mm.bing.net/th?id=OIP-C.YdQb5SkG6H6GP_T4MiCCSgHaE8&w=306&h=204&c=8&rs=1&qlt=90&o=6&dpr=1.38&pid=3.1&rm=2',
'https://tse2-mm.cn.bing.net/th/id/OIP-C.5DPKoPDHLOPtGeYdPQMKYgHaC9?w=309&h=140&c=7&r=0&o=5&dpr=1.38&pid=1.7',
'https://tse3-mm.cn.bing.net/th/id/OIP-C.ttDBZdgY1KlHjlLikkbrugHaFT?w=243&h=180&c=7&r=0&o=5&dpr=1.38&pid=1.7'
]
const items = imgs.map((img, index) => (
<Swiper.Item key={index}>
<div
className='img-item'
>
<img src={img} alt="" />
</div>
</Swiper.Item>
))
export default function UniBanner() {
return (
<Wrapper>
<Swiper autoplay loop>{items}</Swiper>
</Wrapper>
)
}
效果
其他
剩余的部分都是简单的列表,都采用display:flex布局,图标是iconfont库中引入的
总结
至此,项目结束,遗憾的是,侧滑删除组件没能实现,查阅了一些相关文档,尝试了去做侧滑删除组件,但都没能成功,今后会加以完善。通过这次项目实践,我认识到,对于新手而言,一定要去尝试把项目写出来,不要觉得难,分成一小块一小块的去写,总能写出来,实践是解决问题的唯一办法。