现在的位置: 首页 > 综合 > 正文

一个智能的 Web 界面测试系统

2014年09月05日 ⁄ 综合 ⁄ 共 13237字 ⁄ 字号 评论关闭

Web2.0 技术使 Web 界面更加丰富多彩,使信息交流更加灵活,同时也使得相关的 Web 技术测试需求越来越多。那么,如何提高 Web 界面的测试效率,保证新技术得到高质量应用?是否可以让测试人员脱离枯燥地点击鼠标,让机器自动地根据脚本运行?随着项目需求的变化,能否有一个比较快速地配置管理测试任务的方法?所有这些都可以通过一个智能的 Web 界面测试系统来实现。这个系统结合 TestNG, Ant, Selenium 还有 Flex 技术,实现方式简单、运行高效灵活,对单元测试,功能测试和集成测试都有益,能够提高团队的工作效率。

介绍 Web2.0 相关技术

Web2.0 是一个体现当代 WWW 技术发展趋势的流行概念。它极力促进创造性、信息交互性以及用户间协作性的 Web 设计思想的推广。这些想法带来了各种丰富多彩的基于 Web 的互动和资讯服务的开发和演变。例如,社交网站,WIKI 以及博客。

Web2.0 最突出的特色就是丰富的客户端技术。主要有三大类:

  • Ajax 和 JSON
    Ajax(Asynchronous JavaScript + XML)是 Web2.0 的主要技术。网页浏览不再是单击一下,然后等待整个页面重新装载,而是可以用鼠标顺畅地滚动地图,等待局部数据的自动刷新。典型的事例应用是 Google Map。
    JSON(JavaScript Object Notation)是 Ajax 的衍生技术之一。Web 数据通常通过 XML 传输。而 JSON 对象是一系列以逗号分隔的 name:value 对,与 XML 相比更加的简洁,传输效率高,适合大规模数据传输。典型的应用事例是 Live Search Box。
  • Restful
    REST(Representational State Transfer)是一种轻量级的面向数据库的 Web 服务架构。REST 架构遵循 CRUD 原则,对于资源只需要四种行为:Create(创建)、Read(读取)、Update(更新)和 Delete(删除)就可以完成对其操作和处理。典型的应用事例包括 Facebook 和 Flickr。
  • RIA
    RIA(Rich Internet Application)是如今非常流行的 Web 技术。它的界面类似于一般的桌面程序,比一般的 Web 程序更加丰富并且互动。目前比较流行的技术有三项:Abobe Flex, 微软的 Silverlight 和 Sun 推广的 JavaFX。三种技术都有自己的 SDK 和开发工具

    -------------------------------------------------------------------------------------------------
    介绍测试系统流程

以上我们简要介绍了 Web2.0 的概念和相关客户端技术。为确保客户端产品的质量,我们需要使用与此相应的 Web 测试工具,从而方便地融合于产品测试中。此外,为适应 Web 开发的灵活性,我们同时需要一个能够快速配置、部署、运行和汇报结果的测试系统,从而实现智能高效的测试流程,降低软件研发的成本。
基于以上论述,下面将介绍一个智能的 Web 界面测试系统。该系统有四大模块组成,主体主要由 Python 语言实现,结合几种开发工具和技术,包括 Ant、Selenium、TestNG、XML 和 Flex。系统有两个控制方式:时间和 Web 管理站点。时间逻辑在 Python 脚本中实现,当时间到来时系统会按顺序下载代码、部署应用程序、运行自动测试、发布报告;而 Web 管理站点通过 Flex 和 JAVA 技术来实现,用户可以按需在线配置某些模块,并要求立即执行自动测试。
下面将详细介绍每个模块的具体工作内容。测试流程参见图 1。

图 1. 测试系统概述

源代码和安装包的按需下载

在软件开发过程中,每天都会因新的功能而更改源代码。此外,很多项目需要国际团队合作,这些情况下代码的更新频率更加高。本土和国外团队经常需要共享源代码,而源代码可能被存放在固定的站点上面。当源代码文件量大而站点距离遥远的时候,下载代码的任务就比较耗时。为了节省这方面的时间,提高团队整体的工作效率,有必要让这部分工作自动化起来。所以,系统首先实现了一个结合 Python 和 XML 的下载控制模块。它的逻辑比较简单(参见图 2)。Python 程序定时读取配置文件,判断该任务当前是否可以运行。如果此刻时间和配置的时间一致,就访问站点,下载代码包。否则,放入等待队列,获取下一个任务。在等待队列里面的任务会在一定时间后重新启动。

图 2. 下载流程

下载配置文件的内容参见以下代码:

清单 1.下载配置文件的代码

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <items>
  3. <item name="download source files" type="source" weekday="-1" time="7:-1"
  4. source="https://sample.sourcecode.com"
  5. target="C:/sourcetar/"/>
  6. </items>

读取下载配置文件的代码如下:

清单 2.读取下载配置文件的代码

  1. from xml.dom.minidom import parse, parseString
  2. from MyDownloadTask import MyDownloadTask
  3. def readFromProperty(xmlfile):
  4.     tasklist = []
  5.     dom = parse(xmlfile)
  6.     for node in dom.getElementsByTagName('item'):
  7.         name = node.getAttribute('name')
  8.         weekday = node.getAttribute('weekday')
  9.         time = node.getAttribute('time')
  10.         source = node.getAttribute('source')
  11.         target = node.getAttribute('target')
  12.         type = node.getAttribute('type')        
  13. // 定义一个下载任务
  14.         task = MyDownloadTask()
  15.         task.create(name, weekday, time, source, target, type)
  16. // 加入下载任务列表
  17.         tasklist.append(task)        
  18.     return tasklist

实现具体的下载逻辑如下:

清单 3.实现下载的代码

  1. class MyDownloadTask(object):
  2.    
  3.     …… // 定义一些变量
  4.    
  5.     def create(self, name, weekday, time, source, target, type):
  6.         self.name = name
  7.         self.weekday = int(weekday)
  8.         self.source = source
  9.         self.target = target
  10.         index = time.find(":")
  11.         self.hour = int(time[0:index])
  12.         self.minute =  int(time[index+1:])
  13.         self.type = type
  14.      
  15.      def run()
  16. // 使用用户名和密码通过防火墙
  17. password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
  18. password_mgr.add_password(None, self.source, username, password)
  19. handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
  20. opener = urllib.request.build_opener(handler)
  21. urllib.request.install_opener (opener)
  22. // 从站点下载文件
  23. content = urllib.request.urlopen(self. source).read()
  24. f=open(self.target + self.buildno,"wb")
  25. f.write(content)
  26. f.close()

此例指出目前有一项下载任务,下载类型是源代码,链接为 https://sample.sourcecode.com, 将其保持到 C 盘 sourcetar 文件夹下,时间为每天早上 7 点钟。(-1 代表任意时间)。项目组可以根据需要增加下载项。比如,我们项目组由于资源文件经常要修改,所以需要每天上午和下午都要检测是否有新的代码包,所以配置了两个下载任务。上午的时间一般都在 7 点,这样可以在每位同事上班打开电脑的那个时刻就获得大洋彼岸美国项目组下班时候的最新进展。
正如前文所述,该测试系统中我们提供一个 Web 管理站点,用户可以根据项目的需要配置下载任务,不必麻烦系统管理员来修改下载配置文件。该模块使用 Flex 和 Java 技术实现(参见图 3)。用户可以通过它了解当前系统已有的下载任务。此外,用户也可以通过管理站点创建,修改和删除下载任务。这些更改最终都会被保存到 XML 配置文件中。

图 3. 下载管理界面

应用程序的部署
对 Web 应用程序而言,测试的第一步是部署。有了源代码后,测试系统使用 Ant 编写的脚本编译源代码,停止当前已有的 Web 服务器,部署产品代码,然后重新启动 Web 服务器。在这段时间中,产品的测试环境可能暂时无法访问(参见以下代码)。

清单 4.代码部署

  1. def deploybuild(self):
  2.        … // 清理文件夹,将源代码解压到 d 盘 buildtar 目录
  3.        try:
  4.             tar = tarfile.open(self.target + myHtml.srcbuild, "r:gz")
  5.             for tarinfo in tar:
  6.                 print(tarinfo.name, "is", tarinfo.size, "bytes in size")
  7.             tar.extractall("d:/buildtar")
  8.             tar.close()
  9.         except :
  10.            …
  11.         // 编译源代码
  12.         os.chdir("d:/buildtar")
  13.         os.system("Ant all")
  14.         // 关闭 web 服务
  15.         os.system("net stop /”servicename/"");
  16.         … // 部署系统
  17.         // 开启 web 服务
  18.         os.system("net start /"servicename/"")

运行自动测试脚本

当新版本的 Web 应用程序部署完后,测试系统就开始进入自动测试。本系统使用 SVN 对测试脚本进行版本控制。所以有必要在一开始通过 SVN 更新最近的测试脚本。这里使用 Ant 来调用 SVN 的命令行工具,参见下图。
使用如下 Python 脚本调用相应的 Ant 任务:

清单 5.Python 脚本

  1. def runseleniumtestcase(self):
  2.     // 下载测试脚本
  3.     os.chdir("d:/")
  4.     os.system("Ant download-testcase -buildfile=task.xml")
  5. // 运行测试脚本
  6. os.chdir("d:/v510")
  7. os.system("Ant start-selenium -buildfile=build.xml")
  8. <target name="download-testcase">
  9. <delete dir ="v100"/>
  10. <property environment="env"/>
  11.   <exec executable="cmd">
  12.   <env key="PATH" path="${env.PATH}:C:/Program Files/CollabNet Subversion Client"/>
  13.     <arg value="/c"/>
  14.     <arg value="svn"/>
  15.     <arg value="checkout"/>
  16.     <arg value="svn://9.19.199.9/web/v100"/>
  17. </exec>
  18. </target>

复制代码

事例中的任务 download-testcase 用来从站点 9.19.199.9 的 web/v100 目录下载最新版本的测试脚本。start-selenium 任务将启动 selenium 的 proxy server,然后按照 TestNG 配置的顺序运行测试脚本。
测试系统的脚本应用 Selenium 和 TestNG 的测试工具,实现对各类型 Web 界面的测试需求。在第三节中将详细介绍 Selenium 工具在本系统的应用,并在第四章中介绍 TestNG 工具在配置 Selenium 测试脚本中的作用。
为适应项目开发周期不同阶段的测试需求,Web 管理站点将列出当前所以测试案例。用户可以自由地挑选下次测试需要的用例。当用户选择保存后,这些测试脚本就会在下次系统自动部署时候被运行。如果用户选择运行,那么系统可以马上在当前已部署的产品环境上运行测试脚本。

发布自动测试报告

运行完自动测试用例后,测试结果自动生成于测试工具目录下面。这样会覆盖原先的文件,不利于项目的跟踪和信息的交流。所以,我们的测试系统会将测试报告发布到 Web 管理站点。用户只需按照上面的时间点击链接,就可以看到每次测试的运行结果(参见图 4)。

图 4. 测试报告界面

----------------------------------------------------------------
Selenium 在系统中的应用

自动测试模块式该系统的一个核心部分,它使用了 Selenium 这个工具。Selenium 是一款针对 Web 产品的测试工具。它根据测试脚本,向浏览器发出指令,代替人手工点击界面之苦。Selenium 有三类命令:动作命令(Action)、辅助命令(Accessor)、断言(Assertion)。动作命令可以向应用程序发出指令,比如点击按钮;辅助命令检查当前应用程序的状态并保存于变量中;断言类似于单元测试中经常使用的 Assert 方法,但是在 Selenium 中还有许多结合 Xpath 获取元素属性的方法。
通过这些命令,用户可以方便地实现对网页的各类操作、定位网页上面的元素并判断元素的值。此外,Selenium 有两个主要的运行模式:Selenium Core 和 Selenium Remote Control。
Selenium Core 将测试用例以 Iframe 形式嵌入一个 TestRunner 页面(参见图 5),然后将它们上传到和应用程序一起的 Web 服务器端。用户可以使用右上角的 Selenium TestRunner 控制模块来操作测试脚本,获得测试结果。

图 5. Selenium Core 实例

Selenium Remote Control 更加适合测试复杂的基于 Ajax 技术的应用程序。它提供一个 Selenium Server 服务器,作为应用程序客户端的代理向应用程序的服务器端发送请求(参见图 6)。在这种模式下,测试人员可以编写一些测试脚本来完成一些复杂的测试。

图 6. Selenium Remote Control 原理

与 IBM 的 Rational Functional Tester 工具相比,Selenium Remote Control 有三个主要特点:

  • Selenium 基于 Http 协议来实现。它根据测试脚本提供的 Xpath 定位网页上面的元素并对其进行操作,就如我们通常使用绝对和相对路径查询操作系统文件一样;IBM 的 Rational Functional Tester 则基于对象模型设计实现的。它抓取浏览器在屏幕上实时展现的对象集合,根据用户输入的元素属性在上面寻找可能的对象。实践证明,当页面非常复杂时,这个方法不精确高效。
  • Selenium 是一个开源项目,可编写测试脚本的 API 包支持好几种语言,包括 Java, C++, .Net, Python 等;IBM 的 Rational Functional Tester 需要 license,并且只支持 Java。
  • Selenium 在运行时使用 JavaScript 向应用程序发出测试命令,所以它对当前浏览器的位置和电脑的屏幕没有要求;IBM 的 Rational Functional Tester 需要获得当前浏览器和电脑屏幕的位置,要求屏幕始终打开。这需要人围的支持,提高项目开发的成本。

本测试模块运用 Selenium Remote Control 的模式,新建了一个 Java 项目,编写了一系列 Java 的测试脚本。项目的目录结构参见图 7。

图 7. 测试源代码文件结构

该项目结构类似于普通的 Java 项目。其中,我们导入了运行 Selenium Remote Control 所需的 jar 包。另外,build 目录下的 test-output 是每次运行后测试报告生成的地方。文件 build.xml 中定义了很多 Ant 的任务,用来编译和运行测试脚本。
下面是一个测试脚本的事例,它将一个集群加入管理服务列表。Selenium 的测试脚本类似于一般的 Junit 测试用例。首先,需要新建一个 Selenium 的会话。这个会话需要的参数包括 Selenium 代理服务器的地址(selenSrvrAddr)、端口(4444)、需要测试的 Web 应用的服务器地址(bpath)以及 Web 应用在服务器上面的相对路径(appPath)。然后,定义具体方法,对应用程序进行操作。比如在下面事例中使用 Open 函数打开页面,选择页面上面的一系列 Frame(这里给出 Frame 的 name 就可以,因为它在页面上是唯一的),然后点击链接(这个链接通过属性 Link 来定位的)。最后,记得关闭这个会话。

清单 6.测试脚本事例

  1. import org.TestNG.annotations.AfterClass;
  2. import org.TestNG.annotations.BeforeClass;
  3. import org.TestNG.annotations.Parameters;
  4. import org.TestNG.annotations.Test;
  5. import com.thoughtworks.selenium.DefaultSelenium;
  6. import com.thoughtworks.selenium.Selenium;
  7. public class TestClusterAddPage {
  8. private Selenium driver = null;
  9. @Parameters({"selen-svr-addr", "brwsr-path", "aut-addr"})
  10. @BeforeClass
  11. private void init(String selenSrvrAddr, String bpath, String appPath){
  12. driver = new DefaultSelenium(selenSrvrAddr, 4444, bpath, appPath);
  13. driver.start();
  14. }
  15. @Parameters({"cluster-ip", "cluster-username", "cluster-pwd"})
  16. @Test
  17. public void testAddCluster(String ip, String username, String password){
  18. driver.open("/ica/Login");
  19. driver.selectFrame("rootFrame");
  20. driver.selectFrame("consoleWorkAreaFrame");
  21. driver.selectFrame("rootFrame");
  22. driver.selectFrame("consolePortfolioFrame");
  23. driver.selectFrame("taskEntriesFrame");
  24. driver.click("link=Clusters");
  25. … ..
  26. }
  27. @AfterClass
  28. private void stop(){
  29. driver.stop();
  30. }
  31. }

从以上事例可以看出,Selenium 在定位页面上元素时比较随机。有时可以使用元素的 name 属性,有时可以使用元素的 id 属性。为此,项目组可以为客户端页面上的元素定义唯一的属性,方便 Selenium 测试。
使用 Selenium Remote Control 模式需要在运行脚本前后启动和停止 Selenium Server 服务器。这里我们在 build.xml 中定义了一个任务名为 start-selenium。该任务第一步就是启动 selenium server 服务器,第二步是调用 TestNG 配置的测试脚本,最后是停止 selenium server 服务器。整个流程比较清晰明了(参见以下代码)。

清单 7.Selenium 流程

  1. <target name="start-selenium">
  2. <java jar="lib/selenium-server-1.0-beta-2/selenium-server.jar" fork = "true"
  3. spawn = "true"/>
  4. <Antcall target="run-task-jar"/>
  5. <get dest="${test.output}/results.txt"
  6. src="http://localhost:4444/selenium-server/driver/?cmd=shutDown" />
  7. </target>

----------------------------------------------------------------------------
TestNG 在配置自动测试脚本中的作用

本测试系统还使用 TestNG 工具来辅助配置自动测试。TestNG 是测试 Java 应用程序的框架之一。它通过一些语义注释来传递测试的参数,定义测试脚本的顺序并配置运行时的性能。用户可以通过配置来生成各式测试报告,十分方便。
TestNG 要求将所有要运行的测试用例都记录在一个叫 testng.xml 的文件中(参见以下代码)。然后根据该文件中的测试用例顺序依次执行测试。用户还可以根据需要在测试用例的具体方法中标识 @BeforeClass,@AfterClass 等语义,更加具体得定义测试顺序。
为方便配置测试 Web 应用程序,我们将所使用的浏览器和应用程序所部署的 IP 地址作为参数定义在 testng.xml 中。这些参数由 parameter 定义,通过 @Parameters 传递到函数中(参见上文)。具体的测试脚本由 Classes 元素中的 Class 来定义。

清单 8.testng.xml

  1. <!DOCTYPE suite SYSTEM "http://beust.com/TestNG/TestNG-1.0.dtd" >
  2. <suite name="My test suite">
  3. <test name="SVCGUI unit test">
  4. <parameter name="selen-svr-addr" value="localhost"></parameter>
  5. <parameter name="aut-addr" value="http://9.9.9.9:9080"></parameter>
  6. <parameter name="brwsr-path" value="*firefox"></parameter>
  7. <parameter name="cluster-ip" value="1.1.1.1"></parameter>
  8. <parameter name="cluster-username" value="admin"></parameter>
  9. <parameter name="cluster-pwd" value="abc"></parameter>
  10. <classes>
  11. <class name="src.cluster.TestClusterAddPage"/>
  12. <class name="src.cluster.TestClusterDeletePage"/>
  13. <class name="src.user.TestUserListPage"/>
  14. </classes>
  15. </test>
  16. </suite>

在上文的事例中,定义了一个叫 My test suite 的测试组,其中包括三个测试脚本。用户可以根据需要在同一个文件中定义其他测试组(suite),或者在一个测试组中定义更多的测试脚本集(classes)。
系统的 Web 管理站点会罗列出该文件中的 class 元素的 name 属性值(参见图 8)。然后让用户自由选择需要的测试用例,保存成一份新的 testng.xml 文件。接着,用户可以选择马上执行自动测试或者保存到下一次系统运行下载流程时执行。

图 8. 测试实例管理界面

TestNG 还能够为系统自动生成一份漂亮的测试报告。我们可以在 build.xml 中给出报告的存放地址和形式(包括 html/text 等,参见如下代码)

清单 9.build.xml

  1. <target name="compile" description="compile the examples" depends="prepare">
  2. <javac   debug="true"
  3. fork="true"
  4. source="1.5"
  5. classpathref="cp"
  6. srcdir="${basedir}/src"
  7. destdir="${basedir}/build/classes"
  8. />
  9. </target>
  10. <target name="testjar"
  11. description="make a test jar with the property file and the classes we need in it">
  12. <jar destfile="${basedir}/example.jar">
  13. <fileset dir="${basedir}/build/classes" includes="**/*.class"/>
  14. <fileset file="${basedir}/TestNG.xml"/>
  15. </jar>
  16. </target>
  17. <target name="run-task-jar" depends="compile, testjar"
  18. description="run examples using TestNG task with just xml">
  19. <testng classpathref="cp" outputdir="${test.output}" testjar="${basedir}/example.jar"/>
  20. </target>

在以上事例中有三个任务,compile/testjar/run-task-jar。这三个任务中 run-task-jar 是主要的任务,它调用了 TestNG 的语义 <testng>,并给出测试报告的生成目录,以及测试脚本的所组成 jar 包名。

--------------------------------------------------------------------------
使用 Flex 实现的管理站点

Web 管理站点在整个测试系统中起到了画龙点睛的作用。它的功能涵盖了下载控制,测试脚本控制和测试报告发布。用户可以通过它来新建,修改,删除和浏览各模块的当前配置。当然,也可以根据项目的需要添加其他模块,比如播放产品的 demo,显示项目的进度等等。
该 Web 管理站点由 Flex 技术实现。基于前文的介绍,作为一个 RIA 技术,Flex 可以帮助我们快速实现一个丰富多彩、灵活互动的客户端(整体效果参见图 9)。

图 9. 图形管理界面

站点的客户端定义了一个新的布局器 PodLayoutManagers, 它让用户通过 XML 文件来配置当前显示的视图(view),各个视图所包含的模块(pod)(参见以下代码)。每种 view 和 pod 类型都有自己的 ID。此外 , 每个 pod 模块需要定义所属的类型。用户可以自定义新类型,但是每个类型都要继承于 PodContentBase 控件,并给出模块的标题(title), 读写的 XML 文件(dataSource)。有些类型还详细到具体读取的 XML 文件中的属性 , 以及它们在表格上门显示的列名。比如,在 XML 中我们定义了一个管理视图,叫做 Project Management Console,它有四个模块,分别是 list, form、case 和 film 类型。其中 , 第二个 pod 标题为“Download To Do”, 读取的数据文件为 data 目录下的 tasks.xml。它会读取文件中的 Name、Type、Weedday、time、Source Url、Target Url 属性 , 并将它们以 name、type、weekday、time、source、target 显示在列表中(参见上文) 。

清单 10.tasks.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <views>
  3. <view
  4. id="view1"
  5. label="Project Management Console">
  6. <pod
  7. id="pod1"
  8. type="list"
  9. title="Unit Test Report"
  10. dataSource="data/testreport.xml"
  11. selectedViewIndex="1"/>
  12. <pod
  13. id="pod2"
  14. type="form"
  15. title="Download To Do"
  16. dataSource="data/tasks.xml"
  17. labels="Name,Type,Weedday,time,Source Url,Target Url"
  18. dataFields="name,type,weekday,time,source,target" />
  19. <pod
  20. id="pod3"
  21. type="case"
  22. title="Test Case"
  23. dataSource="data/TestNG.xml"
  24. labels="Test Case"
  25. dataFields="name" />
  26. <pod
  27. id="pod4"
  28. type="film"
  29. title="Product demo"
  30. dataSource="data/news.xml"/>
  31. </view>
  32. </views>

复制代码

Form 和 case 类型可以选择两种显示的方式:Grid 和 Form。在 Grid 模式下,模块会以列表形式显示数据。在 Form 模式下,模块会显示一个表单,用户可以新建,修改和删除一条记录(参见图 10)。

图 10. 下载管理界面

在这个页面中,首先要在 PodContentBase 控件中加入一个 HBox 来切换两个不同的视图。然后定义一个 ViewStack,并在其中加入两个控件:VBox 和 Canvas。VBox 显示表单,而 Canvas 中用 DataGrid 读取 dataFields 中的数据并以表格方式显示。而页面上面的 Action 主要在 Script 中实现。具体实现参见图 11:

Flex 可以与各类服务器端技术相结合。该 Web 管理站点采用 J2EE 作为服务器端技术,通过 serve let 同客户端通信,完成对各 XML 配置文件的修改。由于篇幅有限,具体实现方式可以参考文章《 Flex 开发入门》。
------------------------------------------------------------------------------------
总结
本文向大家详细介绍了一个智能的 Web 界面测试系统。它的四个主要模块配置灵活、运行高效,可以帮助团队完成 Web 界面的各类测试。我们的开发和测试人员不必再为枯燥乏味的界面操作而叹气。此外,友好的 Web 管理站点进一步体现了系统的灵活、高效、开放的特点。该测试系统的另一个特点就是可扩展性。项目组可以根据自身的需要丰富管理站点的功能,引入一些项目进度、人员时间等管理模块,使项目内部交流更加精密。
希望本文能为相关的软件研发团队提供一些技术和设计上的启发。

参考资料

抱歉!评论已关闭.