49、消费 RESTful Web 服务的全面指南

消费 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 服务,避免重复请求数据,提高应用程序的性能和用户体验。同时,错误处理和跨域请求的处理也确保了应用程序的稳定性和安全性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值