接口自动化平台(三):Promise简介 + 前端 + 后端 + 联调

2023-11-17

目录

1. Promise

2. 接口自动化平台前端开发

2.1 前端环境搭建

2.2 新建用例页 CreateCase

2.2.1 增加路由信息:config/routes.ts

2.2.2 增加对应后端接口的信息:config/proxy.ts

2.2.3 页面展示文件 index.tsx: src/pages/CreateCase/index.tsx

2.2.4 运行结果展示文件 ExecResultItem.tsx: src/pages/CreateCase/components/ExecResultItem.tsx

2.2.5 页面接口文件 services.ts: src/pages/CreateCase/services.ts

2.2.6 页面接口文件 data.d.ts: src/pages/CreateCase/data.d.ts

2.2.7 一些备注事项

2.3 用例列表页 CaseList

2.3.1  路由信息config/routes.ts

2.3.2  对应后端接口的信息:config/proxy.ts

2.3.3 页面展示文件 index.tsx: src/pages/CaseList/index.tsx

2.3.4 页面接口文件 services.ts: src/pages/CaseList/services.ts

2.3.5 页面数据类型定义文件 data.d.ts: src/pages/CaseList/data.d.ts

2.3.6 一些备注事项:

2.4 自动化用例集列表页 AutoList

2.4.1  路由信息routes.ts:config/routes.ts

2.4.2  对应后端接口的信息:config/proxy.ts

2.4.3 页面展示文件 index.tsx: src/pages/AutoList/index.tsx

2.4.4 展示用例结果列表的组件 ExecHistory.tsx: src/pages/AutoList/components

2.4.5  展示结果详情列表的组件 ExecResult.tsx: src/pages/AutoList/components

2.4.6 页面接口文件 services.ts: src/pages/AutoList/

2.4.7 页面数据类型定义文件 data.d.ts:src/pages/AutoList/

2.4.8 一些备注事项

3. 接口自动化平台后端开发

3.1 工程启动类(ApplicationServer.java)

3.2  Entity实体类(domain) :一个实体类对应一个表格

3.2.1 用例Case.java: /entity/Case/Case.java

3.2.2 自动化用例AutoCase.java: /entity/auto/AutoCase.java

3.2.3 自动化结果概要AutoResult.java: /entity/auto/AutoResult.java

3.2.4 自动化结果详情AutoResultDetail.java: /entity/auto/AutoResultDetail.java

3.3  Bean实体类

3.3.1 目录/common下的:Paging.java

3.3.2 目录/dto下的:CaseDto, HttpDto, PagingQueryCaseDto, /auto/AutoCaseDto, /auto/PagingQueryAutoCaseDto

3.3.3  目录/http下的:HttpHeader, HttpRequest, HttpResponse

3.3.4  目录/response下的:RespBean, PageRespBean

3.4 数据接口访问层(Dao),也就是Repository

3.4.1 用例的Repo:CaseRepo

3.4.2 自动化用例的Repo:3个

3.5 数据服务层(Service)

3.5.1 用例的业务程序 CaseService

3.5.2 自动化用例的业务程序 AutoCaseService

3.6 前端控制器(Controller)

3.6.1 用例的控制程序 CaseController

3.6.2 自动化用例的控制程序 AutoCaseController

3.7 配置信息类(config)

3.8 异常处理: handler/DefaultExceptionHandler

3.9 枚举类enums/RespCode

3.10 配置文件(./yaml/.properties/.json等)置于config文件夹下

3.11 工具类(utils)置于com.springboot.utils

3.12 小结

4. 前后端联调

4.1   设置代理 config/proxy.ts

4.2   启动前端工程 antd pro

4.3   启动后端工程 spring boot

4.4  转换接口数据为后端数据

4.5   在http.ts中隐藏删除等接口,依次测试删除等功能

4.6 这里梳理一下前端如何识别到后端,并调用后端的命令来实现效果的?

4.7   新增用例,提交时出现错误

4.8   查询用例

4.9   编辑用例

4.10   执行用例

4.11 几个新的注解


1. Promise

(1)为什么要讨论promise?

跟获取request的结果有关系。

如果在request里面加await拿到的promise, 跟去掉await后定义一个testPromise函数new 一个promise,然后resolve获取promise信息,是一样的。

现在用的比较多的是 sync和 await,所以promise了解即可。

(2)export const testPromise = () => {return request('/api/todos', { })}  :

这个操作,request拿到的是promise, 这里展开讲了promise是什么?然后讲通过async和await的方式,来简化,获取promise里具体值。前端代码中request调用上述操作接口,需要拿到的是promise里的具体的data, success, total…

(3)先记下常用套路:前端代码中request调用语句中:接 async, 调接口语句前加 await。

(4)Promise的概念:

首先,promise是js的抽象异步处理对象实现异步编程的方案。简单的说,就是解决传统js运行单线程的诟病以及异步操作的多层级嵌套带来的麻烦。

从用途上来说:

—promise主要用于异步计算。

—可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果。

—可以在对象之间传递和操作promise,帮助我们处理队列。

从语法上说:

Promise 是一个对象,从它可以获取异步操作的消息。

Promise 提供统一的API,各种异步操作都可以用同样的方法进行处理。

(5)Promise的基本用法?

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

2. 接口自动化平台前端开发

2.1 前端环境搭建

VSCode工具

antd pro组件库

2.2 新建用例页 CreateCase

2.2.1 增加路由信息:config/routes.ts

完整代码略

2.2.2 增加对应后端接口的信息:config/proxy.ts

完整代码略

2.2.3 页面展示文件 index.tsx: src/pages/CreateCase/index.tsx

完整代码略

2.2.4 运行结果展示文件 ExecResultItem.tsx: src/pages/CreateCase/components/ExecResultItem.tsx

作为组件的形式,被index.tsx调用

2.2.5 页面接口文件 services.ts: src/pages/CreateCase/services.ts

完整代码略

2.2.6 页面接口文件 data.d.ts: src/pages/CreateCase/data.d.ts

完整代码略

2.2.7 一些备注事项

(1) 一些数据结构的定义字段除了可以放在data.d.ts中之外,也可以直接放在index.tsx中,放在组件定义(const CreateCase: FC = ( )=>{ })之前。

比如根据新建用例时,根据页面要展示的内容,来在该index.ts中直接定义这样的字段interface CaseInfo{ },供这个页面使用。这些字段的初始化(置空)赋值也放在该位置。

 

(2)组件中除了定义一些state变量 + const columns: PreColumnType<> + return 渲染部分之外,还有什么?

------各种方法定义:比如const onFinish = async () =>{}, onValueChange, handleExec等。

------useEffect(()=>{}, [ ]), 功能类似react的生命周期中的didMount. 在didMount阶段和willUnmount阶段,都会执行。

(3)详细说说useEffect()

这是在组件主渲染之前先做的一个动作,比如通常里在里面实现某些变量的初始化。

备注:const { id } = useParams<{ id?: string }>(); //编辑用例, useParams是框架给的。useParams返回URL参数的键/值对的对象。

 解析:

1)第一个参数:

实现/定义的是一个函数(无参),箭头函数。函数体{ }中,用一个括号包起所有函数语句,后面再接一个空括号,就相当于对这个函数进行了调用。如果没有后面这个括号,就相当于只是定义了函数而已。不符合useEffect第一个参数的要求。

第二个参数是限制了在什么情况下(以哪些内容为监控标准),会执行第一个参数(或者说useEffect才会有效)。如果为空,则只在第一次加载页面或者刷新时,才会执行第一个参数。

2)这里的初始化功能用在了两种情况; 新建用例和编辑用例。因为这两种情况都是在同一个页面(pages/CreateCase/)中进行。区别就是初始化值不同;

新建用例时,初始化用置空字段,即没有传递id,也就是上面的代码中不走id路线,直接setInitData(emptyInit)。

编辑用例时,会通过useParams获取id值,根据id值,传参调用getCase接口方法(定义在service.ts中),获取该id的case信息,用这个信息来初始化setInitData(res?.data); return true;

3)新建用例时,为了先让页面架构使用上面的useEffect进行初始化,可以使用以下句式:

return ( initData ? <Form></Form> : <Pageloading />  )

解析:

意思就是先判断初始化字段是否为undefined?

如果undefined的话,先<Pageloading/>加载页面,同时框架利用useEffect来初始化initData.

初始化后再判断一次,此时不是undefined了,即刻进入<Form>表单。

注意还要在Form标签的属性里使用initialValues这个属性,如下:

<Form   initialValues={initData} //初始化   >

4)现在回到新建用例页面的主体部分return().

因为新建的用例要提交到服务器,所以要把所有的内容放到表单<Form></Form>里。

<Form>表单里再放<PageContainer>和<FooterToolbar>。

<PageContainer>中还是用<Card>美化。

<Card></Card>里面就是多个<Form.Item></Form.Item>。

每个Form.Item表示一个表单项。如果想让多个Form.Item在同一行,用<Input.Group compact></Input.Group>括起来。

5) <Form>标签中的常用属性:

// hideRequiredMark, 还可考虑加这个

这里重点说:onFinish={onFinish} //submit时调用.

即最下面的<FooterToolbar>里有一个按钮,按钮点击事件时使用的是form?.submit()方法提交,提交表单并且数据验证成功时,就调用了这个onFinish方法。

执行onFinish的回调函数时,先判断现在要进行的是新增用例还是编辑用例?怎么判断?通过id。

如果上面有通过useParams获取到id, 那说明要执行编辑用例的操作,否则就进行新增用例操作。这两个操作都在service.ts里定义。

该onFinish操作结束后,跳转到用例列表页。

onValuesChange={onValueChange}  //只要是Form里的值发生了变化,它都能监听到

if (!isPost) { //如果不是post,即从post切换到其他方法,则清空区域。再回到post值也没了

        form.setFieldsValue({

                body: '',

                caseName: 'xxx',   })

}

6)<Card>里有一个执行按钮,所以在Card的属性里加一个extra属性,然后加按钮,按钮实现“执行”功能。点击按钮,调用回调函数。

handleExec() “执行”方法的定义:

这里直接调用form表单里的form.getFieldsValue()方法获取当前表单页面的信息(form来源于: const [form] = Form.useForm(); //antd自己的 + 在<Form>属性里设置的form={form})。

然后通过传参,调用接口方法execCase()(service.ts里定义)来实现“执行”操作。

最后将返回的执行结果保存到state变量execResult里:

7)点击执行后,执行结果如何展示?通常用一个普通的模态框 <Modal></Modal>来展示。

------这个模态框放在哪儿?模态框可以放<PageContainer>内,跟<Card>平齐。

------如何控制模态框出现与否?根据是否有执行结果来控制。

上一步执行步骤进行后,将结果赋值给state变量execResult(是一个autoResultItem),这里就根据execResult来控制模态框的visible。

------模态框页面上应该显示哪些内容,如何显示?

这里可以单独定义一个组件<ExecResultItem data={execResult} />来实现这部分。

将执行结果传进组件,组件用return(<Card><Descriptions>……</Decriptions></Card>)来展示。

8)<Form.Item></Form.Item>之间,可以包含以下这些组件:

<Input></Input>,

<Select><Select.Option></Select.Optino></Select>

<Input.TextArea />

<Form.List></Form.List>等

这里重点说<Form.List></Form.List>,这里动态的增加行,每一行是键值对的形式。

具体代码比较长,直接在antd 组件库里搜相应的效果的代码,拷贝后,修改部分的参数值,达到自己要的效果即可:

<Form.List name="headers">

       {(fields, { add, remove }) => (<>...................</> )}

9) 后面针对新建性能测试用例时,需要增加“正则表达式”和“响应断言”的内容,所以又补充了一个<Card>, 里面放两个可切换的<Tabs>。详情在下一篇性能用例文章中阐述。

10)<FooterToolbar>组件,用来提交新建用例的表单内容。

2.3 用例列表页 CaseList

 

 

2.3.1  路由信息config/routes.ts

2.3.2  对应后端接口的信息:config/proxy.ts

同 2.2.2

2.3.3 页面展示文件 index.tsx: src/pages/CaseList/index.tsx

完整代码略

 

2.3.4 页面接口文件 services.ts: src/pages/CaseList/services.ts

2.3.5 页面数据类型定义文件 data.d.ts: src/pages/CaseList/data.d.ts

2.3.6 一些备注事项:

(1)一个列表页面,(函数)组件的大框架包括3个部分:

1)const 定义一些参数和改变参数的方法(useState)。

2)const columns: PreColumnType<> = [ { }, { }…] 定义列表各列。 每一个{ }代表一列:可以用dataIndex直接调用后台接口的某些值(直接展示,无跳转无链接等)。 还可以在某些列实现一些动态效果,比如:用例名的抽屉显示,删除操作,编辑操作,执行操作等。此时dataIndex可有可无,用render:(_, item) => { return( ) }来实现这些操作。 

3)return ( < > < />)返回要渲染到页面的内容和动作。

(2)展开说第3)点:return()

return后接(),()里必须只有一个根标签。

通常页面用<PageContainer>作为根标签,这个标签自带一些标题格式和内容。

<PageContainer>标签对之间,放<ProTable>单标签,<Drawer>双标签等。

<PageContainer>标签属性里,可以放一般属性,也可以放类似 footer={ } 作为页面底部的内容/操作/按钮(弹窗控制)等。

(3)<ProTable />单标签里,放了页面的主要内容。

比如:

1)列表项的引用 columns={columns};

2)后端接口的调用 request={async (params) => await getCaseList({ ...params })};

3)工具栏(含“新建用例按钮”等)toolBarRender={ () => [... ] };

4) 列表中每一项前面的选择框 rowSelection={ { onChange: (_, selectedRows) => setSelectedRows(selectedRows) } };

5)其他一些属性等,比如 rowKey(注意:这个不能少,没有这个,在点击选择某个列表项的时候,会出现全选的效果)。

6)比如 actionRef={actionRef} 来标识这个ProTable。

(4)<Drawer>双标签,抽屉效果。

1)通过在属性里设置一些值和回调函数,跟const 定义的一些state参数变化而实现 “抽屉是否展示,在点击什么内容时展示,展示时应该传入哪些值” 等效果。

2)常用属性: title, placement, width, closable, onClose={()=>{setRow(undefined)}}, visible={!!row}。  (row是CaseItem)

3)<Drawer>双标签对之间,通常通过<Descriptions></Descriptions>标签,来实现抽屉页面内容的逐行展示。<Descriptions>属性中比较重要的是column={1},这么设置最终的效果就是有两行。

(5)通常会将一些数据结构的定义(包含哪些字段,以及每个字段的数据类型)放在data.d.ts中。然后用CaseItem作为页面上很多项的泛型 <CaseItem>, 限制某些项的数据结构类型,具体内容如图。

(6)通常会将所有调用接口的方法定义放在一个统一的文件service.tsx里。

如下,其中 `/api/case/list`对应于后端所暴露出的后端接口

注意:这里的接口,很多时候可以用反单引号,这个反单引号的作用就是:里面可以直接取参数值,比如${caseId}, 适用于接口路径中带动态参数的情况的实现.

然后,参数看情况来传递,比如:

(7)如何实现页面跳转?用history.push()。

注意这里的history来自“umi”。

onClick={() => { history.push(`/editCase/${ item.id}`); }}  //这里反引号,里面可以直接用变量。

(8) 一些操作后需要弹出一个新的框,在新框里展示内容或者实现操作,比如模态框。

常见的一种确认模态框,用Modal.Confirm({......}).

比如删除操作,就需要弹出一个确认框来确认是否要删除。

onClick={async () => {  Modal.confirm({ }) } }。

属性里最常用的是: title, content, okText, cancelText, onOk: async () => { }.

最后这个回调函数里,实现了删除方法的回调(删除方法在service.ts中定义并暴露)。

(9) 模态框不仅限于确认模态框,常用还有一种针对“需要有内容提交的”表单模态框,比如<ModalForm>表单模态框,双标签:<ModalForm></ModalForm>:

1)可用泛型来限制提交的结构类型,比如<ModalForm<{autoCaseName: string}>。

2)常用的属性:key, title,trigger={ 里面可定义一些按钮标签,用来实现触发动作},onFinish ={async ( ) => { 里面实现一些函数的回调,实现点击按钮后的一些函数/方法/动作 }}。  见下方图。

3)表单模态框里,双标签中间,通常用<ProForm.Group><ProFormText 各个属性/></ProForm.Group>,来在表单模态框中显示内容。

(10) 调用后端接口的语句:request={async (params) => await getCaseList({ ...params })}。

1)getCaseList()在service.ts中定义。

2)如果await getCaseList({ ...params })加花括号,要记得同时加return, 否则报错。

(11) 一个比较严谨的做法:调用某个变量之前,先判断它是否为真,为真的话再继续调用这个变量的相关操作。 {row?.id && (……)}

(12) service.ts中定义的接口方法中常见的参数格式:

async (params?: any) => { }  

async (caseId?: number) => { } 

async (params: {autoCaseName: string; caseIds: number[]}) => { }

   

2.4 自动化用例集列表页 AutoList

2.4.1  路由信息routes.ts:config/routes.ts

2.4.2  对应后端接口的信息:config/proxy.ts

同 2.3.2, 此处截图略

2.4.3 页面展示文件 index.tsx: src/pages/AutoList/index.tsx

完整代码略

2.4.4 展示用例结果列表的组件 ExecHistory.tsx: src/pages/AutoList/components

完整代码略

2.4.5  展示结果详情列表的组件 ExecResult.tsx: src/pages/AutoList/components

完整代码略

2.4.6 页面接口文件 services.ts: src/pages/AutoList/

2.4.7 页面数据类型定义文件 data.d.ts:src/pages/AutoList/

2.4.8 一些备注事项

(1)还是三部分:state变量,columns:ProColumnType<>,return()里用<PageContainer>双标签括起:<ProTable>单标签 + <Drawer>双标签。前者用来展示页面的所有内容。后者用来展示每个用例集的详细内容,抽屉效果。

    

(2) const columns: ProColumnType<AutoListItem>[] = [ ]

1)AutoListItem数据结构先在data.d.ts中定义好,根据页面要展示的项来定义。

2)然后逐个定义columns, 类似用例列表页。这里不重复。

3)其中, autoCaseName定义成<a>标签的形式,点击时调用getCasesByAutoCaseId接口方法,实现:查询单个自动化用例集id关联的case。传递的是所点击的自动化用例集项的id。返回的是接口`/api/auto/${autoCaseId}/cases`的内容。注意反引号的使用。

定义一个state参数,用来存放点击用例集项后返回的该用例集所包含的多个关联case信息,如下。下方<PageContainer>里的<Drawer>将会个根据这个autoCases来决定:是否显示抽屉?以及抽屉里具体展示哪些内容?

4)column里的“操作”包含两个动作:删除和执行。分别都用<a>标签来表示。

删除的<a>标签中,用onClick执行一个回调函数,回调函数即使用Modal.Confirm确认模态框组件,模态框的onOK:()=>{}里调用删除用例集的接口方法deleteAutoCase(item.id)。传的是用例集的id, 返回的是接口`/api/auto/${autoCaseId}`的数据。

执行的<a>标签中,onClick的回调函数,调用接口方法execAutoCase(item?.id)。返回的是这个接口:

return request(`/api/auto/exec/${autoCaseId}`, {

        method: 'post',         })

//用例集的执行不需要重新加载表格数据,所以不需要reload。

(3)根据上面的autoCaseName,这里一起说一下<PageContainer>里的<Drawer>。

这里<Drawer>结构整体和之前类似,比较特殊的一点是:这里获取到的详细信息是包含多个cases, 而前面的只是一个case。 所以这就需要考虑如何在抽屉里展示多个cases的详细内容。用map()方法遍历。

(4)这里重点说列表的折叠效果expandable。

<ProTable>单标签内容如下:

在ProTable的API里找能实现折叠展开效果的expandable,拷贝引用。

因为显示展示内容的步骤比较多,这里单独定义一个组件<ExecHistory />来展示所点击的某个用例集项所包含的cases信息。

(5) <ExecHistory>组件:

1)useEffect初始化

//在组件初始化的时候,先请求执行历史列表数据,然后再渲染在组件上

  

 其中,//查询单个自动化用例集的执行历史列表接口

2)要展示用例集下的多个cases, 因为外部已经有ProTable了,这里就不重复用Table了,用一般的List就行。

<List> 套<List.Item>套 <Row>套<Col>。<List.Item>表示一行。

<List>单标签,属性里有dataSource={data??[]}。 data就是上面useEffect时初始化返回的列表数据。

 注意上面的两个部分。

一个是如何定义列表的每一行。

一个是列表每一行的最后一项是执行详情,设为<a>标签,onClick后触发一个state参数的变化,进而触发出一个模态框。这个模态框只是用来展示数据,所以用常规的<Modal>即可。在<Modal>双标签里再调用一个自定义的组件<ExecResult id={crtResultId} />。 用来展示该case的详情(请求信息,响应结果…)

<Modal>也定义在return里,跟<List>平齐。

3)上面的<ExecResult id={crtResultId} />组件,同样的套路:

3. 接口自动化平台后端开发

3.1 工程启动类

(1)项目启动时,其当前层及子目录中所有controller会被编译

(2)可通过直接运行该类来启动Spring Boot应用。

(3)带有注解 @SpringBootApplication 

(这里另一个注解 @EnableJpaAuditing跟获取时间有关系)

  

3.2  Entity实体类(domain) :一个实体类对应一个表格

小结:

(1)一般创建后,项目启动后,如果没有表格,会在数据库中自动生成一个表格。

(2)实体中的各个属性,对应了表格中的每一列。定义属性,相当于定义表字段。

(3)注意各个注解的使用:

1)这里用到表格,所以需要在@Table(name = "test_case")位置会要求加datasource, 也就是在这个位置要引入数据库表格信息,如果没有,则在IDEA右侧database那里,新增一个自己MYSQL数据库相关的配置信息。如果驱动没下载,则下载。

2)加datasource后,表名位置可能还有红线,因为可能是表还没创建的原因,可忽略红线。在SQLyog连接数据库之后,看看红线还有没有。此时已经定义好了实体类,可以通过重启SpringBoot后,可以自动生成表(刷新或重连后看到)。

3)然后重启IDEA。

3.2.1 用例Case.java: /entity/Case/Case.java

3.2.2 自动化用例AutoCase.java: /entity/auto/AutoCase.java

3.2.3 自动化结果概要AutoResult.java: /entity/auto/AutoResult.java

3.2.4 自动化结果详情AutoResultDetail.java: /entity/auto/AutoResultDetail.java

3.3  Bean实体类

小结:

(1)定义多个bean类, 每个bean类有多个private属性,通过getter, setter的方式获取或赋值。对应不同的前端页面(跟前端页面的展示有联结)。

(2)定义这些实体类,Swagger自动匹配上Bean相关的一些内容,在页面上将Bean的整个结构以JSON的结构/方式标出来。好处是,更改每个Bean实体类中的信息,重启SpringBoot后,Swagger中(接口文档)就会自动更新了。如果不用Swagger,就既要改代码又要改接口文档。

3.3.1 目录/common下的:Paging.java

(1)Paging.java

在分页查询时,需要用到的信息:每页展示的用例数,当前是第几页

3.3.2 目录/dto下的:CaseDto, HttpDto, PagingQueryCaseDto, /auto/AutoCaseDto, /auto/PagingQueryAutoCaseDto

(1)CaseDto

1)对实体类Case的一些展示,因为Case直接对应数据库表,很多时候,不适合将数据库表中的所有数据都展示在前端,因此借助CaseDto,对Case做一层封装(或者说转换),想在展示Case的什么属性,就在dto中定义该属性就行,不想展示的就不定义。

2)两个构造方法,参数不同,后者多了:正则提取器实体类,响应断言实体类

 

(2)HttpDto

整合了两个实体类 HttpRequest和HttpResponse, 为后续统一展示做准备。

(3)PagingQueryCaseDto

补充用例分页查询要展示的项:用例名称

前面定义的Paging中只包含了current和pageSize

(4)/auto/AutoCaseDto

(5)/auto/PagingQueryAutoCaseDto

3.3.3  目录/http下的:HttpHeader, HttpRequest, HttpResponse

(1)HttpHeader

(2)HttpRequest

(3)HttpResponse

3.3.4  目录/response下的:RespBean, PageRespBean

(1)RespBean

1)定义该实体类,用于定义返回信息:

状态,

提示信息,

返回的数据:返回的数据可能是不同类型的,所以这个返回数据用了泛型T。注意,定义实体类的时候,就要带<T>。

(2)PageRespBean

分页查询时,返回的内容跟其他操作略有不同,所以定义一个实体类,继承上面的RespBean, 补充一些属性,并重写RespBean中的方法。

3.4 数据接口访问层(Dao),也就是Repository

3.4.1 用例的Repo:CaseRepo

(1)CaseRepo

3.4.2 自动化用例的Repo:3个

repository是数据库表和实体类entity之间的衔接作用。

(1)/auto/AutoCaseRepo

(2)/auto/AutoResultRepo

(3)/auto/AutoResultDetailRepo

       

3.5 数据服务层(Service)

1)功能跟controller联系紧密。相当于在repository和controller之间的衔接作用。

2)repository是数据库表和实体类entity之间的衔接作用。

3)@Service 注解的作用是:让SpringBoot托管这个service。

3.5.1 用例的业务程序 CaseService

 

 

3.5.2 自动化用例的业务程序 AutoCaseService

 

 

 

         

3.6 前端控制器(Controller)

小结:

(1)置于com.springboot.controller:

用于写入参,定义不同的页面的接口。

通过这不同的接口,可以访问到不同的页面。比如post 页面路径,get页面路径...

(2)注解介绍

@RestController 标签等价于@Controller+@ResponseBody。

@Controller 将当前修饰的类注入SpringBoot IOC容器,使得从该类所在的项目跑起来的过程中,这个类就被实例化。当然也有语义化的作用,即代表该类是充当Controller的作用。

@ResponseBody 它的作用就是指该类中所有的API接口返回的数据,甭管你对应的方法返回Map或是其他Obejct, 它会以Json字符串的形式返回给客户端。

@Autowired 注入注解,是按类型自动装配的,不支持id匹配

3.6.1 用例的控制程序 CaseController

3.6.2 自动化用例的控制程序 AutoCaseController

3.7 配置信息类(config)

小结:

Swagger类,置于com.springboot.config

Config/Swagger: 帮助我们接口定义文档。每当更新一版代码,Swagger自动快速生成接口文档。

里面的内容不用背,网上抄的。

config/SwaggerConfig 注意:

.apis(RequestHandlerSelectors.basePackage("com.road.testdev.autotest.controller"))

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    //    @Value("true")
    private boolean enable = true;
    
    @Bean
    public Docket createDocke() {
        return new Docket(DocumentationType.SWAGGER_2)
                //进入swagger-ui的信息
                .apiInfo(apiInfo())
                .select()
                //暴露所有controller类的所在的包路径
                .apis(RequestHandlerSelectors.basePackage("com.road.testdev.autotest.controller"))
                .paths(PathSelectors.any())
                .build()
                .enable(enable);
    }

    //进入swagger-ui的信息
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                //该项目的名字
                .title("自动化测试平台Api说明文档0903")
                //该项目的描述
                .description("自动化测试平台接口文档0903")
                .version("1.0")
                .build();

    }
}

3.8 异常处理: handler/DefaultExceptionHandler

小结:

捕获异常,这里没有很复杂或者需要改动的,就直接套用这个模板就行。

测试时,可以在casecontroller里输一些异常语句,在Swagger中执行casecontroller中相应的语句对应的页面,它就会自动捕获Exception及其子类的异常。然后在页面的响应结果的部分,看到异常结果。

小结:

(1)这部分底层用了Springboot的切面原理,AOP. 即切到这个项目下各个模块,监控哪个模块有异常。

(2)@ControllerAdvice

是Spring3.2提供的新注解,它是一个Controller增强器, 可对controller中被 @RequestMapping注解的方法加一些逻辑处理。最常用的就是异常处理。

使用 @ControllerAdvice 实现全局异常处理,只需要定义类,添加该注解。

在该类中,可以定义多个方法,不同的方法处理不同的异常,例如专门处理空指针的方法、专门处理数组越界的方法...,也可以直接向上面代码一样,在一个方法中处理所有的异常信息。

(3)@ExceptionHandler :

注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进到这个方法中来。

import com.road.testdev.autotest.bean.response.RespBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
@Slf4j
public class DefaultExceptionHandler {
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public RespBean exceptionHandler(Exception e){
        e.printStackTrace();
        log.error("Default Handler: {}", e.getStackTrace());
        return RespBean.failure(e.toString(), "Default Handler");
    }
}

  

3.9 枚举类enums/RespCode

      

3.10 配置文件(./yaml/.properties/.json等)置于config文件夹下

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://xx.xx.xx.xx:3306/test_db1?charset=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=**********
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

#server.port=8089

3.11 工具类(utils)置于com.springboot.utils

代码比较长,放在文末

     

3.12 小结

新增用例: 从Swagger页面的POST接口接收数据,以CaseDto的形式(页面要求输入啥就输入啥)传给controller, 然后controller调用service (传给service的是CaseDto,尽量让service做业务,controller少做),保存数据到数据库表格。然后回到controller里,将Case再转为CaseDto,返回给页面的结果响应。

推荐顺序: dto, repository, service, controller

(一般先实体类,然后repository, service, 最后controller)

4. 前后端联调

写在前面:

下方汇总的一些问题,是写程序过程中遇到的问题,因为程序在不断优化,所以有的问题跟最终版的程序有出入,比如过程中用模拟接口mock/http.tsx等,所以,这些问题可以忽略。仅供过程记录。

4.1   设置代理 config/proxy.ts

延伸知识点:同源策略,做进一步了解。

Antd中设一个代理,相当于我在页面上请求的还是本地的域名和端口,由代理转发,转发到后端的8080端口,pathrewrite(不用动,默认即可)等…

在proxy.ts中的 target配置成自己的localhost和端口,就看你在后端配置文件中设为多少就写多少。

4.2   启动前端工程 antd pro

yarn start

4.3   启动后端工程 spring boot

运行 AutotestApplication

4.4  转换接口数据为后端数据

此时前端页面上还是显示旧的信息,因为此时前端调用的还是之前用来测试的mock/http.ts接口。

在http.ts中将暴露的这个'GET /api/case/list'接口注释掉(后续其他功能也类似,隐藏旧的端口)

再刷新前端,此时前端页面就能顺利调用后端所连接的数据库表格的信息。

至此,页面上展示用例的功能可以了。

4.5   在http.ts中隐藏删除等接口,依次测试删除等功能

前面删除功能没有真正实现,是因为没有重新加载,这里需要修改CaseList 中的index.tsx,在ProTable中加一个属性actionRef={actionRef} (事先定义:const actionRef = useRef<ActionType>())。在调用删除函数之后,调用actionRef.current.reload()重新加载,实现删除功能。

操作中,删除操作还存在一些问题,有时候确认删除后页面上是存在该项,再删一次,或者再删另外一项之后,可能这一项或者这两项才会消失。。。待研究。

后端中确实实现了删除(逻辑删,isdelete为1)

4.6 这里梳理一下前端如何识别到后端,并调用后端的命令来实现效果的?

答:(1)首先是在前端的config/proxy.ts中设置了后端的路径和端口,当操作前端时,默认的就去这个路径和端口下寻找具体的子路径和命令:

(2)在前端页面上点击删除按钮,调用了前端代码中CaseList/index.tsx中的删除相关的模块,模块里调用了service.ts中定义暴露的删除方法:

export const deleteCase = async (caseId: number) => {

    return request(`/api/case/${caseId}`, {

        method: 'delete',

    })

}

其中这个路径,就对应了后端中的删除方法的路径,进而调用了后端的删除方法,实现了对数据库的删除:

@Api(tags = "Case管理相关接口")

@RestController

@RequestMapping("/api/case")

public class CaseController {

    @Autowired

    CaseService caseService;

…………………………………….

    @ApiOperation("删除用例")

    @DeleteMapping("/{id}")

    public RespBean delCase(@PathVariable("id") Integer id){

        Case dCase = caseService.deleteCase(id);

        return RespBean.success(new CaseDto(dCase));

    }

4.7   新增用例,提交时出现错误

因为:前端定义的用例信息中headers是一个数组,但是目前后端CaseDto中所定义的headers是一个String, 所以这里要改后端中的headers为数组。

Private List<Httprequest.Headers> headers (Httprequest.Headers是一个键值对)

如何操作?

答:

在跟页面直接对接的CaseDto中做修改。

CaseDto接收Case(数据库)来的数据, Case中headers依然保持定义为String.

CaseDto中的headers定义为数组List<Header>,其中Header也单独定义为一个实体,存放键值对。

因此,CaseDto和Case相互转换时,用到了GSon类。

GSon gson = new GSon();

Gson.fromJson: 从JSon   List<> (参数相对复杂,记住就行,不用深究)

Gson.toJson: 从List<>   JSon

问题:

1. 按照上面的方法,CaseDto转为Case时,已经toJson了,但是Case对应的表格中,headers栏好像还是数组形式,如何解决?

2. 测试过程有一些报错:Caused by: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 1 path $

可能是因为有一些历史数据,headers传的是String.后来定义这个为数组了。

处理方式:要么把历史数据清掉,以后都传数组。同时可以try catch Gson那部分的内容,处理异常。

同时,创建handler/DefaultExceptionHandler处理异常。

4.8   查询用例

4.9   编辑用例

存在问题:比如要修改一个POST的用例(带body内容),跳转到修改页面的时候,没有body的内容。如何处理?

4.10   执行用例

1)用到第三方的库,比如httpclient (没有什么套路,直接用别人的)

先添加httpclient的jar包。

建一个util/httpclient : request请求过去后,这个工具类会去调用后端。

补充:深入了解一下httpclient工具类。

(代码中的response有的是真实的response,Headers, apache相关自带的…注意跟自己写的区分)

2)在controller中增加“执行用例的接口”

先看看前端代码中执行用例的部分,调用了下方接口服务,POST方法,传参

export const execCase = async (params: CaseItem) => {

    return request('/api/case/exec', {

        method: 'post',

        data: {

            ...params

        },

    })

}

以上,涉及请求和响应的一些内容(属性),所以这里针对Httprequest和HttpResponse实体中的属性,再去包一层dto.(因为最基本的request和response中的所有属性,执行时不一定都用得到),所以,增加了bean/dto/HttpDto。

4.11 几个新的注解

(1)@Data: Lombok 注解, 是以下注解的集合:

@ToString 作用于类,覆盖默认的toString()方法,可以通过of 属性限定显示某些字段,通过exclude 属性排除某些字段。

@EqualsAndHashCode 作用于类,覆盖默认的equals 和hashCode

@Getter 作用类上,生成所有成员变量的getter 方法;作用于成员变量上,生成该成员变量的getter 方法。可以设定访问权限及是否懒加载等。

@Setter 作用类上,生成所有成员变量的setter 方法;作用于成员变量上,生成该成员变量

的setter 方法。可以设定访问权限及是否懒加载等。

@RequiredArgsConstructor 生成包含final 和@NonNull 注解的成员变量的构造器

(2)@NoArgsConstructor: Lombok 注解,用于生成无参构造器

(3)@AllArgsConstructor: Lombok 注解,用于生成全参构造器

(4)@ApiModel: Swagger 注解,表示对类进行说明,用于参数用实体类接收

(5)@ApiModelProperty: Swagger 注解,用于方法、字段,表示对model 属性的说明或者数据操作更改

补充:工具程序HttpClientUtil.java的代码

package com.road.testdev.autotest.utils;

import com.google.gson.internal.LinkedTreeMap;
import com.road.testdev.autotest.bean.http.HttpHeader;

import com.road.testdev.autotest.bean.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class HttpClientUtil {

    private static final String DEFAULT_ENCODING = "UTF-8";
    
    public static HttpPost getHttpPost(String url) {
        // 创建post方式请求对象
        HttpPost httpPost = new HttpPost(url);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000).setConnectionRequestTimeout(1000)
                .setSocketTimeout(5000).build();
        httpPost.setConfig(requestConfig);
        return httpPost;
    }
    

    public static HttpResponse sendPostDataByMap(String url, Map<String, String> map, String encoding) {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        // 创建post方式请求对象
        HttpPost httpPost = getHttpPost(url);
        // 装填参数
        List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
        if (map != null) {
            for (Map.Entry<String, String> entry : map.entrySet()) {
                nameValuePairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
        }
        // 设置参数到请求对象中
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, encoding));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        // 设置header信息
        // 指定报文头【Content-type】、【User-Agent】
        httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
        httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
        CloseableHttpResponse response = getPostResponse(httpClient, httpPost);
        return getResult(response, encoding);
    }

    
    public static HttpResponse sendPostDataByJson(String url, String json, String encoding) {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        // 创建post方式请求对象
        HttpPost httpPost = getHttpPost(url);
        // 设置参数到请求对象中
        StringEntity stringEntity = new StringEntity(json, ContentType.APPLICATION_JSON);
        stringEntity.setContentEncoding(encoding);
        httpPost.setEntity(stringEntity);
        CloseableHttpResponse response = getPostResponse(httpClient, httpPost);
        return getResult(response, encoding);
    }
    

    public static HttpResponse sendPostDataByRaw(String url, String json, List<HttpHeader> httpHeaders, String encoding) {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        // 创建post方式请求对象
        HttpPost httpPost = getHttpPost(url);
        // 设置参数到请求对象中
        StringEntity stringEntity = new StringEntity(json, ContentType.TEXT_PLAIN);
        stringEntity.setContentEncoding(encoding);
        httpPost.setEntity(stringEntity);

        for (HttpHeader httpHeader : httpHeaders) {
//            LinkedTreeMap header = (LinkedTreeMap) element;
            httpPost.setHeader(String.valueOf(httpHeader.getKey()), String.valueOf(httpHeader.getValue()));
        }

        CloseableHttpResponse response = getPostResponse(httpClient, httpPost);
        return getResult(response, encoding);
    }
    

    public static HttpResponse sendPostDataByXml(String url, String xml, String encoding) {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        // 创建post方式请求对象
        HttpPost httpPost = getHttpPost(url);
        httpPost.addHeader("Content-Type", "text/xml;charset=UTF-8");
        // 设置参数到请求对象中  text/xml和application/xml的区别
        StringEntity stringEntity = new StringEntity(xml, encoding);
        stringEntity.setContentEncoding(encoding);
        httpPost.setEntity(stringEntity);
        CloseableHttpResponse response = getPostResponse(httpClient, httpPost);
        return getResult(response, encoding);
    }
    

    //为了用例的执行功能,做了改动。(String url, List<HttpHeader> headers, String encoding),但依旧没有解决问题
    public static HttpResponse sendGetData(String url, List<HttpHeader> headers, String encoding) {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        // 创建get方式请求对象
        HttpGet httpGet = new HttpGet(url);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000).setConnectionRequestTimeout(1000)
                .setSocketTimeout(5000).build();

        httpGet.setConfig(requestConfig);
        // Content-type设置为application/x-www-form-urlencoded 或者不设置也是可以的(默认为application/x-www-form-urlencoded)
        httpGet.addHeader("Content-type", "application/json");
        // 通过请求对象获取响应对象

        //为了用例的执行功能,做了改动。改动前HttpHeader element : headers
        //for (Object element : headers) {
        for (HttpHeader header : headers) {
           // LinkedTreeMap header = (LinkedTreeMap) element;
            //为了用例的执行功能,做了改动。但依然失败
            //HttpHeader header = element;

           // httpGet.setHeader(String.valueOf(header.get("key")), String.valueOf(header.get("value")));
            //为了用例的执行功能,做了改动。但依然失败
            httpGet.setHeader(String.valueOf(header.getKey()), String.valueOf(header.getValue()));
        }
        CloseableHttpResponse response = getGetResponse(httpClient, httpGet);
        return getResult(response, encoding);
    }


    public static CloseableHttpResponse sendGetData(String url) {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        // 创建get方式请求对象
        HttpGet httpGet = new HttpGet(url);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000).setConnectionRequestTimeout(1000)
                .setSocketTimeout(5000).build();
        httpGet.setConfig(requestConfig);
        // Content-type设置为application/x-www-form-urlencoded 或者不设置也是可以的(默认为application/x-www-form-urlencoded)
        httpGet.addHeader("Content-type", "application/json");
        // 通过请求对象获取响应对象
        return getGetResponse(httpClient, httpGet);
    }


    public static CloseableHttpResponse getPostResponse(CloseableHttpClient httpClient, HttpPost httpPost) {
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpPost);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;
    }
    

    public static CloseableHttpResponse getGetResponse(CloseableHttpClient httpClient, HttpGet httpGet) {
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;
    }


    public static HttpResponse getResult(CloseableHttpResponse response, String encoding) {
        HttpResponse resp = new HttpResponse();
        String res = "";
        int statusCode = response.getStatusLine().getStatusCode();
        List<HttpHeader> httpHttpHeaders = new ArrayList<>();
        //if (statusCode == HttpStatus.SC_OK) {
        try {
            res = EntityUtils.toString(response.getEntity(), encoding);
            resp.setResBody(res);
            resp.setResCode(statusCode);
            org.apache.http.Header[] allHeaders = response.getAllHeaders();

            for (org.apache.http.Header header : allHeaders) {
                String name = header.getName();
                String value = header.getValue();
                httpHttpHeaders.add(new HttpHeader(name, value));
            }
            resp.setResHeaders(httpHttpHeaders);

        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        //}
        // 释放链接
        try {
            response.close();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        return resp;
    }
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

接口自动化平台(三):Promise简介 + 前端 + 后端 + 联调 的相关文章

随机推荐

  • 数据结构学习(1)----数组之螺旋矩阵Ⅱ

    题目 给你一个正整数 n 生成一个包含 1 到 n2 所有元素 且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 思路 模拟过程 设置边界 生成一个 n n 空矩阵 res 二维数组 随后模拟整个向内环绕的填入过程 定义当
  • 【PCL】—RANSAC点云分割算法详解

    参考 https www bbsmax com A rV57lnmVdP 1 点云分割的概念 点云分割可谓是点云处理的精髓 也是三维图像相对二维图像最大优势的体现 点云分割的目的是提取点云中的不同物体 从而实现分而治之 突出重点 单独处理的
  • 什么叫机器人编程课

    什么叫机器人编程课 小孩的学习一直都是家长们非常关心和重视一件事情 很多的家长在培养孩子的学习方面也是非常的多的 就拿现在很多的家长给孩子选择机器人编程的课程来说 有的家长对于什么叫机器人编程课并不是很清楚 今天我们就一起来了解一下什么叫机
  • AFX_PMSG数据结构

    AFX PMSG数据结构 定义 typedef void AFX MSG CALL CCmdTarget AFX PMSG void void AFX MSG CALL CCmdTarget AFX PMSG void AFX MSG CA
  • QT5.12在windows上边的安装

    使用国内镜像源在线安装QT 2023 3 25更新 qt国内镜像 Iotfsd的博客 CSDN博客 先下载 STEP1 下载qt online installer Index of official releases online inst
  • 汽车 Automotive > T-BOX GNSS高精定位测试相关知识

    参考 https en wikipedia org wiki Global Positioning System GPS和GNSS的关系 GPS Global Positioning System 全球定位系统是美国军民两用的导航定位卫星系
  • rsa生成公私钥php,php中rsa生成公私钥和加解密

    php中rsa生成公私钥和加解密 注意 php使用RSA时需要开启openssl扩展 生成公私钥 创建公私钥 res openssl pkey new 获取私钥 openssl pkey export res private key 获取公
  • Python跨平台应用-BeeWare打造移动端应用和构建Android时的异常处理

    目录 简介 安装 创建demo 运行 打包为Windows程序 打包为安卓APP 构建安卓错误处理 gt gt gradle改为本地 报错1 Could not resolve all artifacts for configuration
  • cgo+gSoap+onvif学习总结:1、方案初衷、资料收集及cgo实现helloworld

    cgo gSoap onvif学习总结 1 方案初衷 资料收集及cgo实现helloworld 文章目录 cgo gSoap onvif学习总结 1 方案初衷 资料收集及cgo实现helloworld 1 前言 2 资料收集 3 cgo h
  • Keil转STM32CubeIDE工程移植问题记录

    Keil转STM32CubeIDE工程移植问题记录 1 编译报错问题处理 2 工程相关配置问题 3 调试器配置 从Keil软件转战STM32CubeIDE 转换的过程中遇到了不少问题 在此记录一下 防止以后再踩坑 也给同样有转软件需求的朋友
  • 老派程序员——徒手实现伟大成就

    原文地址 http www csdn net article 2012 08 06 2808178 摘要 本文介绍了三位非常著名的程序员 Ken Thompson Joe Armstrong 和 Jamie Zawinski 他们是如何发明
  • Java中的Set集合接口实现插入对象不重复的原理

    java lang Object中对hashCode的约定 1 在一个应用程序执行期间 如果一个对象的equals方法做比较所用到的信息没有被修改的话 则对该对象调用hashCode方法多次 它必须始终如一地返回同一个整数 2 如果两个对象
  • litemall项目部署,启动后台前端cnpm install报错

    在命令窗口切换到C test file litemall litemall admin目录输入cnpm install 出现报错 1 将node modules 文件删除 2 在命令窗口切换到C test file litemall lit
  • 局域网下远程唤醒主机

    Linux下远程唤醒 Linux下唤醒远程主机使用的命令主要是 wakeonlen 安装 apt get install wakeonlen 使用命令为 wakeonlen AC 48 11 Windows下远程唤醒 Windows下主要的
  • 配置Nginx以隐藏访问端口

    进入usr local nginx conf 编辑nginx conf文件 在http模块中加入下句 include vhost conf 进入usr local nginx conf vhost xxx conf 编写如下内容 nginx
  • Maven依赖仓库

    Maven依赖仓库
  • Linux的目录切换和用户管理

    切换目录 在使用linux系统的时候 会用cd来切换目录 cd 切换到根目录 cd 切换到主目录 cd 切换到之前工作目录 cd 虽然很方便但只能保存一次目录 pushd命令使用目录堆栈可以把多个目录存放起来 配套使用pushd popd
  • android实现共享数据

    计划是在后台开个service定位服务 前台需要经纬度的时候 从service获取 实现过程如下 public GPSService minTime UnTaskCheckingActivity minTime minDistance Un
  • mpc模型预测控制从原理到代码实现 mpc模型预测控制详细原理推导 matlab和c++两种编程实现

    mpc模型预测控制从原理到代码实现 mpc模型预测控制详细原理推导 matlab和c 两种编程实现 四个实际控制工程案例 双积分控制系统 倒立摆控制系统 车辆运动学跟踪控制系统 车辆动力学跟踪控制系统 包含上述所有的文档和代码 ID 564
  • 接口自动化平台(三):Promise简介 + 前端 + 后端 + 联调

    目录 1 Promise 2 接口自动化平台前端开发 2 1 前端环境搭建 2 2 新建用例页 CreateCase 2 2 1 增加路由信息 config routes ts 2 2 2 增加对应后端接口的信息 config proxy