打造一个可视化接口自动化测试系统

现如今,接口开发几乎成为一个互联网公司的标配了,无论是web还是app,哪怕是小程序,都离不开接口作为支撑,当然,这里的接口范围很广,从http到websocket,再到rpc,只要能实现数据通信的都可以称之为接口,面临着如此庞大的接口数据,如果更好的管理和测试他们都是一个比较头疼的问题,更主要的是很多业务场景是需要多个接口进行联调的,因此在接口开发完成后,一轮自动化测试能快速反馈出当前系统的状况,面对这样的需求,一个对测试人员友好的可视化接口自动化测试系统就显得必不可少了。那么,我们今天就来和大家聊聊如何实现一个小型的http接口自动化测试系统!

我们拿DOClever 做为这套系统的范本进行阐述,因为它是开源的,源码随时可以从GitHub和OSChina上获取,同时,这套系统内置了完整的自动化测试框架,从无需一行代码的UI测试用例编写,到更强大更灵活的代码模式,都提供了很友好的支持。

系统需求:
  1. 能在一个测试用例里可以对一个接口自由编辑其入参,运行并判断出参是否正确,同时可以查看该接口完整的输入输出数据

  2. 能在一个测试用例里可以对一组接口进行测试,自由调整他们的执行顺序,并根据上一接口的出参作为下一接口的入参条件。

  3. 能实现基本的逻辑判断,比如if,elseif,同时可以自定义变量用于存储临时值,并且定义当前用例的返回值。

  4. 提供一组辅助工具,可以快速实现数据打印,断言,用户输入,文件上传等操作。

  5. 能在一个测试用例里嵌入其他的测试用例,并自由对其测试用例传参,获取返回值来实现数据上的联动

  6. 当用户输入时,可以实现快速提示,自动完成,让用例的编辑更友好!

准备条件:

1.我们采用nodejs+mongodb的架构设计,node端采用express框架,当然你也可以根据你的喜好选择koa或者其他框架

2.前端我们采用vue+elementUI来实现展示,这样做无非是为了数据的快速响应和element提供丰富的UI支持来帮助我们快速搭建可视化页面。

架构设计:

先给出一张自动化测试的动态图:

那么,我们首先就从最基层的代理服务端来说起如果对接口数据进行转发。

所谓的接口数据转发无非就是用node做一层代理中转,好在node其实很擅长做这样的工作,我们把每一次的接口请求都看作是对代理服务端的一次post请求,接口的真实请求数据就直接作为post请求数据发给代理服务器,接口的host,path,method等数据都会包装在post请求的http header里面,然后我们用node的stream直接pipe到真实请求上去,在接受到真实的接口返回数据后,会把这个数据pipe到原先post请求的response上面去,这样就完成了一次代理转发。

有几点需要注意的是:

1.你在发送请求前需要判断当前的请求是http还是https,因为这涉及到两个不同的node库。

2.你在转发真实请求前,需要对post过来的http header进行一次过滤,过滤掉host,origin等信息,保留客户需要请求的自定义头部和cookies.

3.很多时候,接口返回的可能是一个跳转,那么我们就需要处理这个跳转,再次请求这个跳转地址并接受返回数据.

4.我们需要对接口返回过来的数据进行一个一次过滤,重点是cookie,我们需要处理set-cookie这个字段,去掉浏览器不可写的部分,这样才能保证我们调用登陆接口的时候,可以在本地写入正确的cookie,让浏览器记住当前的登陆状态!

5.我们用一个doclever-request自定义头部来记录一次接口请求的完整request和response过程!下面是实现的核心代码,在此列举出来:


var onProxy = function (req, res) {
    counter++;
    var num = counter;
    var bHttps=false;
    if(req.headers["url-doclever"].toLowerCase().startsWith("https://"))
    {
        bHttps=true;
    }
    var opt,request;
    if(bHttps)
    {
        opt= {
            host:     getHost(req),
            path:     req.headers["path-doclever"],
            method:   req.headers["method-doclever"],
            headers:  getHeader(req),
            port:getPort(req),
            rejectUnauthorized: false,
            requestCert: true,
        };
        request=https.request;
    }
    else
    {
        opt= {
            host:     getHost(req),
            path:     req.headers["path-doclever"],
            method:   req.headers["method-doclever"],
            headers:  getHeader(req),
            port:getPort(req)
        };
        request=http.request;
    }
    var req2 = request(opt, function (res2) {
        if(res2.statusCode==302)
        {
            handleCookieIfNecessary(opt,res2.headers);
            redirect(res,bHttps,opt,res2.headers.location)
        }
        else
        {
            var resHeader=filterResHeader(res2.headers)
            resHeader["doclever-request"]=JSON.stringify(handleSelfCookie(req2));
            res.writeHead(res2.statusCode, resHeader);
            res2.pipe(res);
            res2.on('end', function () {

            });
        }
    });
    if (/POST|PUT|PATCH/i.test(req.method)) {
        req.pipe(req2);
    } else {
        req2.end();
    }
    req2.on('error', function (err) {
        res.end(err.stack);
    });
};

给大家截取一个向代理服务器发送post请求的数据截图:

可以看到在request headers里面headers-doclever,methos-doclever,path-doclever,url-doclever都代表了真实接口的请求基本数据信息。而在request payload里面则是真实请求的请求体。

那么,我们顺着请求分发往上走,先来看看整个自动化测试的最上层,也就是h5可视化界面的搭建(核心部分留到最后再说)。

先给各位上个图:

ok,看起来界面并不复杂,我先来说下大概的思路。

  1. 上图中每一个按钮都可以生成一个测试节点,比如我点击接口,就会插入一个接口在图上的下半部分显示,每一个节点都有自己的数据格式。

  2. 每一个节点都会生成一个ID,代表这个节点的唯一标识,我们可以拖拽节点改变节点的位置,但是ID是不变的。

当我们点击运行按钮的时候,系统会根据当前的节点顺序生成伪代码。

上图生成的伪代码就是

var $0=await 获取培训列表数据({param:{},query:{},header:{},body:{},});
log("打印log:");
var $2=await 天天(...[true,"11",]);
var $3=await ffcv({param:{},query:{},header:{aa:Number("3df55"),gg:"",},body:{},});
var $4=await mm(...[]);

上图中蓝色部分就是需要测试的接口,而橘黄色就是嵌入的其他用例,我们可以看到接口的运行我们是可以传入我们自定义的入参的,param,query,header和body的含义我相信大伙都能明白,而用例的传参我们则是用了es6的一个语法参数展开符来实现,这样就可以把一个数组展开成参数,在这里有几点要说明的:

  1. 因为无论是接口还是用例执行的都是一个异步调用的过程,所以我们在这里需要用await来等待异步的执行完成(这也决定了该系统只能运行在支持es6的现代浏览器上)

  2. 那些蓝色和橘黄色文字的本质是什么呢,在这里是一个html的link标签,在后面会被转换成一个函数闭包(后面会详细解释)

         3.关于上下接口数据的关联,因为每个节点都有唯一的ID,这里

0.data.username代表的就是获取培训列表数据这个接口返回数据里面的username这个字段的值。

OK,我们回到我们之前的话题上面来,如何在可视化界面上生成这些测试节点呢,比如我们点击按钮,会发生哪些事情呢。

  1. 首先我们点击接口按钮,会弹出一个选择框让我们选择接口信息,这里的接口数据采集大家可以自定义,选择自己喜欢的格式就行,如下图:

  1. 点击保存后,接口的数据会被以JSON的格式存储在测试节点中,大致格式如下:
    
    {
        type:"interface",
        id:id,
        name: "info",   //接口名称
        data:JSON.stringify(obj),   //obj就是接口的json数据
        argv:{                //这里是外界的接口入参,也就是上图中被转换成伪代码的接口入参部分
            param:{},
            query:{},
            header:{},
            body:{}
        },
        status:0,   //当前接口的运行状态
        modify:0      //接口数据是否被修改
    }

3.然后我们用一个array存储这个节点信息,在vue里面用一个v-for加上el-row就可以将这些节点展现出来。
那么如何去决定一个测试用例的是否测试通过呢,我们这里会用到测试用例的返回值,如下图所示:

未判定就是表示当前用例执行结果未知,通过就是用例通过,不通过就是用例不通过,同时,我们还可以定义返回参数。该节点生成的数据结构如下:

{
    type:"return",
    id:_this.getNewId(),      //获取新的ID
    name:(ret=="true"?"通过":(ret=="false"?"不通过":"未判定")),
    data:ret,     //true:通过,false:未通过 undefined:未判定
    argv:argv    //返回参数
}

所有节点的完整数据结构信息可以参考GitHub和OSChina里面的源代码
好的,我们继续往下说,当我们点击运行按钮的时候,测试节点会被转换成伪代码,这一块比较好理解,比如接口节点就会根据数据结构信息转换成
var $0=await 获取培训列表数据({param:{},query:{},header:{},body:{},});
这样的形式,核心转换代码如下:


helper.convertToCode=function (data) {
    var str="";
    data.forEach(function (obj) {
        if(obj.type=="interface")
        {
            var argv="{";
            for(var key in obj.argv)
            {
                argv+=key+":{";
                for(var key1 in obj.argv[key])
                {
                    argv+=key1+":"+obj.argv[key][key1]+","
                }
                argv+="},"
            }
            argv+="}"
            str+=`<div class='testCodeLine'>var $${obj.id}=await <a href='javascript:void(0)' style='cursor: pointer; text-decoration: none;' type='1' varid='${obj.id}' data='${obj.data.replace(/\'/g,"&apos;")}'>${obj.name}</a>(${argv});</div>`
        }
        else if(obj.type=="test")
        {
            var argv="[";
            obj.argv.forEach(function (obj) {
                argv+=obj+","
            })
            argv+="]";
            str+=`<div class='testCodeLine'>var $${obj.id}=await <a type='2' href='javascript:void(0)' style='cursor: pointer; text-decoration: none;color:orange' varid='${obj.id}' data='${obj.data}' mode='${obj.mode}'>${obj.name}</a>(...${argv});</div>`
        }
        else if(obj.type=="ifbegin")
        {
            str+=`<div class='testCodeLine'>if(${obj.data}){</div>`
        }
        else if(obj.type=="elseif")
        {
            str+=`<div class='testCodeLine'>}else if(${obj.data}){</div>`
        }
        else if(obj.type=="else")
        {
            str+=`<div class='testCodeLine'>}else{</div>`
        }
        else if(obj.type=="ifend")
        {
            str+=`<div class='testCodeLine'>}</div>`
        }
        else if(obj.type=="var")
        {
            if(obj.global)
            {
                str+=`<div class='testCodeLine'>global["${obj.name}"]=${obj.data};</div>`
            }
            else
            {
                str+=`<div class='testCodeLine'>var ${obj.name}=${obj.data};</div>`
            }
        }
        else if(obj.type=="return")
        {
            if(obj.argv.length>0)
            {
                var argv=obj.argv.join(",");
                str+=`<div class='testCodeLine'>return [${obj.data},${argv}];</div>`
            }
            else
            {
                str+=`<div class='testCodeLine'>return ${obj.data};</div>`
            }
        }
        else if(obj.type=="log")
        {
            str+=`<div class='testCodeLine'>log("打印${obj.name}:");log((${obj.data}));</div>`
        }
        else if(obj.type=="input")
        {
            str+=`<div class='testCodeLine'>var $${obj.id}=await input("${obj.name}",${obj.data});</div>`
        }
        else if(obj.type=="baseurl")
        {
            str+=`<div class='testCodeLine'>opt["baseUrl"]=${obj.data};</div>`
        }
        else if(obj.type=="assert")
        {
            str+=`<div class='testCodeLine'>if(${obj.data}){</div><div class='testCodeLine'>__assert(true,${obj.id},"${obj.name}");${obj.pass?"return true;":""}</div><div class='testCodeLine'>}</div><div class='testCodeLine'>else{</div><div class='testCodeLine'>__assert(false,${obj.id},"${obj.name}");</div><div class='testCodeLine'>return false;</div><div class='testCodeLine'>}</div>`
        }
    })
    return str;
}

可以看到,上面的代码把每个测试节点就转换成了html的节点,这样既可以在网页上直接展示,也方便接下来的解析成真正的javascript可执行代码。
好,接下来我们进入整个系统最核心,最复杂的部分,如何把上述的伪代码转换成可执行代码去请求真实的接口,并将接口的状态和信息返回的呢!
我们先来用一张表表示下这个过程:

如果对软件测试、接口测试、自动化测试、面试经验交流。感兴趣可以加软件测试交流:1085991341,还会有同行一起技术交流。
我们一个个步骤来看下:1.对转换后的html节点进行解析,将接口和测试用例的link节点替换成函数闭包,基本代码表示如下:


var ele=document.createElement("div");
ele.innerHTML=code;      //将html的伪代码赋值到新节点的innerHTML中
var arr=ele.getElementsByTagName("a"); //获取当前所有接口和用例节点
var arrNode=[];
for(var i=0;i<arr.length;i++)
{
    var obj=arr[i].getAttribute("data");  //获取接口和用例的json数据
    var type=arr[i].getAttribute("type"); //获取类型:1.接口 2.用例
    var objId=arr[i].getAttribute("varid"); //获取接口或者用例在可视化节点中的ID
    var text;
    if(type=="1")     //节点
    {
        var objInfo={};
        var o=JSON.parse(obj.replace(/\r|\n/g,""));
        var query={
            project:o.project._id
        }
        if(o.version)
        {
            query.version=o.version;
        }
        objInfo=await 请求当前的接口数据信息并和本地接口入参进行合并;
        opt.baseUrls=objInfo.baseUrls;
        opt.before=objInfo.before;
        opt.after=objInfo.after;
        text="(function (opt1) {return helper.runTest("+obj.replace(/\r|\n/g,"")+",opt,test,root,opt1,"+(level==0?objId:undefined)+")})"   //生成函数闭包,等待调用
    }
    else if(type=="2")   //为用例
    {
        代码略
     }
    var node=document.createTextNode(text);
    arrNode.push({
        oldNode:arr[i],
        newNode:node
    });
}
//将转换后的新text节点替换原来的link节点
arrNode.forEach(function (obj) {
    if(obj)
    {
        obj.oldNode.parentNode.replaceChild(obj.newNode,obj.oldNode);
    }
})

2.得到完整的执行代码后,如何去请求接口呢,我们来看下runTest函数里面的基本信息:


helper.runTest=async function (obj,global,test,root,opt,id) {
    root.output+="开始运行接口:"+obj.name+"<br>"
    if(id!=undefined)
    {
 window.vueObj.$store.state.event.$emit("testRunStatus","interfaceStart",id);
    }
    var name=obj.name
    var method=obj.method;
    var baseUrl=obj.baseUrl=="defaultUrl"?global.baseUrl:obj.baseUrl;
/**
这里的代码略,是对接口数据的param,query,header,body数据进行填充
**/
var startDate=new Date();
var func=window.apiNode.net(method,baseUrl+path,header,body);  // 这里就是网络请求部分,根据你的喜好选择ajax库,我这里用的是vue-resource
return func.then(function (result) {
    var res={
    req:{
        param:param,
        query:reqQuery,
        header:filterHeader(Object.assign({},header,objHeaders)),
        body:reqBody,
        info:result.header["doclever-request"]?JSON.parse(result.header["doclever-request"]):{}
    }
};
res.header=result.header;
res.status=String(result.status);
res.second=(((new Date())-startDate)/1000).toFixed(3);
res.type=typeof (result.data);
res.data=result.data;
if(id!=undefined)
{
    if(result.status>=200 && result.status<300)
    {
        window.vueObj.$store.state.event.$emit("testRunStatus","interfaceSuccess",id,res);  //这里就会将接口的运行状态传递到前端可视化节点中
    }
    else
    {
        window.vueObj.$store.state.event.$emit("testRunStatus","interfaceFail",id,res);
    }
}
root.output+="结束运行接口:"+obj.name+"(耗时:<span style='color: green'>"+res.second+"秒</span>)<br>"
return res;
})

3.最后我们来看下如何执行整个js代码,并对测试用例进行返回的:


var ret=eval("(async function () {"+ele.innerText+"})()").then(function (ret) { //这里执行的就是刚才转换后真实的javascript可执行代码
    var obj={
        argv:[]
    };
    var temp;
    if(typeof(ret)=="object" && (ret instanceof Array))
    {
        temp=ret[0];
        obj.argv=ret.slice(1);
    }
    else
    {
        temp=ret;
    }
    if(temp===undefined)
    {
        obj.pass=undefined;
        test.status=0;
        if(__id!=undefined)
        {
            root.unknown++;
            window.vueObj.$store.state.event.$emit("testRunStatus","testUnknown",__id);   //将当前用例的执行状态传递到前端可视化节点上去
            window.vueObj.$store.state.event.$emit("testCollectionRun",__id,root.output.substr(startOutputIndex),Date.now()-startTime);
        }
        root.output+="用例执行结束:"+test.name+"(未判定)";
    }
    else if(Boolean(temp)==true)
    {
        obj.pass=true;
        test.status=1;
        if(__id!=undefined)
        {
            root.success++;
            window.vueObj.$store.state.event.$emit("testRunStatus","testSuccess",__id);
            window.vueObj.$store.state.event.$emit("testCollectionRun",__id,root.output.substr(startOutputIndex),Date.now()-startTime);
        }
        root.output+="用例执行结束:"+test.name+"(<span style='color:green'>已通过</span>)";
    }
    else
    {
        obj.pass=false;
        test.status=2;
        if(__id!=undefined)
        {
            root.fail++;
            window.vueObj.$store.state.event.$emit("testRunStatus","testFail",__id);
            window.vueObj.$store.state.event.$emit("testCollectionRun",__id,root.output.substr(startOutputIndex),Date.now()-startTime);
        }
        root.output+="用例执行结束:"+test.name+"(<span style='color:red'>未通过</span>)";
    }
    root.output+="</div><br>"
    return obj;
});

好的,大体上我们这个可视化的接口自动化测试平台算是完成了,但是这里面涉及到细节非常多,我大致列举下:
1.eval是不安全的,如何让浏览器端安全的执行js代码呢
2.如果遇到需要文件上传的接口,需要怎么去做呢
3.既然可以在前端自动化测试,那么我可不可以把这些测试用例放到服务端然后自动轮询呢

Python接口自动化测试零基础入门到精通(2023最新版)

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/123619.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2022ICPC济南站

K Stack Sort 题意&#xff1a;给你一个长度为n的排列&#xff0c;设有m个栈&#xff0c;你需要将这n个数按出现顺序入栈&#xff0c;每次入栈操作从m个栈中选择一个栈从栈顶入栈。当所有元素入栈完成后&#xff0c;需要不断选择栈&#xff0c;将栈中元素弹空。需满足出栈顺序…

ubuntu, nvidia driver, cuda, cudnn, pytorch-gpu版本安装

文章目录 1.常用指令1.1查看cpu是intel还是amd:1.2.查看ubuntu版本1.3.查看架构1.4.查看已安装的nvidia驱动1.5.进入tty模式 2.安装ubuntu22.04 和 nvidia 驱动3.ubuntu 安装 anaconda4.安装pytorch gpu版本5.安装完整版cuda 和 cudnn6.nvidia-driver, cuda-toolkit, cudnn 1.常…

OpenCV 在ImShow窗体上选择感兴趣的区域

窗体上选择感兴趣ROI区域 在计算机视觉处理中, 通常是针对图像中的一个特定区域进行处理, 有时候这个特定区域需要人来选择, OpenCV 也提供了窗口选择ROI机制. 窗体支持两种选择ROI区域的方法, 一个是单选, 一个是多选, 操作方法如下: 单选: 通过鼠标在屏幕上选择区域, 然后通过…

第三章:人工智能深度学习教程-基础神经网络(第三节-Tensorflow 中的多层感知器学习)

在本文中&#xff0c;我们将了解多层感知器的概念及其使用 TensorFlow 库在 Python 中的实现。 多层感知器 多层感知也称为MLP。它是完全连接的密集层&#xff0c;可将任何输入维度转换为所需的维度。多层感知是具有多个层的神经网络。为了创建神经网络&#xff0c;我们将神…

Flink SQL -- 概述

1、Flink SQL中的动态表和连续查询 1、动态表&#xff1a; 因为Flink是可以做实时的&#xff0c;数据是在不断的变化的&#xff0c;所以动态表指的是Flink中一张实时变换的表&#xff0c;表中会不断的有新的数据。但是这张表并不是真正的物理表。 2、连续查询&#xff1a; 连续…

深度学习之基于YoloV5交通信号标志识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 基于YoloV5交通信号标志识别系统介绍 基于YoloV5的交通信号标志识别系统是一种深度学习应用&#xff0c;旨在通过使…

kotlin 基本语法

const val INFO "ZZZ is Success Result" fun main(){ var name: String? "zzz" name null name?.capitalize() //?问号的意思是如果name是null ,后面的方法不执行&#xff0c;如果name不是null&#xff0c;后面方法执行 var name: String? &q…

xdcms漏洞合集-漏洞复现

目录 xdcms v3.0.1漏洞 环境搭建 代码审计 目录总览 配置文件总览 登陆处sql注入 漏洞分析 漏洞复现 注册处sql注入漏洞 漏洞分析 漏洞复现 getshell 任意文件删除 xdcms订餐网站管理系统v1.0漏洞 简介 环境搭建 全局变量的覆盖 漏洞分析 漏洞复现 后台任意…

Leetcode—102.二叉树的层序遍历【中等】

2023每日刷题&#xff08;二十四&#xff09; Leetcode—102.二叉树的层序遍历 C语言BFS实现代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/ /*** Return an array of arr…

API接口自动化测试

本节介绍&#xff0c;使用python实现接口自动化实现。 思路&#xff1a;讲接口数据存放在excel文档中&#xff0c;读取excel数据&#xff0c;将每一行数据存放在一个个列表当中。然后获取URL,header,请求体等数据&#xff0c;进行请求发送。 结构如下 excel文档内容如下&#x…

Spring Task定时任务框架

二十四、Spring Task 24.1 介绍 Spring Task 是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑。 定位&#xff1a;定时任务框架 作用&#xff1a;定时自动执行某段Java代码 为什么要在Java程序中使用Spring Task&#xff1f; 应用场景…

进入网络安全行业有哪些大公司推荐

随着互联网的普及和数字化进程的加速&#xff0c;网络安全问题日益凸显。从个人信息的泄露到国家基础设施的被攻击&#xff0c;网络安全已经不再只是一个技术问题&#xff0c;而是关乎到每个人、每个企业和国家的核心利益。在这场没有硝烟的战争中&#xff0c;一些大公司凭借其…

Cygwin工具制作Redis服务端Window版本

文章目录 前言一、cygwin是什么&#xff1f;二、cygwin安装Redis源码编译 前言 在学习到redis&#xff0c;经常需要用到一个redis服务端&#xff0c;如果有买服务器或者本机可以支持经常开虚拟机&#xff0c;也是可以的&#xff0c;如果不具备这些条件&#xff0c;还是本机win…

黑客(网络安全)技术——高效自学

前言 前几天发布了一篇 网络安全&#xff08;黑客&#xff09;自学 没想到收到了许多人的私信想要学习网安黑客技术&#xff01;却不知道从哪里开始学起&#xff01;怎么学 今天给大家分享一下&#xff0c;很多人上来就说想学习黑客&#xff0c;但是连方向都没搞清楚就开始学习…

认证服务-SpringSecurity及Oauth2介绍

认证服务-SpringSecurity及Oauth2介绍 统一身份认证服务 统一身份认证服务系统&#xff1a;以统一身份认证服务为核心&#xff0c;用户登录统一身份认证服务后&#xff0c;即可以使用所有支持统一身份认证服务的管理应用系统。 统一认证服务的提供方在项目实施中通常由公司平…

【Linux精讲系列】——vim详解

​作者主页 &#x1f4da;lovewold少个r博客主页 ⚠️本文重点&#xff1a;c入门第一个程序和基本知识讲解 &#x1f449;【C-C入门系列专栏】&#xff1a;博客文章专栏传送门 &#x1f604;每日一言&#xff1a;宁静是一片强大而治愈的神奇海洋&#xff01; 目录 目录 ​作者…

XML解析文档解析

1.首先是我的项目结构以及我所引入的依赖&#xff1a; 2.引入的依赖&#xff1a;jdk用的是17 <properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target> </properties> <dep…

【uniapp】通用列表封装组件

uniapp页面一般都会有像以下的列表页面&#xff0c;封装通用组件&#xff0c;提高开发效率&#xff1b; &#xff08;基于uView前端框架&#xff09; 首先&#xff0c;通过设计图来分析一下页面展示和数据结构定义 w-table组件参数说明 参数说明类型可选值默认值toggle列表是…

读者自荐的 4 个 GitHub 项目

本期推荐的 4 个开源项目&#xff0c;为读者在开源项目 Awesome-GitHub-Repo 的评论区自推的, 如果你开源了不错的项目&#xff0c;想让大家看到&#xff0c;也可以去 Awesome-GitHub-Repo 进行投稿。 本期推荐开源项目目录&#xff1a; 1. DB-GPT 2. 定制中国传统节日头像 3. …

零代码编程:用ChatGPT批量将Mp4视频转为Mp3音频

文件夹中有很多mp4视频文件&#xff0c;如何利用ChatGPT来全部转换为mp3音频呢&#xff1f; 在ChatGPT中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个批量将Mp4视频转为Mp3音频的任务&#xff0c;具体步骤如下&#xff1a; 打开文件夹&#xff1a;…