一、理解

1. 前置说明

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

2. 常用的 ajax 请求库

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

二、axios

1. axios 的安装和使用

1.1 下载

npm i axios

1.2 引入

import axios from 'axios'

1.3 文档

https://github.com/axios/axios

1.4 相关 API

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

1.5 案例

App.jsx:

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:

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中追加如下配置:

"proxy": "http://1ocalhost:5000"

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

App.jsx

	// 发送请求
    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:

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:

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配置具体代理规则:
// 引入内置模块
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

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来生成静态页面的文件;
<!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替换成classNamestyle改写成双花括号的格式style={{}}

App.jsx:

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:

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

在这里插入图片描述

index.html:

<!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:

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. 抽取 ListItem 组件;

List/index.jsx:

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:

.welcome {
    font-size: 30px;
    box-sizing: border-box;
}

Item/index.jsx:

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:

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. 搜索按钮绑定点击事件;
	<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获取用户的输入;
	<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还可以换一种写法——使用连续解构赋值

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

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

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

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

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

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

// 搜索按钮绑定点击事件
search = () => {
    // 获取用户的输入(连续解构赋值 + 重命名)
    const {keywordElement: {value: keyword}} = this;
    console.log(keyword);
}
  1. 发送网络请求;
 // 引入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:

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

修改请求路径:

// 搜索按钮绑定点击事件
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:

...
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:

// 搜索按钮绑定点击事件
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:

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:

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:

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

App.jsx:

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

App.jsx:

...
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:

  // 搜索按钮绑定点击事件
  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:

  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:

...
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. 下载
npm install pubsub-js --save
  1. 引入
import PubSub from "pubsub-js"
  1. 使用

发布(Publish):

import PubSub from "pubsub-js";

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

订阅(Subscribe):

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无需成为SearchList间的通信媒介,所以关于state的代码可以全部删掉。

App.jsx:

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:

...
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:

...
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请求:

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

POST请求:

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:

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

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

在这里插入图片描述

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

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

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

在这里插入图片描述
为了输出我们要的Json数据,我们将得到的Promise实例对象返回,再进行一次.then的链式调用:

// 发送网络请求——使用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里停下来,不要继续往下走:

// 发送网络请求——使用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兜底,统一处理错误。

// 发送网络请求——使用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 可使异步代码更清晰。

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:

  // 搜索按钮绑定点击事件
  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小知识点:【解构赋值+重命名
let obj = {a:{b:1}}
const {a}= obj;  //传统解构赋值
const {a:{b}}= obj;  //连续解构赋值
const {a:{b:value}}= obj;  //连续解构赋值 + 重命名
  1. 消息订阅与发布机制

(1)先订阅,再发布(理解:有一种隔空对话的感觉);
(2)适用于任意组件间的通信;
(3)要在组件的componentWillUnmount中取消订阅;

  1. fetch发送请求(关注分离的设计思想)
fetch(`/search/users?q=${keyword}`).then(res => {
	// 第一步:到底看看服务器能不能联系成功
	res.json()
}).then(res => {
	// 第二步:它才把真正的数据给你
	console.log("请求成功", res);
}).catch(err => {
	console.log("请求出错", err);
})
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐