消费 RESTful Web 服务的全面指南
1. 修改对象与请求方法
在修改对象时,会发送一个 PUT 请求,该请求包含用于标识要修改对象的 URL,示例代码如下:
this.SendRequest("put", `${this.BASE_URL}/${data.id}`, callback, data);
Web 服务会返回完整的更新对象,用于调用回调函数。虽然并非所有 Web 服务都会返回完整对象,但这种做法很常见,因为它能确保 Web 服务应用的任何额外转换都能在客户端体现。
2. 增删改数据的应用支持
为了支持数据的创建和编辑,在
src
文件夹中添加了
IsolatedEditor.js
文件,定义了如下组件:
import React, { Component } from "react";
import { RestDataSource } from "./webservice/RestDataSource";
import { ProductEditor } from "./ProductEditor";
export class IsolatedEditor extends Component {
constructor(props) {
super(props);
this.state = {
dataItem: {}
};
this.dataSource = this.props.dataSource
|| new RestDataSource("http://localhost:3500/api/products");
}
save = (data) => {
const callback = () => this.props.history.push("/isolated");
if (data.id === "") {
this.dataSource.Store(data, callback);
} else {
this.dataSource.Update(data, callback);
}
}
cancel = () => this.props.history.push("/isolated");
render() {
return <ProductEditor key={ this.state.dataItem.id }
product={ this.state.dataItem } saveCallback={ this.save }
cancelCallback={ this.cancel } />
}
componentDidMount() {
if (this.props.match.params.mode === "edit") {
this.dataSource.GetOne(this.props.match.params.id,
data => this.setState({ dataItem: data}));
}
}
}
IsolatedEditor
组件利用现有的
ProductEditor
及其属性,从 Web 服务数据源提供数据和回调。当用户选择编辑对象时,会使用
GetOne
方法根据当前路由详情请求单个对象的详细信息,然后使用
Store
或
Update
方法将更改发送回 Web 服务。
同时,在
IsolatedTable.js
文件中添加了对对象创建和编辑的支持,还添加了删除按钮:
import React, { Component } from "react";
import { RestDataSource } from "./webservice/RestDataSource";
import { Link } from "react-router-dom";
export class IsolatedTable extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.dataSource = new RestDataSource("http://localhost:3500/api/products")
}
deleteProduct(product) {
this.dataSource.Delete(product,
() => this.setState({products: this.state.products.filter(p =>
p.id !== product.id)}));
}
render() {
return <table className="table table-sm table-striped table-bordered">
<thead>
<tr><th colSpan="5"
className="bg-info text-white text-center h4 p-2">
(Isolated) Products
</th></tr>
<tr>
<th>ID</th><th>Name</th><th>Category</th>
<th className="text-right">Price</th>
<th></th>
</tr>
</thead>
<tbody>
{
this.state.products.map(p => <tr key={ p.id }>
<td>{ p.id }</td><td>{ p.name }</td><td>{p.category}</td>
<td className="text-right">
${ Number(p.price).toFixed(2)}
</td>
<td>
<Link className="btn btn-sm btn-warning mx-2"
to={`/isolated/edit/${p.id}`}>
Edit
</Link>
<button className="btn btn-sm btn-danger mx-2"
onClick={ () => this.deleteProduct(p)}>
Delete
</button>
</td>
</tr>)
}
</tbody>
<tfoot>
<tr className="text-center">
<td colSpan="5">
<Link to="/isolated/create"
className="btn btn-info">Create</Link>
</td>
</tr>
</tfoot>
</table>
}
componentDidMount() {
this.dataSource.GetData(data => this.setState({products: data}));
}
}
IsolatedTable
组件显示创建、编辑和删除按钮。创建和编辑按钮会向用户展示编辑器组件,通过发送 POST 或 PUT 请求更新 Web 服务;删除按钮通过发送 DELETE 请求移除关联对象。
最后,更新
Selector
组件的路由配置,使
/isolated/edit
和
/isolated/create
URL 选择
IsolatedEditor
组件:
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect }
from "react-router-dom";
import { ToggleLink } from "./routing/ToggleLink";
import { RoutedDisplay } from "./routing/RoutedDisplay";
import { IsolatedTable } from "./IsolatedTable";
import { IsolatedEditor } from "./IsolatedEditor";
export class Selector extends Component {
render() {
const routes = React.Children.map(this.props.children, child => ({
component: child,
name: child.props.name,
url: `/${child.props.name.toLowerCase()}`,
datatype: child.props.datatype
}));
return <Router getUserConfirmation={ this.customGetUserConfirmation }>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/isolated">Isolated Data</ToggleLink>
{ routes.map(r => <ToggleLink key={ r.url } to={ r.url }>
{ r.name }
</ToggleLink>)}
</div>
<div className="col">
<Switch>
<Route path="/isolated" component={ IsolatedTable }
exact={ true } />
<Route path="/isolated/:mode/:id?"
component={ IsolatedEditor } />
{ routes.map(r =>
<Route key={ r.url }
path={ `/:datatype(${r.datatype})/:mode?/:id?`}
component={ RoutedDisplay(r.datatype)} />
)}
<Redirect to={ routes[0].url } />
</Switch>
</div>
</div>
</div>
</Router>
}
}
3. 错误处理
应用默认假设所有 HTTP 请求都会成功,这是不现实的乐观做法。HTTP 请求可能因多种原因失败,如连接问题或服务器故障。错误边界无法处理异步操作(如 HTTP 请求)中的问题,因此需要不同的方法。在
RestDataSource.js
文件中进行了如下修改:
import Axios from "axios";
export class RestDataSource {
constructor(base_url, errorCallback) {
this.BASE_URL = base_url;
this.handleError = errorCallback;
}
GetData(callback) {
this.SendRequest("get", this.BASE_URL, callback);
}
async GetOne(id, callback) {
this.SendRequest("get", `${this.BASE_URL}/${id}`, callback);
}
async Store(data, callback) {
this.SendRequest("post", this.BASE_URL, callback, data)
}
async Update(data, callback) {
this.SendRequest("put", `${this.BASE_URL}/${data.id}`, callback, data);
}
async Delete(data, callback) {
this.SendRequest("delete", `${this.BASE_URL}/${data.id}`, callback, data);
}
async SendRequest(method, url, callback, data) {
try {
callback((await Axios.request({
method: method,
url: url,
data: data
})).data);
} catch(err) {
this.handleError("Operation Failed: Network Error");
}
}
}
通过
SendRequest
方法整合所有请求的优势在于,可以使用单个
try/catch
块处理所有请求类型的错误。捕获块处理请求中出现的错误,并调用作为构造函数参数接收的回调函数。
为了向用户显示错误消息,在
src/webservice
文件夹中添加了
RequestError.js
文件:
import React, { Component } from "react";
import { Link } from "react-router-dom";
export class RequestError extends Component {
render() {
return <div>
<h5 className="bg-danger text-center text-white m-2 p-3">
{ this.props.match.params.message }
</h5>
<div className="text-center">
<Link to="/" className="btn btn-secondary">OK</Link>
</div>
</div>
}
}
同时,在
Selector
组件中添加新的路由:
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect }
from "react-router-dom";
import { ToggleLink } from "./routing/ToggleLink";
import { RoutedDisplay } from "./routing/RoutedDisplay";
import { IsolatedTable } from "./IsolatedTable";
import { IsolatedEditor } from "./IsolatedEditor";
import { RequestError } from "./webservice/RequestError";
export class Selector extends Component {
render() {
const routes = React.Children.map(this.props.children, child => ({
component: child,
name: child.props.name,
url: `/${child.props.name.toLowerCase()}`,
datatype: child.props.datatype
}));
return <Router getUserConfirmation={ this.customGetUserConfirmation }>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/isolated">Isolated Data</ToggleLink>
{ routes.map(r => <ToggleLink key={ r.url } to={ r.url }>
{ r.name }
</ToggleLink>)}
</div>
<div className="col">
<Switch>
<Route path="/isolated" component={ IsolatedTable }
exact={ true } />
<Route path="/isolated/:mode/:id?"
component={ IsolatedEditor } />
<Route path="/error/:message"
component={ RequestError } />
{ routes.map(r =>
<Route key={ r.url }
path={ `/:datatype(${r.datatype})/:mode?/:id?`}
component={ RoutedDisplay(r.datatype)} />
)}
<Redirect to={ routes[0].url } />
</Switch>
</div>
</div>
</div>
</Router>
}
}
在
IsolatedTable.js
文件中,为数据源提供回调,当出现问题时导航到
/error
URL,并添加一个按钮来创建错误:
import React, { Component } from "react";
import { RestDataSource } from "./webservice/RestDataSource";
import { Link } from "react-router-dom";
export class IsolatedTable extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.dataSource = new RestDataSource("http://localhost:3500/api/products",
(err) => this.props.history.push(`/error/${err}`));
}
deleteProduct(product) {
this.dataSource.Delete(product,
() => this.setState({products: this.state.products.filter(p =>
p.id !== product.id)}));
}
render() {
return <table className="table table-sm table-striped table-bordered">
<thead>
<tr><th colSpan="5"
className="bg-info text-white text-center h4 p-2">
(Isolated) Products
</th></tr>
<tr>
<th>ID</th><th>Name</th><th>Category</th>
<th className="text-right">Price</th>
<th></th>
</tr>
</thead>
<tbody>
{
this.state.products.map(p => <tr key={ p.id }>
<td>{ p.id }</td><td>{ p.name }</td><td>{p.category}</td>
<td className="text-right">
${ Number(p.price).toFixed(2)}
</td>
<td>
<Link className="btn btn-sm btn-warning mx-2"
to={`/isolated/edit/${p.id}`}>
Edit
</Link>
<button className="btn btn-sm btn-danger mx-2"
onClick={ () => this.deleteProduct(p)}>
Delete
</button>
</td>
</tr>)
}
</tbody>
<tfoot>
<tr className="text-center">
<td colSpan="5">
<Link to="/isolated/create"
className="btn btn-info">Create</Link>
<button className="btn btn-danger mx-2"
onClick={ () => this.dataSource.GetOne("err")}>
Error
</button>
</td>
</tr>
</tfoot>
</table>
}
componentDidMount() {
this.dataSource.GetData(data => this.setState({products: data}));
}
}
点击
IsolatedTable
渲染的错误按钮会发送一个请求,从 Web 服务接收错误响应,从而触发导航到显示错误消息的 URL。
4. 跨域请求
默认情况下,浏览器实施安全策略,仅允许 JavaScript 代码在与包含它们的文档相同的源内进行异步 HTTP 请求。此策略旨在降低跨站脚本攻击的风险。对于 Web 应用程序开发人员来说,当使用 Web 服务时,同源策略可能会成为问题,因为 Web 服务通常位于包含应用程序 JavaScript 代码的源之外。
两个 URL 若具有相同的协议、主机和端口,则被视为同一源;否则为不同源。本章中使用的 RESTful Web 服务的 URL 与主应用程序使用的 URL 具有不同的源,因为它们使用不同的 TCP 端口。
跨域资源共享(CORS)协议用于向不同源发送请求。使用 CORS 时,浏览器会在异步 HTTP 请求中包含标头,向服务器提供 JavaScript 代码的源。服务器的响应包含标头,告知浏览器是否愿意接受该请求。
在本章中,提供 RESTful Web 服务的
json-server
包支持 CORS,会接受来自任何源的请求;用于进行 HTTP 请求的
Axios
包会自动应用 CORS。在为自己的项目选择软件时,必须选择允许通过单个源处理所有请求的平台,或配置 CORS 以使服务器接受应用程序的数据请求。
下面用 mermaid 流程图展示跨域请求的流程:
graph LR
A[浏览器发起请求] --> B[浏览器添加 CORS 标头]
B --> C[服务器接收请求]
C --> D{服务器是否接受请求}
D -- 是 --> E[服务器返回响应并包含允许信息]
D -- 否 --> F[服务器返回拒绝信息]
E --> G[浏览器处理响应]
F --> H[浏览器提示错误]
5. 使用数据存储消费 Web 服务
前面定义的组件相互隔离,仅通过 URL 路由系统进行协调。这种方法的优点是简单,但可能导致用户在应用程序中导航时反复从 Web 服务请求相同的数据,每个组件在挂载时都会发送其 HTTP 请求。如果应用程序使用数据存储,则可以在组件之间共享数据。
5.1 创建新的中间件
为了实现上述功能,需要创建新的 Redux 中间件,该中间件将拦截现有的操作并向 Web 服务发送相应的 HTTP 请求。在
src/webservice
文件夹中添加了
RestMiddleware.js
文件,内容如下:
import { STORE, UPDATE, DELETE} from "../store/modelActionTypes";
import { RestDataSource } from "./RestDataSource";
import { PRODUCTS, SUPPLIERS } from "../store/dataTypes";
export const GET_DATA = "rest_get_data";
export const getData = (dataType) => {
return {
type: GET_DATA,
dataType: dataType
}
}
export const createRestMiddleware = (productsURL, suppliersURL) => {
const dataSources = {
[PRODUCTS]: new RestDataSource(productsURL, () => {}),
[SUPPLIERS]: new RestDataSource(suppliersURL, () => {})
}
return ({dispatch, getState}) => next => action => {
switch (action.type) {
case GET_DATA:
if (getState().modelData[action.dataType].length === 0) {
dataSources[action.dataType].GetData((data) =>
data.forEach(item => next({ type: STORE,
dataType: action.dataType, payload: item})));
}
break;
case STORE:
action.payload.id = null;
dataSources[action.dataType].Store(action.payload, data =>
next({ ...action, payload: data }))
break;
case UPDATE:
dataSources[action.dataType].Update(action.payload, data =>
next({ ...action, payload: data }))
break;
case DELETE:
dataSources[action.dataType].Delete({id: action.payload },
() => next(action));
break;
default:
next(action);
}
}
}
这里定义了一个新的操作
GET_DATA
用于从 Web 服务请求数据。
createRestMiddleware
函数接受产品和供应商数据的数据源,并返回一个中间件,该中间件处理新的
GET_DATA
操作以及现有的
STORE
、
UPDATE
和
DELETE
操作,通过向 Web 服务发送请求并在收到结果时调度额外的操作。
5.2 将中间件添加到数据存储
在
src/store
文件夹的
index.js
文件中添加新的中间件,代码如下:
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import modelReducer from "./modelReducer";
import stateReducer from "./stateReducer";
import { customReducerEnhancer } from "./customReducerEnhancer";
import { multiActions } from "./multiActionMiddleware";
import { asyncEnhancer } from "./asyncEnhancer";
import { createRestMiddleware } from "../webservice/RestMiddleware";
const enhancedReducer = customReducerEnhancer(
combineReducers(
{
modelData: modelReducer,
stateData: stateReducer
})
);
const restMiddleware = createRestMiddleware(
"http://localhost:3500/api/products",
"http://localhost:3500/api/suppliers");
export default createStore(enhancedReducer,
compose(applyMiddleware(multiActions),
applyMiddleware(restMiddleware),
asyncEnhancer(2000)));
export { saveProduct, saveSupplier, deleteProduct, deleteSupplier }
from "./modelActionCreators";
需要注意中间件的应用顺序,
multiActions
中间件必须放在前面,否则新的中间件将无法正确处理操作。
5.3 完成应用程序更改
为了按需自动请求数据,在
src
文件夹中添加了
DataGetter.js
文件,定义了高阶组件:
import React, { Component } from "react";
import { PRODUCTS, SUPPLIERS } from "./store/dataTypes";
export const DataGetter = (dataType, WrappedComponent) => {
return class extends Component {
render() {
return <WrappedComponent { ...this.props } />
}
componentDidMount() {
this.props.getData(PRODUCTS);
if (dataType === SUPPLIERS) {
this.props.getData(SUPPLIERS);
}
}
}
}
该组件在挂载后请求数据,并且知道供应商数据需要产品数据的补充才能正确显示给用户。
在
src/store
文件夹的
TableConnector.js
文件中添加对新高阶组件的支持:
import { connect } from "react-redux";
//import { startEditingProduct, startEditingSupplier } from "./stateActions";
import { deleteProduct, deleteSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS } from "./dataTypes";
import { withRouter } from "react-router-dom";
import { getData } from "../webservice/RestMiddleware";
import { DataGetter } from "../DataGetter";
export const TableConnector = (dataType, presentationComponent) => {
const mapStateToProps = (storeData, ownProps) => {
if (dataType === PRODUCTS) {
return { products: storeData.modelData[PRODUCTS] };
} else {
return {
suppliers: storeData.modelData[SUPPLIERS].map(supp => ({
...supp,
products: supp.products.map(id =>
storeData.modelData[PRODUCTS]
.find(p => p.id === Number(id)) || id)
.map(val => val.name || val)
}))
}
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
getData: (type) => dispatch(getData(type)),
deleteCallback: dataType === PRODUCTS
? (...args) => dispatch(deleteProduct(...args))
: (...args) => dispatch(deleteSupplier(...args))
}
}
const mergeProps = (dataProps, functionProps, ownProps) => {
let routedDispatchers = {
editCallback: (target) => {
ownProps.history.push(`/${dataType}/edit/${target.id}`);
},
deleteCallback: functionProps.deleteCallback,
getData: functionProps.getData
}
return Object.assign({}, dataProps, routedDispatchers, ownProps);
}
return withRouter(connect(mapStateToProps,
mapDispatchToProps, mergeProps)(DataGetter(dataType,
presentationComponent)));
}
最后,移除用于填充数据存储的静态内容。
下面用表格总结使用数据存储消费 Web 服务的步骤:
|步骤|操作|文件|
| ---- | ---- | ---- |
|1|创建新的中间件|
RestMiddleware.js
|
|2|将中间件添加到数据存储|
index.js
|
|3|定义高阶组件按需请求数据|
DataGetter.js
|
|4|在
TableConnector
中添加对高阶组件的支持|
TableConnector.js
|
|5|移除静态内容|相关文件|
综上所述,通过上述一系列操作,可以实现更高效地消费 RESTful Web 服务,避免重复请求数据,提高应用程序的性能和用户体验。同时,错误处理和跨域请求的处理也确保了应用程序的稳定性和安全性。
超级会员免费看
877

被折叠的 条评论
为什么被折叠?



