从零开始用Axios撸一个Postman Chrome浏览器插件

image.png

image.png

自己在学习Golang Web开发的时候,碰到了post请求和get请求,既要写页面又要开发接口,稀里糊涂的实在头大,所以突发奇想自己动手搞一个简易postman吧!

为了追求速度,所以直接用了基于Vue的饿了么ElementUI,接口请求用的是Axios,使用<pre></pre>标签直接格式化json,虽然不怎么好看,但是够用,凡事讲个够用,哈哈,再根据自己项目优化,岂不美哉。

好了废话不多说,上代码!

postman.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Magic Postman</title>
    <link rel="stylesheet" href="css/style.css">
    <link rel="stylesheet" href="css/element-ui@2.12.0.css">
    <link rel="icon" type="img/png" href="img/icon-128.png">
</head>
<body>
    <div id="main" class="flex" style="height: 100%;">
        <!-- template 防止首次打开抖动出现{{}} -->
        <template>
            <!-- 左侧边栏 -->
            <aside class="postman-aside shrink0 flex-view">
                <div class="flex1 scroll-y">
                    <el-menu default-active="2" class="el-menu-vertical-demo">
                        <el-submenu index="常用">
                            <template slot="title">
                                <i class="el-icon-folder-opened"></i>
                                <span>常用</span>
                            </template>
                            <el-menu-item @click="handleTabsEdit(false, 'add', api)" v-for="(api, index) in apisObject['常用']" :key="index" :index="api.randomIndex">{{api.alias||api.url}}</el-menu-item>
                        </el-submenu>
                        <el-submenu :index="item" v-for="(item, index) in groupList" :key="index">
                            <template slot="title">
                                <i class="el-icon-folder-opened"></i>
                                <span>{{item}}</span>
                            </template>
                            <el-menu-item @click="handleTabsEdit(false, 'add', api)" v-for="(api, index) in apisObject[item]" :key="index" :index="api.randomIndex">{{api.alias||api.url}}</el-menu-item>
                        </el-submenu>
                    </el-menu>
                </div>
                <!-- 左侧边栏底部 -->
                <footer class="postman-footer center">
                    <el-button type="text" @click="groupFuncType = 2;dialog.group = true" icon="el-icon-setting">组管理</el-button>
                    <el-button type="text" @click="dialog.introduce = true" icon="el-icon-info">说明</el-button>
                </footer>
            </aside>
            <!-- 右侧主功能 -->
            <section class="postman-main flex1 mlr12 mtb12">
                <el-tabs v-model="editableTabsValue" type="card" editable @edit="handleTabsEdit">
                    <el-tab-pane :key="item.name" v-for="(item, index) in editableTabs" :label="item.name" :name="item.name">
                        <div class="flex">
                            <!-- 请求类型 -->
                            <el-select v-model="item.method" style="min-width: 100px;">
                                <el-option v-for="(item, index) in methods" :key="index" :label="item" :value="item">
                                </el-option>
                            </el-select>
                            <!-- 发送 -->
                            <el-input @keyup.native.enter="send(item)" v-focus placeholder="请输入要请求的链接地址" v-model="item.url" clearable class="flex1 mlr12"></el-input>
                            <el-dropdown split-button type="primary" style="min-width: 121px;" @click="send(item)" :loading="item.loading">
                                <i class="el-icon-s-promotion"></i> Send <el-dropdown-menu slot="dropdown">
                                    <!-- handleCommand -->
                                    <span @click="handleCommand('常用', item)">
                                        <el-dropdown-item>保存到“常用”</el-dropdown-item>
                                    </span>
                                    <span v-for="(name, index) in groupList" :key="index" @click="handleCommand(name, item)">
                                        <el-dropdown-item>保存到“{{name}}”</el-dropdown-item>
                                    </span>
                                    <span @click="handleCommand('新分组', item)">
                                        <el-dropdown-item>保存为...</el-dropdown-item>
                                    </span>
                                </el-dropdown-menu>
                            </el-dropdown>
                        </div>
                        <div class="flex mtb12">
                            <div class="flex1 fxmiddle">
                                <el-radio-group v-model="item.reqDisplayType">
                                    <el-radio label="key">键对值</el-radio>
                                    <el-radio label="json">JSON文本</el-radio>
                                </el-radio-group>
                            </div>
                            <div>
                                <el-button type="text" @click="setCookie" disabled="true">配置Cookie</el-button>
                            </div>
                        </div>
                        <ul class="">
                            <li v-if="item.reqDisplayType == 'key'">
                                <ul>
                                    <li class="flex mb12" v-for="(p, index) in item.params" :key="index">
                                        <el-input placeholder="请输入Key" @keyup.native="editParams(item.params, 1)" v-model.trim="p.key" clearable style="width: 230px;" class=""></el-input>
                                        <el-input placeholder="请输入Value,或者粘贴对象到此处试试。提示:复杂json请使用json文本模式" v-model="p.value" clearable class="flex1 mlr12" @paste.native="paste(event, item)"></el-input>
                                        <div class="center">
                                            <el-button type="danger" :disabled="item.params.length===1" icon="el-icon-delete" size="mini" circle @click="editParams(item.params, 0, index)"></el-button>
                                        </div>
                                    </li>
                                </ul>
                            </li>
                            <li v-else>
                                <el-input v-model="item.data" placeholder="JSON文本.....复杂json我也能搞定!" type="textarea" :autosize="{ minRows: 4, maxRows: 15}"></el-input>
                            </li>
                        </ul>
                        <div class="mtb12">
                            <!-- <el-input placeholder="返回....." type="textarea" rows="10"><pre>{{item.response}}</pre></el-input> -->
                            <div contenteditable="true" class="response-box">
                                <pre>{{item.response}}</pre>
                            </div>
                        </div>
                    </el-tab-pane>
                </el-tabs>
            </section>
            <!-- 分组设置 -->
            <el-dialog :title="groupFuncType === 2 ? '组管理' : '请选择一个分组'" :visible.sync="dialog.group" width="600px">
                <ul>
                    <li class="flex">
                        <el-radio v-model="selectGroup" label="">
                            <el-input v-focus @keyup.native.enter="createNewGroup()" v-model.trim="newGroupName" style="width: 525px;" placeholder="若新建分组,请在这里输入新分组名称,回车键确认" :maxlength="16" clearable class="flex1 ml12"></el-input>
                        </el-radio>
                    </li>
                    <li class="flex mt12">
                        <div class="flex1 fxmiddle">
                            <el-radio v-model="selectGroup" label="常用"><span class="ml12">常用</span></el-radio>
                        </div>
                        <div class="center">
                            <el-button type="danger" @click="createNewGroup('常用')" icon="el-icon-delete" size="mini" circle></el-button>
                        </div>
                    </li>
                    <li class="flex mt12" v-for="(item, index) in groupList" :key="index">
                        <div class="flex1 fxmiddle">
                            <el-radio v-model="selectGroup" :label="item"><span class="ml12">{{item}}</span></el-radio>
                        </div>
                        <div class="center">
                            <el-button type="danger" icon="el-icon-delete" size="mini" circle @click="createNewGroup(item, index)"></el-button>
                        </div>
                    </li>
                </ul>
                <span slot="footer" class="dialog-footer">
                    <el-button type="primary" @click="saveAs" :disabled="!selectGroup||groupFuncType === 2">确 定</el-button>
                </span>
            </el-dialog>
            <!-- 说明 -->
            <el-dialog title="说明以及待开发" :visible.sync="dialog.introduce" width="600px">
                <ul class="">
                    <li class="middle flex mb12">
                        <img src="img/icon-96.png" alt="">
                        <p class="padtop10">Magic Postman ver 1.0.0</p>
                    </li>
                    <li class="flex mt12">
                        <el-checkbox><span class="ml12">自定义携带cookie功能待开发,碰到此类网站,可以先登录,登录后随意跨域调试</span></el-checkbox>
                    </li>
                    <li class="flex mt12">
                        <el-checkbox><span class="ml12">删除组内接口,给接口设定别名后保存待开发</span></el-checkbox>
                    </li>
                    <li class="flex mt12">
                        <el-checkbox><span class="ml12">特殊情况未捕获可以F12打开调试面板查看</span></el-checkbox>
                    </li>
                    <li class="flex mt12">
                        <el-checkbox><span class="ml12">页面UI优化,比如链接遮挡,样式丑陋等</span></el-checkbox>
                    </li>
                </ul>
            </el-dialog>
        </template>
    </div>
</body>
<script type="text/javascript" src="js/vue.min.js"></script>
<script src="js/element-ui@2.12.0.js"></script>
<script type="text/javascript" src="js/vue.directive.js"></script>
<script type="text/javascript" src="js/axios@0.19.0.js"></script>
<script type="text/javascript" src="js/postman.js"></script>
</html>

js/postman.js如下

/* cookie */
axios.defaults.withCredentials = true
let app = new Vue({
    el: "#main",
    data: {
        /* 支持的方式 */
        methods: ['GET', 'POST', 'PUT', 'DELETE'],
        /* 打开的Tabs */
        editableTabs: [],
        editableTabsValue: '1',
        /* 初始化显示api配置 */
        config: {
            method: 'GET',
            name: '',
            url: '',
            reqDisplayType: 'key',
            params: [{
                key: '',
                value: ''
            }],
            data: '{ "":"" }',
            response: '{}',
            loading: false
        },
        /* 弹窗控制层 */
        dialog: {
            group: false,
            introduce: false
        },
        /* Api分组相关 开始 */
        groupList: [],
        selectGroup: '',
        tabIndex: 0,
        userApiGroup: [{ name: '常用', list: [] }],
        newGroupName: '',
        apisObject: {},
        /* 组管理2,保存1 */
        groupFuncType: 0,
        /* Api分组相关结束 */
    },
    methods: {
        /* 发送请求 */
        send(item) {
            if (!item.url) { return }
            if(!item.url.includes('http://')&&!item.url.includes('https://')){
                return this.$message.warning('链接好像不怎么对~')
            }
            let config = { ...item }
            let temp = {}
            if (item.reqDisplayType === 'key') {
                for (let m of item.params) {
                    if (m.key !== '') {
                        temp[m.key] = m.value
                    }
                }
            } else {
                temp = JSON.parse(item.data)
            }
            /* 删除key为空的对象 */
            for (let i in temp) {
                if (i === '') {
                    delete temp[i]
                }
            }
            /* 处理Axios,get方法使用params,post方法用data */
            config.data = temp
            if (config.method === "GET") {
                config.params = config.data
                delete config.data
            }
            delete config.reqDisplayType
            delete config.response
            console.log(config)
            item.loading = true
            axios(config).then(res => {
                item.response = res.data
                item.loading = false
            }).catch(err => {
                let res = err.response
                item.response = res.data || { status: res.status, statusText: res.statusText }
                console.log(err.response)
                item.loading = false
            })
        },
        /* tabs 增加tab和删除tab操作 */
        handleTabsEdit(targetName, action, api) {
            if (action === 'add') {
                let newTabName = 'New Tab ' + (++this.tabIndex + '')
                if (api) {
                    let temp = { ...api }
                    temp.name = newTabName
                    temp.response = '{}'
                    this.editableTabs.push(temp)
                } else {
                    this.config.name = newTabName
                    this.editableTabs.push({ ...this.config })
                }
                this.editableTabsValue = newTabName
            }
            if (action === 'remove') {
                let tabs = this.editableTabs;
                if (tabs.length <= 1) {
                    return this.$message.warning('做人留一线,日后好相见!')
                }
                let activeName = this.editableTabsValue
                if (activeName === targetName) {
                    tabs.forEach((tab, index) => {
                        if (tab.name === targetName) {
                            let nextTab = tabs[index + 1] || tabs[index - 1]
                            if (nextTab) {
                                activeName = nextTab.name
                            }
                        }
                    });
                }
                this.editableTabsValue = activeName
                this.editableTabs = tabs.filter(tab => tab.name !== targetName)
            }
        },
        /* 键对值操作,自动新增一条空行, []params, action: (1新增,0删除),index: 操作的行*/
        editParams(items, action, index) {
            let length = items.length
            if (action) {
                let empty = items.filter(m => m.key === '')
                /* 有空key就不新增行 */
                if (!empty.length) {
                    items.push({ key: '', value: '' })
                }
            } else {
                /* 如果是最后一个不删除,只清空 */
                if (length === 1) {
                    items[0].key = items[0].value = ''
                } else {
                    items.splice(index, 1)
                }
            }
        },
        /* 处理粘贴操作 */
        paste(event, item) {
            let clipboardData = event.clipboardData || window.clipboardData
            if (clipboardData) {
                let txt = clipboardData.getData('Text')
                txt = txt.replace(/\/n/g, '').replace(/\/r/g, '')
                if (txt) {
                    try {
                        /* 只处理是对象的情况,使用eval是方式{"key": ''}, 中key没有引号包裹无法转换 */
                        txt = eval('(' + txt + ')')
                        let arr = []
                        if (txt.constructor === Object) {
                            for (let i in txt) {
                                arr.push({
                                    key: i,
                                    value: txt[i]
                                })
                            }
                            setTimeout(() => {
                                item.params = arr
                            }, 100)
                        }
                    } catch (e) {}
                }
            }
        },
        /* str要转化的内容, type:函数接收的类型 */
        str2json(str, type) {
            try {
                let m = JSON.parse(str)
                if (type === 'Object') {
                    return m.constructor === Object ? m : ''
                } else if (type === 'Array') {
                    return m.constructor === Array ? m : ''
                } else {
                    return m
                }
            } catch (e) {
                console.log(e)
                return ''
            }
        },
        /* 自定义携带的cookie待开发 */
        setCookie() {},
        /* 保存到分组 */
        saveAs() {
            this.handleCommand(this.selectGroup, this.tempApiConfig)
        },
        /* 另存下拉点击事件 name:组名,api:当前展开的api*/
        handleCommand(name, api) {
            if (name === '新分组') {
                this.groupFuncType = 1
                this.dialog.group = true
                this.tempApiConfig = api
            } else {
                if (!api.url) {
                    return this.$message.warning('无法另存空链接')
                }
                // 处理当前Api信息
                let item = { ...api }
                delete item.response
                delete item.name
                delete item.loading
                item.randomIndex = Math.random()
                let oldApis = this.apisObject[name] || []
                oldApis.push(item)
                localStorage.setItem('_UserApis_' + name, JSON.stringify(oldApis))
                this.getGroupList()
                this.mapAllGroup()
                this.dialog.group = false
            }
        },
        /* 新建或删除分组 item:分组名,index:行号*/
        createNewGroup(item, index) {
            if (item) {
                /* 删除操作 */
                localStorage.clear('_UserApis_' + item)
                if (item === '常用') {
                    this.$message.success('组内所有api已清空')
                    this.apisObject['常用'] = []
                } else {
                    this.groupList.splice(index, 1)
                }
            } else {
                /* 创建 */
                let ngm = this.newGroupName
                let gl = this.groupList
                if (!ngm) { return }
                if (gl.includes(ngm) || ngm === '常用' || ngm === '新分组') {
                    return this.$message.warning(`组名:${ngm} 已存在,或不可用`)
                } else {
                    this.groupList.push(ngm)
                }
                this.selectGroup = ngm
                this.newGroupName = ''
            }
            localStorage.setItem('_UserGroups', JSON.stringify(this.groupList))
            this.getGroupList()
        },
        /* 从本地读取组并赋值 */
        getGroupList() {
            let groups = this.str2json(localStorage.getItem('_UserGroups'), 'Array')
            this.groupList = groups || []
        },
        /* 根据组名获取所有的api */
        getApisByGroup(name) {
            let apis = this.str2json(localStorage.getItem('_UserApis_' + name), 'Array')
            if (apis) {
                this.apisObject[name] = apis
            }
        },
        /* 遍历所有组并获取api */
        mapAllGroup() {
            this.getApisByGroup('常用')
            this.groupList.forEach(item => {
                this.getApisByGroup(item)
            })
        }
    },
    computed: {},
    created() {
        /* 创建一个tab */
        this.handleTabsEdit(false, 'add')
        /* 获取所有组 */
        this.getGroupList()
        /* 遍历所有组并获取api */
        this.mapAllGroup()
    }
})

代码中我也尽可能增加了注释,最关键的是manifest.json的配置,关于browser_action,permissions,以及对popup添加添加点击事件,对插件鼠标右键添加事件

/*--------------右键菜单添加--------------*/
chrome.contextMenus.create({
    title: '接口调试工具',
    onclick() {
        window.open('../postman.html')
    }
}, function (e) {
})
/* 为图标添加点击事件 */
chrome.browserAction.onClicked.addListener(function() {
    window.open('../postman.html')
})

manifest.json解释了权限,在chrome插件中使用vue,做了详细备注

{
    "background": {
        /* 只执行一次,用来写入到鼠标右键 */
        "scripts": ["js/background.js"]
    },
    "author": "TEEMO",
    "description": "基于Vue,Axios的简易Postman插件,UI基于Eleme 2.1.2.0",
    "icons": {
      "128": "img/icon-128.png",
      "16": "img/icon-16.png",
      "32": "img/icon-32.png",
      "48": "img/icon-48.png",
      "96": "img/icon-96.png"
   },
    "manifest_version": 2,
    "name": "Magic Postman",
    "offline_enabled": true,
    "browser_action": {
        "default_icon": "img/icon-96.png",
        /* "default_popup": "postman.html", */
        "default_title": "Magic Postman"
    },
    /* ***重要*** 配置安全选项,配置后才可以使用Vue*/
    "content_security_policy": "style-src 'self' 'unsafe-inline';script-src 'self' 'unsafe-eval'; object-src 'self' ; media-src 'self' filesystem:",
    /* 插件要获取的浏览器用户权限 */
    "permissions": [
        "background", 
        "contextMenus", 
        "geolocation", 
        "management", 
        "topSites", 
        "bookmarks", 
        "unlimitedStorage", 
        "topSites", 
        "identity",
        /* ***重要*** 允许所有http,https请求可以跨域 */
        "http://*/", "https://*/", 
        "chrome://favicon/", 
        "history", 
        "alarms", 
        "notifications", 
        "tabs", 
        "storage", 
        "activeTab", 
        "declarativeContent", 
        "webNavigation", 
        "webRequest", 
        "webRequestBlocking", 
        "cookies"
    ],
    "version": "1.0.0"
}

最终的代码请看 https://gitlab.com/qiaen/magic-postman

看到这篇文章的小伙伴们加油,砥砺前行,在编程的路上越走越顺,用双手创造美好的生活!

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章