【React自学笔记02】React必学~工欲善其事必先利其器
三、React应用
3.1. 创建react应用
3.1.1. react脚手架
-
xxx脚手架: 用来帮助程序员快速创建一个基于xxx库的模板项目
创建基于React脚手架的模板项目-简称创建脚手架
-
包含了所有需要的配置(语法检查、jsx编译、devServer…)
-
下载好了所有相关的依赖
-
可以直接运行一个简单效果
-
react提供了一个用于创建react项目的脚手架库: create-react-app
-
项目的整体技术架构为: react + webpack + es6 + eslint
-
使用脚手架开发的项目的特点: 模块化, 组件化, 工程化
3.1.2. 创建项目并启动
第一步,全局安装:npm i -g create-react-app
第二步,切换到想创项目的目录,使用命令:create-react-app hello-react
第三步,进入项目文件夹:cd hello-react
第四步,启动项目:npm start
3.1.3. react脚手架项目结构
🚩public ---- 静态资源文件夹
- favicon.icon ------ 网站页签图标 %PUBLIC_URL%代表public文件夹的路径
- index.html -------- 主页面
- logo192.png ------- logo图
- logo512.png ------- logo图
- manifest.json ----- 应用加壳的配置文件
- robots.txt -------- 爬虫协议文件
🚩 src ---- 源码文件夹
- App.css -------- App组件的样式
- App.js --------- App组件
- App.test.js ---- 用于给App做测试
- index.css ------ 样式
- index.js ------- 入口文件
- logo.svg ------- logo图
- reportWebVitals.js --- 页面性能分析文件(需要web-vitals库的支持)
- setupTests.js ---- 组件单元测试的文件(需要jest-dom库的支持)
补充:样式模块化,防止引入的样式产生冲突
- 将css样式文件改名为XXX.module.css
- 修改引入样式文件 import hello from './index.module.css'
- 添加样式
3.1.4. 功能界面的组件化编码流程
-
拆分组件: 拆分界面,抽取组件
-
实现静态组件: 使用组件实现静态页面效果
-
实现动态组件
- 动态显示初始化数据
- 数据类型
- 数据名称
- 保存在哪个组件?
- 交互(从绑定事件监听开始)
3.2. 组件的组合使用
案例TodoList
功能: 组件化实现此功能
-
显示所有todo列表
-
输入文本,点击按钮显示到列表的首位,并清除输入的文本
💕 总结:
-
拆分组件、实现静态组件,注意:className、style的写法
-
动态初始化列表,如何确定将数据放在那个组件的state中
——某个组件中使用:放在自身的state中
——某些组件使用:放在 他们共同的父组件state中(官方称此操作为:状态提升)
-
关于父子之间通信:
——父传子:标签属性props
——子传父:父组件写好方法,通过标签属性传给子组件(父提前给子传递一个函数),子组件通过this.props.方法名调用该方法
-
注意defaultChecked和checked的区别:
——defaultChecked只能指定一次,之后再改变勾选状态不起效
——checked要配合onChange使用
-
状态在哪里,操作状态的方法就在哪里
-
勾选、取消勾选修改item的状态
-
如果是input输入框 --event.target.value
-
如果是input多选框 --event.target.checked
-
🌀 代码片段
//App.js
import React, { Component } from 'react'
import Header from './components/Header'
import List from "./components/List";
import Footer from './components/Footer'
import './index.css'
export default class App extends Component {
state = {
todos: [{
id: '001', content: '吃饭', ischecked: false
}, {
id: '002', content: '睡觉', ischecked: true
}, {
id: '003', content: '写代码', ischecked: true
}]
}
addTodo = (todoObj) => {
if (todoObj.content.trim() === '') return
const newTodos = [todoObj, ...this.state.todos]
this.setState({ todos: newTodos })
}
delTodo = (id) => {
let { todos } = this.state
const newTodos = todos.filter((todo) => {
return todo.id !== id
})
this.setState({ todos: newTodos })
}
changeTodo = (id, ischecked) => {
let { todos } = this.state
todos.forEach((todo) => {
if (id === todo.id) {
todo.ischecked = !ischecked
}
})
console.log(todos);
this.setState({ todos: todos })
}
updateCheck = (ischecked) => {
let { todos } = this.state
todos.forEach((todo) => {
if (todo.ischecked !== ischecked) {
todo.ischecked = !todo.ischecked
}
})
this.setState({ todos: todos })
}
delAllTodos = () => {
let { todos } = this.state
let newTodos = todos.filter((todo) => {
return !todo.ischecked
})
this.setState({ todos: newTodos })
}
render() {
const { todos } = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo} />
<List todos={todos} delTodo={this.delTodo} changeTodo={this.changeTodo} />
<Footer todos={todos} updateCheck={this.updateCheck} delAllTodos={this.delAllTodos} />
</div>
</div>
)
}
}
//Header.jsx
import React, { Component } from 'react'
import { nanoid } from 'nanoid'
import PropTypes from 'prop-types'
import './index.css'
export default class Header extends Component {
static propTypes = {
addTodo: PropTypes.func.isRequired
}
handleKeyup = (event) => {
if ((event.keyCode || event.which) !== 13)
return;
const todoObj = { id: nanoid(), content: event.target.value, ischecked: false }
this.props.addTodo(todoObj)
event.target.value = ''
}
render() {
return (
<div>
<div className="todo-header">
<input onKeyUp={this.handleKeyup} type="text" placeholder="请输入你的任务名称,按回车键确认" />
</div>
</div>
)
}
}
//List.js
import React, { Component } from 'react'
import Item from '../Item'
import './index.css'
export default class List extends Component {
render() {
const { todos, delTodo, changeTodo } = this.props
return (
<ul className="todo-main">
{
todos.map((todo) => {
return <Item key={todo.id} todo={todo} delTodo={delTodo} changeTodo={changeTodo} />
})
}
</ul>
)
}
}
//Item.jsx
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
state = { mouse: false }
handleMouse = () => {
const { mouse } = this.state
this.setState({ mouse: !mouse })
}
handleDel = (id) => {
return () => {
if (window.confirm('确定删除吗?')) {
this.props.delTodo(id)
}
}
}
handleChecked = (id, ischecked) => {
return () => {
this.props.changeTodo(id, ischecked)
}
}
render() {
const { todo } = this.props
let { mouse } = this.state
return (
<li style={{ backgroundColor: mouse ? '#ddd' : 'white' }} onMouseEnter={this.handleMouse} onMouseLeave={this.handleMouse}>
<label>
<input type="checkbox" checked={todo.ischecked} onChange={this.handleChecked(todo.id, todo.ischecked)} />
<span>{todo.content}</span>
</label>
<button onClick={this.handleDel(todo.id)} className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }}>删除</button>
</li>
)
}
}
//Footer.jsx
import React, { Component } from 'react'
import './index.css'
export default class Footer extends Component {
handleChecked = (event) => {
this.props.updateCheck(event.target.checked)
}
handleDel = () => {
if (window.confirm('确定删除吗?')) {
this.props.delAllTodos()
}
}
render() {
const { todos } = this.props
let countNum = 0;
todos.forEach((todo) => {
if (todo.ischecked) countNum++
})
return (
<div className="todo-footer">
<label>
<input type="checkbox" checked={(countNum === todos.length) && todos.length !== 0 ? true : false} onChange={this.handleChecked} />
</label>
<span>
<span>已完成{countNum}</span> / 全部{todos.length}
</span>
<button className="btn btn-danger" onClick={this.handleDel}>清除已完成任务</button>
</div>
)
}
}
四、React Ajax
4.1 理解
4.1.1 前置说明
-
React本身只关注于界面, 并不包含发送ajax请求的代码
-
前端应用需要通过ajax请求与后台进行交互(json数据)
-
react应用中需要集成第三方ajax库(或自己封装)
4.1.2 常用的ajax请求库
-
jQuery: 比较重, 如果需要另外引入不建议使用
-
axios: 轻量级, 建议使用
(1)封装XmlHttpRequest对象的ajax
(2)promise风格
(3)可以用在浏览器端和node服务器端
——————————————记录于2022.05.19——————————————
4.2 react脚手架配置代理
跨域问题产生的本质:Ajax引擎拦截响应
代理没有Ajax引擎,不受同源策略的限制,代理通过请求转发的形式接收数据
方法一
在package.json中同级追加如下配置,重启脚手架
"proxy":"http://localhost:5000"
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀。
- 缺点:适合配置一个代理,不能配置多个代理。
- 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)
方法二
-
第一步:创建代理配置文件,react脚手架自动找
在src下创建配置文件:src/setupProxy.js
-
编写setupProxy.js配置具体代理规则:
const proxy = require('http-proxy-middleware') module.exports = function(app) { app.use( proxy('/api1', { //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000) target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址) changeOrigin: true, //控制服务器接收到的请求头中host字段的值 /* changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000 changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000 changeOrigin默认值为false,但我们一般将changeOrigin值设为true */ pathRewrite: {'^/api1': ''} //重写请求路径,去除请求前缀,将请求路径中的/api1替换为'',保证交给后台服务器的是正常请求地址(必须配置) }), proxy('/api2', { target: 'http://localhost:5001', changeOrigin: true, pathRewrite: {'^/api2': ''} }) ) }
若使用新版本http-proxy-middleware最新写法:
const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { app.use( createProxyMiddleware('/api1', { target: 'http://localhost:5000', changeOrigin: true, pathRewrite: { '^/api1': '' } }) ) };
-
前端请求数据时
//如果3000没有要请求的数据,那么就找5000下的 axios.get('http://localhost:3000/api1/students').then( response=>{console.log('成功了',response.data);}, error=>{console.log('失败了',error);} )
说明:
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
- 缺点:配置繁琐,前端请求资源时必须加前缀。
开启后端服务器
const express = require('express')
const app = express()
app.use((request,response,next)=>{
console.log('有人请求服务器1了');
console.log('请求来自于',request.get('Host'));
console.log('请求的地址',request.url);
next()
})
app.get('/students',(request,response)=>{
const students = [
{id:'001',name:'tom',age:18},
{id:'002',name:'jerry',age:19},
{id:'003',name:'tony',age:120},
]
response.send(students)
})
app.listen(5000,(err)=>{
if(!err) console.log('服务器1启动成功了,请求学生信息地址为:http://localhost:5000/students');
})
4.3 案例-GitHub搜索栏
该工程分为Search组件和LIst组件:
- search组件负责对搜索信息进行搜索 ,通过axios发起get请求,由于跨域限制,需要配置代理。将请求返回的数据传给父组件App
- App父组件收到Search发来的数据后保存到state中
- App组件再将数据通过props传送给List组件,在List组件中展示数据
//1.数据请求阶段,请求成功发送给父组件
import React, { Component } from 'react'
import axios from 'axios'
export default class Search extends Component {
search = () => {
const { keywordEle: { value: keyword } } = this;
axios.get(`http://localhost:5000/search/users2/?q=${keyword}`).then(
response => { this.props.saveUsers(response.data.items) },
error => { console.log('失败了', error); }
)
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">Search Github Users</h3>
<div>
<input ref={cNode => this.keywordEle = cNode} type="text" placeholder="enter the name you search" />
<button onClick={this.search}>Search</button>
</div>
</section>
)
}
}
💕 问题:
-
站在3000访问5000的数据产生跨域问题,需要设置代理
-
使用第二种方式配置代理后,重启脚手架
search = () => { const { keywordEle: { value: keyword } } = this; axios.get(`http://localhost:3000/api1/search/users2/?q=${keyword}`).then( response => { this.props.saveUsers(response.data.items) }, error => { console.log('失败了', error); } ) }
注意:代理也是属于3000的
//2.父组件接收并放到state中
import React, { Component } from 'react'
import List from './components/List'
import Search from './components/Search'
export default class App extends Component {
state = { users: [] }
saveUsers = (usersObj) => {
this.setState({ users: usersObj })
}
render() {
return (
<div className="container">
<Search saveUsers={this.saveUsers} />
<List users={this.state.users} />
</div>
)
}
}
//3.List组件接收数据并展示
import React, { Component } from 'react'
import './index.css'
export default class List extends Component {
render() {
const { users } = this.props
return (
<div className="row">
{
users.map((userObj) => {
return (
<div className="card" key={userObj.id}>
<a href={userObj.html_url} target="_blank" rel="noopener noreferrer">
<img src={userObj.avatar_url} style={{ width: '100px' }} alt="#" />
</a>
<p className="card-text">{userObj.login}</p>
</div>
)
})
}
</div>
)
}
}
——————————————记录于2022.05.20——————————————
4.4 任意组件通信
4.4.1 消息订阅-发布机制
-
工具库: PubSubJS
-
下载: npm install pubsub-js --save
-
使用:
(1) import PubSub from 'pubsub-js' //引入
(2)PubSub.subscribe('delete', function(data){ }); //订阅
(3)PubSub.publish('delete', data) //发布消息
4.4.2 修改gitHub案例
-
取消App组件充当接收传递数据的职责:把之前存放在App组件中的数据给最需要它的组件,放到该组件中;不在子组件中添加标签属性
//app.jsx export default class App extends Component { render() { return ( <div className="container"> <Search /> <List /> </div> ) } }
-
List最需要该数据,所以把state定义到List组件中
//List.app state = { users: [], isFirst: true, isLoading: false, err: '', }
-
Search组件不再把数据发送给App组件,而是发送给兄弟组件;
Search发送请求前通知List更新状态;
请求成功后通知LIst更新状态;
请求失败后通知List更新状态;
//Search.jsx
List要接收数据,先要订阅,在组件一放到页面时订阅所以放到钩子 函数里(初始化、订阅消息);
//List.jsx import PubSub from 'pubsub-js' componentDidMount(){ //只要发布delete消息,就调用后面的函数 PubSub.subscribe('delete', function (data) { }); }
完整代码如下:
//List.jsx
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import './index.css'
export default class List extends Component {
state = {
users: [],
isFirst: true,
isLoading: false,
err: '',
}
componentDidMount() {
PubSub.subscribe('updateState', (_, stateObj) => {
this.setState(stateObj)
});
}
render() {
const { users, isFirst, isLoading, err } = this.state
return (
<div className="row">
{
isFirst ? <h2>Welcome to use,please input and search</h2> :
isLoading ? <h2>Loading......</h2> :
err ? <h2 style={{ color: 'red' }}>{err}</h2> :
users.map((userObj) => {
return (
<div className="card" key={userObj.id}>
<a href={userObj.html_url} target="_blank" rel="noopener noreferrer">
<img src={userObj.avatar_url} style={{ width: '100px' }} alt="#" />
</a>
<p className="card-text">{userObj.login}</p>
</div>
)
})
}
</div>
)
}
}
//Search.jsx
import React, { Component } from 'react'
import axios from 'axios'
import PubSub from 'pubsub-js'
export default class Search extends Component {
search = () => {
const { keywordEle: { value: keyword } } = this;
PubSub.publish('updateState', { isFirst: false, isLoading: true })
axios.get(`http://localhost:3000/api1/search/users2/?q=${keyword}`).then(
response => {
PubSub.publish('updateState', { users: response.data.items, isLoading: false })
},
error => { PubSub.publish('updateState', { isLoading: false, err: error.message }) }
)
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">Search Github Users</h3>
<div>
<input ref={cNode => this.keywordEle = cNode} type="text" placeholder="enter the name you search" />
<button onClick={this.search}>Search</button>
</div>
</section>
)
}
}
4.5 fetch发送请求
发送Ajax请求
-
xhr
-
axios:第三方库,对xhr的封装
-
jquery
-
-
fetch:内置,不是第三方库,promise风格
第一步:确定是否与服务器建立连接
第二步:进行请求数据
fetch未优化写法:
fetch(`/api1/search/users2/?q=${keyword}`).then(
response=>{
console.log('联系服务器成功了');
return response.json()
}
).then(
response=>{console.log('获取数据成功了',response);}
).catch(
error=>{
console.log('请求出错',error);
}
)
fetch终极优化写法:
try {
const response=await fetch(`/api1/search/users2/?q=${keyword}`)
const data=await response.json()
PubSub.publish('updateState', { users: data.items, isLoading: false })
} catch(error) {
console.log('请求出错',error)
PubSub.publish('updateState', { isLoading: false, err: error.message })
}
💕 总结:GitHub搜索栏案例相关知识点
-
ES6解构赋值+重命名
let obj={a:{b:1}} const {a}=obj;//传统结构赋值 const {a:{b}}=obj;//连续解构赋值 const {a:{b:value}}=obj;//连续结构赋值+重命名
-
消息订阅与发布机制
- 先订阅再发布
- 适用于任意组件间通信
- 要在 组件的componentWillUnmount中取消订阅
-
fetch发送请求(关注分离的设计思想)
——————————————2022.05.21 本章笔记已完成——————————————