现值秋招,没有一个拿的出手的项目怎么行!!!笔者在品读了神三元的小册React Hooks 与 Immutable 数据流实战之后,灵光一现,便使用React 简单仿造了一下极客时间APP。
项目整体使用:react + hooks + redux + mocker-api
优化: better-scroll, styled-component, react-config-router, react-lazyload, 防抖, 路由懒加载,CssTransition等
话不多说,先上成果图:
路由,简单点说就是我们如何从一个页面跳转到另一个页面。光是跳转页面,我们想到的解决办法可能有如下几种:window.location.href = "https://www.baidu.com/"
,跳转>
, self.location = "https://www.baidu.com/"
,你能想到这几种代表你的基础知识已经很扎实,接下来我们将再深入了解点有内涵的东西 - 「路由」。
假如我们要用 React 开发一个手机端 App,手机底部导航栏有五个不同的按钮,每当我点击一个按钮时 App 都会自动切换到一个页面,并且底部导航栏一直固定保持不变。(效果如下)你会怎么做?
首先,我们来剖析一下完成这道题的基本思路。
import React from 'react';
import { renderRoutes } from 'react-router-config';
const Layout = ({ route }) => <>{renderRoutes(route.routes)}</>
export default Layout;
import React from 'react';
import { NavLink } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
function Foot({ route }) {
return (
<div>
{renderRoutes(route.routes)}
<div className="gk-foot">
<NavLink
to="/find"
className="gk-foot--item gk-foot--item__on"
activeClassName="selected">
<span className="gk-foot--icon iconfont"></span>
<p className="gk-foot--label">发现</p>
</NavLink>
<NavLink to="/lecture" className="gk-foot--item" activeClassName="selected">
<span className="gk-foot--icon iconfont"></span>
<p className="gk-foot--label">讲堂</p>
</NavLink>
<NavLink to="/horde" className="gk-foot--item" activeClassName="selected">
<span className="gk-foot--icon iconfont"></span>
<p className="gk-foot--label">部落</p>
</NavLink>
<NavLink to="/study" className="gk-foot--item" activeClassName="selected">
<span className="gk-foot--icon iconfont"></span>
<p className="gk-foot--label">学习</p>
</NavLink>
<NavLink to="/user" className="gk-foot--item" activeClassName="selected">
<span className="gk-foot--icon iconfont"></span>
<p className="gk-foot--label">我的</p>
</NavLink>
</div>
</div>
);
}
export default Foot;
import React from 'react';
function Find() {
return (
<div>
Find
</div>
)
}
export default Find;
import React from "react";
function Lessons() {
return (
<div>
Lessons
</div>
)
}
export default Lessons;
import React from 'react';
function Horde() {
return (
Horde
)
}
export default Horde;
import React from 'react';
function Study() {
return (
<div>
Study
</div>
)
}
export default Study;
import React from 'react';
function HomePage() {
return (
<div>
My
</div>
)
}
export default HomePage;
├─demo # 项目根目录
│ ├─src # 组件根目录
│ ├─assets # 静态文件svg
│ ├─components # 共用组件
│ ├─Foot.js
│ ├─layouts # 页面目录
│ ├─Find.js # 发现页面
│ ├─Lessons.js # 课程页面
│ ├─Horde.js # 部落页面
│ ├─Study.js # 学习页面
│ ├─HomePage.js # 主页
│ ├─routes # 路由
│ ├─index.js # 路由配置页面
│ ├─BlankLayout.js # 空白页面
├─README.md # README
最后,在 index.js 文件中,配置路径时我们这样写
import React from 'react';
import BlankLayout from './BlankLayout';
import { Redirect } from 'react-router-dom';
import FootLayout from '../layouts/Foot';
import FindLayout from '../layouts/Find';
import LessonsLayout from '../layouts/Lessons';
import HordeLayout from '../layouts/Horde';
import StudyLayout from '../layouts/Study';
import HomePageLayout from '../layouts/HomePage';
export default [
{
component: BlankLayout, //最初的空白页面
routes: [
{
path: "/",
component: FootLayout, //固定住底部导航栏-“父路由”
routes: [
{
path: "/",
exact: true,
render: () => <Redirect to={"/find"} />, //重定向到发现页面
},
{
path: "/find",
component: FindLayout,
},
{
path: "/lecture",
component: LessonsLayout,
},
{
path: "/horde",
component: HordeLayout,
},
{
path: "/study",
component: StudyLayout,
},
{
path: "/homepage",
component: HomePageLayout,
},
],
},
],
},
];
到这里为止,凡是我们配置了路径的页面都能拿到路径的信息了,我们可以从页面的props中解构出route来看一下路径
这些路径我们需要从父路由里面结构出来,然后在父组件中使用「NavLink」标签,紧接着当我们点击「NavLink」标签时,我们需要重新渲染一下路径,因此在父组件中{renderRoutes(route.routes)}是必不可少的,如果你没加这句话你会看到路径虽然发生了变化,但是页面是完全没有变化的,现在你可以动手尝试一下啦!
万变不离其中的固定原理: 既然我们上面已经配置了两层路由,顺水推舟,多层路由嵌套也就是再在子路由里嵌套子路由罢了
现在我的 App 已经有了底部导航栏并且可以切换到不同的页面,现在我需要头部导航栏让我可以进入更详细的页面。
根据上道题的思路,我们可以在 index.js 文件的 Lesson.js 页面里更深层次的嵌套路径,原因有以下三点:
import React from 'react';
import BlankLayout from './BlankLayout';
import { Redirect } from 'react-router-dom';
import FootLayout from '../layouts/Foot';
import FindLayout from '../layouts/Find';
import LessonsLayout from '../layouts/Lessons';
import HordeLayout from '../layouts/Horde';
import StudyLayout from '../layouts/Study';
import HomePageLayout from '../layouts/HomePage';
import CampLayout from '../layouts/Lessons/Camp';
import DailyLayout from '../layouts/Lessons/Daily';
export default [
{
component: BlankLayout, //最初的空白页面
routes: [
{
path: "/",
component: FootLayout, //固定住底部导航栏-“父路由”
routes: [
{
path: "/",
exact: true,
render: () => <Redirect to={"/find"} />, //重定向到发现页面
},
{
path: "/find",
component: FindLayout,
},
{
path: "/lecture",
component: Header,
routes: [
{
path: "/lecture",
exact: true,
render: () => <Redirect to={"/lecture/lessons"} />
},
{
path: '/lecture/lessons',
component: LessonsLayout
},
{
path: '/lecture/camp',
component: CampLayout
},
{
path: '/lecture/daily',
component: DailyLayout
},
]
},
{
path: "/horde",
component: HordeLayout,
},
{
path: "/study",
component: StudyLayout,
},
{
path: "/homepage",
component: HomePageLayout,
},
],
},
],
},
];
├─demo # 项目根目录
│ ├─src # 组件根目录
│ ├─assets # 静态文件svg
│ ├─components # 共用组件
│ ├─Foot.js
│ ├─layouts # 页面目录
│ ├─Find.js # 发现页面
│ ├─Lessons # 课程目录
│ ├─camp # 训练营文件
│ ├─index.js # 训练营页面
│ ├─daily # 每日一课文件
│ ├─index.js # 每日一课页面
│ ├─lessons # 课程文件
│ ├─index.js # 课程页面
│ ├─Horde.js # 部落页面
│ ├─Study.js # 学习页面
│ ├─HomePage.js # 主页
│ ├─routes # 路由
│ ├─index.js # 路由配置页面
│ ├─BlankLayout.js # 空白页面
├─README.md # README
这样我们就很巧妙的解决了多层路由嵌套问题,在最外层的路由里一层又一层的嵌套新的路由,一般一个页面最多嵌套3-4层路由,如果你的项目路由嵌套过多,想想是否可以稍微改进一下呢?「此外牢记无论嵌套了多少层路由,为了显示目前这个页面,我们需要找到它的父路由,然后在它的父路由里面解构出route,最后渲染一下页面{renderRoutes(route.routes)},页面就显示出来了,不然看到的就一直是空白页面哦」
懒加载功能可以很好的提升用户的体验感,懒加载是指只有屏幕可视区的内容被加载,其它不可视区的内容等待加载,当用户滑动到页面底部时会发送一个请求请求更多资源,它又被称为分页功能,这样极大的节省了为了一次性加载全部内容和图片的超出时间
案例如下:
这里为了方便大家观察懒加载功能,特意调成了3G网,大家实际生活中几乎都是4G或5G网,因此加载的时间并不会这么长,顶多一眨眼的时间或者你根本观察不到变化
实现懒加载功能的基本思路是当页面滑动到最底端时,我需要等待一会把后面的内容加载出来,它一般用于数据内容比较多时,也称分页功能。首先你需要装一个插件,在vscode里你可以执行下面命令行
npm i react-lazyload -S
我们需要加上共用组件,当你需要懒加载功能时,直接复制下面代码即可
import React from 'react';
import styled, { keyframes } from 'styled-components';
import style from '../../asset/global-style/global-style';
const loading = keyframes`
0%, 100% {
transform: scale(0.0);
}
50% {
transform: scale(1.0);
}
`
const LoadingWrapper = styled.div`
>div {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
margin: auto;
width: 60px;
height: 60px;
opacity: 0.6;
border-radius: 50%;
background-color: ${style["theme-color"]};
animation: ${loading} 1.4s infinite ease-in;
}
>div:nth-child(2) {
animation-delay: -0.7s;
}
`
function Loading() {
return (
<LoadingWrapper>
<div></div>
<div></div>
</LoadingWrapper>
);
}
export default React.memo(Loading);
这里我使用了 styled-components 在js文件里导出css样式,不了解的同学可以直接复制,也可以把它们写在css文件中,效果是一样的
这是上面所需要的global-style文件
export default {
"theme-color": "#d44439",
"font-size-ss": "10px",
"font-size-s": "12px",
"font-size-m": "14px",
"font-size-l": "16px",
"font-size-ll": "18px",
}
由于懒加载功能是伴随着滚动产生的,因此我们还需要添加手机端滚动组件,由于它也是一个共用组件,我们把它放在baseUI文件夹中,每个页面都可以使用它
import React, { forwardRef, useState,useEffect, useRef, useImperativeHandle, useMemo } from "react"
import PropTypes from "prop-types"
import BScroll from "better-scroll"
import styled from 'styled-components';
import Loading from '../../baseUI/loading/index';
import { debounce } from "../../api/utils";
const ScrollContainer = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
`
const PullUpLoading = styled.div`
position: absolute;
left:0; right:0;
bottom: 5px;
width: 60px;
height: 60px;
margin: auto;
z-index: 100;
`
export const PullDownLoading = styled.div`
position: absolute;
left:0; right:0;
top: 0px;
height: 30px;
margin: auto;
z-index: 100;
`
const Scroll = forwardRef((props, ref) => {
const [bScroll, setBScroll] = useState();
const scrollContaninerRef = useRef();
const { direction, click, refresh, pullUpLoading, pullDownLoading, bounceTop, bounceBottom } = props;
const { pullUp, pullDown, onScroll } = props;
let pullUpDebounce = useMemo(() => {
return debounce(pullUp, 500)
}, [pullUp]);
let pullDownDebounce = useMemo(() => {
return debounce(pullDown, 500)
}, [pullDown]);
useEffect(() => {
const scroll = new BScroll(scrollContaninerRef.current, {
scrollX: direction === "horizental",
scrollY: direction === "vertical",
probeType: 3,
click: click,
bounce:{
top: bounceTop,
bottom: bounceBottom
}
});
setBScroll(scroll);
return () => {
setBScroll(null);
}
// eslint-disable-next-line
}, []);
useEffect(() => {
if(!bScroll || !onScroll) return;
bScroll.on('scroll', onScroll)
return () => {
bScroll.off('scroll', onScroll);
}
}, [onScroll, bScroll]);
useEffect(() => {
if(!bScroll || !pullUp) return;
const handlePullUp = () => {
//判断是否滑动到了底部
if(bScroll.y <= bScroll.maxScrollY + 100){
pullUpDebounce();
}
};
bScroll.on('scrollEnd', handlePullUp);
return () => {
bScroll.off('scrollEnd', handlePullUp);
}
}, [pullUp, pullUpDebounce, bScroll]);
useEffect(() => {
if(!bScroll || !pullDown) return;
const handlePullDown = (pos) => {
//判断用户的下拉动作
if(pos.y > 50) {
pullDownDebounce();
}
};
bScroll.on('touchEnd', handlePullDown);
return () => {
bScroll.off('touchEnd', handlePullDown);
}
}, [pullDown, pullDownDebounce, bScroll]);
useEffect(() => {
if(refresh && bScroll){
bScroll.refresh();
}
});
useImperativeHandle(ref, () => ({
refresh() {
if(bScroll) {
bScroll.refresh();
bScroll.scrollTo(0, 0);
}
},
getBScroll() {
if(bScroll) {
return bScroll;
}
}
}));
const PullUpdisplayStyle = pullUpLoading ? { display: "" } : { display: "none" };
const PullDowndisplayStyle = pullDownLoading ? { display: "" } : { display: "none" };
return (
<ScrollContainer ref={scrollContaninerRef}>
{props.children}
{/* 滑到底部加载动画 */}
<PullUpLoading style={ PullUpdisplayStyle }><Loading></Loading></PullUpLoading>
{/* 顶部下拉刷新动画 */}
<PullDownLoading style={ PullDowndisplayStyle }><Loading></Loading></PullDownLoading>
</ScrollContainer>
);
})
Scroll.defaultProps = {
direction: "vertical",
click: true,
refresh: true,
onScroll:null,
pullUpLoading: false,
pullDownLoading: false,
pullUp: null,
pullDown: null,
bounceTop: true,
bounceBottom: true
};
Scroll.propTypes = {
direction: PropTypes.oneOf(['vertical', 'horizental']),
refresh: PropTypes.bool,
onScroll: PropTypes.func,
pullUp: PropTypes.func,
pullDown: PropTypes.func,
pullUpLoading: PropTypes.bool,
pullDownLoading: PropTypes.bool,
bounceTop: PropTypes.bool,//是否支持向上吸顶
bounceBottom: PropTypes.bool//是否支持向下吸顶
};
export default Scroll;
你需要执行以下命令行:
npm i prop-types better-scroll styled-components -S
完整代码
import React, { useEffect } from "react";
import Path from "./learn-path/Path";
import Direction from "./lesson-direction/Direction";
import AllLessons from "./allLessons/allLessons";
import { Tab, Content, EnterLoading } from "./index.style";
import { connect } from "react-redux";
import * as actionTypes from "./store/actions";
import { renderRoutes } from "react-router-config";
import { forceCheck } from "react-lazyload";
import Scroll from "../../../components/scroll/Scroll";
import Loading from "../../../baseUI/loading/index";
function Lessons(props) {
const {
route,
studyPath,
lessonsDirection,
allLessons,
getLessonsListDataDispatch,
} = props;
const {
enterLoading,
pullUpLoading,
pullDownLoading,
pullUpRefresh,
pageCount,
pullDownRefresh,
} = props;
useEffect(() => {
if (!studyPath.length) {
getLessonsListDataDispatch();
}
}, []);
const handlePullUp = () => {
pullUpRefresh(allLessons, pageCount);
};
const handlePullDown = () => {
pullDownRefresh(allLessons, pageCount);
};
return (
<>
{renderRoutes(route.routes)}
<Content>
<Scroll
onScroll={forceCheck}
pullUp={handlePullUp}
pullDown={handlePullDown}
pullUpLoading={pullUpLoading}
pullDownLoading={pullDownLoading}
>
<Tab>
<Path data={studyPath} />
<Direction data={lessonsDirection} />
<AllLessons data={allLessons} path={route} />
</Tab>
{/* 入场加载动画 */}
{enterLoading ? (
<EnterLoading>
<Loading></Loading>
</EnterLoading>
) : null}
</Scroll>
</Content>
</>
);
}
export default connect(
function mapStateToProps(state) {
return {
studyPath: state.lessons.studyPath,
lessonsDirection: state.lessons.lessonsDirection,
allLessons: state.lessons.allLessons,
enterLoading: state.lessons.enterLoading,
pullUpLoading: state.lessons.pullUpLoading,
pageCount: state.lessons.listOffset,
pullDownLoading: state.lessons.pullDownLoading,
};
},
function mapDispatchToProps(dispatch) {
return {
getLessonsListDataDispatch() {
dispatch(actionTypes.getLessonsList());
},
pullUpRefresh() {
dispatch(actionTypes.changePullUpLoading(true));
dispatch(actionTypes.refreshMoreLessonsInfoRequest());
},
//顶部下拉刷新
pullDownRefresh() {
dispatch(actionTypes.getLessonsList());
dispatch(actionTypes.changePullDownLoading(true));
},
};
}
)(Lessons);
你可能会好奇,Content 组件是什么?在这里我要强调一下,better-scroll 的原理并不复杂,就是在容器元素高度固定,当子元素高度超过容器元素高度时,通过 transfrom 动画产生滑动效果,因此它的使用原则就是外部容器必须是固定高度,不然没法滚动。而 Content 就是这个外部容器。
因此我们在style.js文件中添加以下代码
import styled from'styled-components';
export const Content = styled.div`
position: fixed;
top: 90px;
bottom: 0;
width: 100%;
`
CSSTransition 效果可以说给我们做的项目锦上添花了,为了使我们做的App受大众喜爱,不来点动感怎么行呢
话不多说,我们直接上效果图
不知道老铁们有没有发现,每当我切换到下一个页面时,页面似乎是“弯着腰”进来的,在这里我们使用的就是CSSTransition效果了,相信当老铁们学会了CSSTransition效果后还能做出更花里胡哨的页面hhhhh
插件安装
npm i react-transition-group -S
使用
import React, { useState, useEffect } from "react";
import { NavLink } from "react-router-dom";
import { Container, Head, Content, Background } from "./style";
import Scroll from "../../../../components/scroll/Scroll";
import TabHead from "./head/Head";
import Nav from "./Nav/index";
import { renderRoutes } from "react-router-config";
import { CSSTransition } from "react-transition-group";
import Foot from "./foot/index";
function Page({ route }) {
const [showMessage, setShowMessage] = useState(false);
useEffect(() => {
setShowMessage(true);
}, []);
return (
<CSSTransition
in={showMessage}
timeout={300}
classNames="fly"
unmountOnExit
>
<Background>
{renderRoutes(route.routes)}
<Head>
<NavLink to="/lecture/lessons">
<svg width="42" height="42" style={{ marginLeft: "-2vw" }}>
<polyline
points="25, 13 16, 21 25, 29"
stroke="#ccc"
strokeWidth="2"
fill="none"
/>
</svg>
</NavLink>
<img src="/asserts/star.png" className="star" />
<img src="asserts/send.png" />
</Head>
<Content>
<Scroll>
<Container>
<TabHead />
<Nav route={route} />
</Container>
</Scroll>
</Content>
<Foot />
</Background>
</CSSTransition>
);
}
export default Page;
对应的style.js
import styled from "styled-components";
export const Container = styled.div`
background-color: rgb(245, 245, 245);
transform-origin: right bottom;
font-family: PingFang SC, Lantinghei SC, Microsoft Yahei, Hiragino Sans GB,
Microsoft Sans Serif, WenQuanYi Micro Hei, Helvetica, sans-serif;
`;
export const Background = styled.div`
position: fixed;
top: 0;
z-index: 200;
width: 100vw;
background-color: red;
z-index: 22;
height: 100vh;
&.fly-enter,
&.fly-appear {
/* transform-origin: 100% 100%;
transform: rotateZ(90deg); */
transform: rotateZ(30deg) translate3d(100%, 0, 0);
}
&.fly-enter-active,
&.fly-appear-active {
/* transform-origin: 100% 100%;
transition: all .3s;
transform: rotateZ(0deg); */
transition: transform .3s;
transform: rotateZ(0deg) translate3d(0, 0, 0);
}
&.fly-exit {
/* transform-origin: 100% 100%;
transform: rotateZ(0deg); */
transform: rotateZ(0deg) translate3d(0, 0, 0);
}
&.fly-exit-active {
/* transform-origin: 100% 100%;
transition: all .3s;
transform: rotateZ(90deg); */
transition: transform .3s;
transform: rotateZ(30deg) translate3d(100%, 0, 0);
}
`;
export const Head = styled.div`
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100%;
padding: 0 15px;
display: flex;
align-items: center;
background: #fff;
& > img {
width: 8vw;
height: 4vh;
margin-top: 1vh;
margin-right: 2vw;
}
& > .star {
margin-left: 65vw;
}
`;
export const Content = styled.div`
position: fixed;
top: 48px;
bottom: 15px;
width: 100%;
z-index: 100;
`;
在这里我们需要注意以下几点:
以上就是CSSTransition组件的基本使用方法,老铁们想要了解更多内容可以直接查看文档,链接参上http://reactcommunity.org/react-transition-group/css-transition