前言
什么是组件?
- 组件是一个广泛的概念,现在流行的框架中都有组件;
- 一个组件就是用户界面的一部分,它可以有自己 的 逻辑 和 外观 ,组件之间 可以 相互嵌套 ,也可以 复用 多次;
一、React组件
1.1 基本概念 及 注意事项
- 在 React 中,一个 组件 就是 首字母 大写 的 函数 ,内部存放了 组件的 逻辑 和 视图UI,渲染组件只需要把组件 当成 标签 书写 即可;
- ❗ 注意 :
- React组件是常规的JS函数,但 组件的名称 必须以 大写字母 开头,否则它们将无法运行;
- React组件 必须 要有一个 根标签;
- React组件函数也可以是箭头函数;
- 如果你的标签和
return
关键字不在同一行,则必须把它包裹在一对括号中(小括号);
- 没有括号包裹的话,任何在
return
下一行的代码 都将被忽略;- 所有的标签都必须是闭合标签;
- 如果是单标签:必须自闭合;
1.2 定义组件
jsx
// 1. 定义函数
function Button() {
// 组件内部逻辑
// 2. 添加标签
return <button> click me </button>
}
// 3. 导出组件
export default Button;
1.3 使用组件(渲染组件)
- 正常的定义组件都是以上三个步骤,这里为了简单方便,就直接在 App.js 中定义组件;
jsx
// 定义组件
const Button = () => {
// 业务逻辑 及 组件逻辑
const onClick = (name, e) => {
console.log(name, e);
};
return <button onClick={(e) => onClick('禁止摆烂_才浅', e)}>Click Me</button>;
}
// 使用组件
function App() {
return (
<div>
{/* 单标签 ===> 自闭合 */}
<Button />
{/* 双标签 */}
<Button></Button>
</div>
);
}
export default App;
二、useState 基础使用
2.1 基本介绍
useState
是一个 React Hook(函数),它允许我们向组件添加一个 状态变量,从而控制影响组件的渲染结果;- ❗ 本质 :
- 和普通JS变量不同的是,状态变量一旦发生变化,组件的视图UI也会跟着变化(数据驱动试图);
- 就比如说,我将 状态变量 count 的值 从 0 => 1,那么视图上的显示结果也会从 0 => 1;
2.2 语法
- ❗ 注意 :
- 使用之前需要先导入;
- React钩子
useState
不能在顶层被调用;- React Hooks 必须在 React函数组件 或 自定义React Hook函数 中 调用;
jsx
import { useState } from 'react';
const [状态变量, 修改状态变量的函数] = useState(初始值);
// const [状态变量, set状态变量] = useState(初始值);
-
useState
是一个函数 ,返回值 是 一个 数组; -
数组中的:
- 第一个参数 :是 状态变量;
- 第二个参数 :是
set
函数,用来修改 状态变量 的 值 ;- 该函数的参数就是状态变量修改之后的值(想让状态变量变成多少,就写多少);
- 调用该函数的作用 :
- 修改 状态变量 的值(用传入的新值 替换 旧值);
- 重新使用新的 状态变量 渲染视图
-
useState
的 参数 将作为 状态变量 的 初始值; -
代码展示:
jsximport { useState } from 'react'; const Button = () => { // 调用 useState 添加一个状态变量 // num ===> 状态变量 // setNum ===> 修改状态变量的函数 const [num, setNum] = useState(0); // 点击事件 - num自增 const onClick = () => { // 调 setNum 的作用 // 1. 修改 状态变量 num 的值(用传入的新值修改状态变量) // 2. 重新使用新的 状态变量 num 渲染视图 setNum(num + 1); }; return ( <div> <button onClick={onClick}>Click Me</button> <br /> <span>状态变量的值 --- {num}</span> </div> ); }; function App() { return ( <div> <Button /> </div> ); } export default App;
2.3 修改状态的规则
2.3.1 修改 基本数据类型 的状态
- 在 React 中,状态 被认为是 只读 的,我们应该始终 替换 而不是 修改 它,直接修改 状态 不能引起视图更新;
- ❗ 注意 :
- 直接修改 状态变量 的 值,状态变量 能被 修改,但是 不会引起 视图 的 更新;
- 既要 修改状态 变量的值,还想要 视图同时更新,只能通过
useState
的set
函数去修改状态变量的值;
-
代码展示:
jsximport { useState } from 'react'; const Button = () => { // 调用 useState 添加一个状态变量 // num ===> 状态变量 // setNum ===> 修改状态变量的函数 let [num, setNum] = useState(0); // 点击事件 - num自增 const onClick = () => { setNum(num + 1); }; const onClick1 = () => { num++; }; return ( <div> <button onClick={onClick1}>Click Me</button> <br /> <span>直接修改状态变量的值 --- {num}</span> <br /> <hr /> <br /> <button onClick={onClick}>Click Me</button> <br /> <span>使用useState的set函数状态变量的值 --- {num}</span> </div> ); }; function App() { return ( <div> <Button /> </div> ); } export default App;
-
演示效果:
2.3.2 修改 对象、数组 的状态
-
规则 :
- 对于对象类型的状态变量,应该始终传给
set
一个 全新的对象 来进行修改;
- 对于对象类型的状态变量,应该始终传给
-
代码展示:
jsximport { useState } from 'react'; const Button = () => { // 对象格式 const [info, setInfo] = useState({ name: '张三', age: 22, gender: '男' }); const onClick = () => { setInfo({ ...info, name: '李四', age: 58 }); }; const onClick1 = () => { info.name = '王麻子'; info.age = 44; console.log(info); }; // 数组形式 const [numArr, setNumArr] = useState([0, 1, 2, 3, 4]); const onChange = () => { setNumArr([1, 2, 3, 4, 5]); }; const onChange1 = () => { numArr[0] = 100; console.log(numArr); }; return ( <div> <button onClick={onClick1}>Click Me</button> <br /> <span> 直接修改状态变量的值 --- {info.name} - {info.age} </span> <br /> <hr /> <br /> <button onClick={onClick}>Click Me</button> <br /> <span> 使用useState的set函数状态变量的值 --- {info.name} - {info.age} </span> <br /> <hr /> <br /> <button onClick={onChange1}>Click Me</button> <br /> <span>直接修改状态变量的值 --- {numArr}</span> <br /> <hr /> <br /> <button onClick={onChange}>Click Me</button> <br /> <span>使用useState的set函数状态变量的值 --- {numArr}</span> </div> ); }; function App() { return ( <div> <Button /> </div> ); } export default App;
-
演示效果:
三、组件的样式处理
- React组件基础的样式控制有两种控制方案:
- ✅ class类名控制(单独写样式,导入到组件文件中);
- ❌ 行内样式(极不推荐);
3.1 ✅ class类名控制
3.1.1 固定类名
-
代码展示:
css.box { width: 100px; height: 100px; background-color: red; }
jsximport './App.css'; const App = () => { return <div className="box box1"></div>; }; export default App;
- ❗ 注意 :
- 在 React 中,使用类名的时候,需要使用
className
关键字 替代之前的class
;
3.1.2 动态 添加 或 删除 类名
动态判断添加 单类名
jsx
<div className={item.readState === 0 ? 'no-read' : null}></div>
已有多类名,动态判断再添加类型
jsx
// 数组方法
<div className={['box', classA, item.readState === 0 ? 'no-read' : null].join(' ')}></div>
<div className={['box', classA, item.readState === 0 && 'no-read'].join(' ')}></div>
// 模板字符串方法
<div className={`box ${classA} ${item.readState === 0 ? 'no-read' : null}`}></div>
<div className={`box ${classA} ${item.readState === 0 && 'no-read}`}></div>
- ❗ 注意 :
- 数组方法时:
- 要使用 空格 将数组转为字符串;
- 模板字符串方法时:
- 类名之间 必须要有 空格;
✅ 使用 classnames 依赖
在实际开发中,我们通常需要根据某个条件去判断类名,此时我们可以使用
classnames
这个第三方包进行设置;
js
// 安装依赖
npm i classnames
jsx
import classNames from 'classnames';
<div className={classNames('box', {'no-read': item.readState === 0 })}></div>
3.2 ❌ 行内样式
- 行内样式有两种方案:
- 直接将样式写在行内;
- 将样式属性写在一个对象中,将这个对象绑定到对应的元素上;
- 代码展示:
-
将样式写在行内:
jsxconst App = () => { return <div style={{ width: '100px', height: '100px', backgroundColor: 'red' }}>Hello World</div>; }; export default App;
-
使用对象:
jsxconst style = { width: '100px', height: '100px', backgroundColor: 'red', color: '#fff' }; const App = () => { return <div style={style}>Hello World</div>; }; export default App;
-
四、受控表单组件绑定
4.1 概念
- 使用 React 组件状态的状态(
useState
)控制表单的状态;
4.2 使用步骤
-
准备一个React状态值
- 使用
useState
声明状态;
jsxconst [value, setValue] = useState('');
- 使用
-
通过
value
属性绑定状态,通过onChange
事件绑定状态同步的函数,通过事件对象e拿到输入框最新的值,反向修改react的状态;jsx<input type="text" value={value} onChange={(e) => setValue(e.target.value)}/>
- ❗ 注意 :
- 当给表单元素设置
value
属性的时候,这个字段呈现一个 只读字段;- 如果该字段应该是可变的,则使用
defaultValue
;- 否则,设置
onChange
或readOnly
;
五、获取DOM元素
-
在 React 中 获取 / 操作 DOM,需要使用
useRef
钩子函数,分为两步:-
使用
useRef
创建ref
对象,并于 JSX 绑定;- 在组建内部调用;
jsxconst inputRef = useRef(null); <input type="text" ref={inputRef} />
-
-
在DOM可用时,通过
ref名称.current
拿到DOM对象;jsxconsole.log(inputRef.current);
- ❗ 什么是DOM可用?
- 组件渲染完毕之后才可用的;
- 渲染之前获取DOM得到的是
null
;
六、案例展示 - B站评论 - 普通版
- 效果展示:
- 功能需求:
- 渲染评论列表;
- 实现删除评论;
- 只有自己的评论才显示删除按钮;
- 点击删除按钮,删除当前评论,列表中不再显示;
- 渲染导航Tab和高亮实现;
- 评论列表排序功能实现;
- 最新:评论列表按照创建时间排序(新的在前);
- 最热:点赞数排序(点赞数多的在前);
- 实现评论功能;
- 点击发布之后,需要清空输入框的内容,并且自动获取焦点;
- 回车也可以发送评论;
- 核心思路:
- 使用
useState
维护评论列表;- 使用
map
对列表进行遍历渲染(一定要添加key
);
- 代码展示:
jsx
import { useRef, useState } from 'react';
// 需要安装 lodash
import _ from 'lodash';
// 导入所需样式 - 放在本文的末尾
import './bilibili-review.scss';
// 大家自己在本地虽败找一张图片
import avatar from './images/bozai.png';
// 评论列表数据
const defaultList = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/comment/zhoujielun.jpeg',
uname: '周杰伦'
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-18 08:15',
// 喜欢数量
like: 98,
// 0:未表态 1: 喜欢 2: 不喜欢
action: 0
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/comment/xusong.jpeg',
uname: '许嵩'
},
content: '我寻你千百度 日出到迟暮',
ctime: '11-13 11:29',
like: 88,
action: 2
},
{
rpid: 1,
user: {
uid: '30009257',
avatar,
uname: '黑马前端'
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
action: 1
}
];
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端'
};
// 头部Tab配置项
const tabOptions = [
{ type: 'latest', text: '最新' },
{ type: 'hottest', text: '最热' }
];
const App = () => {
// 评论列表数据
const newList = _.orderBy(defaultList, ['ctime'], ['desc']);
const [list, setList] = useState(newList);
// 记录活跃的Tab状态
const [activeTab, setActiveTab] = useState('latest');
/** 更新评论列表 - 删除 + 排序 */
const updateList = (type, id) => {
let newList = [];
if (type && !['del'].includes(type)) setActiveTab(type);
switch (type) {
case 'del':
newList = list.filter((item) => item.rpid !== id);
break;
case 'latest':
newList = _.orderBy(list, ['ctime'], ['desc']);
break;
case 'hottest':
newList = _.orderBy(list, ['like'], ['desc']);
break;
default:
newList = list;
break;
}
setList(newList);
};
/** 保存输入框内容 */
const [value, setValue] = useState('');
/** 输入框ref */
const inputRef = useRef(null);
/** 发布评论 */
const addReview = (e) => {
if (e.key === 'Enter') e.preventDefault();
if (!value || (e.type === 'keydown' && e.key !== 'Enter')) return;
const item = {
rpid: new Date().getTime(),
user,
content: value,
ctime: new Date().toLocaleDateString(),
like: 0,
action: 0
};
setList([item, ...list]);
setValue('');
inputRef.current.focus();
};
return (
<div className="app">
{/* 头部 */}
<div className="header align-center">
<div className="title align-center">
评论<em>{list.length}</em>
</div>
{/* tab栏 */}
<ul className="tab align-center">
{tabOptions.map((item, index) => {
return (
// className={['item', 'align-center', activeTab === item.type ? 'active' : null].join(' ')}
// className={['item', 'align-center', activeTab === item.type && 'active'].join(' ')}
// className={`item align-center ${activeTab === item.type ? 'active' : null}`}
// className={`item align-center ${activeTab === item.type && 'active'}`}
<li
className={`item align-center ${activeTab === item.type && 'active'}`}
onClick={() => updateList(item.type)}
key={item.type}>
<span>{item.text}</span>
{tabOptions.length - 1 !== index && <span className="split-line"></span>}
</li>
);
})}
</ul>
</div>
<div className="review-box">
{/* 评论框 */}
<div className="post-review align-center">
<img src={user.avatar} alt={user.uname} className="avatar" />
<div className="input align-center">
<textarea
type="text"
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={addReview}
placeholder="发一条友善的评论"
/>
<button onClick={addReview}>发布</button>
</div>
</div>
{/* 评论列表 */}
<div className="review-list">
{list.map(({ user: { avatar, uname, uid }, content, ctime, like, rpid }) => {
return (
<div className="review-item" key={rpid}>
<div className="left">
<img src={avatar} alt={uname} />
</div>
<div className="right">
<div className="user-name">{uname}</div>
<div className="content">{content}</div>
<div className="bottom">
<div className="time">{ctime}</div>
<div className="like-num">点赞数:{like}</div>
<ul className="controls">
{/* 只有自己的评论才展示删除按钮 */}
{uid === user.uid && (
<li className="del" onClick={() => updateList('del', rpid)}>
删除
</li>
)}
</ul>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
export default App;
-
bilibili-review.scss
scss* { margin: 0; padding: 0; box-sizing: border-box; } body { background-color: #c9ccd0; } li { list-style: none; } em { font-style: normal; } .align-center { display: flex; align-items: center; } .app { padding: 100px; background-color: #fff; .header { justify-content: flex-start; height: 24px; .title { font-weight: bold; font-size: 18px; color: #333; em { margin-left: 10px; color: #666; font-size: 12px; font-weight: 400; } } .tab { justify-content: flex-start; height: 100%; margin-left: 30px; color: #666; font-size: 12px; li.item { cursor: pointer; &:hover { color: #000; } .split-line { height: 11px; margin: 0 12px; border-right: 1px solid #9499a0; } } .active { color: #00aeec; } } } .review-box { width: 100%; padding-left: 20px; .post-review { align-items: flex-start; width: 100%; height: 70px; margin-top: 16px; img { width: 40px; height: 40px; border-radius: 50%; margin-right: 16px; } .input { width: 100%; align-items: flex-start; textarea { width: calc(100% - 100px - 10px) !important; height: 50px; margin-right: 10px; padding-left: 10px; border: 1px solid #f1f2f3; background-color: #f1f2f3; border-radius: 6px; font-size: 16px; line-height: 50px; outline: none; resize: none; transition: height 0.2s; appearance: none; -webkit-appearance: none; &:hover, &:focus { border-color: #c9ccd0; background-color: #fff; } &:focus { height: 70px; } &::placeholder { font-size: 12px; } &::-webkit-scrollbar { display: none; } } button { width: 100px; height: 50px; background-color: #00aeec; border: none; border-radius: 6px; color: #fff; font-size: 15px; cursor: pointer; opacity: 0.5; &:hover { opacity: 1; } } } } .review-list { .review-item { display: flex; align-items: flex-start; width: 100%; margin-top: 20px; .left { width: 56px; height: 100%; img { width: 40px; height: 40px; border-radius: 50%; cursor: pointer; } } .right { width: calc(100% - 56px); height: 100%; padding-bottom: 20px; border-bottom: 2px solid #eee; .user-name { color: #61666d; font-size: 13px; cursor: pointer; } .content { margin: 16px 0 10px; font-size: 15px; } .bottom { display: flex; align-items: center; justify-content: flex-start; color: #9499a0; font-size: 13px; .like-num { margin: 0 20px; } .controls { li { cursor: pointer; } .del:hover { color: #000; } } } } } } } }