文章目录
- 快速搭建 API 服务器
- 在 Search 组件中实现 Axios 发送请求
- 在 App 组件中管理 List 组件的用户列表状态
- 在 List 组件中更新渲染用户列表数据
- 优化完善
- 完整源码
最终效果:
快速搭建 API 服务器
根据下面步骤来操作,就可以快速搭建一个符合本案例使用 Express 服务器。
-
在你电脑的某个位置创建一个
node-server
的目录; -
通过命令行工具进入到
node-server
; -
在
node-server
目录下执行npm i express
命令安装 Express; -
在
node-server
目录下创建app.js
文件,并键入以下内容:// file: node-server/app.js // 导入 Express const express = require("express"); const axios = require("axios"); // 创建 web 服务器 const app = express(); // 全局中间件 app.use((req, res, next) => { // console.log("请求来自于:", req.get("Host")); // console.log("请求地址:", req.url); next(); }); app.get("/", (req, res) => { res.send("hello, express"); }); app.get("/search/users2", (req, res) => { const users = [ { id: "001", login: "tom", avatar_url: "http://api.btstu.cn/sjtx/api.php?lx=a1&format=images", html_url: "", }, { id: "002", login: "jerry", avatar_url: "http://api.btstu.cn/sjtx/api.php?lx=b1&format=images", html_url: "", }, { id: "003", login: "tony", avatar_url: "http://api.btstu.cn/sjtx/api.php?lx=c1&format=images", html_url: "", }, ]; res.send(users); }); app.get("/search/users", async (req, res, next) => { try { const data = await axios.get( `https://api.github.com/search/users?q=${req.query.q}` ); res.send(data.data); } catch (err) { next(err); } }); // 设置端口,监听启动服务器的回调 app.listen(5000, () => { console.log("服务器启动成功:http://localhost:5000"); console.log("请求github真实数据请访问:http://localhost:5000/search/users"); console.log("请求本地模拟数据请访问:http://localhost:5000/search/users2"); });
-
打开
node-server
目录下的package.json
文件,添加如下内容:"scripts": { "start": "node app.js" }
-
通过
npm start
启动服务器,启动成功控制台输入如下:PS E:\node-server> npm start > start > node app.js 服务器启动成功:http://localhost:5000 请求github真实数据请访问:http://localhost:5000/search/users 请求本地模拟数据请访问:http://localhost:5000/search/users2
-
启动成功后,在浏览器访问接口,看是否可正常返回数据。
在 Search 组件中实现 Axios 发送请求
-
在 Search 组建中引入 axios:
// file: src/components/Search/index.jsx import axios from "axios";
-
给 input 添加 ref 属性,用来在点击搜索按钮的回调中获取输入的值,也就是查询关键词。
// file: src/components/Search/index.jsx <input ref={(c) => (this.keyWordElement = c)} type="text" placeholder="输入关键词点击搜索" />
-
定义点击搜索按钮的事件回调:
// file: src/components/Search/index.jsx search = () => { // 获取用户的输入 const { keyWordElement: { value: keyWord }, } = this; // 此处运用了连续解构赋值并重命名的写法 // 发送网络请求 axios.get(`/search/users?q=${keyWord}`).then( (res) => { // 此处调用App组件通过props传入的saveUsers方法来更新用户状态,本文下面部分内容有实现 this.props.saveUsers(res.data.items); }, (err) => { console.log("失败了", err); } ); };
注意:
- 需要配置跨域代理,此处我通过 package.json 配置的,增加以下代码即可:
"proxy": "http://localhost:5000"
当然你也可以使用增加 setupProxy.js 文件的方式。 - 请求地址
http://localhost:3000/search/users?q=${keyWord}
可以简写为/search/users?q=${keyWord}
。
- 需要配置跨域代理,此处我通过 package.json 配置的,增加以下代码即可:
-
给搜索按钮绑定 click 事件:
// file: src/components/Search/index.jsx <button onClick={this.search}>搜索</button>
在 App 组件中管理 List 组件的用户列表状态
-
在 App.js 中对用户列表数据进行初始化,并提供更新该状态的方法,代码如下:
// file: src/App.js // 初始化状态 state = { users: [] }; // 保存更新用户数据状态 saveUsers = (users) => { this.setState({ users }); };
-
在 App.js 中通过 Search 组件的 props 把 saveUsers 方法传递给 Search 组件,代码如下:
// file: src/App.js <Search saveUsers={this.saveUsers} />
-
在 App.js 中通过 List 组件的 props 把 users 数据传递给 List 组件,这样 List 组件就动态渲染数据了,代码如下:
// file: src/App.js <List users={users} />
在 List 组件中更新渲染用户列表数据
-
在 List 组件中通过 props 接收到 users 数据,并渲染到界面,代码如下:
// file: src/components/List/index.jsx render() { const { users } = this.props; return ( <div className="row"> {users.map((userObj) => { return ( <div key={userObj.id} className="card"> <a href={userObj.html_url} target="_blank" rel="noreferrer"> <img src={userObj.avatar_url} style={{ width: "100px" }} alt="avatar" /> </a> <p className="card-text">{userObj.login}</p> </div> ); })} </div> ); }
至此,在输入框输入关键字,点击搜索时,用户列表就会显示搜索结果(请求可能会比较慢,后面优化)。
优化完善
上文已实现功能交互,但是体验比较差。体现在以下几点:
- 首次进入页面时,List 组件区域一片空白,体验不好;
- 点击搜索后,请求比较慢比较耗时的情况下,List 组件区域要么空白要么还显示搜索前的数据,也就是在结果返回的这段时间里显示的内容体验不好;
- 还有请求异常或报错的情况未处理。
下面针对以上三点统一进行优化:
-
首先,在 App.js 中新增定义了 3 个状态,并把之前单独更新 users 的方法重构为一个可以批量更改状态的方法 updateAppState,具体说明如下代码所示:
// file: src/App.js state = { users: [], // users 初始值为数组 isFirst: true, // 是否为第一次打开页面 isLoading: false, // 是否处于加载中 err: "", // 存储请求相关的错误信息 }; // 更新App的state updateAppState = (stateObj) => { this.setState(stateObj); };
-
然后,在 Search 组件中的代码逻辑中来控制 isFirst、isLoading、err 的状态:
- isFirst 控制逻辑:只要点击一次搜索按钮,就要把 isFirst 设置为 false,直到重新打开页面。
- isLoading 控制逻辑:axios 发送请求前把 isLoading 设置为 true,请求返回数据(不论成功还是失败)时,设置为 false。
- err 控制逻辑:当请求失败时来存储错误信息。(注意:每次请求前要清空 err。)
主要是将 Search 组件中 search 方法代码重构如下:
// file: src/components/Search/index.jsx search = () => { const { keyWordElement: { value: keyWord }, } = this; // 1、发送请求前通知App更新状态 this.props.updateAppState({ isFirst: false, isLoading: true, err: "" }); // 发送网络请求 axios.get(`/search/users?q=${keyWord}`).then( (res) => { // 2、发送请求成功后通知App更新状态 this.props.updateAppState({ isLoading: false, users: res.data.items }); }, (err) => { // 3、发送请求失败后通知App更新状态 this.props.updateAppState({ isLoading: false, err: err.message }); } ); };
-
最后,在 List 组件中通过上面新增的 3 个状态,来控制在不同交互场景下的页面显示内容:
- 首次打开页面(即 isFirst 为 true)时,List 组件区域显示文案“欢迎使用,请输入关键字,随后点击搜索”。
- 如果加载中(即 isLoading 为 true)时,List 组件区域显示为“Loading…”。
- 如果有错误信息(即 err 不为空)时,则显示错误信息
- 否则,正常显示用户数据。
// file: src/components/List/index.jsx render() { const { users, isFirst, isLoading, err } = this.props; return ( <div className="row"> {isFirst ? ( <h4>欢迎使用,请输入关键字,随后点击搜索</h4> ) : isLoading ? ( <h4>Loading......</h4> ) : err ? ( <h4 style={{ color: "red" }}>{err}</h4> ) : ( users.map((userObj) => { return ( <div key={userObj.id} className="card"> <a href={userObj.html_url} target="_blank" rel="noreferrer"> <img src={userObj.avatar_url} style={{ width: "100px" }} alt="avatar" /> </a> <p className="card-text">{userObj.login}</p> </div> ); }) )} </div> ); }
完整源码
-
App.js
// file: src/App.js import React, { Component } from "react"; import Search from "./components/Search"; import List from "./components/List"; export default class App extends Component { // 初始化状态 state = { users: [], // users 初始值为数组 isFirst: true, // 是否为第一次打开页面 isLoading: false, // 是否处于加载中 err: "", // 存储请求相关的错误信息 }; // 更新App的state updateAppState = (stateObj) => { this.setState(stateObj); }; render() { return ( <div className="container"> <Search updateAppState={this.updateAppState} /> <List {...this.state} /> </div> ); } }
-
Search 组件
// file: src/component/Search.jsx import React, { Component } from "react"; import axios from "axios"; export default class Search extends Component { search = () => { // 获取用户的输入 const { keyWordElement: { value: keyWord }, } = this; // 此处运用了连续解构赋值并重命名的写法 // 发送请求前通知App更新状态 this.props.updateAppState({ isFirst: false, isLoading: true, err: "" }); // 发送网络请求 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 }); } ); }; render() { return ( <section className="jumbotron"> <h3 className="jumbotron-heading">搜索 Github 用户</h3> <div> <input ref={(c) => (this.keyWordElement = c)} type="text" placeholder="输入关键词点击搜索" /> <button onClick={this.search}>搜索</button> </div> </section> ); } }
-
List 组件
// file: src/component/List.jsx import React, { Component } from "react"; import "./index.css"; export default class List extends Component { render() { const { users, isFirst, isLoading, err } = this.props; return ( <div className="row"> {isFirst ? ( <h4>欢迎使用,请输入关键字,随后点击搜索</h4> ) : isLoading ? ( <h4>Loading......</h4> ) : err ? ( <h4 style={{ color: "red" }}>{err}</h4> ) : ( users.map((userObj) => { return ( <div key={userObj.id} className="card"> <a href={userObj.html_url} target="_blank" rel="noreferrer"> <img src={userObj.avatar_url} style={{ width: "100px" }} alt="avatar" /> </a> <p className="card-text">{userObj.login}</p> </div> ); }) )} </div> ); } }