开源一个通用的 HTTP 请求前端组件

Posted sp42a

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开源一个通用的 HTTP 请求前端组件相关的知识,希望对你有一定的参考价值。

像 Postman 这样可视化的 HTTP 请求工具是调试 API 不可或缺的利器。Postman 虽好但也越来越重,而且如果要整合到其他工具中,显然 Postman 又不是一个可行的方案。于是我想打造一个简单的前端组件(widget),它是一个标准 Vue 可复用的组件,能够轻易地被整合。整个组件最终界面如下。

该组件特性:

  • 基于 vue2/iview 方案,标准 npm 前端项目
  • 代码短小精悍,不过几个文件,不到一千行代码。
  • 依赖只有 iview 和 vue-codemirror,低耦合
  • 功能简单清晰易上手,代码易于理解与扩展

构建这么一个小组件可以说根本没什么难度,我也是一边仿着 Postman,一边“画界面”,两三天就完事了。当然后续还有很多的想法,很多的功能需要添加。不过前期肯定先出个初版,从最简单的开始。

名字就叫平淡无奇的 “api-helper” 吧~ 在线演示 | 源码 | NPM 发布页

使用方式

这是标准 vue 组件,安装组件:

npm i @ajaxjs/aj-api-helper

或者在 package.json 中添加依赖然后执行 npm i

  "dependencies": 
    "@ajaxjs/aj-api-helper": "1.0.0"
    ……
  ,

引入方式:

import ApiHelperMain from "@ajaxjs/aj-api-helper";

export default 
  components:  ApiHelper: ApiHelperMain.ApiHelper ,
  ……

标签中引入:<ApiHelper />

开发历程心得

整体界面

整体界面就是调用 ivew 组件库,这部分没什么好多说的了。代码编辑器使用了 vue-codemirror,也比较简单。

<!-- JSON 源码编辑器-->
<codemirror class="code-editor" v-model="responseBody" :options="cmOption" style="height:300px;"></codemirror>

vue-codemirror 配置如下:

cmOption: 
    tabSize: 4,
    styleActiveLine: true,
    lineNumbers: true,
    mode: "application/json",
    // theme: "monokai"
,

编辑表格

各种 Form、QueryString、Head 需要一个表格放置参数,类似于 Postman 的:

一开始打算使用 iView 的 table 组件,但感觉太笨重,于是还是用原生 <table> 自己搞一个。


这样无论源码还是界面显得清爽很多。实际源码如下:

<template>
  <table class="input-table">
    <thead>
      <th width="50"></th>
      <th>Key</th>
      <th>Value</th>
      <th>说明</th>
      <th>操作</th>
    </thead>
    <tr v-for="(item, index) in tableData" :key="index" :class="disable: !item.enable">
      <td align="center">
        <input type="checkbox" v-model="item.enable" />
      </td>
      <td>
        <input @focus="onInputFocus" @blur="onInoutBlur" @input="onInput(index)" v-model="item.key" />
      </td>
      <td>
        <input @focus="onInputFocus" @blur="onInoutBlur" v-model="item.value" />
      </td>
      <td>
        <input @focus="onInputFocus" @blur="onInoutBlur" v-model="item.desc" />
      </td>
      <td align="center">
        <Icon type="md-trash" class="delBtn" title="删除" @click="delRow(index)" />
      </td>
    </tr>
  </table>
</template>

<script>
export default 
  data() 
    return 
      tableData: this.data,
    ;
  ,
  props: 
    data:  type: Array, required: true ,
  ,
  methods: 
    onInputFocus(e) 
      let input = e.target;
      if (
        input.parentNode &&
        input.parentNode.parentNode &&
        input.parentNode.parentNode.tagName == "TR"
      ) 
        let tr = input.parentNode.parentNode;
        tr.classList.add("highlight");
      
    ,
    onInoutBlur(e) 
      let input = e.target;
      if (
        input.parentNode &&
        input.parentNode.parentNode &&
        input.parentNode.parentNode.tagName == "TR"
      ) 
        let tr = input.parentNode.parentNode;
        tr.classList.remove("highlight");
      
    ,
    onInput(index) 
      if (index + 1 == this.tableData.length) 
        // 最后一行
        this.tableData.push(
          enable: true,
          key: "",
          value: "",
          desc: "",
        );
      
    ,
    delRow(index) 
      if (this.tableData.length == 1) 
       else 
        this.$delete(this.tableData, index);
      
    ,
  ,
;
</script>

<style lang="less" scoped>
.input-table 
  width: 100%;
  border-collapse: collapse;

  input 
    border: 1px solid transparent;
    outline: none;
    padding: 0px 3px;
    width: 100%;
  

  input:focus 
    border: 1px solid lightgray !important;
    background-color: white !important;
  

  tr 
    &.highlight 
      background-color: #f9f9f9;

      input 
        border-color: #f9f9f9;
        background-color: #f9f9f9;
      
    

    &.disable 
      input 
        color: lightgray;
      
    
  

  td,
  th 
    padding: 5px 5px;
    border: 1px solid lightgray;
  


.delBtn 
  cursor: pointer;

</style>

输入的 tableData 格式如下。

 tableData: [
   
     enable: true,
     key: "sdsd",
     value: "sdssds3",
   ,
   
     enable: false,
     key: "sdsd",
     value: "sdssds3",
   ,
 ],

XHR 请求

HTTP 请求的核心自然是 XMLHttpRequest(); 的使用。必须要高度订制化,而不是复用某个 xhr 组件。写原生 xhr 实际也很简单,参见我的 xhr.js 源码。

//  XHR 发送组件
export default 
    data() 
        return 
            loading: false,
            response: 
                readyState: 0,
                status: 0,
                elapsed: 0,
            ,
        ;
    ,

    methods: 
        doRequest(method, url, params, cfg) 
            let el = new Date();
            let xhr = new XMLHttpRequest();
            xhr.open(method, url);
            xhr.timeout = 5000; // 设置超时时间为5秒
            xhr.ontimeout = () => this.loading = false;// 请求超时后的处理
            xhr.onreadystatechange = () => 
                this.loading = true;
                this.response.readyState = xhr.readyState;
                this.response.status = xhr.status;

                if (xhr.readyState === 4) 
                    try 
                        if (!xhr.responseText) 
                            this.$Message.error('服务端返回空的字符串');
                            this.loading = false;

                            return;
                        

                        // 跨域可能不能获取完整的响应头 https://qzy.im/blog/2020/09/can-not-get-response-header-using-javascript-in-cors-request/
                        let heads = xhr.getAllResponseHeaders();
                        heads = heads.split(';').join('\\n');
                        this.responseHead = heads;

                        let parseContentType = cfg && cfg.parseContentType;
                        switch (parseContentType) 
                            case "text":
                                data = responseText;
                                break;
                            case "xml":
                                data = xhr.responseXML;
                                break;
                            case "json":
                            default:
                                this.responseBody = JSON.stringify(JSON.parse(xhr.responseText), null, 2);
                        
                     catch (e) 
                        alert("HTTP 请求错误:\\n" + e + "\\nURL: " + url); // 提示用户 异常
                     finally 
                        this.loading = false;
                        this.response.elapsed = new Date() - el;
                    
                
            ;

            let requestAll = 'HEAD \\n' + method.toUpperCase() + ' ' + url + '\\n';

            if (cfg && cfg.header) 
                for (let i in cfg.header) 
                    requestAll += i + " : " + cfg.header[i] + '\\n';
                    xhr.setRequestHeader(i, cfg.header[i]);
                
            

            if (params)
                requestAll += 'BODY:\\n' + params;

            this.requestAll = requestAll;
            xhr.send(params || null);
        ,

        formatStatusCode() 
            let code = this.response.status;
            let str = code + '';

            if (str[0] === '2')
                return `<span style="color:green">$code</span>`;
            else if (str[0] === '4' || str[0] === '5')
                return `<span style="color:red">$code</span>`;
            else
                return str;
        
    ,
;

它基于 vue 的 mixins 特性进行分离。主意是控制一些请求状态之类的,已经返回特定的数据给前端显示。

工具函数

还有一些小的工具函数值得说下。

格式化 JSON

格式化 JSON,利用 JSON.stringify(),指定第三个参数即可加入缩进(indent)。

formatJs() 
    let json = this.requestParams.raw.json;
    json = JSON.stringify(JSON.parse(json), null, 4);
    this.requestParams.raw.json = json;

读写剪贴板

用 Javascript 的 navigator.clipboard 对象来复制文本或图片到剪贴板,但运行时会遇到以下错误:

navigator.clipboard undefined

这个错误的原因是 navigator.clipboard 对象只能在安全网络环境中才能使用,换言之,localhost、127.0.0.1 或者 https 中才能正常使用,否则用 http 或 IP 地址不允许访问。

最终的读取方法:

// 读取粘贴板
try 
    navigator.clipboard.readText().then((v) => 
        console.log("获取剪贴板成功:", v);
        this.requestParams.head.unshift(
            enable: true,
            key: "Authorization",
            value: "Bearer " + v,
            desc: "认证用的 token",
        );
    ).catch((v) => 
        console.log("获取剪贴板失败: ", v);
    );
 catch (e) 
    console.log(e);
    this.$Message.error('不支持读取粘贴板');

写入剪切板却有兼容方法。

/**
* 复制文字到剪切板
* 
* @param * text 
*/
aj.copyToClipboard = function (text) 
    if (navigator.clipboard) 
        // clipboard api 复制
        navigator.clipboard.writeText(text);
     else 
        var textarea = document.createElement('textarea');
        document.body.appendChild(textarea);
        // 隐藏此输入框
        textarea.style.position = 'fixed';
        textarea.style.clip = 'rect(0 0 0 0)';
        textarea.style.top = '10px';
        // 赋值
        textarea.value = text;
        // 选中
        textarea.select();
        // 复制
        document.execCommand('copy', true);
        // 移除输入框
        document.body.removeChild(textarea);
    

小结

虽然这只是个小工具,但仍有不少的想象空间。不一定都把功能加到这个组件里面,但可能跟其他组件有更多的联动。希望我有时间,能够进一步丰富各种功能,也希望你们用户能提出多的宝贵意见!

以上是关于开源一个通用的 HTTP 请求前端组件的主要内容,如果未能解决你的问题,请参考以下文章

源码时代前端干货分享|从零动手封装一个通用的vue按钮组件

Vue2.0的通用组件

饿了么基于Vue2.0的通用组件开发之路(分享会记录)

Vue通用组件的封装

Vue多标签页后台管理系统

android基于开源网络框架asychhttpclient,二次封装为通用网络请求组件