20-编写类postman前端页面

840 阅读8分钟
写在前面

本次章节,由于要完成postman页面,因此篇幅预计在3万字上下。且因为文档最后会放上开源连接,因此在此只在文章中展示核心代码。

目的

image.png

需求文档(仿)

前端需求文档 - 基于 Postman 的原型开发

  1. 概述

    本文档描述了基于 Postman 原型的前端开发需求,旨在实现一个简化的 API 请求工具,提供类似 Postman 的功能和界面,支持用户进行 GET 和 POST 请求的测试。

  2. 功能模块

    2.1. 请求类型支持

    • 支持用户选择请求类型,包括 GET 和 POST。
    • 用户可以通过单选按钮选择请求类型。

    2.2. URL 输入

    • 提供一个输入框,用于用户输入请求的 URL 地址。

    2.3. 请求参数和请求体

    • 支持用户输入请求参数,以键值对的形式。
    • 对于 POST 请求,支持用户输入请求体,可以选择 JSON 或 x-www-form-urlencoded 格式。

    2.4. 发送请求

    • 提供一个 "Send" 按钮,用户点击后会发送请求。
    • 提供一个 "Save" 按钮,用户点击后会保存请求(留给后期自动化使用)。

    2.5. 多个请求类型

    • 对于 POST 请求,用户可以选择请求体的格式,包括 JSON 和 x-www-form-urlencoded。
    • 如果选择 x-www-form-urlencoded,用户可以输入多个键值对。

    2.7. 响应展示

    • 在页面上展示请求的响应结果,包括响应状态码、响应体和响应头。
    • 响应体可以根据类型(JSON 或文本)进行展示。
设计前端结构

image.png

讲解

  • JSONAceEditor.tsx文件中,主要存放一个JSON的样式文件
  • MaterialOneDark.js文件中同理,也是一个样式文件
  • Table文件夹中,主要存放于一系列的表单样式
  • EditableTable.tsx存放需求中的请求参数的表单样式
  • FormDataTable.tsx主要存放form-data样式
  • xFormTable.tsx主要存放xform的样式
  • services目录中是咱们存放的一系列请求信息
  • 新增的request.ts文件,用来存放咱们的发送请求功能
开始编写样式
  1. 请求类型+url输入框+send按钮
    return (
        <PageContainer title="在线HTTP测试工具">
            <Card>
                <Row gutter={[8, 8]}>
                    <Col span={18}>
                        <Input
                            size="large"
                            value={url}
                            addonBefore={selectBefore}
                            placeholder="请输入要请求的url"
                            onChange={(e) => {
                                setUrl(e.target.value);
                                splitUrl(e.target.value);
                            }}
                        />
                    </Col>
                    <Col span={6}>
                        <Button
                            onClick={onRequest}
                            loading={loading}
                            type="primary"
                            size="large"
                            style={{marginRight: 16, float: 'right'}}
                        >
                            Save{' '}
                        </Button>
                        <Button
                            onClick={onRequest}
                            loading={loading}
                            type="primary"
                            size="large"
                            style={{marginRight: 16, float: 'right'}}
                        >
                            Send{' '}
                        </Button>
                    </Col>
                </Row>
            </Card>
        </PageContainer>
    );

此样式中可以看到有几个变量:

  • url:是我们后期输入的url,因为要作为传参,因此初始化一个url变量
  • selectBefore:请求方法下拉框函数,此函数中编写了我们的请求方式,函数如下:
    // 请求方法下拉选择框
    const selectBefore = (
        <Select
            value={method}
            onChange={(data) => setMethod(data)}
            style={{width: 120, fontSize: 16, textAlign: 'left'}}
        >
            <Option key="GET" value="GET">GET</Option>
            <Option key="POST" value="POST">POST</Option>
            {/*<Option key="PUT" value="PUT">PUT</Option>*/}
            {/*<Option key="DELETE" value="DELETE">DELETE</Option>*/}
        </Select>
    );
  • setUrl是我们自定义的一个变量
  • splitUrl是我们编写的一个自动根据body输入内容更新url内容的一个函数,函数如下:
    // 拆分 URL 并更新 paramsData 和 editableKeys 状态
    const splitUrl = (nowUrl: string) => {
        const split = nowUrl.split('?');
        if (split.length < 2) {
            setParamsData([]);
        } else {
            const params = split[1].split('&');
            const newParams: { key: string; value: string; id: number; description: string }[] = [];
            const keys: number[] = [];
            params.forEach((item, idx) => {
                const [key, value] = item.split('=');
                const now = Date.now();
                keys.push(now + idx + 10);
                newParams.push({key, value, id: now + idx + 10, description: ''});
            });
            setParamsData(newParams);
            setEditableRowKeys(keys);
        }
    };

函数中的一些重设置,主要是一些自定义变量

  1. 编写Body部分

咱们先来尝试编写Body部分,先看看postman的页面:

image.png 这次主要针对红框部分来编写,且此部分比较统一,因此我们可以把它拆出来作为一个表单样式。

  • 新增src/components/Table/EditableTable.tsx文件,编写以下内容作为我们这一栏的样式:
import React, { useEffect } from 'react';
import { EditableProTable } from '@ant-design/pro-table';

// 定义表格列配置的接口
interface TableColumn {
    title: string;
    key: string;
    dataIndex: string;
    // 可以根据实际情况添加其他属性
}

// 定义表格数据项的接口
interface TableDataItem {
    id: number;
    key: string;
    value: string;
    description: string;
    // 可以根据实际情况添加其他属性
}

// 定义可编辑表格的属性接口
interface EditableTableProps {
    columns: TableColumn[];
    dataSource: TableDataItem[];
    title: string;
    setDataSource: (data: TableDataItem[]) => void;
    editableKeys: React.Key[];
    setEditableRowKeys: (keys: React.Key[]) => void;
    extra?: (recordList: TableDataItem[]) => void;
}

// 可编辑表格组件
const EditableTable: React.FC<EditableTableProps> = ({
                                                         columns,
                                                         dataSource,
                                                         title,
                                                         setDataSource,
                                                         editableKeys,
                                                         setEditableRowKeys,
                                                         extra,
                                                     }) => {
    // 当数据源变化时,更新可编辑行的键值列表
    useEffect(() => {
        setEditableRowKeys(dataSource.map((v) => v.id));
    }, [dataSource]);

    return (
        <EditableProTable
            headerTitle={title}
            columns={columns}
            rowKey="id"
            value={dataSource}
            onChange={setDataSource}
            recordCreatorProps={{
                newRecordType: 'dataSource',
                record: () => ({
                    id: Date.now(),
                }),
            }}
            editable={{
                type: 'multiple',
                editableKeys,
                actionRender: (row, config, defaultDoms) => {
                    return [defaultDoms.delete];
                },
                onValuesChange: (record, recordList) => {
                    if (extra) {
                        extra(recordList);
                    }
                    setDataSource(recordList);
                },
                onChange: setEditableRowKeys,
            }}
        />
    );
};

export default EditableTable;

  • 新增我们的index文件中的主样式
<Row style={{marginTop: 8}}>
                    <Tabs defaultActiveKey="1" style={{width: '100%'}}>
                        <TabPane tab="Params" key="1">
                            <EditableTable
                                columns={columns('params')}
                                title="Query Params"
                                dataSource={paramsData}
                                setDataSource={setParamsData}
                                extra={joinUrl}
                                editableKeys={editableKeys}
                                setEditableRowKeys={setEditableRowKeys}>
                            </EditableTable>
                        </TabPane>
                        <TabPane tab="Headers" key="2">
                            <EditableTable
                                columns={columns('headers')}
                                title="Headers"
                                dataSource={headers}
                                setDataSource={setHeaders}
                                editableKeys={headersKeys}
                                setEditableRowKeys={setHeadersKeys}>
                            </EditableTable>
                        </TabPane>
                        <TabPane tab="Body" key="3">
                            //较为复杂,在这进行逻辑占位,详细部分下面讲解
                        </TabPane>
                    </Tabs>
                </Row>

由此就可以见到我们的Parames和Headers两种样式了,并且可以设置相应的表单。

  • 下一个目标,我们要在选择body时,出现json,raw等选项,也就是说,在刚才较为复杂的占位中,进行补充。

目标如图:

image.png

老规矩,先上代码,在进行讲解:

<TabPane tab="Body" key="3">
                            <Row>
                                <Radio.Group
                                    defaultValue={'none'}
                                    value={bodyType}
                                    onChange={(e) => {
                                        setBodyType(e.target.value)
                                    }}
                                >
                                    <Radio value={'none'}>none</Radio>
                                    <Radio value={'form-data'}>form-data</Radio>
                                    <Radio value={'x-www-form-urlencoded'}>x-www-form-urlencoded</Radio>
                                    <Radio value={'raw'}>raw</Radio>
                                    <Radio value={'binary'}>binary</Radio>
                                    <Radio value={'GraphQL'}>GraphQL</Radio>
                                </Radio.Group>
                                {/*如果 bodyType 等于 1,则会渲染一个下拉菜单,菜单内有一个链接,显示 rawType 的值和一个向下的箭头图标。
                                当用户点击链接时,会显示下拉菜单的内容,而不会触发页面跳转。*/}
                                {bodyType === 'raw' ? (
                                    <Dropdown style={{marginLeft: 8}} overlay={menu} trigger={['click']}>
                                        <a onClick={(e) => e.preventDefault()}>
                                            {rawType} <DownOutlined/>
                                        </a>
                                    </Dropdown>
                                ) : null}
                            </Row>
                            {getBody(bodyType)}
                        </TabPane>

由此可见,当我们点到Body时,是会有一些Radio按钮的,如none,form-data等按钮。此为我们body部分的第一部份功能。

  • 第二部分:当我们点几raw的时候,出现一个下拉选择框,来选择JSON,TEXT等

在代码中,也有相应注释,由此可见我们是有一个menu函数来展示的,代码如下:

    const menu = (
        <Menu>
            <Menu.Item key="Text">
                <a onClick={() => onClickMenu('Text')}>Text</a>
            </Menu.Item>
            <Menu.Item key="JavaScript">
                <a onClick={() => onClickMenu('JavaScript')}>JavaScript</a>
            </Menu.Item>
            <Menu.Item key="JSON">
                <a onClick={() => onClickMenu('JSON')}>JSON</a>
            </Menu.Item>
            <Menu.Item key="HTML">
                <a onClick={() => onClickMenu('HTML')}>HTML</a>
            </Menu.Item>
            <Menu.Item key="XML">
                <a onClick={() => onClickMenu('XML')}>XML</a>
            </Menu.Item>
        </Menu>
    );
  1. 编写body文本部分

老规矩,上postman页面,红框就是我们的任务!

image.png

image.png

image.png

在此举例展示,不一一展示了。大概意思就是:

  • none:展示body主体为一个文字描述
  • form-data:展示主体为一个可选类型的表单
  • x-www-form-urlencoded:展示主体为一个表单
  • raw:展示主体为一个编辑框
  • binary,GraphQL。暂时只作为一个占位展示

开始编写body文本部分!

大家应该可以看到,我们在新增body主体样式的时候,有一个{getBody(bodyType)}函数,我们的body文本部分,就写在此函数中。老规矩,先上代码!

    // 根据 Body 类型生成不同的显示内容
    const getBody = bd => {
        if (bd === 'none') {
            return <div style={{height: '20vh', lineHeight: '20vh', textAlign: 'center'}}>
                This request does not have a body
            </div>
        }
        if (bd === 'form-data') {
            return <FormData
                columns={columns('FormData')}
                dataSource={formData}
                setDataSource={setFormData}
                editableKeys = {formDataKeys}
                setEditableRowKeys = {setFormDataKeys}>
            </FormData>
        }
        if (bd === 'binary') {
            return <div style={{height: '20vh', lineHeight: '20vh', textAlign: 'center'}}>
                binary占位!,先给常用功能做出
            </div>
        }
        if (bd === 'GraphQL') {
            return <div style={{height: '20vh', lineHeight: '20vh', textAlign: 'center'}}>
                GraphQL占位!,先给常用功能做出
            </div>
        }
        if (bd === 'x-www-form-urlencoded') {
            return <XForm
                columns={columns('XForm')}
                dataSource={xform}
                setDataSource={setxForm}
                editableKeys = {xformKeys}
                setEditableRowKeys = {setxFormKeys}>
            </XForm>
        }
        return <Row style={{marginTop: 12}}>
            <Col span={24}>
                <Card bodyStyle={{padding: 0}}>
                    <JSONAceEditor value={body} onChange={e => setBody(e)} height="20vh" setEditor={setEditor}/>
                </Card>
            </Col>
        </Row>
    }

代码讲解:

  • 如果类型为none,则显示一个文本
  • binary和GraphQL同理,暂时进行占位
  • 若类型为form-data,则展示一个formData表单,具体代码后面贴出
  • 若类型为x-www-form-urlencoded,则展示xForm表单,具体代码后面贴出
  • 若都不匹配,则显示一个编辑窗口,即raw类型

formData表单代码:

import React, { useState } from 'react';
import { Form, Select } from 'antd';
import { EditableProTable } from '@ant-design/pro-table';

const { Option } = Select;

// 定义表格列配置的接口
interface TableColumn {
    key: string;
    dataIndex: string;
    // 可以根据实际情况添加其他属性
}

// 定义表格数据项的接口,新增了 'type' 属性
interface TableDataItem {
    id: number;
    key: string;
    value: string;
    description: string;
    type: string; // 添加 'type' 属性
    // 可以根据实际情况添加其他属性
}

// 定义 FormDataComponent 组件的属性接口
interface FormDataProps {
    dataSource: TableDataItem[];
    setDataSource: (data: TableDataItem[]) => void;
    setEditableRowKeys: (keys: React.Key[]) => void;
    extra?: (recordList: TableDataItem[]) => void;
    editableKeys: React.Key[];
}

// FormDataComponent 可编辑表格组件
const FormDataComponent: React.FC<FormDataProps> = ({
                                                        dataSource,
                                                        setDataSource,
                                                        setEditableRowKeys,
                                                        extra,
                                                        editableKeys,
                                                    }) => {
    const [form] = Form.useForm();
    const [editingKey, setEditingKey] = useState<string>('');

    // 表格列配置
    const columns: TableColumn[] = [
        {
            title: 'KEY',
            key: 'key',
            dataIndex: 'key',
        },
        {
            title: 'TYPE',
            key: 'type',
            dataIndex: 'type',
            width: '10%',
            valueType: 'select',
            fieldProps: {
                defaultValue: 'TEXT', // 设置默认值为 'TEXT'
            },
            renderFormItem: (_, { value, onChange }) => (
                <Select value={value} onChange={onChange}>
                    <Option value="TEXT">TEXT</Option>
                    {/*<Option value="FILE">FILE</Option>*/}
                </Select>
            ),
        },
        {
            title: 'VALUE',
            key: 'value',
            dataIndex: 'value',
        },
        {
            title: 'DESCRIPTION',
            key: 'description',
            dataIndex: 'description',
        },
        {
            title: 'OPTION',
            valueType: 'option',
        },
    ];

    return (
        <EditableProTable
            columns={columns}
            rowKey="id"
            value={dataSource}
            onChange={setDataSource}
            recordCreatorProps={{
                newRecordType: 'dataSource',
                record: () => ({
                    id: Date.now(),
                    type: 'TEXT', // 设置默认类型为 'TEXT'
                }),
            }}
            editable={{
                type: 'multiple',
                editableKeys,
                actionRender: (row, config, defaultDoms) => {
                    return [defaultDoms.delete];
                },
                onValuesChange: (record, recordList) => {
                    if (extra) {
                        extra(recordList);
                    }
                    setDataSource(recordList);
                },
                onChange: setEditableRowKeys,
            }}
        />
    );
};

export default FormDataComponent;

xForm表单样式代码:

import React from 'react';
import { Props } from "@floating-ui/react-dom-interactions";
import { EditableProTable } from "@ant-design/pro-table";

// 定义表格列配置的接口
interface TableColumn {
    key: string;
    dataIndex: string;
    // 可以根据实际情况添加其他属性
}

// 定义表格数据项的接口
interface TableDataItem {
    id: number;
    key: string;
    value: string;
    description: string;
    // 可以根据实际情况添加其他属性
}

// 定义 xFormComponent 组件的属性接口
interface xFormProps {
    dataSource: TableDataItem[];
    setDataSource: (data: TableDataItem[]) => void;
    setEditableRowKeys: (keys: React.Key[]) => void;
    extra?: (recordList: TableDataItem[]) => void;
    editableKeys: React.Key[];
}

// xFormComponent 组件,可编辑表格
const xFormComponent: React.FC<xFormProps> = ({
                                                  dataSource,
                                                  setDataSource,
                                                  setEditableRowKeys,
                                                  extra,
                                                  editableKeys
                                              }) => {
    // 表格列配置
    const columns = ({ columnType, onDelete }: Props): TableColumn[] => {
        return [
            {
                title: 'KEY',
                key: 'key',
                dataIndex: 'key',
            },
            {
                title: 'VALUE',
                key: 'value',
                dataIndex: 'value',
            },
            {
                title: 'DESCRIPTION',
                key: 'description',
                dataIndex: 'description',
            },
            {
                title: 'OPTION',
                valueType: 'option',
            },
        ];
    };

    return (
        <EditableProTable
            columns={columns('prod')}
            rowKey="id"
            value={dataSource}
            onChange={setDataSource}
            recordCreatorProps={{
                newRecordType: 'dataSource',
                record: () => ({
                    id: Date.now(),
                }),
            }}
            editable={{
                type: 'multiple',
                editableKeys,
                actionRender: (row, config, defaultDoms) => {
                    return [defaultDoms.delete];
                },
                onValuesChange: (record, recordList) => {
                    if (extra) {
                        extra(recordList);
                    }
                    setDataSource(recordList);
                },
                onChange: setEditableRowKeys,
            }}
        />
    );
};

export default xFormComponent;

这两部分代码,分别在Table中管理。如图:

image.png

  1. 编写Response部分

老规矩,原型图如下:

image.png

由此可见,其实我们编写返回值部分,需要完成的有几个部分:

  • Body,Cookies,Headers等选项
  • 接口响应信息
  • 接口数据信息

相应样式的核心代码:

<Row gutter={[8, 8]}>
                    {Object.keys(response).length === 0 ? null : (
                        <Tabs style={{width: '100%'}} tabBarExtraContent={tabExtra(response)}>
                            <TabPane tab="Body" key="1">
                                <JSONAceEditor
                                    readOnly={true}
                                    setEditor={setEditor}
                                    language={response.response_data && response.response_headers.indexOf("json") > -1 ? 'json' : 'text'}
                                    value={response.response_data && typeof response.response_data === 'object' ? JSON.stringify(response.response_data, null, 2) : response.response_data || ''}
                                    height="30vh"
                                />
                            </TabPane>
                            <TabPane tab="Cookie" key="2">
                                <Table
                                    columns={resColumns}
                                    dataSource={toTable('cookies')}
                                    size="small"
                                    pagination={false}
                                />
                            </TabPane>
                            <TabPane tab="Headers" key="3">
                                <Table
                                    columns={resColumns}
                                    dataSource={toTable('response_headers')}
                                    size="small"
                                    pagination={false}
                                />
                            </TabPane>
                        </Tabs>
                    )}
                </Row>

TODO:因为编写代码时忘记了接口耗时等信息,所以有关接口耗时等信息会暂时跳过,留在下一篇。

实际上很简单,大致意思无非就是,tab匹配到body时,展示body的样式,以此类推。在此就不做过多解释,稍后贴出个人信息,先把项目开源出来,大家感兴趣的可以一起加群交流。

  1. 开源相关

组织地址

后端代码地址

前端代码地址

加群一起讨论相关问题呀!如果群二维码过期了,可以加我个人微信: yyi11yy 我拉你进群~

image.png

image.png