基础学习路径
AJAX是Asynchronous javascript and xml 的缩写,表示异步javascript 和 xml ,是一种交互式网页应用开发技术。
AJAX是一些老技术的新应用, 应用到了html css javascript dom , 以XMLHttpRequest为技术核心实现网页异步数据传输。
最大特点就是:网页不用刷新进行数据传输
用户注册
AJAX分页效果
图片加载瀑布流效果
传统的数据提交方式
AJAX数据提交方式
通过上述对比,客户端用户部分减少了2个流程,交给了ajax去处理,那么就减少了用户的等待时间,用户体验大大的提升
减少服务器带宽,按需获得数据
注意:在一些有列表展示功能的地方优势特别突出
无刷新更新页面,减少用户的实际和心理等待时间
注:用户注册,用户登录。多数据信息的展示
更好的用户体验,传统数据提交会刷新页面,易丢失用户填写数据
主浏览器都支持
XHR = new XMLHttpRequest();
创建请求头使用OPEN,主要实现(请求类型,请求地址)
对象.open(请求类型GET/POST,请求地址,[同步true/异步false]);
默认:同步 True
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','demo.php');
主要实现请求服务器操作
对象.send(POST请求则填写POST的数据/GET请求可以不用写);
【HTML代码】
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','demo.php');
//发送请求 对象.send(POST请求则填写POST的数据/GET请求可以不用写);
XHRObj.send();
</script>
【PHP代码】
<?php
echo 'test';
脚下留心:
一定要在服务器目录下面运行AJAX-发送请求.html,不能直接用浏览器浏览该文件
对象.responseText (获取服务器响应的字符串数据)
对象.responseXML(获取服务器响应的xml数据)
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','demo.php');
//发送请求 对象.send(POST请求则填写POST的数据/GET请求可以不用写);
XHRObj.send();
//alert返回的数据
alert(XHRObj.responseText);
</script>
案例运行结果
脚下留心:无法响应数据
原因:在AJAX没有完成请求这个时候是没有返回值的,所以获取数据是没有结果的。
解决方法:通过判断reaystate == 4 是否AJAX请求完成
Onreadystatechange 作用:ajax在请求的过程中发生任何状态的变化都会调用该方法
Readystate 作用:返回ajax的请求状态
状态说明:
最终代码:以及结果
思考:为什么没有打印0~4
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','demo.php');
//发送请求 对象.send(POST请求则填写POST的数据/GET请求可以不用写);
console.log(XHRObj.readyState);
//发送请求
XHRObj.send();
//当请求状态发送变化时ajax会自动调用该方法onreadystatechange
XHRObj.onreadystatechange=function(){
console.log(XHRObj.readyState);
if(XHRObj.readyState==4){
console.log(XHRObj.responseText);
}
}
console.log(XHRObj.readyState+'sdf');
</script>
说明:
程序是从上往下进行的, 里面的代码是等发送异步请求完了才去执行的。
状态0是无法获取的,因为实例化AJAX的对象,然而监听需要对象对象里面的属性来完成,所以0装就是实例化对象的时候。
思考:避免接口写错
例如:
在实际使用中,我们为了只有在请求的接口正确的时候获取相应的数据,一般我们要判断返回的HTTP状态是否正确,
使用:
对象.status == 200
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','demo1.php');
//发送请求 对象.send(POST请求则填写POST的数据/GET请求可以不用写);
// console.log(XHRObj.readyState);
//发送请求
XHRObj.send();
//当请求状态发送变化时ajax会自动调用该方法onreadystatechange
XHRObj.onreadystatechange=function(){
// console.log(XHRObj.readyState);
if(XHRObj.readyState == 4 && XHRObj.status == 200){
console.log(XHRObj.responseText);
}
// else{
// //以邮件或者短信的形式发送通网站管理员
// alert('服务器错误,很忙......');
// }
}
// console.log(XHRObj.readyState+'sdf');
</script>
判断用户名admin是否存在,当存在的时候返回【不可用】,当不存在的时候返回【可用】
明确需求:
第一步:给按钮增加点击事件
第二步:获取用户输入的值username
第三步:使用AJAX将内容发送给服务器端PHP文件
第四步:编写PHP文件checkuser.php 判断用户是否存在,返回结果
第五步:将PHP返回的结果弹出来
<!--
第一步:给按钮增加点击事件
第二步:获取用户输入的值username
第三步:使用AJAX将内容发送给服务器端PHP文件
第四步:编写PHP文件checkuser.php 接受参数,并且判断用户是否存在,返回结果
第五步:将PHP返回的结果弹出来
-->
<script type="text/javascript">
//按钮绑定事件,给input增加ID属性
var checkObj = document.getElementById('check');
checkObj.onclick = function(){
var username = document.getElementById('username').value;
//创建ajax对象
var XHRObj = new XMLHttpRequest();
//创建请求头,设置请求发送的地址和类型,并且将参数传递给服务端
XHRObj.open('get','check.php?username='+username);
//获取服务器端返回的数据
XHRObj.onreadystatechange = function(){
if(XHRObj.readyState == 4 && XHRObj.status == 200){
alert(XHRObj.responseText);
}
}
//发送请求
XHRObj.send();
}
</script>
<?php
//定义一个用户数组
$user = array('admin','xiaoming','xiaohong','xiaoqiang');
//获取传递的参数
$username = $_GET['username'];
//判断用户是否存在在数组中
if(in_array($username,$user)){
echo '不可用';
}else{
echo '可用';
}
进化版本
1)进化要求
当用户名可用的时候后面增加√说可用,当用户名不可用的时候出现一个×提示当前用户名太火,请换一个
HTML代码
<style>
.error{
color: red;
font-size: 14px;
}
.green{
color: green;
font-size: 14px;
}
</style>
<body>
<!-- <span class="error">×此用户名太首欢迎,请换一个</span>-->
<!-- <span class="green">√恭喜你,该用户可用</span>-->
<table border="1">
<th colspan="2">用户注册</th>
<tr>
<td><input id="username" name="username" type="text"/><div id='error'></div></td>
<td><input id="check" type="button" value="检测用户"/></td>
</tr>
</table>
</body>
<!--
第一步:给按钮增加点击事件
第二步:获取用户输入的值username
第三步:使用AJAX将内容发送给服务器端PHP文件
第四步:编写PHP文件checkuser.php 接受参数,并且判断用户是否存在,返回结果
第五步:将PHP返回的结果弹出来
-->
<script type="text/javascript">
//按钮绑定事件,给input增加ID属性
var checkObj = document.getElementById('check');
checkObj.onclick = function(){
var username = document.getElementById('username').value;
//创建ajax对象
var XHRObj = new XMLHttpRequest();
//创建请求头,设置请求发送的地址和类型,并且将参数传递给服务端
XHRObj.open('get','check.php?username='+username);
//获取服务器端返回的数据
XHRObj.onreadystatechange = function(){
if(XHRObj.readyState == 4 && XHRObj.status == 200){
// alert(XHRObj.responseText);
if(XHRObj.responseText =='可用'){
document.getElementById('error').innerHTML='<span class="green">√恭喜你,该用户没有被注册</span>';
}else{
document.getElementById('error').innerHTML=' <span class="error">×此用户名太首欢迎,请换一个</span>';
}
}
}
//发送请求
XHRObj.send();
}
</script>
数据方面:GET受浏览器的影响
POST 原则上是不受限制的,可以通过PHP配置POST_MAX_SIZE进行更改
安全方面:POST比GET要安全
文件上传:GET不能进行文件上传
说明:在请求地址后面增加参数,例如:demo.php?a=111&b=222&c=333
【HTML代码】
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头,请求方式,请求链接
XHRObj.open('get','test.php?a=111&b=222&c=333');
//发送请求
XHRObj.send();
</script>
【PHP代码】
<?php
var_dump($_GET);
Open(‘POST’,请求地址);
设置发送的数据格式,采用URL编码格式
对象.setRequestHeader(‘content-type’,’application/x-www-form-urlencoded’);
对象.send(发送的数据);
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
XHRObj.onreadystatechange = function() {
if (XHRObj.readyState == 4) {
alert(XHRObj.responseText);
}
}
//创建请求头,请求方式,请求链接
XHRObj.open('post','test.php');
//发送post的数据
var postData = 'name=123123&age=rrr';
//设置数据编码格式,使用URL编码格式
XHRObj.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
//发送请求
XHRObj.send(postData);
</script>
【PHP代码】
<?php
var_dump($_POST);
效果
4.练习:表单无刷新数据录入
同步:等待服务器响应完成在执行下一段JS代码 (阻塞模式)
异步:不等服务器响应完成直接执行下一段JS代码(非阻塞模式)
设置open(方式,请求地址,false/同步);
HTML【代码】
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','demo.php',false);
//时时监控
XHRObj.onreadystatechange = function(){
if(XHRObj.readyState == 4){
console.log('111');
}
}
//发送请求 对象.send(POST请求则填写POST的数据/GET请求可以不用写);
XHRObj.send();
console.log('js执行完成');
</script>
【PHP代码】
为了增加延迟效果使用sleep
<?php
sleep(8);
echo 'test';
【HTML代码】
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','demo.php');
//时时监控
XHRObj.onreadystatechange = function(){
if(XHRObj.readyState == 4){
console.log('111');
}
}
//发送请求 对象.send(POST请求则填写POST的数据/GET请求可以不用写);
XHRObj.send();
console.log('js执行完成');
</script>
【PHP代码】
<?php
sleep(8);
echo 'test';
我们再浏览一些网站的时候浏览器为了方便用户再次访问的时候增加用户访问体验会将一些静态资源文件缓存到本地
缓存的位置在:
选中IE浏览器右键
打开
静态缓存目录就出现再这里面了
说明:IE存在缓存
代码设置:?t=Math.random()
缺点:
1.不能保证URL绝对唯一
2.产生大量缓存文件
代码设置:?t=new Date().getTime(); //取得毫秒时间戳
优点:保证URL绝对唯一
缺点:依然产生大量缓存文件
代码设置:对象.setRequestHeader("If-Modified-Since","0");
原理分析:
If-Modified-Since是标准的HTTP请求头标签,在发送HTTP请求时,把浏览器端缓存页面的最后修改时间一起发到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行比较。
如果时间一致,那么返回HTTP状态码304(不返回文件内容),客户端接到之后,就直接把本地缓存文件显示到浏览器中。
如果时间不一致,就返回HTTP状态码200和新的文件内容,客户端接到之后,会丢弃旧文件,把新文件缓存起来,并显示到浏览器中。
代码设置:header("Cache-Control: no-cache, must-revalidate");
原理分析:
利用php的header函数向响应头中写数据,写的是告诉客户端:不要对本次的结果进行缓存。
这种做法,可以从根本上解决缓存问题,不产生任何缓存文件。
普通字符串文本格式:responseText
XML数据格式:responseXML
JSON 字符串数据格式:responseText (在实际工作中用到最多,最广泛的格式)
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','text.php');
//时时监控
XHRObj.onreadystatechange = function(){
if(XHRObj.readyState == 4 && XHRObj.status == 200){
document.getElementById('content').innerHTML=XHRObj.responseText;
}
}
//发送请求 对象.send(POST请求则填写POST的数据/GET请求可以不用写);
XHRObj.send();
</script>
<body>
<div id="content"></div>
</body>
<?php
echo '<h1>返回的文本</h1>';
最终效果
<script type="text/javascript">
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//创建请求头
XHRObj.open('GET','xml.php');
//时时监控
XHRObj.onreadystatechange = function(){
if(XHRObj.readyState == 4 && XHRObj.status == 200){
var xmlObj = XHRObj.responseXML;
//以前获取html文档我们使用 document.getElementByTagName()
books = xmlObj.getElementsByTagName('book');
for(i=0;i<books.length;i++){
//获取第二级的值我们使用childen 方法
for(j=0;j<books[i].children.length;j++){
console.log(books[i].children[j].innerHTML);
}
}
}
}
//发送请求 对象.send(POST请求则填写POST的数据/GET请求可以不用写);
XHRObj.send();
</script>
1.2PHP代码
首先确保PHP写的xml文件能再浏览器端访问
<?php
header('Content-Type:text/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="utf-8"?>
<books>
<book>
<name>西游记</name>
<price>50.12</price>
</book>
<book>
<name>三国演义</name>
<price>876.12</price>
</book>
</books>
';
JSON(javascript Object Notation js 对象标记) 是一种轻量级的数据交换格式。
数据格式比较简单,易于读写, 格式都是压缩的,占用带宽小
易于解析这种语言,客户端JavaScript可以简单的通过eval()进行JSON数据的读取
因为JSON格式能够直接为服务器端代码使用, 大大简化了服务器端和客户端的代码开发量, 但是完成的任务不变, 且易于维护
PHP端生成JSON数据使用:json_encode(数组数据格式);
PHP端解析JSON数据使用:json_decode(待解码数据,true/false);
说明:false 解码出来的数据是一个对象,true,解码出来的是一个数组
由于我们返回值的处理使用的是responseText 格式
语法格式:JSON.parse(字符串);
作用:从一个字符串中解析出json数据对象
前提:字符串必须是json格式的字符串
用户会员注册功能,用户填写好根据规则进行验证,如果验证成功提示用户注册成功。
验证规则:
1.用户名不能为空
2.用户名必须是由数字和字母组成,而且是在6~8位之间
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>AJAX-用户注册</title>
</head>
<style>
dd{float: left;}
</style>
<body>
<div>
<dl>
<dd>用户名</dd>
<dd><input type="text" name="username" id="username"></dd>
<dd><input type="button" id="regbtn" value="注册"></dd>
</dl>
</div>
</body>
<script type="text/javascript">
//给注册按钮增加click事件
var regbtn = document.getElementById('regbtn');
regbtn.onclick = function(){
//获取用户输入的值
var username = document.getElementById('username').value;
//创建AJAX对象
var XHRObj = new XMLHttpRequest();
//打开对象
XHRObj.open('get','reg.php?username='+username);
//实时监控AJAX运行状态
XHRObj.onreadystatechange = function(){
//判断服务器是否响应成功
if(XHRObj.readyState==4 && XHRObj.status == 200){
//将json数据转换成对象
jsonObj = JSON.parse(XHRObj.responseText);
//判断用户是否操作成功进行页面跳转
if(jsonObj.state ==1){
location.href='success.html';
}else{
alert(jsonObj.msg);
}
}
}
//发送请求
XHRObj.send();
}
</script>
</html>
最近写博客真的是太痛苦了,倒不是写博客本身,而是写完之后往多个平台发布的过程,一不注意就是十多分钟往上的时间消耗。
为了解决这个问题,之前立项的“解决自媒体一键多平台发布”项目必须得立刻着手完善了,争取早日让自己从发布这件事情上解脱出来专心写文章。
【hxsfx的JavaScript库】这个系列基本上是为“一键多平台发布”项目打基础用的。之所以把各个功能模块拆分出来,是为了尽量让小伙伴能够复制即用(在兼容性方面,因为个人能力的原因,几乎只会兼容Chrome浏览器)。
AJAX 是异步的 JavaScript 和 XML(Asynchronous JavaScript And XML),开发hxsfx.ajax库的主要目的就是希望通过异步加载HTML,从而尽量避免直接在js中写HTML来刷新页面内容。
hxsfx.ajax这个库与jquery的ajax功能基本一致,不过短时间内应该是写不到人家那么完善的。哈哈~
各位小伙伴别问,为什么不用jquery的ajax而要自己再写一个呢?
问就是,博主喜欢造轮子。开玩笑了~
其实原因是为了减少三方库的依赖,达到对项目的全面掌控。
项目地址:https://github.com/hxsfx/hxsfx_web_tools
要自己开发一个ajax库,需要借助Web API接口中的XMLHttpRequest(XHR)对象。
XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。
//hxsfx.js
(function () {
window.hxsfx = {};
})();
//ajax.js
(function () {
window.hxsfx.ajax = {
};
})();
//ajax.js
(function () {
window.hxsfx.ajax = {
loadHTML: function (ele, url) {
}
};
})();
//ajax.js
(function () {
window.hxsfx.ajax = {
loadHTML: function (ele, url) {
const httpRequest = new XMLHttpRequest();
httpRequest.open('GET', url, true);
httpRequest.onreadystatechange = function () {
//为了让代码更健壮,使用try...catch来捕获返回状态判断和处理HTML代码的异常
try {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
//在这处理返回的HTML
}
}
else {
console.log("【ajax.get(" + url + ")请求出错】");
}
}
}
catch (ex) {
console.log("【ajax.get(" + url + ")异常】" + ex.message);
}
};
httpRequest.send();
}
};
})();
//ajax.js
//时间戳用来解决加载页面缓存的问题
var urlTimeStamp = "timeStamp=" + new Date().getTime();
url += ((url.indexOf('?') >= 0) ? "&" : "?") + urlTimeStamp;
//ajax.js
ele.innerHTML = httpRequest.responseText;
var scriptElements = ele.getElementsByTagName("script");
for (var i = 0; i < scriptElements.length; i++) {
var scriptElement = document.createElement("script");
scriptElement.setAttribute("type", "text/javascript");
var src = scriptElements[i].getAttribute("src");
if (src) {
//因为加载的src路径是之前相对加载HTML页面的,需要修改为当前页面的引用路径
src = url.substring(0, url.lastIndexOf('/') + 1) + src;
src += ((src.indexOf('?') >= 0) ? "&" : "?") + urlTimeStamp;
scriptElement.setAttribute("src", src);
}
var scriptContent = scriptElements[i].innerHTML;
if (scriptContent) {
scriptElement.innerHTML = scriptContent;
}
//用生成的script元素去替换html中的script标签,以此来激活script代码
ele.replaceChild(scriptElement, scriptElements[i]);
}
//ajax.js
(function () {
window.hxsfx.ajax = {
loadHTML: function (ele, url) {
//时间戳用来解决加载页面缓存的问题
var urlTimeStamp = "timeStamp=" + new Date().getTime();
url += ((url.indexOf('?') >= 0) ? "&" : "?") + urlTimeStamp;
const httpRequest = new XMLHttpRequest();
httpRequest.open('GET', url, true);
httpRequest.onreadystatechange = function () {
//为了让代码更健壮,使用try...catch来捕获返回状态判断和处理HTML代码的异常
try {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
ele.innerHTML = httpRequest.responseText;
var scriptElements = ele.getElementsByTagName("script");
for (var i = 0; i < scriptElements.length; i++) {
var scriptElement = document.createElement("script");
scriptElement.setAttribute("type", "text/javascript");
var src = scriptElements[i].getAttribute("src");
if (src) {
//因为加载的src路径是之前相对加载HTML页面的,需要修改为当前页
src = url.substring(0, url.lastIndexOf('/') + 1) + src;
src += ((src.indexOf('?') >= 0) ? "&" : "?") + urlTimeStamp;
scriptElement.setAttribute("src", src);
}
var scriptContent = scriptElements[i].innerHTML;
if (scriptContent) {
scriptElement.innerHTML = scriptContent;
}
//用生成的script元素去替换html中的script标签,以此来激活script代
ele.replaceChild(scriptElement, scriptElements[i]);
}
}
}
else {
console.log("【ajax.get(" + url + ")请求出错】");
}
}
}
catch (ex) {
console.log("【ajax.get(" + url + ")异常】" + ex.message);
}
};
httpRequest.send();
}
};
})();
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>js库测试</title>
<script src="Scripts/hxsfx/hxsfx.js"></script>
<script src="Scripts/hxsfx/ajax.js"></script>
<script>
window.onload = function () {
//调用ajax中的loadHTML方法
window.hxsfx.ajax.loadHTML(document.getElementById("ContentContainer"), "Views/test/testPage.html");
};
</script>
</head>
<body>
<div id="ContentContainer"></div>
</body>
</html>
<style>
div#TestPageContainer {
height: 300px;
width: 300px;
background-color: #F0F0F0;
}
</style>
<script src="../../Scripts/hxsfx/test/test.js"></script>
<script>
function updateBackgroundColor() {
try {
var backgroundColor = '#' + (Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'));
document.getElementById("TestPageContainer").style.backgroundColor = backgroundColor;
}
catch (ex) {
console.log(ex.message);
}
}
</script>
<div id="TestPageContainer">
<button onclick="updateBackgroundColor()">更改背景色</button>
<button onclick="modifyPFontColor()">更改下面一句话的字体颜色</button>
<p id="P">这儿是一句话。</p>
</div>
以上内容只是hxsfx.ajax库的开始,后续的内容更新小伙伴可以通过访问Github项目地址进行获取。
题
在浏览器中浏览了网页之后,下一步就是查看其HTML源代码。尽管这种方法很简单,但仍然非常值得去做。查看源代码有两项作用;它可以帮助你发现最明显的安全问题,但最重要的是,它使你能够为将来的测试建立一个比较基准。对攻击失败之前和之后的源代码进行比较,你就能够调整你是输入,了解到哪些通过了,哪些没有通过,并再次尝试。
解决方案
我们推荐使用Firefox,你已经在2.1节中学会了它的安装。首先浏览应用中你所感兴趣的网页。
右击,并选择“查看源文件”或从菜单栏选择“查看”→ "源文件"。
我们推荐Firefox的主要原因是因为它的彩色显示。如图3-1所示,使用这种显示方式,HTML标签和属性都要容易理解得多。相比之下,Internet Explorer在记事本中打开网页。就会难读得多。
讨论
作为比较基准,访问HTML源代码会非常有帮助。最常见的Web漏洞涉及到向Web应用提供恶意输入以修改HTML源代码。在测试这些漏洞时,验证测试通过或失败的最简单的方法就是检查源代码是否被恶意更改。
当心一切未经更改就写进源代码中的输入。我们将在第8章讨论输入验证,然后许多应用根本就不对输入进行验证。在开始讨论更加复杂的内容之前,不妨在源代码中搜索你刚刚提供的输入。然后,使者将可能的危险值作为输入,比如HTML标签或JavaScript,并注意它是否未经修改就直接显示在源代码中。如果是这样的话,那么这就是个警示信号。
注意,你可以像搜索任何其他Firefox页面那样搜索HTML源代码(根据具体情况,使用Ctrl+F或(Windows徽标键)+F)。
在以后的秘诀和章节中,我们将使用工具来自动搜索、解析和比较源代码。记住基本要点;通常,可以通过重复地手动检查源代码以检查怎样做才能使它通过筛选程序或编码找出漏洞。
注意:你在这里看到的静态源代码不能反映JavaScript或AJAX功能所做的任何更改。
搜索微信公众号:TestingStudio霍格沃兹的干货都很硬核
*请认真填写需求信息,我们会在24小时内与您取得联系。