看完这篇教程,你应该就能掌握了以下知识点:
useLocation
这个钩子函数上一篇 [万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解 中,最后渲染的结果是这样的:
既然大体的框架都已经完成的差不多了,那么现在就继续往里面填充一点细节吧。
Header 中的悬浮效果已经做好了,但是对当前所在的 url 没有任何的提示。对网站不熟悉的用户而言,会让用户无法快速识别自己所在的页面,导致不好的用户体验。
因此,下面针对这个地方进行一下优化。
要做这个判断也很简单,react-router-dom 封装好了一个名为 useLocation
的函数,可以用来判断当前所在的 url。现在,导入 useLocation
这个函数,并且在循环中判断当前的 url 与子项中的 url 是否一致,如果一致,则证明该子项就是当前所在页面。
这个判断可以通过三元表达完成,证明当前子项是所在页面之后,可以添加一个新的类名去实现高亮效果。
这样就实现了 React 之中动态添加添加类名的功能。
关于三元表达式,也就是 val = condition ? a : b
的语法,它的意思是当 condition
是 true 的时候,val 的值是 a,不然 val 的值是 b,与下面的 if else
的表达式是一样的:
if (condition === true) {
val = a;
} else {
val = b;
}
// 等同于
val = condition ? a : b;
在简单的逻辑判断中,是非常常用的方法。
这一部分的代码写完之后,效果是这样的:
已经可以看到导航栏下方会出现当前页面的指示器,并且该指示器会随着页面的变动而变动。
下面为修改过的代码:
JavaScript:
import React from 'react';
import NAV_LINKS from '../../../constants/navLinks';
import {
Link } from 'react-router-dom';
// =================新增的代码===============
import {
useLocation } from 'react-router-dom';
// =================新增的代码===============
const Nav = () => {
// =================新增的代码===============
const location = useLocation();
// =================新增的代码===============
return (
<nav className="nav">
<ul className="flex">
{
NAV_LINKS.map((link) => (
<li
key={
link.name}
// =================新增的代码===============
// 如果当前url等于nav中的url,则证明这是当前页面
className={
location.pathname === link.url ? 'active-nav' : null}
// =================新增的代码===============
>
<Link to={
link.url}>{
link.name}</Link>
</li>
))}
</ul>
</nav>
);
};
export default Nav;
CSS:
.nav li {
margin-left: 65px;
/* 下面为修改过的代码 */
padding: 8px;
/* 上面为修改过的代码 */
}
/* 下面为新增的代码 */
.active-nav {
border-bottom: 2px solid #00a4ff;
}
/* 上面为新增的代码 */
此时,又出现了一个小问题,那就是如果定向到了具体的某一门课的时候,那么导航栏的指示器会再一次失去效果:
这是因为导航栏里面只有 /
, /courses
, 和 career-path
的规划,没有办法正确匹配到 /courses/:id
的路径。
这里就偷懒且粗暴的做简单的字符串匹配就好了,只要判断当前 url 里面有 /courses
,并且遍历的子项中也有 /courses
,就算二者匹配了。
这种复杂的逻辑用三元表达写也比较吃力,所以就单独抽出来做了一个函数:
// 忽略一些引用
import * as routePaths from '../../../constants/routerPaths';
const Nav = () => {
const location = useLocation();
// =================新增的代码===============
const isActiveNav = (pathname, url) => {
// 因为目前只考虑 courses/:id 的特殊项,所以写死了这一个判断
let isActive =
pathname === url || // 当path正好与url相等
(pathname.includes(routePaths.COURSES) && // 在 /courses
url.includes(routePaths.COURSES));
return isActive;
};
// =================新增的代码===============
return (
<nav className="nav">
<ul className="flex">
{
NAV_LINKS.map((link) => {
const {
url } = link;
return (
<li
key={
link.name}
// =================修改的代码===============
className={
isActiveNav(location.pathname, url) ? 'active-nav' : null
}
// =================修改的代码===============
>
<Link to={
url}>{
link.name}</Link>
</li>
);
})}
</ul>
</nav>
);
};
export default Nav;
这样,导航栏的优化就已经实现了。
现在就继续实现首页的逻辑,在这一个部分,主要将会实现一下两个模块:
本来这一块我以为是可以单独抽出来做一个 banner 组件的,但是后来发现,这么做的意义不是特别大。
另一个有 banner 的页面在职业规划中,效果图如下
可以看出来,二者的特性在于,看起来都有一个背景图,并且背景图片的宽度都是 100%,这也是我本来想封装的原因。再具体实现的过程中却发现,二者还是有一些不同的:
鉴于不同之处多过相似之处,强行封装未免有种过度封装的感觉,所以原本决定使用的 banner 组件放弃,直接在首页写轮播图。
接下来对首页的 banner 进行业务需求的分析,可以得出,这里可以拆分出 3 个部分——部分,不是模块——分别为:
背景
只是背景颜色,很好做
轮播图
需要让图片动起来的轮播图
轮播图上方的课程模块
处于轮播图上方的课程模块,再细分还能分成:
因此,这里决定新增了 4 个模块去实现对应的功能。
结构如下:
结构为:
|- home
| |- homeCarousel
| | |- courseContent
| | | |- courseSubNav
| | | |- myCourseList
| | |- index.js
|- Home.js
具体实现代码之前,现将基础结构修改一下。
主要就是修改一下 Component 这个目录中,Home.js 之中的类名,删掉 Home
这个 placeholder,开始往里面填充内容,同时更新一下对应的 CSS。
import React from 'react';
const Home = () => {
return <div className="homepage"></div>;
};
export default Home;
只要修改一下 CSS 就好了:
CSS:
.homepage-banner {
height: 420px;
background-color: #1c036c;
}
这里使用的是一个叫做 react-responsive-carousel 依赖包去完成的轮播图,安装插件的方法是在中断输入下面的命令:
npm install yarn add react-responsive-carousel
等安装完成了,就可以在项目之中使用这个依赖包了。
介绍中的使用方法是这样的:
import "react-responsive-carousel/lib/styles/carousel.min.css"; // requires a loader
import {
Carousel } from 'react-responsive-carousel';
class DemoCarousel extends Component {
render() {
return (
<Carousel>
<div>
<img src="assets/1.jpeg" />
<p className="legend">Legend 1</p>
</div>
{
/* 重复上面的 div */}
</Carousel>
);
}
});
也就是说,在
中放入图片就会被自动读取,而它可以接受的属性也用很多,这里挑用得到的讲一下:
showStatus: false
右上角显示当前图片进度的小图,在 PSD 中没有这一块内容,所以改为 false
showArrows: false,
轮播图两边的箭头,PSD 中也没有,所以改为 false
autoPlay: true,
开启自动轮播
showThumbs: false,
展示轮播图状态的小图,默认开启
infiniteLoop: true,
是否无限循环,即到轮播图最后一张图片展示完毕后,从第一张图片重新开始
上面的配置会作为属性传到 Carousel
组件中去。
轮播图完整的代码为:
import React from 'react';
import banner from '../../../asset/img/home/banner.jpg';
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
import {
Carousel } from 'react-responsive-carousel';
import CourseContent from './courseContent';
const HomeBanner = () => {
// 将作为 props 传给 Carousel 这个组件
const settings = {
showStatus: false,
showArrows: false,
autoPlay: true,
showThumbs: false,
infiniteLoop: true,
};
// 手动生成一个由图片组成的数组
const getBanners = () => {
const banners = [];
for (let i = 0; i < 5; i++) {
banners.push(
<div key={
i}>
<img src={
banner} alt="home-banners" />
</div>
);
}
return banners;
};
return (
<div className="homepage-banner">
<div className="container">
<Carousel {
...settings}>{
getBanners()}</Carousel>
</div>
</div>
);
};
export default HomeBanner;
效果图如下:
React 的数据是只能由一个方向传输的,也就是父->子的关系。
关于父子组件传递数据,有两种方法:
数据被组件名所包裹,如上文的 Carousel 所用的那样。这种情况下,必须要有完整的开始标签和闭合标签,才可以正确的读取数据:
<Carousel>
<div>
{
/* 这里的 div 标签就被作为 props 传递到了子组件中 */}
<img src="assets/1.jpeg" />
<p className="legend">Legend 1</p>
</div>
{
/* 重复上面的 div */}
</Carousel>
在组件名中直接传递属性,这种方法可以用于自闭合标签,或是普通标签。这种写法相对而言是比较方便和简单的写法,也是主流的写法。
<Carousel {
...settings}> // 这里 settings 就被展开并且作为属性传递到了子组件中
</Carousel>
// 自闭合标签的用法
<input type='text' {
...props} />
先新建一个父组件,名为 CourseContent,再分成两块,一块是左边的专业方向列表,另外一块是右边的我的课程表。
再重申一遍模块化的概念,模块化是将一个功能单独分割出来以达可以复用或是逻辑清晰的目的。目前来说很难有一个官方既定的标准说你应该怎么怎么分割这个模块。通用的理解就是按照功能分割。
自然这也有可能会造成过渡分割的问题,从而造成文件结构深层嵌套,难以理解。
具体怎么实现只能说怎么选择是仁者见仁,智者见智了,目前真的很难做到统一化。
这里在具体实现的时候产生了一个问题:
内容被挤到了下方,而不是叠加在轮播图上。
这是因为轮播图本身是需要占据空间的,课程模块同样需要空间。这时候使用 position: absolute;
定位即可,这样课程列表就会从文档流中脱离出来,不占用文档流中的空间。
另外,从文档流中脱离出来的内容是没有宽度的,需要使用 width: inherit;
让它继承父元素的宽度。
实现后的效果图:
可以看出这个逻辑还是比较简单的,主要是使用 ul > li > a
这样嵌套的结构。
职业方向是没有办法排列的,所以这里用无序列表会比较合适。
我将具体的课程抽出来做了放入了 constants
这个文件夹之中,模拟从另一个地方接收到数据。
在开发中,这些数据其实很难会被写死,大多数情况下数据会从一些 CMS(Content Management System,内容管理系统) 中传来。为的就是当有一些小细节要变动的时候,只需要修改 CMS 中的内容,而不需要修改结构,减少开发和维护的成本。
JavaScript 代码:
import React from 'react';
import {
courseSubNavList } from '../../../../../constants/home';
const CourseSubNav = () => {
return (
<div className="course-sub-nav">
<ul>
{
courseSubNavList.map((val) => (
<li className="relative" key={
val}>
<a href=".#">{
val}</a>
</li>
))}
;
</ul>
</div>
);
};
export default CourseSubNav;
当然,CSS 方面要修改亿点点细节,这个可以看 index.css 和 Home.css,我主要对悬浮的特效进行了 CSS 的封装,这样大部分的 a 标签和 li 标签在悬浮的时候都会产生字体颜色上的变化。
对 a 标签进行 CSS 的封装可以很好地达到复用性,之前 header 中的导航栏也使用的是同样的 CSS,以及之后很多其他地方,也会用到同样的 当鼠标悬浮时,字体会变色 这样一个特性。
这里的实现效果也不是很难,我最终选择了 div > dl + button
的结构。
首先是因为课程表本身就比较符合 dl > dt + dd
的结构,我的课程表
是 dl 中的标题,具体的课程是 dl 中的数据。
另外我发现这一块和下载 app 的结构很相似,所以我使用了 button
这个元素,并且对其进行了 CSS 样式的封装。
效果如下:
JavaScript 代码:
import React from 'react';
import {
courseSchedule } from '../../../../../constants/home';
const MyCourseList = () => {
const getCourses = () => {
return courseSchedule.map((course, index) => (
<dd key={
course.name}>
<a href=".#" className={
index === 2 ? 'active' : null}>
<h4>继续学习 {
course.name}</h4>
<p>正在学习-{
course.progress}</p>
</a>
</dd>
));
};
return (
<div className="my-course-list">
<dl className="my-course-list-details">
<dt>我的课程表</dt>
{
getCourses()}
</dl>
<button type="button" className="my-course-list-button">
全部课程
</button>
</div>
);
};
export default MyCourseList;
先看一下效果:
再回顾一下 home 这个组件的结构:
结构为:
|- home
| |- homeCarousel
| | |- courseContent
| | | |- courseSubNav
| | | |- myCourseList
| | |- index.js
|- Home.js
现在要整合的是 courseContent 下的 index.js 以及 homeCarousel 下的 index.js
courseContent/index.js
import React from 'react';
import CourseSubNav from './courseSubNav';
import MyCourseList from './myCourseList';
const CourseContent = () => {
return (
<div className="absolute course-content flex space-between">
<CourseSubNav />
<MyCourseList />
</div>
);
};
export default CourseContent;
homeCarousel/index.js
import React from 'react';
import banner from '../../../asset/img/home/banner.jpg';
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
import {
Carousel } from 'react-responsive-carousel';
import CourseContent from './courseContent';
const HomeBanner = () => {
// 将作为 props 传给 Carousel 这个组件
const settings = {
showStatus: false,
showArrows: false,
autoPlay: true,
showThumbs: false,
infiniteLoop: true,
};
// 手动生成一个由图片组成的数组
const getBanners = () => {
const banners = [];
for (let i = 0; i < 5; i++) {
banners.push(
<div key={
i}>
<img src={
banner} alt="home-banners" />
</div>
);
}
return banners;
};
return (
<div className="homepage-banner">
<div className="container">
<Carousel {
...settings}>{
getBanners()}</Carousel>
<CourseContent />
</div>
</div>
);
};
export default HomeBanner;
就是这个部分:
最终采取的还是 div > dl + div
的结构,原因和课程表的一样。
这个部分最终没有单独整合成一个模块,而是放在了一个函数里面直接渲染。
倒是这里对 CSS 进行了逻辑的抽出,这样之后在另一个页面也可以重用 CSS。
import React from 'react';
import HomeBanner from './homeCarousel';
import './Home.css';
import {
fieldSuggested } from '../../constants/home';
const Home = () => {
const getFieldSuggestion = () => {
return (
<div className="card flex space-between">
<dl className="flex">
<dt>{
fieldSuggested.title}</dt>
{
fieldSuggested.suggestedFields.map((field) => (
<dd key={
field}>
<a href="/#">{
field}</a>
</dd>
))}
</dl>
<div className="modify-interested-field">
<a href=".#">修改兴趣</a>
</div>
</div>
);
};
return (
<div className="homepage relative">
<HomeBanner />
<div className="container">{
getFieldSuggestion()}</div>
</div>
);
};
export default Home;
有人和我说直接贴源码的,有点不太好跟逻辑,我想了下,因为我已经习惯了这种写法可能还好,对于不习惯这种写法的,可能会存在这个问题。
所以这次我直接把源码打包放到了资源里面,可以直接下载:react 学成在线 part2
这一章主要讲的内容有:
功能方面:
以及头部提到的一些 React 知识点的学习:
useLocation
这个钩子函数