Date: November 15, 2023
Sum: React介绍、JSX、事件绑定、组件、useState、B站评论
Tags: 二刷
日志:
11/20: 优化B站评论的案例实现, 增加基础结构和最终结果
概念: React由Meta公司研发,是一个用于 构建Web和原生交互界面的库
优势: 1-组件化的开发方式 2-优秀的性能 3-丰富的生态 4-跨平台开发
使用create-react-app快速搭建开发环境
create-react-app是一个快速 创建React开发环境的工具,底层由Webpack构建,封装了配置细节,开箱即用
执行命令:
npx create-react-app react-basic
之后切入到文件夹下, 使用 npm start
启动项目
创建React项目的更多方式: https://zh-hans.react.dev/learn/start-a-new-react-project
工作方式:
理解:
index.js | 项目入口 |
---|---|
App.js | 项目的根组件 |
index.html | html文件 |
项目的根组件以React组件的方式渲染到index.html中
Code:
index.js 简化后的内容
// 项目的入口 从这里开始
// React必要的两个核心包
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 把App根组件渲染到id为root的dom节点上
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
**概念:**JSX是JavaScript和XML(HTML)的缩写,表示在JS代码中编写HTML模版结构,它是React中编写UI模版的方式
优势:
JSX并不是标准的JS语法,它是JS的语法扩展,浏览器本身不能识别,需要通过解析工具做解析之后才能在浏览器中运行
在JSX中可以通过 大括号语法{} 识别 JavaScript中的表达式,比如常见的变量、函数调用、方法调用等等
注意:if语句、switch语句、变量声明属于语句,不是表达式,不能出现在{}中
案例:
Code:
// 项目的根组件
// App -> index.js -> public/index.html(root)
const count = 100
function getName() {
return 'jack'
}
function App() {
return (
this is App
{/* 使用引号传递字符串 */}
{'this is message'}
{/* 识别js变量 */}
{count}
{/* 函数调用 */}
{getName()}
{/* 方法调用 */}
{new Date().getDate()}
{/* 使用js对象 */}
{ color: 'red'}}>this is div
)
}
export default App
效果:
**语法:**在JSX中可以使用原生JS中的map方法遍历渲染列表
案例:
const list = [
{ id: 1001, name: 'Vue'},
{ id: 1002, name: 'React'},
{ id: 1003, name: 'Angular'}
]
function App() {
return (
this is App
{/* 渲染列表 */}
{/* { list.map(item => - Vue
) } */}
{ list.map(item => - { item.name }
) }
)
}
export default App
效果:
要点:
1-key值绑定
作用: 提升性能
方式: 每一项要加上一个独一无二的key值
**语法:**在React中,可以通过逻辑与运算符&&、三元表达式(?:)实现基础的条件渲染
案例:
const isLogin = true
function App() {
return (
{/* 逻辑与 && */}
{ isLogin && this is span}
{/* 三目运算符 */}
{ isLogin ? this is span : this is div}
{/* 逻辑或 || */}
{ isLogin || this is span}
)
}
export default App
效果:
this is spanthis is span
需求:列表中需要根据文章状态适配三种情况,单图,三图,和无图三种模式
解决方案:自定义函数 + if判断语句
案例:
// 定义文章类型
const articleType = 3 // 0 1 3
// 定义核心函数(根据文章类型返回不同的JSX模版)
function getArticleTem () {
if (articleType === 0) {
return 我是无图文章
} else if (articleType === 1) {
return 我是单图模式
} else {
return 我是三图模式
}
}
function App () {
return (
{/* 调用函数渲染不同的模版 */}
{getArticleTem()}
)
}
export default App
效果:
我是三图模式
**语法:**on + 事件名称 = { 事件处理程序 },整体上遵循驼峰命名法
Code:
function App() {
const handleClick = () => {
console.log('button被点击了');
}
return (
)
}
export default App
Res:
使用事件对象参数
语法:在事件回调函数中设置形参e
Code:
function App() {
const handleClick = (e) => {
console.log('button被点击了', e);
}
return (
)
}
export default App
Res:
传递自定义参数
语法:事件绑定的位置改成箭头函数的写法,在执行clickHandler实际处理业务函数的时候传递实参
Code:
function App() {
const handleClick = (name) => {
console.log('button被点击了', name);
}
return (
)
}
export default App
Res:
注意:不能直接写函数调用,这里事件绑定需要一个函数引用
同时传递事件对象和自定义参数
语法:在事件绑定的位置传递事件实参e和自定义参数,clickHandler中声明形参,注意顺序对应
Code:
function App() {
const handleClick = (name, e) => {
console.log('button被点击了', name, e);
}
return (
)
}
export default App
Res:
概念:一个组件就是用户界面的一部分,它可以有自己的逻辑和外观,组件之间可以互相嵌套,也可以复用多次
组件化开发可以让开发者像搭积木一样构建一个完整的庞大的应用
在React中,一个组件就是首字母大写的函数,内部存放了组件的逻辑和视图UI, 渲染组件只需要把组件当成标签书写即可
Code:
function Button() {
return
}
function App() {
return (
{/* 自闭合 */}
{/* 成对标签 */}
)
}
export default App
Res:
概念:
useState 是一个 React Hook(函数),它允许我们向组件添加一个状态变量, 从而控制影响组件的渲染结果
本质:和普通变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)
const [count, setCount] = useState(0)
1- useState是一个函数, 返回值是一个数组
2-数组中的第一个参数是状态变量, 第二个参数是set函数用来修改状态变量
3-useState的参数将作为count的初始值
Case:
func: 点击按钮, 数值不断增加
Code:
import { useState } from "react"
function App() {
// 1. 调用 useState 添加一个状态变量
// count 状态变量
// setCount 修改状态变量的方法
const [count, setCount] = useState(0)
// 2. 点击事件
const handleClick = () => {
setCount(count + 1)
}
return (
)
}
export default App
Res:
在React中,状态被认为是只读的,我们应该始终替换它而不是修改它,直接修改状态不能引发视图更新
Case:
Code:
import { useState } from "react"
function App() {
// 1. 调用 useState 添加一个状态变量
// count 状态变量
// setCount 修改状态变量的方法
const [count, setCount] = useState(0)
// 2. 点击事件
const handleClick = () => {
setCount(count + 1)
}
return (
)
}
export default App
规则:对于对象类型的状态变量,应该始终传给set方法一个全新的对象来进行修改
直接修改原对象,不引发视图变化
调用set传入新对象用于修改
理解: 这里先用展开运算符做个拷贝, 然后再用后面重复属性进行替换即可.
Case:
func: 点击按变换名字
Code:
import { useState } from "react"
function App() {
const [form, setForm] = useState({ name: 'jack' })
const handleClick = () => {
setForm({
...form,
name: 'rose'
})
}
return (
)
}
export default App
Res:
复习知识点:
1-…展开匀速符: 深浅拷贝问题
展开运算符使用的对象如果只是针对简单的一级基础数据,就是深拷贝;
展开运算符使用的对象内容包含二级或更多的复杂的数据,那就是浅拷贝;
React组件基础的样式控制有俩种方式
Case:
Code:
index.js
import './css/index.css'
const style = {
color: 'red',
fontSize: '20px'
}
function App() {
return (
{/* 行内样式控制 */}
{ color: 'red'}}>this is span
{/* 内部样式控制 */}
this is span
{/* 外部样式控制 */}
this is span
)
}
export default App
index.css
.outer {
background-color: blue;
}
项目基础结构:
Code: App.js
import './App.scss'
import avatar from './images/bozai.png'
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
return (
{/* 导航 Tab */}
-
评论
{/* 评论数量 */}
{10}
-
{/* 发表评论 */}
{/* 当前用户头像 */}
{/* 评论框 */}
{/* 发布按钮 */}
{/* 评论列表 */}
{/* 评论项 */}
)
}
export default App
注: 里面代码需要微调一下, 具体可以结合视频里的代码
核心思路:
key Code:
const [commentList, setCommentList] = useState([])
{/* 评论项 */}
{commentList.map(item => )}
Code: App.js
import { useState } from 'react'
import './App.scss'
import avatar from './images/bozai.png'
// 评论列表数据
const list = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '周杰伦'
},
// 评论内容
content: '哎呦, 不错哦',
// 评论事件
ctime: '10-18 08:15',
like: 88,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
// 渲染评论列表
// 1. 使用 useState 维护list
const App = () => {
const [commetList, setCommetList] = useState(list)
return (
{/* 导航 Tab */}
-
评论
{/* 评论数量 */}
{10}
-
{/* 发表评论 */}
{/* 当前用户头像 */}
{/* 评论框 */}
{/* 发布按钮 */}
{/* 评论列表 */}
{/* 评论项 */}
{commetList.map(item => (
{/* 头像 */}
{/* 用户名 */}
{item.user.uname}
{/* 评论内容 */}
{item.content}
{/* 评论时间 */}
{item.ctime}
{/* 评论数量 */}
点赞数:{item.like}
{/* 条件:user.id === item.user.id */}
删除
))}
)
}
export default App
需求:
核心思路
handleDel实现列表过滤
// 删除功能
const handleDel = (id) => {
console.log(id)
// 对commentList做过滤处理
setCommentList(commentList.filter(item => item.rpid !== id))
}
匹配删除 onDel
{/* 条件:user.id === item.user.id */}
{ user.uid === item.user.uid && (
handleDel(item.rpid)}>
删除
)}
Code: App.js
import { useState } from 'react'
import './App.scss'
import avatar from './images/bozai.png'
// 评论列表数据
const list = [
{
// 评论id
rpid: 1,
// 用户信息
user: {
uid: '13258165',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '周杰伦'
},
// 评论内容
content: '哎呦, 不错哦',
// 评论事件
ctime: '10-18 08:15',
like: 88,
},
{
// 评论id
rpid: 2,
// 用户信息
user: {
uid: '12258164',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '许嵩'
},
// 评论内容
content: '我寻你千百度',
// 评论事件
ctime: '10-18 10:15',
like: 88,
},
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '30009257',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '黑马 '
},
// 评论内容
content: '来黑马!',
// 评论事件
ctime: '10-19 09:15',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
// 渲染评论列表
// 1. 使用 useState 维护list
const [commetList, setCommetList] = useState(list)
// 2. 删除评论
const handleDel = (id) => {
console.log(id);
// 对commetList做过滤处理
setCommetList(commetList.filter(item => item.rpid !== id))
}
return (
{/* 导航 Tab */}
-
评论
{/* 评论数量 */}
{10}
-
{/* 发表评论 */}
{/* 当前用户头像 */}
{/* 评论框 */}
{/* 发布按钮 */}
{/* 评论列表 */}
{/* 评论项 */}
{commetList.map(item => (
{/* 头像 */}
{/* 用户名 */}
{item.user.uname}
{/* 评论内容 */}
{item.content}
{/* 评论时间 */}
{item.ctime}
{/* 评论数量 */}
点赞数:{item.like}
{/* 条件:user.id === item.user.id */}
{ user.uid === item.user.uid && (
handleDel(item.rpid)}>
删除
)}
))}
)
}
export default App
需求:点击哪个tab项,哪个做高亮处理
核心思路:
点击谁就把谁的type(独一无二的标识)记录下来,然后和遍历时的每一项的type做匹配,谁匹配到就设置负责高亮的类名
Code:
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
...
// tab切换功能
// 1. 点击谁就把谁的type记录下来
// 2. 通过记录的type和每一项遍历时的type做匹配 控制激活类名的显示
const [type, setType] = useState('hot')
const handleTabChange = (type) => {
console.log(type)
setType(type)
// 基于列表的排序
if (type === 'hot') {
// 根据点赞数量排序
// lodash
setCommentList(_.orderBy(commentList, 'like', 'desc'))
} else {
// 根据创建时间排序
setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
}
}
...
-
{/* 高亮类名: active */}
{tabs.map(item =>
handleTabChange(item.type)}
className={classNames('nav-item', { active: type === item.type })}>
{item.text}
)}
关键点:
复杂样式的写法
className={classNames('nav-item', {active: type === item.type})}
Code:
import { useState } from 'react'
import './App.scss'
import avatar from './images/bozai.png'
import classNames from 'classnames'
// 评论列表数据
const list = [
{
// 评论id
rpid: 1,
// 用户信息
user: {
uid: '13258165',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '周杰伦'
},
// 评论内容
content: '哎呦, 不错哦',
// 评论事件
ctime: '10-18 08:15',
like: 88,
},
{
// 评论id
rpid: 2,
// 用户信息
user: {
uid: '12258164',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '许嵩'
},
// 评论内容
content: '我寻你千百度',
// 评论事件
ctime: '10-18 10:15',
like: 88,
},
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '30009257',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '黑马 '
},
// 评论内容
content: '来黑马!',
// 评论事件
ctime: '10-19 09:15',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
// 渲染评论列表
// 1. 使用 useState 维护list
const [commetList, setCommetList] = useState(list)
// 2. 删除评论
const handleDel = (id) => {
console.log(id);
// 对commetList做过滤处理
setCommetList(commetList.filter(item => item.rpid !== id))
}
// tab切换功能
// 1. 点击谁就把谁的type记录下来
// 2. 通过记录的type值去过滤评论列表 控制激活类名的显示
const [type, setType] = useState('hot')
const handleTabChange = (type) => {
// console.log(type);
setType(type)
}
return (
{/* 导航 Tab */}
-
评论
{/* 评论数量 */}
{10}
-
{/* 高亮类名: active */}
{tabs.map(item =>
handleTabChange(item.type)}
// Plan1:
className={classNames('nav-item', {active: type === item.type})}
// Plan2:
// className={`nav-item ${type === item.type && 'active'}`}
>{item.text}
)}
{/* 发表评论 */}
{/* 当前用户头像 */}
{/* 评论框 */}
{/* 发布按钮 */}
{/* 评论列表 */}
{/* 评论项 */}
{commetList.map(item => (
{/* 头像 */}
{/* 用户名 */}
{item.user.uname}
{/* 评论内容 */}
{item.content}
{/* 评论时间 */}
{item.ctime}
{/* 评论数量 */}
点赞数:{item.like}
{/* 条件:user.id === item.user.id */}
{ user.uid === item.user.uid && (
handleDel(item.rpid)}>
删除
)}
))}
)
}
export default App
需求:
点击最新,评论列表按照创建时间倒序排列(新的在前),点击最热按照点赞数排序(多的在前)
**核心思路:**把评论列表状态数据进行不同的排序处理,当成新值传给set函数重新渲染视图UI
Code:
method: 采用 lodash 来进行排序处理
// 基于列表的排序
if (type === 'hot') {
// 根据点赞数量排序
// lodash
setCommentList(_.orderBy(commentList, 'like', 'desc'))
} else {
// 根据创建时间排序
setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
}
拓展:
如果你想要一开始就是最热的模式, 那么在useState的时候就可以设定
const [commetList, setCommetList] = useState(_.orderBy(list, 'like', 'desc'))
Code:
import { useState } from 'react'
import './App.scss'
import avatar from './images/bozai.png'
import classNames from 'classnames'
import _ from 'lodash'
// 评论列表数据
const list = [
{
// 评论id
rpid: 1,
// 用户信息
user: {
uid: '13258165',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '周杰伦'
},
// 评论内容
content: '哎呦, 不错哦',
// 评论事件
ctime: '10-18 08:15',
like: 99,
},
{
// 评论id
rpid: 2,
// 用户信息
user: {
uid: '12258164',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '许嵩'
},
// 评论内容
content: '我寻你千百度',
// 评论事件
ctime: '10-18 10:15',
like: 55,
},
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '30009257',
avatar: 'http://toutiao.itheima.net/resources/images/98.jpg',
uname: '黑马 '
},
// 评论内容
content: '来黑马!',
// 评论事件
ctime: '10-19 09:15',
like: 66,
},
]
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '黑马前端',
}
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
const App = () => {
// 渲染评论列表
// 1. 使用 useState 维护list
const [commetList, setCommetList] = useState(_.orderBy(list, 'like', 'desc'))
// 2. 删除评论
const handleDel = (id) => {
console.log(id);
// 对commetList做过滤处理
setCommetList(commetList.filter(item => item.rpid !== id))
}
// tab切换功能
// 1. 点击谁就把谁的type记录下来
// 2. 通过记录的type值去过滤评论列表 控制激活类名的显示
const [type, setType] = useState('hot')
const handleTabChange = (type) => {
console.log(type);
setType(type)
// 基于列表的排序
if(type === 'hot') {
// 根据点赞数排序
setCommetList(_.orderBy(commetList, 'like', 'desc'))
}else {
// 根据创建时间排序
setCommetList(_.orderBy(commetList, 'ctime', 'desc'))
}
}
return (
{/* 导航 Tab */}
-
评论
{/* 评论数量 */}
{10}
-
{/* 高亮类名: active */}
{tabs.map(item =>
handleTabChange(item.type)}
// Plan1:
className={classNames('nav-item', {active: type === item.type})}
// Plan2:
// className={`nav-item ${type === item.type && 'active'}`}
>{item.text}
)}
{/* 发表评论 */}
{/* 当前用户头像 */}
{/* 评论框 */}
{/* 发布按钮 */}
{/* 评论列表 */}
{/* 评论项 */}
{commetList.map(item => (
{/* 头像 */}
{/* 用户名 */}
{item.user.uname}
{/* 评论内容 */}
{item.content}
{/* 评论时间 */}
{item.ctime}
{/* 评论数量 */}
点赞数:{item.like}
{/* 条件:user.id === item.user.id */}
{ user.uid === item.user.uid && (
handleDel(item.rpid)}>
删除
)}
))}
)
}
export default App
参考: https://lodash.com/docs/4.17.15#orderBy
classnames是一个简单的JS库,可以非常方便的通过条件动态控制class类名的显示
现在的问题:字符串的拼接方式不够直观,也容易出错
参考:
classnames: https://github.com/JedWatson/classnames
React: