热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

十、练习——导航和网络请求

十、练习——导航和网络请求在我们开始这一章之前,重要的是你要明白,我们在这本书的第三部分的重点将

十、练习——导航和网络请求

在我们开始这一章之前,重要的是你要明白,我们在这本书的第三部分的重点将基于练习和例子,这些练习和例子将帮助你磨练你的测试技能,积累我们在这本书的前几章中可能无法涵盖的知识。在这一节中,我们将采取动手操作的方法,目标是尽可能多地进行示例和练习。在我们深入这一章之前,重要的是你已经完成了每一章,现在正在寻求建立在我们学习 Cypress 如何用于测试时你所获得的理论知识的基础上。

在本章中,我们将重点关注涵盖以下主题的练习和示例:


  • 实现导航请求

  • 实现网络请求

  • 高级导航请求配置

一旦你完成了每一个练习,你将有信心成为一个更好的测试者,并在导航和网络请求领域进入更复杂的测试。

技术要求

首先,建议您从 GitHub 中克隆包含源代码和我们将在本章中编写的所有测试的存储库。

本章的 GitHub 资源库可在https://GitHub . com/packt publishing/端到端-Web-Testing-with-Cypress 上找到。

本章的源代码可以在chapter-10目录中找到。

在我们的 GitHub 存储库中,我们有一个财务测试应用,当我们完成本章时,我们将把它用于关于 Cypress 导航和 Cypress 请求的不同示例和练习。

重要说明:在 Windows 中运行命令

注意:默认的 Windows 命令提示符和 PowerShell 不能正确解析目录位置。

请遵循进一步列出的专门在以单词*windows为后缀的 Windows 操作系统上工作的 Windows 命令。

要确保测试应用正在您的计算机上运行,请从您的终端上的应用根目录运行以下命令。

npm run cypress-init命令将安装应用运行所需的依赖项,而另一方面,npm run cypress-app命令将只是启动应用。或者,您可以使用npm run cypress-app-reset命令重置应用状态。重置应用会删除已添加的不属于应用的任何数据,从而将应用状态恢复到克隆存储库时的状态。我们可以在终端中进一步运行命令,如下所示:

$ cd cypress/chapter-10;
$ npm install -g yarn or sudo npm install -g yarn
$ npm run cypress-init; (for Linux or Mac OS)
$ npm run cypress-init-windows; (for Windows OS)
// run this command if it's the first time running the application
or
$ npm run cypress-app (for Linux or Mac OS)
$ npm run cypress-app-windows; (for Windows OS)
// run this command if you had already run the application previously
Optionally
$ npm run cypress-app-reset; (for Linux or Mac OS)
$ npm run cypress-app-reset-windows; (for Windows OS)
// run this command to reset the application state after running your tests

重要说明

我们在chapter-10目录中有两个主文件夹,一个文件夹包含我们将用于示例和测试练习的应用,而第二个文件夹包含我们测试应用的 Cypress 测试。为了正确运行我们的测试,我们必须同时运行我们的应用和 Cypress 测试,因为测试是在运行于我们机器本地的实时应用上运行的。还需要注意的是,应用将要求我们使用端口 3000 作为前端应用,使用端口 3001 作为后端应用。

掌握前面的命令将确保您能够运行应用,重置应用状态,甚至安装应用的依赖项。现在让我们从导航请求开始。

实现导航请求

Cypress 导航涉及导航到应用的网页的行为。在我们在本书中介绍的许多测试中,您可能记得在测试之前,我们有cy.visit()命令,它包含我们正在导航到的或正在测试的页面的 URL。cy.visit()命令是导航命令的一个例子,帮助我们在 Cypress 前端测试中提出导航请求。在本节中,我们将通过示例和练习来使用不同的 Cypress 导航命令。在本节结束时,我们将对 Cypress 导航命令有更深入的了解,这将有助于我们在本书前几章已经掌握的导航知识的基础上更进一步。

cy.visit()

我们使用 Cypress 中的cy.visit()导航到被测应用的远程页面。通过使用这个命令,我们还可以向命令传递配置信息,配置方法、URL、超时选项甚至查询参数等选项;我们将在本章后面深入探讨这个命令的配置选项。

在我们的 GitHub 存储库中,在chapter-10/cypress-realworld-app目录中,我们有一个应用,我们将用于我们的示例和练习。

重要说明

我们位于chapter-10/cypress-realworld-app目录的财务应用记录交易。使用该应用,我们可以通过为应用中已经存在的交易请求或支付用户来创建交易。我们可以看到已发生交易的通知,还可以查看联系人和已发生交易的日志。

该应用利用了一个 JSON 数据库,因此在将所有数据加载到我们的应用时有点慢。在我们的测试中,我们实现了一个“安全开关”,通过确保在beforeEach方法中,我们等待所有初始的XHR(XMLHttpRequest)请求加载数据,然后才开始我们的测试执行请求。参见下面代码块中的beforeEach方法的更多信息。

在我们的第一个示例中,在navigation.spec.js中,如下面的代码块所示,我们将使用cy.visit()命令导航到应用的通知页面:

describe('Navigation Tests', () => {
beforeEach(() => {
cy.loginUser();
cy.server();
cy.intercept('bankAccounts').as('bankAccounts');
cy.intercept('transactions/public').as('transactions')
;
cy.intercept('notifications').as('notifications');
cy.wait('@bankAccounts');
cy.wait('@transactions');
cy.wait('@notifications');
});
afterEach(() => { cy.logoutUser()});
it('Navigates to notifications page', () => {
cy.visit('notifications', { timeout: 30000 });
cy.url().should('contain', 'notifications');
});
});

该代码块说明了cy.visit()命令的用法,在该命令中,我们访问通知路由(http://localhost:3000/notifications)的远程网址,然后验证我们访问的远程网址是否符合我们的预期。在我们的导航命令中,我们还添加了超时选项,该选项确保在导航测试失败之前,Cypress 将等待 30 秒钟的“页面加载”事件,然后才能通过测试。

下面的截图显示了我们正在执行的测试和 Cypress 通过等待从后端接收的 XHR 请求,等待从我们的 JSON 数据库加载的所有数据:

Figure 10.1 – XHR API requests and responses

图 10.1–XHR API 请求和响应

在这个截图中,我们导航到/signin页面,然后在等待所有资源加载后,我们使用 Cypress cy.visit()命令导航到/notifications页面,在测试应用预览右侧可见。我们的测试断言进一步验证了这一点,它验证了被访问的网址包含名称notifications。以下练习将帮助您更好地理解如何使用cy.visit()命令实现测试。

练习 1

使用 GitHub 存储库中提供的位于cypress-real-world-app文件夹根目录下的金融应用,进行以下练习,测试您对cy.visit()命令的了解程度:


  1. 登录我们的测试应用,并使用cy.visit()命令导航至http://localhost:3000/bankaccounts网址。

  2. 创建新的银行账户,然后检查新银行账户创建后应用是否重定向回/bankaccounts网址。

  3. 登录应用,使用cy.visit()命令,尝试导航至http://localhost:3000/signin

  4. 测试用户成功登录后,验证该网址是否重定向到仪表板而不是/signin页面。

练习的解答可以在chapter-10/integration/navigation/navigation-exercise-solutions目录中找到。

本练习将测试您理解cy.visit()命令的能力,确保作为 Cypress 用户,您可以有效地使用该命令导航到不同的 URL,并且还可以将参数和配置选项传递给该命令。

cy.go()

Cypresscy.go()导航命令使用户能够在被测应用中向前导航或向后导航。使用cy.go()命令时,将'back'选项传递给该命令将引导浏览器导航到浏览器历史的上一页,而'forward'选项将引导浏览器导航到页面的前一页历史。我们也可以使用该命令通过输入数字选项作为参数来点击前进和后退按钮,其中'-1'选项将导航应用后退,而通过'1'将从浏览器历史记录中引导前进导航。

通过使用cy.go(),我们能够通过后退到浏览器历史的上一页以及前进到浏览器历史的下一页来操纵浏览器的导航行为。

重要说明

我们只在cy.visit()命令中使用/bankaccounts,因为我们已经在cypress.json 文件中声明了baseUrlbaseUrl是 URL 的完整版本,我们不需要每次在cy.visit()cy.intercept()命令中使用它时都重复它。您可以在本章开始时克隆的 GitHub 存储库中查看更多信息。

在下面的代码块中,我们将使用我们的财务应用来验证我们在导航到/bankaccounts页面后是否可以导航回仪表板:

describe('Navigation Tests', () => {
it('cy.go(): Navigates front and backward', () => {
cy.visit('bankaccounts');
cy.url().should('contain', '/bankaccounts');
cy.go('back');
cy.url().should('eq', 'http://localhost:3000/');
});
});

在这个测试中,导航到/bankaccounts网址后,我们使用 Cypress 内置的 cy.go('back')命令导航回仪表板网址,然后我们验证我们已经成功导航回该网址。下面的练习将更好地解释如何使用cy.go()命令。

练习 2

使用 GitHub 存储库中提供的位于chapter-10/cypress-real-world-app目录中的金融应用,执行以下练习来测试您对cy.go()命令的了解:


  1. 登录后,在交易仪表盘上,点击好友标签,然后点击矿山标签。

  2. 使用cy.go()命令,使用 Cypress 导航回好友标签。

  3. 登录后,点击位于应用导航栏右上角的新建按钮,创建新交易。

  4. 然后使用cy.go() Cypress 命令导航回仪表板页面并返回新交易。

练习的解答可以在chapter-10/integration/navigation/navigation-exercise-solutions目录中找到。

本练习将有助于培养您使用cy.go()命令测试向前和向后导航的技能。它还将有助于在测试应用时建立您使用导航的信心。

cy.reload()

Cypresscy.reload()命令是负责重装一页。该命令只有一组可以传递给它的选项,要么在清除缓存的同时重新加载页面,要么在缓存保留在应用内存中的情况下重新加载页面。当true的布尔值被传递给cy.reload()方法时,Cypress 不会用缓存重新加载页面;相反,它会清除缓存并加载关于页面的新信息。布尔值的省略导致 Cypress 在启用缓存的情况下重新加载页面。在下面的代码块中,我们在登录应用后重新加载仪表板;这将刷新仪表板页面的状态:

it('cy.reload(): Navigates to notifications page', () => {
cy.reload(true);
});

在这个测试中,如果我们的浏览器中有任何缓存的项目,Cypress 将重新加载页面并使缓存无效,以确保在执行我们的测试时创建页面的新状态和缓存。让我们看看下面的练习,了解更多使用cy.reload()命令的场景。

练习 3

使用 GitHub 存储库中提供的位于chapter-10/cypress-real-world-app目录中的金融应用,执行以下练习来测试您对cy.reload()命令的了解:


  1. 导航至账户菜单项,在此我们有用户设置。

  2. 在点击保存按钮之前,编辑测试用户的第一个和第二个名字。

  3. 重新加载页面并确认cy.reload()命令重置了所有尚未保存的设置。

练习的解答可以在chapter-10/integration/navigation/navigation-exercise-solutions目录中找到。

在本练习中,我们了解到 reload 命令仅重置暂时存储在浏览器中的项目。通过使用cy.reload()命令,我们了解了如何重置应用的缓存存储以及如何测试它。

回顾–实施导航请求

在本节中,我们通过评估示例和练习了解了导航请求如何在 Cypress 上工作。我们还探索了各种导航命令,如cy.visit()cy.go()cy.reload(),它们在执行 Cypress 的导航请求时都起着主要作用。在下一节中,我们将了解如何使用实践练习和示例来实现网络请求。

实现网络请求

网络请求包括对后台服务的 AJAX 和 XHR 请求的处理。Cypress 通过其内置的cy.request()cy.intercept()命令来处理这个问题。在本节中,我们将采取动手操作的方法,并使用示例和练习深入探讨如何在 Cypress 中实现网络请求。我们之前在本书的 第 9 章【Cypress 测试跑者的高级使用】中与网络请求进行过互动,这一章将帮助您建立在您已经熟悉的理论知识和概念上。

cy.request()

Cypresscy.request()命令负责向 API 端点发出 HTTP 请求。该命令可用于执行 API 请求和接收响应,而无需创建或导入外部库来生成和处理我们的 API 请求和响应。我们的 Cypress 金融应用使用基于 JSON 数据库的后端应用编程接口。为了了解cy.request()命令的工作原理,我们将向数据库发出请求并检查响应。下面的代码块是从我们的应用编程接口获取所有事务的请求:

it('cy.request(): fetch all transactions from our JSON database', () => {
cy.request({
url: 'http://localhost:3001/transactions',
method: 'GET',
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.results).to.be.an
('array');
})
});

在前面的测试中,我们正在验证我们的后端是否响应了一个200状态代码和事务数据,这是一个数组。我们将在下面的练习中了解更多关于cy.request()命令的信息。

练习 4

使用 GitHub 存储库中提供的位于chapter-10/cypress-real-world-app目录的金融应用,进行以下练习,测试您对cy.server()命令的了解。该练习的解决方案可以在chapter-10/integration/navigation/network-requests-excercise-solutions目录中找到:


  1. 登录后,使用您的浏览器,调查我们首次登录时由cypress-realworld应用加载的 XHR 请求。

  2. From the observation, write a test that returns data for the following:

    应用中的联系人

    应用中的通知


通过做这个练习,你将更好地理解cy.request()命令,并增加你对 Cypress 请求如何工作的知识。接下来,我们将看看 Cypress 路由。

cy.intercept()

cy.intercept()命令管理测试网络层的 HTTP 请求行为。有了这个命令,我们就可以知道是否有人提出了 XHR 请求,以及对我们请求的响应是否符合我们的期望。我们甚至可以使用命令从路由中截取响应。借助cy.intercept(),我们可以剖析响应,并确保我们在测试中的应用实际上有正确的响应。cy.intercept()命令让我们可以在所有阶段完全访问我们的 Cypress 测试的所有 HTTP 请求。

重要说明

我们必须在测试中引用它们之前调用cy.intercept(),以便在测试中调用它们之前记录路线,从下面的测试中,我们可以在beforeEach()命令块中观察到该行为。在接下来的测试中,我们在开始运行 Cypress 测试之前调用了cy.intercept命令。

network-request.spec.js文件中的以下代码块中,我们正在验证当被测应用发出正确的登录请求时,我们是否有用户信息的响应:

describe('Netowork request routes', () => {
beforeEach(() => {
cy.intercept('POST','login').as('userInformation');
});
it('cy.intercept(): verify login XHR is called when
user logs in', () => {
cy.login();
cy.wait('@userInformation').its('
response.statusCode').should('eq', 200)
});
});

在这个代码块中,我们正在验证应用向登录端点发出了POST请求,并且我们收到了200的成功状态,这是一次成功的登录。cy.login()命令导航到应用的登录页面。在下面的练习中,我们将进一步与cy.intercept()命令交互。

练习 5

使用 GitHub 存储库中提供的位于chapter-10/cypress-real-world-app目录的金融应用,进行以下练习,测试您对cy.intercept()命令的了解。该练习的解决方案可以在chapter-10/integration/navigation/network-requests-exercise-solutions目录中找到:


  1. 登录到测试应用并导航到帐户页面。

  2. 使用 Cypress cy.route()命令检查在更改用户信息时,Cypress 是否验证用户已登录。

是时候快速回顾一下了。

回顾–实施网络请求

在本节中,我们探讨了 Cypress 网络请求是如何工作的,我们通过使用示例和练习来理解cy.request()cy.intercept()在 Cypress 测试中是如何使用的。利用这些例子和练习,我们还可以扩展我们如何使用命令的知识,例如cy.intercept()来操纵和存根。现在我们已经了解了网络请求,并且可以轻松地编写涉及 Cypress 网络请求的测试,在下一节中,我们将深入研究导航请求的高级配置。

高级导航请求配置

导航是正确运行测试的最重要方面之一。通过使用cy.visit()cy.go()甚至cy.reload()命令,我们有能力知道在编写测试时要走什么捷径。导航命令也大大简化了测试工作流程。大多数前端测试都需要导航,因此,掌握高级配置不仅会让您的生活更加轻松,还会在编写测试时带来更流畅的体验。在本节中,我们将主要关注cy.visit()的 Cypress 高级命令配置,因为它是 Cypress 的主要导航命令。

cy.visit()配置选项

下表显示了cy.visit()命令的配置选项以及没有选项传递给选项对象时加载的默认值:

cy.visit()命令接受不同类型的参数,这决定了传递给它的配置和选项。以下是命令接受的参数:


  • 只有网址的配置:

    js
    cy.visit(url)
    e.g. cy.visit('https://test.com');


  • 配置以网址和选项为对象:

    js
    cy.visit(url, options)
    e.g. cy.visit('https://test.com', {timeout: 20000});


  • 仅将选项作为对象的配置:

    js
    cy.visit(options);
    e.g. cy.visit('{timeout: 30000}');


现在是回顾时间!

回顾–高级导航请求配置

在本节中,我们学习了如何使用不同的选项配置cy.visit()命令,以及该命令接受的不同类型的参数。我们还了解到了 Cypress 为我们提供的不同的默认选项,当它们没有并且已经通过了options对象时,这使得使用cy.visit()命令的过程变得容易,因为我们只为命令提供了我们在测试中需要覆盖的选项。

总结

在本章中,我们学习了 Cypress 如何执行导航,如何创建请求,以及 Cypress 如何为我们的测试执行过程解释和返回它们。我们采用实践的方法来学习三个基本的 Cypress 导航命令,以及 Cypress 用于发出和解释请求的三个命令。这些练习为您提供了一个走出舒适区的渠道,并对 Cypress 的高级用途进行了一些研究,以及我们如何整合逻辑和我们在本书中积累的知识,编写有意义的测试,为正在测试的应用增加价值。最后,我们看了cy.visit()命令的高级配置选项。我相信,在本章中,您已经学习了在测试中处理和实现导航和网络请求以及配置导航请求的技巧。

现在,我们已经实际探索了使用手动方法在 Cypress 的导航和请求,我们将在下一章中使用相同的方法来解决使用 Cypress 的测试中的存根和间谍问题。


推荐阅读
  • 在Ubuntu系统中配置Python环境变量是确保项目顺利运行的关键步骤。本文介绍了如何将Windows上的Django项目迁移到Ubuntu,并解决因虚拟环境导致的模块缺失问题。通过详细的操作指南,帮助读者正确配置虚拟环境,确保所有第三方库都能被正确识别和使用。此外,还提供了一些实用的技巧,如如何检查环境变量配置是否正确,以及如何在多个虚拟环境之间切换。 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
  • 在 CentOS 7 系统中安装 Scrapy 时遇到了一些挑战。尽管 Scrapy 在 Ubuntu 上安装简便,但在 CentOS 7 上需要额外的配置和步骤。本文总结了常见问题及其解决方案,帮助用户顺利安装并使用 Scrapy 进行网络爬虫开发。 ... [详细]
  • 在Ubuntu系统中安装Android SDK的详细步骤及解决“Failed to fetch URL https://dlssl.google.com/”错误的方法
    在Ubuntu 11.10 x64系统中安装Android SDK的详细步骤,包括配置环境变量和解决“Failed to fetch URL https://dlssl.google.com/”错误的方法。本文详细介绍了如何在该系统上顺利安装并配置Android SDK,确保开发环境的稳定性和高效性。此外,还提供了解决网络连接问题的实用技巧,帮助用户克服常见的安装障碍。 ... [详细]
  • 数字图书馆近期展出了一批精选的Linux经典著作,这些书籍虽然部分较为陈旧,但依然具有重要的参考价值。如需转载相关内容,请务必注明来源:小文论坛(http://www.xiaowenbbs.com)。 ... [详细]
  • 如何在Linux系统中实现Windows风格的桌面环境:将Ubuntu 18.04定制为Windows主题界面
    如果您是从Windows转到Linux系统的用户,可能会觉得默认的Ubuntu主题和桌面环境缺乏吸引力和可定制性。尤其是对于习惯了Windows风格的任务栏和主题的用户,Ubuntu 18.04的橙色主题可能显得过于简洁。为了提升用户体验,可以通过安装特定的桌面环境和主题来实现类似Windows的界面效果。本文将详细介绍如何在Ubuntu 18.04中配置和定制桌面环境,使其具备Windows风格的外观和功能。 ... [详细]
  • 在开发过程中,我最初也依赖于功能全面但操作繁琐的集成开发环境(IDE),如Borland Delphi 和 Microsoft Visual Studio。然而,随着对高效开发的追求,我逐渐转向了更加轻量级和灵活的工具组合。通过 CLIfe,我构建了一个高度定制化的开发环境,不仅提高了代码编写效率,还简化了项目管理流程。这一配置结合了多种强大的命令行工具和插件,使我在日常开发中能够更加得心应手。 ... [详细]
  • REST与RPC:选择哪种API架构风格?
    在探讨REST与RPC这两种API架构风格的选择时,本文首先介绍了RPC(远程过程调用)的概念。RPC允许客户端通过网络调用远程服务器上的函数或方法,从而实现分布式系统的功能调用。相比之下,REST(Representational State Transfer)则基于资源的交互模型,通过HTTP协议进行数据传输和操作。本文将详细分析两种架构风格的特点、适用场景及其优缺点,帮助开发者根据具体需求做出合适的选择。 ... [详细]
  • 本文介绍了如何在 Windows 系统上利用 Docker 构建一个包含 NGINX、PHP、MySQL、Redis 和 Elasticsearch 的集成开发环境。通过详细的步骤说明,帮助开发者快速搭建和配置这一复杂的技术栈,提升开发效率和环境一致性。 ... [详细]
  • 在 Kubernetes 中,Pod 的调度通常由集群的自动调度策略决定,这些策略主要关注资源充足性和负载均衡。然而,在某些场景下,用户可能需要更精细地控制 Pod 的调度行为,例如将特定的服务(如 GitLab)部署到特定节点上,以提高性能或满足特定需求。本文深入解析了 Kubernetes 的亲和性调度机制,并探讨了多种优化策略,帮助用户实现更高效、更灵活的资源管理。 ... [详细]
  • 利用树莓派畅享落网电台音乐体验
    最近重新拾起了闲置已久的树莓派,这台小巧的开发板已经沉寂了半年多。上个月闲暇时间较多,我决定将其重新启用。恰逢落网电台进行了改版,回忆起之前在树莓派论坛上看到有人用它来播放豆瓣音乐,便萌生了同样的想法。通过一番调试,终于实现了在树莓派上流畅播放落网电台音乐的功能,带来了全新的音乐享受体验。 ... [详细]
  • 在GitHub上克隆vue-element-admin项目时遇到依赖安装错误
    在 GitHub 上克隆 vue-element-admin 项目后,使用 `npm install` 安装依赖时遇到了未知的 Git 错误。具体错误信息为 `npm ERR! code 128`,提示命令执行失败。这可能是由于网络问题、Git 配置不正确或某些依赖包的仓库地址无效导致的。建议检查网络连接、更新 Git 版本并确保所有依赖项的 URL 正确无误。 ... [详细]
  • 掌握Android UI设计:利用ZoomControls实现图片缩放功能
    本文介绍了如何在Android应用中通过使用ZoomControls组件来实现图片的缩放功能。ZoomControls提供了一种简单且直观的方式,让用户可以通过点击放大和缩小按钮来调整图片的显示大小。文章详细讲解了ZoomControls的基本用法、布局设置以及与ImageView的结合使用方法,适合初学者快速掌握Android UI设计中的这一重要功能。 ... [详细]
  • 在CentOS 7上部署WebRTC网关Janus
    在CentOS 7上部署WebRTC网关Janus ... [详细]
  • Linux Shell变量初探:初始值解析与使用指南
    本文探讨了Linux Shell中变量的基本概念及其在BASH中的应用。变量是用于存储可变数据的标识符,能够代表不同的值。文章详细介绍了BASH shell的主要优势,包括强大的命令编辑能力、自动补全功能、命令别名设置、作业控制以及前后台任务管理。此外,还涵盖了编程脚本编写和通配符的使用方法,为初学者提供了全面的指导。 ... [详细]
author-avatar
冒泡鱼的快乐2011
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有