【第四章-react ajax】

第四章-react ajax

  • 一、理解
    • [1. 前置说明](#1. 前置说明)
    • [2. 常用的 ajax 请求库](#2. 常用的 ajax 请求库)
  • 二、axios
    • [1. axios 的安装和使用](#1. axios 的安装和使用)
      • [1.1 下载](#1.1 下载)
      • [1.2 引入](#1.2 引入)
      • [1.3 文档](#1.3 文档)
      • [1.4 相关 API](#1.4 相关 API)
        • [1.4.1 GET 请求](#1.4.1 GET 请求)
        • [1.4.2 POST 请求](#1.4.2 POST 请求)
      • [1.5 案例](#1.5 案例)
    • [2. react脚手架配置代理](#2. react脚手架配置代理)
      • [2.1 配置一个代理](#2.1 配置一个代理)
      • [2.2 配置多个代理](#2.2 配置多个代理)
    • [3. 案例 - github用户搜索](#3. 案例 - github用户搜索)
      • [3.1 拆分组件](#3.1 拆分组件)
      • [3.2 完成静态页面](#3.2 完成静态页面)
      • [3.2 axios发送请求](#3.2 axios发送请求)
      • [3.3 展示数据](#3.3 展示数据)
      • [3.4 细节和完善](#3.4 细节和完善)
      • [3.5 消息订阅与发布机制](#3.5 消息订阅与发布机制)
        • [3.5.1 工具库 PubSubJS](#3.5.1 工具库 PubSubJS)
        • [3.5.2 使用 PubSubJS 完成兄弟组件间的通信](#3.5.2 使用 PubSubJS 完成兄弟组件间的通信)
      • [3.6 fetch发送请求](#3.6 fetch发送请求)
        • [3.6.1 fetch](#3.6.1 fetch)
        • [3.6.2 结合案例使用 fetch 发送网络请求](#3.6.2 结合案例使用 fetch 发送网络请求)
      • [3.7 总结](#3.7 总结)

一、理解

1. 前置说明

  • React 本身只关注于界面,并不包含发送 ajax 请求的代码;
  • 前端应用需要通过 ajax 请求与后台进行交互(json 数据);
  • React 应用中需要集成第三方 ajax 库(或自己封装)

2. 常用的 ajax 请求库

  1. jQuery:比较重,如果需要另外引入不建议使用;
  2. axios :轻量级,建议使用;
    -- 封装XmlHttpRequest对象的 ajax;
    -- promise 风格;
    -- 可以用在浏览器端和 node 服务器端;

二、axios

1. axios 的安装和使用

1.1 下载

javascript 复制代码
npm i axios

1.2 引入

javascript 复制代码
import axios from 'axios'

1.3 文档

https://github.com/axios/axios

1.4 相关 API

1.4.1 GET 请求
javascript 复制代码
axios.get('/user?ID=12345').then(function (response) {
    console.log(response.data)
}).catch(function (error) {
    console.log(error);
})
1.4.2 POST 请求
javascript 复制代码
axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
}).then(function (response) {
    return response.data;
}).catch(function (error) {
    console.log(error);
})

1.5 案例

App.jsx:

javascript 复制代码
import React, {Component} from 'react';
import axios from 'axios'
import './App.css'

class App extends Component {
    render() {
        return (
            <div>
                <button onClick={this.getStudentData}>点击获取学生数据</button>
            </div>
        );
    }
    
	// 发送请求
    getStudentData = () => {
        axios.get('http://localhost:5000/students').then(res => {
            console.log(res.data)
        }, err => {
            console.log(err)
        })
    }
}

export default App;

我们使用node+express来搭建后台服务器1。

server1.js:

javascript 复制代码
const express = require("express")
const app = express()

app.use((request, response, next) => {
    console.log('有人请求服务器1了')
    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')
})

我们当前本地的环境是http://localhost:3000,但是我们要给http://localhost:5000发请求,由于跨域 我们的请求可以发送出去,但是同源策略 导致无法返回,此时我们需要在react脚手架里配置代理去解决。

2. react脚手架配置代理

2.1 配置一个代理

在package.json中追加如下配置:

javascript 复制代码
"proxy": "http://1ocalhost:5000"

再把请求地址中的5000端口调整为3000:

App.jsx

javascript 复制代码
	// 发送请求
    getStudentData = () => {
        axios.get('http://localhost:3000/students').then(res => {
            console.log(res.data)
        }, err => {
            console.log(err)
        })
    }
}

该请求就可以发送成功了!

总结:

  1. 优点:配置简单,前端请求资源时可以不加任何前缀;
  2. 缺点:不能配置多个代理;
  3. 工作方式:上述方式配置代理,当请求了3000存在的资源时,那么该请求不会转发给5000,则会直接使用3000存在的资源;当请求了3000不存在的资源时,那么该请求会转发给5000(优先匹配前端资源)。

2.2 配置多个代理

若此时除了server1.js,还有另一个服务器server2.js。

server2.js:

javascript 复制代码
const express = require("express")
const app = express()

app.use((request, response, next) => {
    console.log('有人请求服务器2了')
    next()
})

app.get('/cars', (request, response) => {
    const cars = [
        {id: '001', name: '奔驰', price: 199},
        {id: '002', name: '马自达', price: 109},
        {id: '003', name: '捷达', price: 120},
    ]
    response.send(cars)
})

app.listen(5001, (err) => {
    if (!err) console.log('服务器2启动成功了,请求汽车信息地址为:http://localhost:5001/cars')
})

App.jsx:

javascript 复制代码
import React, {Component} from 'react';
import axios from 'axios'
import './App.css'

class App extends Component {
    render() {
        return (
            <div>
                <button onClick={this.getStudentData}>点击获取学生数据</button>
                <button onClick={this.getCarData}>点击获取汽车数据</button>
            </div>
        );
    }

    getStudentData = () => {
        axios.get('http://localhost:5000/students').then(res => {
            console.log(res.data)
        }, err => {
            console.log(err)
        })
    }

    getCarData = () => {
        axios.get('http://localhost:5001/cars').then(res => {
            console.log(res.data)
        }, err => {
            console.log(err)
        })
    }
}

export default App;

server2.js请求的地址为http://localhost:5001/cars,若两台服务器同时存在,那如何配置多个代理呢?

  1. 第一步:创建代理配置文件

在src下创建配置文件:src/setupProxy.js

  1. 编写setupProxy.js配置具体代理规则:
javascript 复制代码
// 引入内置模块
const proxy = require('http-proxy-middleware')

module.exports = function (app){
    app.use(
        proxy('/api1', {  //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
            target: 'http://localhost:5000',  // 配置转发目标地址(能返回数据的服务器地址)
            changeOrigin: true,  // 控制服务器接收到的请求头中host字段的值
            /*
            * changedrigin设置为true时,服务器收到的请求头中的host为:1ocalhost:5000
            * changeorigin设置为fa1se时,服务器收到的请求头中的host为:1oca1host:3000
            * changeorigin设置为fa1se时,服务器收到的请求头中的host为:1oca1host:3000
            * */
            pathRewrite: {'^/api1':''}  // 去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
        }),
        proxy('/api2', {
            target: 'http://localhost:5001',
            changeOrigin: true,
            pathRewrite: {'^/api2':''}
        })
    )
}

App.jsx

javascript 复制代码
getStudentData = () => {
    axios.get('http://localhost:3000/api1/students').then(res => {
        console.log(res.data)
    }, err => {
        console.log(err)
    })
}

getCarData = () => {
    axios.get('http://localhost:3000/api2/cars').then(res => {
        console.log(res.data)
    }, err => {
        console.log(err)
    })
}

总结:

  1. 优点:可以配置多个代理,可以灵活的控制请求是否走代理;
  2. 缺点:配置繁琐,前端请求资源时必须添加前缀。

3. 案例 - github用户搜索

页面默认有一个搜索框和提示语,当用户输入关键字进行搜索后,下方会展示和关键词有关的 github 用户列表。


请求地址: https://api.github.com/search/users?q=xxxxxx

3.1 拆分组件

建立组件文件夹:

3.2 完成静态页面

  1. 我们使用Gemini来生成静态页面的文件;
javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Search Github Users</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        /* 微调卡片边框颜色以匹配原图 */
        .user-card {
            border: 1px solid #efefef;
        }
    </style>
</head>
<body class="bg-white text-gray-800">

<header class="bg-gray-100 py-12 px-4 mb-8">
    <div class="max-w-5xl mx-auto">
        <h1 class="text-3xl font-medium mb-4">Search Github Users</h1>
        <div class="flex gap-2">
            <input
                    type="text"
                    placeholder="enter the name you search"
                    class="border border-gray-300 px-3 py-1 w-64 focus:outline-none focus:ring-1 focus:ring-blue-500"
            >
            <button class="bg-gray-200 border border-gray-400 px-4 py-1 hover:bg-gray-300 transition-colors text-sm">
                Search
            </button>
        </div>
    </div>
</header>

<main class="max-w-6xl mx-auto px-4">
    <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">

        <div class="user-card flex flex-col items-center py-8">
            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                 class="w-24 h-24 mb-2 bg-black p-2">
            <p class="text-sm text-gray-600">reactjs</p>
        </div>

        <div class="user-card flex flex-col items-center py-8">
            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                 class="w-24 h-24 mb-2 bg-black p-2">
            <p class="text-sm text-gray-600">reactjs</p>
        </div>

        <div class="user-card flex flex-col items-center py-8">
            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                 class="w-24 h-24 mb-2 bg-black p-2">
            <p class="text-sm text-gray-600">reactjs</p>
        </div>

        <div class="user-card flex flex-col items-center py-8">
            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                 class="w-24 h-24 mb-2 bg-black p-2">
            <p class="text-sm text-gray-600">reactjs</p>
        </div>

        <div class="user-card flex flex-col items-center py-8">
            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                 class="w-24 h-24 mb-2 bg-black p-2">
            <p class="text-sm text-gray-600">reactjs</p>
        </div>

    </div>
</main>

</body>
</html>
  1. 将生成的静态页面代码复制到App.jsx中,将class替换成className,style改写成双花括号的格式style={``{}};

App.jsx:

javascript 复制代码
import React, {Component} from 'react';
import './App.css'

class App extends Component {
    render() {
        return (
            <div className="app bg-white text-gray-800">

                <header className="bg-gray-100 py-12 px-4 mb-8">
                    <div className="max-w-5xl mx-auto">
                        <h1 className="text-3xl font-medium mb-4">Search Github Users</h1>
                        <div className="flex gap-2">
                            <input
                                type="text"
                                placeholder="enter the name you search"
                                className="border border-gray-300 px-3 py-1 w-64 focus:outline-none focus:ring-1 focus:ring-blue-500"
                            />
                            <button
                                className="bg-gray-200 border border-gray-400 px-4 py-1 hover:bg-gray-300 transition-colors text-sm">
                                Search
                            </button>
                        </div>
                    </div>
                </header>

                <main className="max-w-6xl mx-auto px-4">
                    <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">

                        <div className="user-card flex flex-col items-center py-8">
                            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                                 className="w-24 h-24 mb-2 bg-black p-2"/>
                            <p className="text-sm text-gray-600">reactjs</p>
                        </div>

                        <div className="user-card flex flex-col items-center py-8">
                            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                                 className="w-24 h-24 mb-2 bg-black p-2"/>
                            <p className="text-sm text-gray-600">reactjs</p>
                        </div>

                        <div className="user-card flex flex-col items-center py-8">
                            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                                 className="w-24 h-24 mb-2 bg-black p-2"/>
                            <p className="text-sm text-gray-600">reactjs</p>
                        </div>

                        <div className="user-card flex flex-col items-center py-8">
                            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                                 className="w-24 h-24 mb-2 bg-black p-2"/>
                            <p className="text-sm text-gray-600">reactjs</p>
                        </div>

                        <div className="user-card flex flex-col items-center py-8">
                            <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                                 className="w-24 h-24 mb-2 bg-black p-2"/>
                            <p className="text-sm text-gray-600">reactjs</p>
                        </div>

                    </div>
                </main>

            </div>
        );
    }
}

export default App;

App.css:

javascript 复制代码
.user-card {
    border: 1px solid #efefef;
}
  1. 对于Tailwind CSS这种成型的第三方样式库,我们统一放在public文件夹下面,并在index.html中引入;

index.html:

javascript 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>github用户搜索</title>
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
    <!-- 引入Tailwind CSS -->
    <script src="%PUBLIC_URL%/tailwindcss.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
  1. 抽取Search组件;

Search/index.jsx:

javascript 复制代码
import React, {Component} from 'react';

class Search extends Component {
    render() {
        return (
            <header className="bg-gray-100 py-12 px-4 mb-8">
                <div className="max-w-5xl mx-auto">
                    <h1 className="text-3xl font-medium mb-4">Search Github Users</h1>
                    <div className="flex gap-2">
                        <input
                            type="text"
                            placeholder="enter the name you search"
                            className="border border-gray-300 px-3 py-1 w-64 focus:outline-none focus:ring-1 focus:ring-blue-500"
                        />
                        <button
                            className="bg-gray-200 border border-gray-400 px-4 py-1 hover:bg-gray-300 transition-colors text-sm">
                            Search
                        </button>
                    </div>
                </div>
            </header>
        );
    }
}

export default Search;
  1. 抽取 List 和 Item 组件;

List/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import Item from '../Item'
import './index.css'

class List extends Component {


    render() {
        return (
            <main className="max-w-6xl mx-auto px-4">
                <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
                    <Item/>
                </div>
            </main>
        );
    }
}

export default List;

List/index.css:

javascript 复制代码
.welcome {
    font-size: 30px;
    box-sizing: border-box;
}

Item/index.jsx:

javascript 复制代码
import React, {Component} from 'react';

class Item extends Component {
    render() {
        return (
            <div className="user-card flex flex-col items-center py-8">
                <img src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg" alt="reactjs"
                     className="w-24 h-24 mb-2 bg-black p-2"/>
                <p className="text-sm text-gray-600">reactjs</p>
            </div>
        );
    }
}

export default Item;

App.jsx:

javascript 复制代码
import React, {Component} from 'react';
import Search from './components/Search';
import List from './components/List';
import './App.css'

class App extends Component {
    render() {
        return (
            <div className="bg-white text-gray-800">
                <Search/>
                <List/>
            </div>
        );
    }
}

export default App;

页面效果:

3.2 axios发送请求

  1. 搜索按钮绑定点击事件;
javascript 复制代码
	<button
		className="bg-gray-200 border border-gray-400 px-4 py-1 hover:bg-gray-300 transition-colors text-sm"
		onClick={this.search}>
		Search
	</button>
	...
    // 搜索按钮绑定点击事件
    search = () => {
    
    }
}
  1. 使用ref获取用户的输入;
javascript 复制代码
	<input
		type="text"
		placeholder="enter the name you search"
		ref={c => this.keywordElement = c}
		className="border border-gray-300 px-3 py-1 w-64 focus:outline-none focus:ring-1 focus:ring-blue-500"
	/>
	<button
		className="bg-gray-200 border border-gray-400 px-4 py-1 hover:bg-gray-300 transition-colors text-sm"
		onClick={this.search}>
		Search
	</button>
	...
    // 搜索按钮绑定点击事件
    search = () => {
    	// 获取用户输入
    	const {value} = this.keywordElement;
        console.log(value);
    }
}

上面获取value还可以换一种写法------使用连续解构赋值。

javascript 复制代码
// 搜索按钮绑定点击事件
search = () => {
    const {keywordElement:{value}} = this;
    console.log(value)
}

如果你的value还是一个对象,你可以继续写下去。

javascript 复制代码
// 常规写法
const obj = {a: {b: {c: 1}}};
console.log('c', obj.a.b.c);  // 1
// 连续解构赋值的写法
let {a: {b: {c}}} = obj;
console.log('解构赋值c', c);  // 1

连续解构赋值的写法想必你已经学会了,我们再加深一下:连续结构赋值的同时,再对变量重命名。

javascript 复制代码
// 连续结构赋值 + 重命名
const obj2 = {a: {b: 2}};
let {a: {b: data}} = obj2;
console.log(data);  // 2

那我们按照上面的方式,使用【连续解构赋值+重命名】来获取一下输入框的内容。

javascript 复制代码
// 搜索按钮绑定点击事件
search = () => {
    // 获取用户的输入(连续解构赋值 + 重命名)
    const {keywordElement: {value: keyword}} = this;
    console.log(keyword);
}
  1. 发送网络请求;
javascript 复制代码
 // 引入axios
 import axios from 'axios';
 ...
// 搜索按钮绑定点击事件
search = () => {
	// 获取用户的输入(连续解构赋值 + 重命名)
	const {keywordElement: {value: keyword}} = this;
	// 发送网络请求
	axios.get(`https://api.github.com/search/users?q=${keyword}`).then(res => {
		console.log(res.data);
	}, err => {
		console.log(err);
	});
}

搜索atguigu:

返回数据:

  1. 配置proxy

src/setupProxy.js:

javascript 复制代码
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app){
    app.use(
        createProxyMiddleware('/search', {
            target: 'https://api.github.com',
            changeOrigin: true
        })
    )
}

修改请求路径:

javascript 复制代码
// 搜索按钮绑定点击事件
search = () => {
    // 获取用户的输入(连续解构赋值+重命名)
    const {keywordElement: {value: keyword}} = this;
    // 发送网络请求
    axios.get(`/search/users?q=${keyword}`).then(res => {
        console.log(res.data);
    },err => {
        console.log(err);
    });
}

3.3 展示数据

  1. Search组件将获取到的数据传给父组件App,然后再交给List做展示。

App.jsx:

javascript 复制代码
...
class App extends Component {
    // 初始化状态
    state = {
        // 用户数据
        users: []
    }
    
    render() {
    	const {users} = this.state;
        return (
            <div className="bg-white text-gray-800">
                <Search saveUsers={this.saveUsers}/>
                <List users={users}/>
            </div>
        );
    }
    
    // 更新用户数据
    saveUsers = (users) => {
        this.setState({users});
    }

}
...

Search/index.jsx:

javascript 复制代码
// 搜索按钮绑定点击事件
search = () => {
    // 获取用户的输入(连续解构赋值+重命名)
    const {keywordElement: {value: keyword}} = this;
    // 发送网络请求
    axios.get(`/search/users?q=${keyword}`).then(res => {
    		// 调用App.jsx的方法,更新users
        this.props.saveUsers(res.data.items);
    },err => {
        console.log(err);
    });
}
  1. List组件接收数据,然后遍历users展示Item。

List/index.jsx:

javascript 复制代码
class List extends Component {
    render() {
        return (
            <main className="max-w-6xl mx-auto px-4">
                <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
                    {
                        this.props.users.map((user) => {
                            return <Item user={user} key={user.id}/>
                        })
                    }
                </div>
            </main>
        );
    }
}

我们需要的 3 个用户对象中的属性:

  • avatar_url:头像地址;
  • login:用户名;
  • html_url:个人主页地址;

Item/index.jsx:

javascript 复制代码
import React, {Component} from 'react';

class Item extends Component {
    render() {
        const {user} = this.props;
        return (
            <div className="user-card flex flex-col items-center py-8" onClick={this.jumpHtml}>
                <img src={user.avatar_url} alt={user.login}
                     className="w-24 h-24 mb-2 bg-black p-2"/>
                <p className="text-sm text-gray-600">{user.login}</p>
            </div>
        );
    }
    // 跳转到个人主页
    jumpHtml = () => {
        window.open(this.props.user.html_url);
    }
}

export default Item;

页面效果:

3.4 细节和完善

这个案例的基本功能已经完成,我们再来完善一些细节:

  • 初次进入页面,下方应该显示欢迎词Welcome to enter name to search!
  • 搜索和返回数据的空档期,应当展示loading状态;
  • 请求出错时,下方应该提示用户错误信息;
  1. 我们想让List组件中有 4 种不同的展示行为(用户列表,欢迎词,加载中,错误信息),需要由 4 种不同的状态去控制,我们先在App组件中声明这些状态。

App.jsx:

javascript 复制代码
  // 初始化状态
  state = {
    // 用户数据
    users: [],
    // 是否为第一次打开页面
    isFirst: true,
    // 标识是否处于加载中
    isLoading: false,
    // 存储请求相关的错误信息
    err: null,
  };
  1. 按照我们之前的方式,还需要在App.jsx中声明 4 个函数(saveUsers,changeIsFirst,changeIsLoading和saveErr)用来更新对应的状态值,但是那样太麻烦了...这次我们换一种更加通用和简洁的写法,只声明一个统一管理状态 的方法:updateAppState。

App.jsx:

javascript 复制代码
  // 更新App的state
  updateAppState = (stateObj) => {
    this.setState(stateObj);
  };
  1. 我们将该方法传给Search组件,用于Search组件进行搜索后来通知App更新状态。

App.jsx:

javascript 复制代码
...
class App extends Component {
  // 初始化状态
  state = {
    // 用户数据
    users: [],
    // 是否为第一次打开页面
    isFirst: true,
    // 标识是否处于加载中
    isLoading: false,
    // 存储请求相关的错误信息
    err: null,
  };
  render() {
    const { users } = this.state;
    return (
      <div className="bg-white text-gray-800">
        <Search updateAppState={this.updateAppState} />
        <List/>
      </div>
    );
  }
  // 更新App的state
  updateAppState = (stateObj) => {
    this.setState(stateObj);
  };
}

export default App;

Search/index.jsx:

javascript 复制代码
  // 搜索按钮绑定点击事件
  search = () => {
    // 发送请求前通知 App 更新状态
    this.props.updateAppState({ isFirst: false, isLoading: true });
    // 获取用户的输入(连续解构赋值+重命名)
    const {keywordElement: { value: keyword }} = this;
    // 发送网络请求
    axios.get(`/search/users?q=${keyword}`).then(
      (res) => {
        // 请求成功后通知 App 更新状态
        this.props.updateAppState({ isLoading: false, users: res.data.items });
      },
      (err) => {
        // 请求失败后通知 App 更新状态
        this.props.updateAppState({ isLoading: false, err: err.message });
      },
    );
  };
}
  1. App组件接收到新的状态对象后,将状态对象传递给List组件;

App.jsx:

javascript 复制代码
  render() {
    const { users } = this.state;
    return (
      <div className="bg-white text-gray-800">
        <Search updateAppState={this.updateAppState} />
        <List users={users} {...this.state}/>
      </div>
    );
  }
  1. List组件根据状态值来做不同的展示;

这里涉及到条件判断的逻辑,我们使用三元表达式 来代替以往的if...else。

List/index.jsx:

javascript 复制代码
...
class List extends Component {
  render() {
    const { users, isFirst, isLoading, err } = this.props;
    return (
      <main className="max-w-6xl mx-auto px-4">
        {isFirst ? (
          <h2 style={{ fontSize: "24px" }}>Welcome to enter name to search!</h2>
        ) : isLoading ? (
          <h2>loading...</h2>
        ) : err ? (
          <h2 style={{ color: "red" }}>{err}</h2>
        ) : (
          <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
            {users.map((user) => {
              return <Item user={user} key={user.id} />;
            })}
          </div>
        )}
      </main>
    );
  }
}
...
  1. 页面效果;

初次进入页面,展示欢迎词的效果:

点击搜索按钮,显示加载中的效果:

将请求地址随便改一下,比如users234,模拟一下请求出错的效果:

3.5 消息订阅与发布机制

我们把state放在App组件中,是因为目前无法实现Search组件和List组件之间的通信,接下来我们来学习消息订阅-发布机制,让兄弟组件或任意组件直接实现数据的通信。

3.5.1 工具库 PubSubJS

有很多的库可以实现这种机制,我们使用比较主流的 PubSubJS。

  1. 下载
javascript 复制代码
npm install pubsub-js --save
  1. 引入
javascript 复制代码
import PubSub from "pubsub-js"
  1. 使用

发布(Publish):

javascript 复制代码
import PubSub from "pubsub-js";

PubSub.publish("eventName", "Hello PubSub");

订阅(Subscribe):

javascript 复制代码
import PubSub from "pubsub-js";

const token = PubSub.subscribe("eventName", (msg, data) => {
// msg ==> 消息名 "eventName"
// data ==> 数据 "Hello PubSub"
  console.log(msg, data);
});

// 取消订阅
PubSub.unsubscribe(token);

下面我们就把这个技术应用到我们的Search组件和List组件上。

3.5.2 使用 PubSubJS 完成兄弟组件间的通信
  1. 首先,我们要明确一下我们的 订阅者 和 发布者 ;
  • Search :发布者,Search决定了List的状态变化,搜索后通知List更新状态;
  • List :订阅者,接收Search传递过来的数据做判断展示;

结论:谁接收,谁订阅消息;谁传递,谁发布消息。

  1. 准备性工作,优化App组件,删掉无用代码;

因为App无需成为Search和List间的通信媒介,所以关于state的代码可以全部删掉。

App.jsx:

javascript 复制代码
import React, { Component } from "react";
import Search from "./components/Search";
import List from "./components/List";
import "./App.css";

class App extends Component {
  render() {
    return (
      <div className="bg-white text-gray-800">
        <Search />
        <List />
      </div>
    );
  }
}

export default App;

这样才是App本该有的样子。

  1. 在List组件中定义初始化状态,并订阅消息;

List/index.jsx:

javascript 复制代码
...
import PubSub from "pubsub-js";
...
class List extends Component {
  // 初始化状态
  state = {
    // 用户数据
    users: [],
    // 是否为第一次打开页面
    isFirst: true,
    // 标识是否处于加载中
    isLoading: false,
    // 存储请求相关的错误信息
    err: null,
  };
  
  render() {
    const { users, isFirst, isLoading, err } = this.state;
    return (
      <main className="max-w-6xl mx-auto px-4">
        {isFirst ? (
          <h2 style={{ fontSize: "24px" }}>
            Welcome to enter name to search!
          </h2>
        ) : isLoading ? (
          <h2>loading...</h2>
        ) : err ? (
          <h2 style={{ color: "red" }}>{err}</h2>
        ) : (
          <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
            {users.map((user) => {
              return <Item user={user} key={user.id} />;
            })}
          </div>
        )}
      </main>
    );
  }

  componentDidMount() {
    // 订阅消息
    this.token = PubSub.subscribe("updateListState", (_, stateObj) => {
        this.setState(stateObj);
    });
  }

  componentWillUnmount() {
    // 取消订阅消息
    PubSub.unsubscribe(this.token);
  }
}
...
  1. 在Search组件中发布消息,通知List组件更新状态;

Search/index.jsx:

javascript 复制代码
...
import PubSub from "pubsub-js";
...

  // 搜索按钮绑定点击事件
  search = () => {
    // 发送请求前通知 List 更新状态
    PubSub.publish("updateListState", { isFirst: false, isLoading: true })
    // 获取用户的输入(连续解构赋值+重命名)
    const {keywordElement: { value: keyword }} = this;
    // 发送网络请求
    axios.get(`/search/users?q=${keyword}`).then(
      (res) => {
        // 请求成功后通知 List 更新状态
        PubSub.publish("updateListState", { isLoading: false, users: res.data.items })
      },
      (err) => {
        // 请求失败后通知 List 更新状态
        PubSub.publish("updateListState", { isLoading: false, err: err.message })
      },
    );
  };
  ...

这种通信方式不仅仅适用于兄弟组件间的沟通,还适用于任意组件间的沟通。

3.6 fetch发送请求

3.6.1 fetch
  1. 文档

使用 Fetch API - MDN
Fetch API 教程 - 阮一峰
传统 Ajax 已死,Fetch 永生

  1. 特点
  • fetch :原生函数,不再使用 XmlHttpRequest 对象提交 ajax 请求;
  • 基于 Promise,语法简单;
  • 老版本浏览器可能不支持;
  1. 相关 API

GET请求:

javascript 复制代码
fetch("https://api.example.com/data")
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error("Error:", error));

POST请求:

javascript 复制代码
fetch("https://api.example.com/data", {
  method: "POST", 
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    key: "value",
  }),
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error("Error:", error));
3.6.2 结合案例使用 fetch 发送网络请求
  1. 使用fetch发送(未优化版)

Search/index.jsx:

javascript 复制代码
// 发送网络请求------使用fetch发送
fetch(`/search/users?q=${keyword}`).then(res => {
	console.log('请求成功', res)
}, err => {
	console.log('请求失败', err)
})

我们打开控制台,查看返回的数据`:

fetch 返回的 response 是一个 Response 对象,读取 Body 数据需要使用response.json(),解析为 JSON 对象:

javascript 复制代码
// 发送网络请求------使用fetch发送
fetch(`/search/users?q=${keyword}`).then(res => {
	console.log('请求成功', res.json())
}, err => {
	console.log('请求失败', err)
})

这次返回的是一个Promise 实例:

为了输出我们要的Json数据,我们将得到的Promise实例对象返回,再进行一次.then的链式调用:

javascript 复制代码
// 发送网络请求------使用fetch发送
fetch(`/search/users?q=${keyword}`).then(res => {
	return res.json()
}, err => {
	console.log('请求失败', err)
}).then(res => {
	console.log("请求成功", res);
}, err => {                      
	console.log("请求失败", err);
})

现在我们已经成功的拿到了我们想要的数据了!

现在有一个问题:如果数据请求失败,此时第一个then里面毫无疑问的会执行err函数,那么第二个then还会继续执行吗?如果执行的话,它会走成功回调函数还是走失败回调函数呢?

我们可以将控制台的Network选项卡调整为Offline ,用断网来模拟出请求失败的效果:

我们看到请求失败后,第二个then会继续执行,且走的是成功回调函数!

这是因为第一个then里面的err没有写返回值,这就相当于返回了undefined,而undefined属于非 Promise 值,那么第一个then返回的Promise 实例状态就是成功的,值为undefined,所以第二个函数就走了成功的回调函数。

我们需要让第一个then里面的err里停下来,不要继续往下走:

javascript 复制代码
// 发送网络请求------使用fetch发送
fetch(`/search/users?q=${keyword}`).then(res => {
	return res.json()
}, err => {
	// 返回一个初始化状态的Promise,来中断Promise链
	return new Promise()
}).then(res => {
	console.log("请求成功", res);
}, err => {                      
	console.log("请求失败", err);
})
  1. 使用 fetch 发送(优化版)

我们可以不在每个then里面声明失败回调函数,而是使用.catch兜底,统一处理错误。

javascript 复制代码
// 发送网络请求------使用fetch发送
fetch(`/search/users?q=${keyword}`).then(res => {
	res.json()
}).then(res => {
	console.log("请求成功", res);
}).catch(err => {
	console.log("请求出错", err);
})
  1. 使用 Async/Await (更优雅的异步)

使用 async/await 可使异步代码更清晰。

javascript 复制代码
search = async () => {
    try {
      const res = await fetch(`/search/users?q=${keyword}`);
      const data = await res.json();
      console.log("请求成功", data);
    } catch (err) {
      console.log("请求出错", err);
    }
}    

Search/index.jsx:

javascript 复制代码
  // 搜索按钮绑定点击事件
  search = async () => {
    // 发送请求前通知List更新状态
    PubSub.publish("updateListState", { isFirst: false, isLoading: true });
    // 获取用户的输入(连续解构赋值+重命名)
    const {
      keywordElement: { value: keyword },
    } = this;

    // 使用await+async发送网络请求
    try {
      const res = await fetch(`/search/users?q=${keyword}`);
      const data = await res.json();
      PubSub.publish("updateListState", {
        isLoading: false,
        users: data.items,
      });
    } catch (err) {
      PubSub.publish("updateListState", { isLoading: false, err: err.message });
    }
  };

3.7 总结

  1. 设计状态时要考虑全面,例如:带有网络请求的组件,要考虑请求失败怎么办;
  2. ES6小知识点:【解构赋值+重命名】
javascript 复制代码
let obj = {a:{b:1}}
const {a}= obj;  //传统解构赋值
const {a:{b}}= obj;  //连续解构赋值
const {a:{b:value}}= obj;  //连续解构赋值 + 重命名
  1. 消息订阅与发布机制

(1)先订阅,再发布(理解:有一种隔空对话的感觉);

(2)适用于任意组件间的通信;

(3)要在组件的componentWillUnmount中取消订阅;

  1. fetch发送请求(关注分离的设计思想)
javascript 复制代码
fetch(`/search/users?q=${keyword}`).then(res => {
	// 第一步:到底看看服务器能不能联系成功
	res.json()
}).then(res => {
	// 第二步:它才把真正的数据给你
	console.log("请求成功", res);
}).catch(err => {
	console.log("请求出错", err);
})
相关推荐
卸任2 小时前
Vibe coding工具使用心得
前端·ai编程
brien93 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance3 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M3 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写3 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李3 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易4 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马4 小时前
前沿视界
前端
kyriewen4 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易4 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员