自学JavaScript第四天- JS 进阶:AJAX Promise Canvas

37 篇文章 4 订阅
9 篇文章 0 订阅

AJAX

使用 XMLHttpRequest

现代浏览器上写AJAX主要依靠 XMLHttpRequest 对象

// 定义成功的回调函数
function success(text) {
	var textarea = documnet.getElementById('test-response-text');
	textarea.value = text;
}
// 定义失败的回调函数
function fail(code) {
	var textarea = documnet.getElementById('test-response-text');
	textarea.value = 'Error code:' + code;
}
var request = new XMLHttpRequest();		// 新建 XMLHttpRequest 对象
// 状态发生变化时,函数被回调
// 也可以使用添加事件监听 request.addEventListener('readystatechange', () => {})
request.onreadystatechange = function() {		
	if (request.readyState === 4) {		// 成功完成
		// 判断响应结果:
		if (request.status === 200) {
			// 成功,通过 responseText 拿到响应文本:
			return success(request.responseText);
		} else {
			// 失败,根据响应码判断失败原因:
			return fail(request.status);
		}
	} else {
		// HTTP 请求还在继续
	}
}
// 建立发送的请求,true 表示异步发送
request.open('GET', '/api/categories', true);
// 设置请求头
request.setRequestHeader('Content-Type', 'application/json;charset=utf-8');
// 设置请求体(请求数据)
let data = {'status':'ready'};
// 因为原生 ajax 发送的数据必须是字符串,所以需要序列化
data = JSON.stringify(data);

// 发送请求,在此直线需要定义回调函数
request.send(data);

当创建了 XMLHttpRequest 对象后,要先设置 onreadystatechange 的回调函数。在回调函数中,通常我们只需通过 readyState === 4 判断请求是否完成(刚连接上服务器为 1 ,向服务器发送数据是 2,服务器传送数据完成是 4),如果已完成,再根据 status ===200 判断是否是一个成功的响应。

XMLHttpRequest 对象的 open() 方法有3个参数,第一个参数指定是 GET 还是 POST ,第二个参数指定URL地址,第三个参数指定是否使用异步,默认是 true ,所以不用写。如果设置为同步,则浏览器会停止响应,呈现“假死”状态,直到 AJAX 请求完成。

最后调用 send() 方法才真正发送请求。 GET 请求不需要参数, POST 请求需要把body部分以字符串或者 FormData 对象传进去。

<input type='file' id='upload' onchange='uploadImg(this.files[0])'>

<script>
	// 自定义 $ 函数,获取 dom 对象
    function $(domID) {
        return document.getElementById(domID)
    }
    function uploadImg(file) {
    	// 判断文件类型
        if (file.type.startsWith('image/')) {
        	// 判断文件大小
            if (file.size <= 1024 * 1024 * 2) {
            	// 设置上传地址
                let url = "/upload";
                let request = new XMLHttpRequest();
                // 创建监听事件
                request.onload = function (ev) {
                    if (request.status === 200 && request.readyState === 4) {
                        let respText = request.responseText;
                        // 解析为 json 对象
                        let respJson = JSON.parse(respText);
						console.log(respJson);
                    }
                };
                // 创建上传的数据 form 对象
                let dataform = new FormData();
                // 添加字段和内容
                dataform.append('photo', file);
                // 建立请求
                request.open('post', url, true);
                // 发送请求
                request.send(dataform)
            } else {
            	// 文件大小超过限制
                alert('上传的图片大小在2M内');
            }
        } else {
        	// 文件类型超过限制
            alert('只能上传图片文件');
        }
    }
</script>

获取表单数据对象

ajax 使用表单数据对象时,有两种方法:

// 创建 FormData 对象,并将需要传送的数据添加至对象中
let dataform = new FormData();
let file = document.getElementById('file').files[0];
let name = document.getElementById('name').value;
let school = document.querySelector('#school').value;
dataform.append('file', file);
dataform.append('name', name);
dataform.append('school', school);
// 直接获取 form 的 Dom 对象,因为一个页面可以有多个 form,所以需使用 form 的 name 属性进行指定
let form = document.forms.login		// <form name="login"> 
// 也可以使用列表索引的方式	document.forms[0]
// 将 form 的 Dom 对象填入 FormData 对象中
let dataform = new FormData(form);

需注意的是,使用 ajax 传送 FormData 对象,只能使用 POST 发送 multipart/form-data 类型。如果需要发送其他类型,如 json,则需进行转换、序列化。

使用 fetch() 方法

fetch 是 XMLHttpRequest 的升级版,,用于在 JavaScript 脚本里面发出 HTTP 请求。fetch() 的功能与 XMLHttpRequest 基本相同,但有三个主要的差异

  • fetch() 使用 Promise,不用回调函数,简化了写法
  • 采用模块化设计,API分散在多个对象上(Response、Request、Headers),使用更合理
  • fetch() 通过数据流(Stream对象)处理数据,可以分块读取,有利于提高性能,减少内存占用,适用于大文件或网速慢的场景

简单使用

function login() {
	// 设置请求参数
	let option = {
		method: 'post',		// 请求方法
		body: JSON.stringify({		// 请求体数据
			'name': '张三',
			'pwd': '123456'
		}),
		headers: { 'Content-Type': 'application/json'},		// 请求头数据
		mode: 'cors'		// 请求模式,使用 cors 跨域(只在跨域时使用)
	};
	// 参数1是请求地址,参数2是请求参数
	fetch('http://text.io/login', option)
		.then(response=>return response.json())	// 执行的是 resolve,即成功后的处理方法
		.catch(err=>console.log(err))		// 执行的 reject ,即失败的处理方法
}

then 中进行数据处理时需要注意,返回的结果包裹在一个 Promise 对象里面, 故可使用 .then 接收, res 是 fetch 包装的一个原始对象,如果想要拿到后端返回的结果则需要使用 res.json() 获取到使用 Promise 包装的后端返回的(响应体 body)数据。即

fetch(url).then(resp=>{
	res=resp.json();
	console.log(res);		// 可以发现 res 是一个 promise 对象
	return res
	}).then(res=>{
	console.log(res);		// 此时传入的数据是 json 对象
	})

使用 Promise

//请求的网址
var url = '网址';;
//发起get请求
var promise = fetch(url).then(function(response) {

   //response.status表示响应的http状态码
   if(response.status === 200){
     //json是返回的response提供的一个方法,会把返回的json字符串反序列化成对象,也被包装成一个Promise了
     return response.json();
   }else{
     return {}
   }
});

// 获取 promise 对象,在其 then 方法中进行数据处理,相当于连续 then 分开写
promise = promise.then(function(data){
  //响应的内容
	console.log(data);
}).catch(function(err){
	console.log(err);
})

显式异步代码

fetch('网址')
	// fetch()接收到的response是一个 Stream 对象
	// response.json()是一个异步操作,取出所有内容,并将其转为 JSON 对象
  .then(response => response.json()) 
  .then(json => console.log(json))//获取到的json数据
  .catch(err => console.log('Request Failed', err)); 

// 等价于以下写法
async function getJSON() {
  let url = '网址';
  try {
    let response = await fetch(url);		// 在fetch外获取 response 对象
    return await response.json();
  } catch (error) {
    console.log('Request Failed', error);
  }
}
console.log(getJSON());	// 获取到的json数据

参数选项

fetch() 的参数选项有:

method:HTTP 请求的方法,POSTDELETEPUT都在这个属性设置。

headers:一个对象,用来定制 HTTP 请求的标头。

body:POST 请求的数据体。

cache:指定如何处理缓存。可能的取值如下:
/*
	default:默认值,先在缓存里面寻找匹配的请求。
	no-store:直接请求远程服务器,并且不更新缓存。
	reload:直接请求远程服务器,并且更新缓存。
	no-cache:将服务器资源跟本地缓存进行比较,有新的版本才使用服务器资源,否则使用缓存。
	force-cache:缓存优先,只有不存在缓存的情况下,才请求远程服务器。
	only-if-cached:只检查缓存,如果缓存里面不存在,将返回504错误。
*/
mode: 指定请求的模式。可能的取值如下:
/*
	cors:默认值,允许跨域请求。
	same-origin:只允许同源请求。
	no-cors:请求方法只限于 GET、POST 和 HEAD,并且只能使用有限的几个简单标头,不能添加跨域的复杂标头,相当于提交表单所能发出的请求。
*/
credentials:指定是否发送 Cookie。可能的取值如下:
/*
	same-origin:默认值,同源请求时发送 Cookie,跨域请求时不发送。
	include:不管同源请求,还是跨域请求,一律发送 Cookie。
	omit:一律不发送。
*/
signal:指定一个 AbortSignal 实例,用于取消fetch()请求

keepalive:用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。
/*
一个典型的场景就是,用户离开网页时,脚本向服务器提交一些用户行为的统计信息。
这时,如果不用keepalive属性,数据可能无法发送,因为浏览器已经把页面卸载了。
*/
redirect: 指定 HTTP 跳转的处理方法。可能的取值如下:
/*
	follow:默认值,fetch()跟随 HTTP 跳转。
	error:如果发生跳转,fetch()就报错。
	manual:fetch()不跟随 HTTP 跳转,但是response.url属性会指向新的 URL,response.redirected属性会变为true,由开发者自己决定后续如何处理跳转。
*/
integrity:指定一个哈希值,用于检查 HTTP 回应传回的数据是否等于这个预先设定的哈希值。
/*
比如,下载文件时,检查文件的 SHA-256 哈希值是否相符,确保没有被篡改
fetch('http://site.com/file', {
  integrity: 'sha256-abcdef'
});
*/
referrer: 用于设定fetch请求的referer标头。
/*
这个属性可以为任意字符串,也可以设为空字符串(即不发送referer标头)。
*/
referrerPolicy: 用于设定Referer标头的规则。可能的取值如下:
/*
	no-referrer-when-downgrade:默认值,总是发送Referer标头,除非从 HTTPS 页面请求 HTTP 资源时不发送。
	no-referrer:不发送Referer标头。
	origin:Referer标头只包含域名,不包含完整的路径。
	origin-when-cross-origin:同源请求Referer标头包含完整的路径,跨域请求只包含域名。
	same-origin:跨域请求不发送Referer,同源请求发送。
	strict-origin:Referer标头只包含域名,HTTPS 页面请求 HTTP 资源时不发送Referer标头。
	strict-origin-when-cross-origin:同源请求时Referer标头包含完整路径,跨域请求时只包含域名,HTTPS 页面请求 HTTP 资源时不发送该标头。
	unsafe-url:不管什么情况,总是发送Referer标头。
*/

使用fetch时有以下需要注意:

  • fetch 响应的结果是一个 Promise 对象
  • 只要相应成功 Promise 状态都为 resolve + 响应成功且状态码 200,响应失败情况 (fetch 返回的 Promise 状态为 reject)。但是响应成功但状态码在 200~299 之外,ok 值为 false。
  • 只要 fetch 的请求有响应,fetch() 返回的 Promise 状态都会标记为 resolve,即使响应的状态码不是 200 (404、502 其他)。只有 fetch() 无响应的时候返回的 Promise 状态才为 reject (一般网络问题或请求被拦截才会出现)。
  • fetch() 的第一个参数也可以为 Request 对象, 效果和只传入一个 String 类型的 url 效果是一样的。

终止

使用 AbortController 接口的 abort() 方法能够终止 fetch 请求。

let controller = new AbortController();
let signal = controller.signal;
fetch('http://localhost:8000/getInfo',{
    method: 'get', 
    signal: signal
}).then(res=>{
    console.log(res);
})
.catch(error=>{
    console.log('出错了:',error);
})
// 发起请求后立即取消请求
controller.abort()

例如超时终止

let controller = new AbortController();
let signal = controller.signal;
setTimeout(() => controller.abort(), 6000);
fetch(new Request('http://localhost:8000/getInfo'),{
    method: 'get', 
    signal: signal
}).then(res=>{
    console.log(res);
})
.catch(error=>{
    console.log('出错了:',error);
})

这种方式超时后就终止了 fetch ,但是没有收到任何返回结果。

也可以将计时器也设置为 Promise 对象,让计时器和 fetch 一起比赛执行,计时器到时后可以返回一个自定义的 Response 对象。

let controller = new AbortController();
let signal = controller.signal;

let timeoutPromise = (timeout) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(new Response("timeout", { status: 504, statusText: "timeout " }));
            controller.abort();
        }, timeout);
    });
};
let requestPromise = (url) => {
    return fetch(url, {
        signal: signal
    });
};

Promise.race([timeoutPromise(1000), requestPromise("https://www.baidu.com")])
    .then(resp => {
        console.log(resp);
    })
    .catch(error => {
        console.log(error);
    });

获取 response 内容

最常获取 response 内容信息,就是响应头和响应内容了。

响应头是 Response.headers属性 ,可以使用 for ... of ... 进行遍历,也可以使用 response.headers.get() 方法获取指定响应头信息。也可以使用 response.headers.forEach() 来依次遍历标头,每个标头都会执行一次参数函数。

响应内容可以使用以下方法获取:

  • response.text():得到文本字符串,用于获取文本数据,比如 HTML 文件。
  • response.json():得到 JSON 对象。
  • response.blob():得到二进制 Blob 对象,例如读取图片文件,显示在网页上。
  • response.formData():得到 FormData 表单对象,主要用在 Service Worker 里面,拦截用户提交的表单,修改某些数据以后,再提交给服务器。
  • response.arrayBuffer():得到二进制 ArrayBuffer 对象,主要用于获取流媒体文件。

需注意的是,fetch 使用的是数据流 Steam 对象处理信息,Stream 对象只能读取一次,读取完就没了。这意味着五个读取方法,只能使用一个,否则会报错。Response 对象提供Response.clone()方法,创建Response对象的副本,实现多次读取(每个副本也只能读取一次)。

使用 axios

axios 是通过 Promise 实现对 ajax 技术的一种封装。fetch 是底层次的 api,浏览器原生支持的,而 axios 是一个封装好的框架,所以 axios 有一些 fetch 没有的优点。

因为 axios 是封装好的框架,所以使用需要先引入

<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.js"></script>

axios 发送并处理请求

axios 发送 get 和 post 请求
// get请求
axios({
  method: 'GET',		// 请求方式
  url: '/user',	// 接口地址
  params:{				// get请求参数
      name:'zs',		
      age:20
  }  
}).then(res=>{		// 成功处理函数
    console.log(res);
});

// post请求
axios({
  method: 'POST',		// 请求方式
  url: '/user',			// 接口地址
  data:{				// post请求参数, 请求体
      name:'zs',		
      age:20
  }
}).then(res=>{			// 成功处理函数
    console.log(res);
})
axios.get()
axios.get('数据接口地址',{
    // get请求的参数
    params:{
        name:'zs',
        age:20
    }
}).then(res=>{		// 成功处理函数
    console.log(res);
})
axios.post()
axios.post('数据接口地址',{	
    name:''		// 请求体
}).then(res=>{	// 成功处理函数
    console.log(res);
})

拦截器

全局配置

处理 AJAX 数据

当 AJAX 拿到响应后,可以使用 request.responseText 获取响应文本。如今大多数数据都是基于 JSON 格式的,所以要使用时也需要将响应文本解析成 JSON 对象。

let json = JSON.parse(request.responseText)		// 将响应文本解析成为 JSON 对象

相应的,发送数据时,也需要将数据对象序列化为 JSON 对象才能发送

let json = JSON.stringify(obj);

安全限制

默认情况下,JavaScript在发送AJAX请求时,URL的域名必须和当前页面完全一致。如果使用别的域名,则会报错,这是因为浏览器的同源策略导致的。完全一致的意思是,域名要相同( www.example.com 和 example.com 不同),协议要相同( http 和 https 不同),端口号要相同(默认是 :80 端口,它和 :8080 就不同)。有的浏览器口子松一点,允许端口不同,大多数浏览器都会严格遵守这个限制。

使用 js 请求外域的URL,主要有四种方法

  • 通过 flash 插件,现在已经被抛弃
  • 通过同源域名下的代理服务器转发
  • 使用 JSONP,但是有个限制,只能使用 GET 请求,并且要求返回 JavaScript。
  • 如果浏览器支持 HTML5 ,可以使用新的跨域策略 CORS,这也是现在推荐使用的方式。

注:由于同源的安全策略是浏览器限制,所以请求的发送和响应是可以进行的,只是浏览器不接受罢了。另此策略不是对所有请求均制约,对 XmlHttpRequest 是制约的,对 img、iframe、script 等有 src 属性的标签不进行制约(所以这些标签可以跨域使用)。

跨域方案 CORS

CORS全称Cross-Origin Resource Sharing,是HTML5规范定义的如何跨域访问资源。

Origin表示本域,也就是浏览器当前页面的域。当JavaScript向外域发起请求后,浏览器收到响应后,首先检查 Access-Control-Allow-Origin 是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript将无法获取到响应的任何数据。

可见,跨域能否成功,取决于对方服务器是否愿意给你设置一个正确的 Access-Control-Allow-Origin ,决定权始终在对方手中。

上面这种跨域请求,称之为“简单请求”。简单请求包括GET、HEAD和POST(POST的Content-Type类型 仅限 application/x-www-form-urlencoded 、 multipart/form-data 和 text/plain ),并且不能出现任何自定义头(例如, X-Custom: 12345 ),通常能满足90%的需求。

非简单请求的跨域请求,需要进行预检。PUT、DELETE以及其他类型如 application/json 的POST请求,在发送AJAX请求之前,浏览器会先发送一个 OPTIONS 请求(称为preflighted请求)到这个URL上,询问目标服务器是否接受,服务器必须响应并明确指出允许的Method,浏览器确认服务器响应的 Access-Control-Allow-Methods 头确实包含将要发送的AJAX请求的Method,才会继续发送AJAX,否则,抛出一个错误。除此外,还包括检查是否符合 Access-Control-Allow-Headers 等设置的要求。

由于以 POST 、 PUT 方式传送JSON格式的数据在REST中很常见,所以要跨域正确处理 POST 和 PUT 请求,服务器端必须正确响应 OPTIONS 请求。

前端代码

前端使用 XMLHttpRequest 或 jQuery 没有什么变化,正常发送 ajax 请求即可。对于简单请求后端会添加 Access-Control-Allow-Origin ,然后返回数据,浏览器检查 Access-Control-Allow-Origin 并决定是否接受此响应。对于非简单请求会自动发送 options 请求进行预检,预检完成后,发送负载请求。即所有工作在后端和浏览器进行。另外如果使用 fetch() 方法发送 ajax 请求,则注意需要在请求参数中添加 mode:'cors',表示使用 CORS 跨域请求。

Promise 对象

在JavaScript的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现。异步操作会在将来某个时间触发一个函数调用(回调),但是正常的写法(例如AJAX)很不好看,而且不利于代码复用,于是就出现了 Promise 对象。

Promise 会“承诺将来会执行”回调函数,Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。例如 AJAX 可以写成

// ajax 函数将返回 Promise 对象:
function ajax(method, url, data) {
	var request = new XMLHttpRequest();
	return new Promise(function (resolve, reject) {
		request.onreadystatechange = function() {
			if (request.readyState === 4) {
				if (request.status === 200) {
					resolve(request.responseText);
				} else {
					reject(request.status);
				}
			}
		};
		request.open(method, url);
		request.send(data);
	});
}
// 调用 ajax
var log = documnet.getElementById('test-promise-ajax-result');
var p = ajax('GET', '/api/categories');
p.then(function (text) {		// 如果 ajax 成功,获得响应内容
	log.innerText = text;
}).catch(function (status) {	// 如果 ajax 失败,获得响应代码
	log.innerText = 'ERROR:' + status;
});

即,将一个异步函数作为参数构建 Promise 对象,此函数有两个参数 resolve 和 reject,分别表示调用 成功 / 失败的回调。那么 Promise 对象就可以通过 then 和 catch 方法调用相应的回调函数。

并行执行异步任务

可以同时执行多个异步任务,并等待其执行结果。使用 Pormise.all() 方法,并将需要执行的 promise 任务对象放在数组中传入即可。

// 同时执行 p1 和 p2, 并在它们都完成后执行 then:
Promise.all([p1, p2]).then(function (results) {
	console.log(results);		// 获得一个各任务结果组成的 Array
});

有时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用 Promise.race() 方法实现,其他后返回的结果会被丢弃。

Canvas

Canvas是HTML5新增的组件,它就像一块幕布,可以用JavaScript在上面绘制各种图表、动画等。没有Canvas的年代,绘图只能借助Flash插件实现,页面不得不用JavaScript和Flash进行交互。有了Canvas,我们就再也不需要Flash了,直接使用JavaScript完成绘制。

一个Canvas定义了一个指定尺寸的矩形框,在这个范围内我们可以随意绘制:

 <canvas id="test-canvas" width="300" height="200"></canvas>

由于浏览器对HTML5标准支持不一致,所以,通常在 <canvas> 内部添加一些说明性HTML代码,如果浏览器支持Canvas,它将忽略 <canvas> 内部的HTML,如果浏览器不支持Canvas,它将显示 <canvas> 内部的HTML:

<!-- HTML代码 -->
<canvas id="test-canvas" width="200" heigth="100">
	<p>你的浏览器不支持Canvas</p>
</canvas>

getContext('2d') 方法让我们拿到一个 CanvasRenderingContext2D 对象,所有的绘图操作都需要通过这个对象完成。

var ctx = canvas.getContext('2d');

如果需要绘制3D怎么办?HTML5还有一个WebGL规范,允许在Canvas中绘制3D图形:

gl = canvas.getContext("webgl");

绘制形状

Canvas的坐标以左上角为原点,水平向右为X轴,垂直向下为Y轴,以像素为单位,所以每个点都是非负整数。CanvasRenderingContext2D 对象有若干方法来绘制图形:

var
	canvas = documnet.getElementById('test-canvas'),
	ctx = canvas.getContext('2d');
// 绘制形状
ctx.clearRect(0, 0, 200, 200);		// 擦除(0,0)位置大小为200x200的矩形,擦除的意思是把该区域变透明
ctx.fillStyle = '#dddddd';		// 设置颜色
ctx.fillRect(10, 10, 130, 130);		// 把(10,10)位置大小为130x130的矩形涂色
// 利用Path绘制复杂路径
var path = new Path2D();
// 绘制圆弧路径,圆心坐标为(75,75),半径为50,圆弧起点是0,终点的弧度是2倍的PI,逆时针绘画
path.arc(75, 75, 50, 0, Math.PI*2, true);		
path.moveTo(110,75);
path.arc(75, 75, 35, 0, Math.PI, false);
ctx.strokeStyle = '#0000ff';		// 设置画笔颜色
ctx.stroke(path);		// 沿预设的路径绘画

绘制文本

绘制文本就是在指定的位置输出文本,可以设置文本的字体、样式、阴影等,与CSS完全一致

var
	canvas = documnet.getElementById('test-canvas'),
	ctx = canvas.getContext('2d');
// 绘制文本
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 2;
ctx.shadowColor = '#666666';
ctx.font = '24px Arial';
ctx.fillStyle = '#333333';
ctx.fillText('带阴影的文字', 20, 40);

其他

Canvas除了能绘制基本的形状和文本,还可以实现动画、缩放、各种滤镜和像素转换等高级操作。如果要实现非常复杂的操作,考虑以下优化方案:

  • 通过创建一个不可见的Canvas来绘图,然后将最终绘制结果复制到页面的可见Canvas中;
  • 尽量使用整数坐标而不是浮点数;
  • 可以创建多个重叠的Canvas绘制不同的层,而不是在一个Canvas中绘制非常复杂的图;
  • 背景图片如果不变可以直接用 <img> 标签并放到最底层;

详细的可以查看文档

Canvas API 文档

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值