端新人别慌:手把手教你用原生JS搞定AJAX请求(附避坑指南)

前端新人别慌:手把手教你用原生JS搞定AJAX请求(附避坑指南)

一、写在前面:AJAX这东西,真没那么玄乎

说实话,我当年第一次听到"AJAX"这个词的时候,脑子里浮现的是某种神秘的科幻武器,或者至少是像jQuery那样需要花钱买的插件。结果老师甩了一句"异步JavaScript和XML",我整个人都是懵的——这啥?异步我懂,XML我也见过,但它们加在一起怎么就突然高大上了?

后来自己撸了几个项目才明白,AJAX说白了就是"偷偷发请求"的艺术。你想啊,以前网页要获取新数据,得整个页面刷新,用户体验就跟翻书似的,哗啦一下全变了。有了AJAX,页面可以纹丝不动,背后的小弟(JavaScript)已经跑去找服务器要数据了,要到之后悄悄更新页面上的某一块,用户可能都没察觉发生了什么。

但这玩意儿看着简单,坑是真的多。我见过太多新人(包括我自己)在这些地方栽过跟头:跨域报错一脸懵逼、POST请求死活传不到数据、回调函数里的this指向突然变脸……所以今天这篇,我就当是跟你在微信群语音聊天,想到哪说到哪,把原生AJAX的里里外外、坑坑洼洼都唠清楚。代码会贴很多,注释也会写得很啰嗦——没办法,谁让我当年就是被一行没注释的代码坑到凌晨三点呢。


二、AJAX到底是个啥?别被 acronym 吓到

先破除一个迷信:AJAX不是某种新技术,也不是必须要学的框架。它就是个缩写——Asynchronous JavaScript and XML。虽然现在XML用得少了(JSON真香),但这个名字还是保留下来了。

核心就三个东西:

  1. 浏览器内置的 XMLHttpRequest 对象(简称XHR,这名字起得,还以为跟某个不良网站有关)
  2. JavaScript 的异步编程能力
  3. DOM 操作(拿到数据后总得显示出来吧)

整个过程就像点外卖:你(JavaScript)打开手机(创建XHR对象)→ 选餐厅填地址(open)→ 下单(send)→ 等骑手配送(异步等待)→ 收到餐更新订单状态(onload/onreadystatechange回调)。关键是,等外卖的时候你不用站在门口干等着,该刷剧刷剧,该打游戏打游戏——这就是异步的魅力。

不过啊,XHR这个API设计得确实有点反人类。同样是发请求,fetch API就清爽很多,但为啥我们还要学XHR?因为面试要考,而且有些老项目还在用,更重要的是——理解了XHR的底层,你用任何封装库都会心里有底。就像学开车先学手动挡,虽然自动挡更方便,但懂离合器的工作原理,关键时刻能救命。


三、起手式:new 一个 XMLHttpRequest 对象

好,开始写代码。最基础的版本长这样:

// 第一步:创建一个XHR对象
// 注意:IE老版本用的是 ActiveXObject("Microsoft.XMLHTTP"),现在2024年了,除非你在维护古董项目,否则不用管
const xhr = new XMLHttpRequest();

// 第二步:配置请求参数
// open(method, url, async, user, password)
// method: GET/POST/PUT/DELETE等,必须大写,虽然小写也能跑,但规范是大写
// url: 请求的地址,可以是绝对路径或相对路径
// async: 是否异步,true是异步(默认),false是同步(别用,会卡死页面)
xhr.open('GET', 'https://api.example.com/data', true);

// 第三步:发送请求
// GET请求send里面一般传null,POST请求传数据
xhr.send(null);

看起来简单对吧?但这里面的门道多了去了。

关于open方法的几点碎碎念:

  1. method必须大写吗? 规范上是的,但浏览器其实很宽容,'get’和’GET’都能跑。不过为了保险,还是大写吧,万一哪天遇到个严格的浏览器呢。

  2. URL要不要带协议? 如果你请求的是当前域的资源,可以写相对路径/api/data;如果是跨域,必须写完整URL包括https://。这里有个坑:如果你写的是//api.example.com/data,浏览器会自动匹配当前页面的协议(http或https),这在混合内容场景下很有用。

  3. 第三个参数async,99%的情况你都应该传true或者不传(默认就是true)。如果你传false变成同步请求,JavaScript引擎会卡住直到服务器响应,页面在这期间完全冻结,用户啥都点不了。除非你在写Web Worker里的代码,否则别碰同步模式。

send方法的小秘密:

// GET请求
xhr.send(null); // 或者 xhr.send(),效果一样

// POST请求,传表单数据
const formData = 'name=张三&age=25';
xhr.send(formData);

// POST请求,传JSON
const jsonData = JSON.stringify({name: '张三', age: 25});
xhr.send(jsonData);

注意send接收的参数类型可以是 string、Document、Blob、ArrayBuffer、FormData 等。如果你传的是对象,记得先JSON.stringify(),否则服务器收到的是[object Object],别问我怎么知道的。


四、监听响应:onload vs onreadystatechange,该用哪个?

好了,请求发出去了,但服务器还没回话呢。怎么知道它啥时候回?有两个主要方案:

方案一:onload(推荐,现代浏览器支持)

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);

// 请求成功完成时触发(状态码200-299都算成功)
xhr.onload = function() {
  // this指向xhr对象
  if (this.status === 200) {
    console.log('成功拿到数据:', this.responseText);
    // 解析JSON要小心,try-catch保平安
    try {
      const data = JSON.parse(this.responseText);
      console.log('解析后的数据:', data);
    } catch (e) {
      console.error('JSON解析失败,原数据是:', this.responseText);
    }
  } else {
    // 服务器返回了,但状态码不是200,比如404、500
    console.error('服务器报错,状态码:', this.status, this.statusText);
  }
};

// 网络错误时触发(比如断网、DNS解析失败)
xhr.onerror = function() {
  console.error('请求失败,可能是网络问题');
};

// 请求超时触发(需要先设置timeout)
xhr.ontimeout = function() {
  console.error('请求超时了老铁');
};

xhr.send();

方案二:onreadystatechange(兼容性好,能监控全过程)

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);

xhr.onreadystatechange = function() {
  // readyState的四个阶段:
  // 0: UNSENT - 还没调用open()
  // 1: OPENED - open()已调用
  // 2: HEADERS_RECEIVED - send()已调用,已接收响应头
  // 3: LOADING - 正在下载响应体
  // 4: DONE - 全部搞定
  
  if (this.readyState === 4) { // 请求完成
    if (this.status === 200) {
      console.log('搞定:', this.responseText);
    } else {
      console.error('出错了:', this.status);
    }
  }
};

xhr.send();

我该用哪个? 如果你不需要监控下载进度,直接用onload更清爽。但如果你想做个进度条,或者需要在发送过程中做点什么,onreadystatechange是必须的。

关于状态码的碎碎念:

很多人只判断status === 200,但HTTP状态码家族很大:

  • 200 OK:标准成功
  • 201 Created:创建成功(常用于POST)
  • 204 No Content:成功但无返回内容
  • 400 Bad Request:客户端请求参数错了
  • 401 Unauthorized:没登录或token过期
  • 403 Forbidden:没权限
  • 404 Not Found:资源不存在
  • 500 Internal Server Error:服务器炸了

所以更好的判断是:

xhr.onload = function() {
  if (xhr.status >= 200 && xhr.status < 300) {
    // 成功区间
  } else if (xhr.status >= 400 && xhr.status < 500) {
    // 客户端错误,可能是参数传错了
  } else if (xhr.status >= 500) {
    // 服务器错误,找后端撕逼去
  }
};

五、GET请求:拼参数是个技术活

GET请求简单,但URL参数拼起来容易出错。假设你要搜"前端开发",时间范围是最近一周:

// 错误示范,千万别这么干
const keyword = '前端开发';
const timeRange = '最近一周';
const url = 'https://api.example.com/search?keyword=' + keyword + '&time=' + timeRange;
// 结果:https://api.example.com/search?keyword=前端开发&time=最近一周
// 中文没编码,某些服务器会直接400错误,或者拿到乱码

正确做法是用encodeURIComponent,这玩意儿会把特殊字符(包括中文)转成URL安全的格式:

const params = {
  keyword: '前端开发',
  time: '最近一周',
  page: 1
};

// 手动拼
const queryString = Object.keys(params)
  .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
  .join('&');

const url = `https://api.example.com/search?${queryString}`;
console.log(url); 
// 结果:https://api.example.com/search?keyword=%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91&time=%E6%9C%80%E8%BF%91%E4%B8%80%E5%91%A8&page=1

或者用URLSearchParams(现代浏览器支持,IE不行):

const params = new URLSearchParams({
  keyword: '前端开发',
  time: '最近一周'
});
const url = `https://api.example.com/search?${params.toString()}`;

GET请求的缓存陷阱:

浏览器会缓存GET请求,这意味着如果你连续发两次相同的GET请求,第二次可能直接拿缓存,根本没去服务器。这在开发环境很烦人,因为你改了后端代码,前端还是拿到旧数据。

解决方案:加个随机数或时间戳

// Cache Busting
const url = `https://api.example.com/data?t=${Date.now()}`;
// 或者
const url = `https://api.example.com/data?_=${Math.random()}`;

虽然丑,但管用。当然你也可以在请求头里加Cache-Control: no-cache,但有些浏览器不认,还是时间戳最靠谱。


六、POST请求:Content-Type是道坎

POST比GET复杂,因为涉及到请求体(body)的格式。最常见的坑就是忘记设置Content-Type,导致服务器收不到数据。

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/user', true);

// 坑1:没设Content-Type,服务器不知道你传的是啥格式
// xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

const data = 'name=张三&age=25';
xhr.send(data);

如果服务器期待的是表单格式(application/x-www-form-urlencoded),而你没有设置Content-Type,它可能解析不出来。如果期待的是JSON(application/json),你传表单格式也会失败。

常见Content-Type对照表:

  1. 表单格式(传统HTML form提交)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
const data = 'name=张三&age=25'; // 字符串格式,key=value用&连接
xhr.send(data);
  1. JSON格式(现代API主流)
xhr.setRequestHeader('Content-Type', 'application/json');
const data = JSON.stringify({name: '张三', age: 25});
xhr.send(data);
  1. multipart/form-data(上传文件)
// 这个不用手动设,FormData对象会自动设置,包括boundary
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', '张三');
xhr.send(formData);
// 注意:这时候千万别手动设Content-Type,让浏览器自动处理boundary

一个完整的POST-JSON示例:

function postJSON(url, data, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.setRequestHeader('Content-Type', 'application/json');
  
  xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
      callback(null, xhr.response);
    } else {
      callback(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
    }
  };
  
  xhr.onerror = () => callback(new Error('网络请求失败'));
  xhr.send(JSON.stringify(data));
}

// 使用
postJSON('https://api.example.com/user', 
  { name: '李四', email: 'li@si.com' }, 
  (err, result) => {
    if (err) {
      console.error('提交失败:', err);
      return;
    }
    console.log('提交成功:', result);
  }
);

关于POST数据大小的迷思:

网上很多人说GET有长度限制(URL太长服务器会拒绝),POST没限制。这话对也不对。实际上HTTP协议本身对URL长度没限制,但浏览器和服务器有(比如IE 2083字符,Apache 8192字符)。POST虽然body可以很大,但服务器也会设上限(比如PHP默认8M,Node.js的body-parser默认100kb)。所以别拿POST当无限容量用,传大文件还是用专门的文件上传方案。


七、处理响应:别只会responseText

拿到响应后,大多数人直接JSON.parse(xhr.responseText),但如果服务器返回的不是JSON呢?或者返回空字符串呢?

responseType属性:提前告诉浏览器你想要什么格式

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);

// 可选值:''(默认,字符串)、'json'、'text'、'arraybuffer'、'blob'、'document'
xhr.responseType = 'json'; // 告诉浏览器:请自动帮我解析成JSON

xhr.onload = function() {
  if (xhr.status === 200) {
    // 如果responseType是'json',这里直接就是对象,不用parse
    console.log(xhr.response); // 已经是JavaScript对象了
    console.log(xhr.response.userName); // 直接访问属性
  }
};

xhr.send();

responseType的妙用:下载文件

// 下载图片并显示
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/image.png', true);
xhr.responseType = 'blob'; // 二进制大对象

xhr.onload = function() {
  if (xhr.status === 200) {
    const blob = xhr.response;
    const img = document.createElement('img');
    img.src = URL.createObjectURL(blob); // 生成临时URL
    document.body.appendChild(img);
  }
};

xhr.send();

responseText vs response:

  • responseText:始终是字符串,不管服务器返回什么
  • response:根据responseType自动转换后的结果。如果responseType是’‘或’text’,response和responseText一样;如果是’json’,response是解析后的对象。

空响应处理:

有时候服务器会返回204 No Content,或者返回空字符串。这时候JSON.parse会报错:

xhr.onload = function() {
  if (xhr.status === 204 || !xhr.responseText) {
    console.log('服务器没返回数据,但操作成功了');
    return;
  }
  
  try {
    const data = JSON.parse(xhr.responseText);
  } catch (e) {
    console.error('解析失败,原始响应:', xhr.responseText);
  }
};

八、跨域:前端的永恒噩梦,但不该全让前端背锅

这是新人最头疼的问题。你在localhost:8080跑项目,请求api.example.com的数据,浏览器直接给你报错:

Access to XMLHttpRequest at 'https://api.example.com/data' from origin 'http://localhost:8080' has been blocked by CORS policy...

首先搞清楚:这不是bug,是浏览器的安全机制(同源策略)。 浏览器不允许一个域的网页随意访问另一个域的资源,除非对方明确允许。

CORS(跨域资源共享)到底是谁配? 虽然报错出现在前端控制台,但解决方案主要是后端的。后端需要在响应头里加:

Access-Control-Allow-Origin: http://localhost:8080
// 或者通配符(生产环境慎用):
Access-Control-Allow-Origin: *

前端能做什么?

  1. 开发环境代理(最实用)
    如果你用Webpack/Vite,配置个devServer.proxy,把请求转发到同源,浏览器就以为你在请求自己域的资源。
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}
  1. JSONP(老古董,现在很少用,仅支持GET)
    利用<script>标签不受同源策略限制的特性,但只能GET,而且需要后端配合返回callback({data})格式的数据。

  2. 理解简单请求和预检请求(Preflight)

    如果你发的请求满足以下条件,就是简单请求,直接发:

    • 方法:GET/HEAD/POST
    • Content-Type:仅限application/x-www-form-urlencoded、multipart/form-data、text/plain
    • 请求头只有安全头部(Accept、Accept-Language等)

    不满足条件的,比如Content-Type是application/json,或者自定义了Header(如X-Token),浏览器会先自动发个OPTIONS请求(预检),问服务器"我能不能这么干",服务器说可以,浏览器才会发真正的请求。

    这也是为什么有时候你会看到控制台有两个请求,第一个是OPTIONS,第二个才是POST。如果OPTIONS返回403,后面的请求就不会发了。

withCredentials:带cookie的跨域

如果你的请求需要携带cookie(比如登录态),需要前后端配合:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/user', true);
xhr.withCredentials = true; // 允许携带cookie
xhr.send();

后端也要响应:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:8080 // 注意这里不能用*,必须指定具体域名

新手常见误区:

  • “我在Postman里能跑通,为啥浏览器不行?”——因为Postman不受同源策略限制,它就是个HTTP客户端。
  • “我把后端Allow-Origin设成*了,为啥还带cookie时报错?”——因为*withCredentials=true不能同时用,必须指定具体域名。

九、调试排错:当AJAX没反应时,你在想什么?

最崩溃的时刻:代码看着没问题,但控制台一片寂静,onload就是不触发。这时候别砸键盘,按这个 checklist 排查:

第一步:看Network面板

打开F12 -> Network -> XHR(或Fetch/XHR),看有没有你的请求:

  • 完全没有请求记录:可能是JS报错阻止了执行,或者URL拼错了(比如undefined拼进URL里)
  • 请求是红色的:看状态码,4xx是客户端问题,5xx是服务器问题
  • 请求是灰色的(pending):一直挂着没响应,可能是服务器卡了,或者没收到(跨域被拦截了不会出现在这,但网络断开会pending)

第二步:看Console面板的CORS错误

如果有红色的CORS policy报错,那就是跨域问题,按上一节解决。

第三步:检查回调函数是否真的绑上了

// 错误示范:先send后绑定事件
xhr.send();
xhr.onload = function() {}; // 请求可能已经完成了,监听没绑上

// 正确示范:先绑定再send
xhr.onload = function() {};
xhr.send();

第四步:检查this指向

xhr.onload = function() {
  console.log(this.responseText); // 这里的this是xhr对象,没问题
};

// 但如果你用箭头函数...
xhr.onload = () => {
  console.log(this.responseText); // 箭头函数没有自己的this,this指向外层,可能是window,undefined
};

第五步:检查异步问题

let result;
xhr.onload = function() {
  result = xhr.responseText;
};
console.log(result); // 这里一定是undefined,因为请求是异步的,还没执行完

如果你需要等请求完成再执行代码,得用回调或者Promise。

第六步:GET请求缓存作祟

明明后端改了数据,前端拿到的还是旧的?可能是浏览器缓存了GET请求。在URL后加?t=${Date.now()}试试。

一个万能调试技巧:

在onload第一行加debugger;或者console.log('响应回来了', xhr),至少能确认回调有没有执行。


十、实用技巧:让原生AJAX不那么难用

虽然XHR设计得有点 archaic,但我们可以通过封装让它现代化一点。

技巧1:Promise封装,告别回调地狱

function request(method, url, data = null, headers = {}) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    
    // 设置请求头
    Object.keys(headers).forEach(key => {
      xhr.setRequestHeader(key, headers[key]);
    });
    
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
      }
    };
    
    xhr.onerror = () => reject(new Error('网络请求失败'));
    xhr.ontimeout = () => reject(new Error('请求超时'));
    
    xhr.send(data);
  });
}

// 使用
async function getUser() {
  try {
    const data = await request('GET', 'https://api.example.com/user');
    console.log(data);
  } catch (err) {
    console.error('获取失败:', err);
  }
}

技巧2:加个loading提示,提升体验

function requestWithLoading(method, url, data) {
  // 显示loading
  const loading = document.createElement('div');
  loading.textContent = '加载中...';
  loading.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:20px;background:rgba(0,0,0,0.7);color:white;border-radius:8px;z-index:9999;';
  document.body.appendChild(loading);
  
  return request(method, url, data).finally(() => {
    // 不管成功失败都隐藏loading
    loading.remove();
  });
}

技巧3:自动处理JSON

function ajax(url, options = {}) {
  const { method = 'GET', data = null, responseType = 'json' } = options;
  
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    xhr.responseType = responseType;
    
    // 如果是对象且不是FormData,自动转JSON
    if (data && typeof data === 'object' && !(data instanceof FormData)) {
      xhr.setRequestHeader('Content-Type', 'application/json');
      xhr.send(JSON.stringify(data));
    } else {
      xhr.send(data);
    }
    
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject(xhr.statusText);
      }
    };
    xhr.onerror = () => reject('Network Error');
  });
}

// 使用超简单
ajax('https://api.example.com/user', { method: 'GET' })
  .then(data => console.log(data))
  .catch(err => console.error(err));

ajax('https://api.example.com/user', { 
  method: 'POST', 
  data: { name: '王五' } 
});

技巧4:超时设置

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://slow-api.com/data', true);
xhr.timeout = 5000; // 5秒超时
xhr.ontimeout = function() {
  console.error('请求超时,请检查网络或稍后重试');
};
xhr.send();

技巧5:进度监控(上传/下载)

// 下载进度
xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    const percent = (event.loaded / event.total) * 100;
    console.log(`下载进度:${percent}%`);
    // 可以在这里更新进度条
    document.getElementById('progress').style.width = percent + '%';
  }
};

// 上传进度(用于文件上传)
xhr.upload.onprogress = function(event) {
  const percent = (event.loaded / event.total) * 100;
  console.log(`上传进度:${percent}%`);
};

十一、别被现代框架吓住:原生AJAX才是你的内功

现在谁还手写XMLHttpRequest?Vue项目里axios.get(),React项目里fetch(),甚至直接用react-queryswr这种数据获取库。那学原生AJAX还有啥用?

首先,面试要考。 我就被问过"说说AJAX的实现原理"、“fetch和XHR有什么区别”。你要是说"我用axios的",面试官心里可能会扣分。

其次,理解底层才能用好封装。 比如你知道XHR有onprogress事件,用axios的时候就知道怎么配置onDownloadProgress;你知道跨域需要withCredentials,用fetch的时候就知道要设credentials: 'include'

再者,极端环境能救命。 去年我维护一个老项目,跑在嵌入式设备里的webkit浏览器,版本老到fetch都不支持,axios又太大塞不进去,最后只能手写XHR。那时候要是不会原生,项目就黄了。

对比一下fetch和XHR:

fetch API更现代,基于Promise,写法清爽:

fetch('https://api.example.com/data')
  .then(res => {
    if (!res.ok) throw new Error(res.statusText);
    return res.json();
  })
  .then(data => console.log(data))
  .catch(err => console.error(err));

但fetch也有坑:

  • 默认不带cookie(需要手动设credentials: 'include'
  • 404或500不会进catch,只有网络错误才会reject
  • 不支持进度监控(虽然可以用ReadableStream实现,但麻烦)
  • IE不支持(虽然IE已死,但某些内网环境还在用)

XHR虽然老,但功能齐全、兼容性好、控制精细。就像手动挡汽车,虽然现在都开自动挡,但懂离合器和档位的工作原理,关键时刻能救命。


十二、最后唠叨两句

写这篇的时候,我想起了自己第一次用AJAX的场景。那是2016年,我要做一个简单的天气查询页面。照着网上的教程敲代码,结果一直报404,查了两个小时才发现URL拼错了一个字母。那时候觉得AJAX真难,前端真难。

但现在回头看,AJAX其实是个很朴素的技术——创建对象、配置参数、发送、等回调。它的难点不在API本身,而在异步编程的思维转换,以及HTTP协议的各种细节(状态码、Content-Type、跨域等)。

如果你现在还在纠结,别怕,多写几个完整的例子,多踩几个坑,自然就熟了。就像学骑自行车,理论知识看再多,不如摔两跤来得实在。下次再有人问你"为啥不用jQuery的$.ajax?",你可以微微一笑:“我在练手动挡。”

毕竟,懂底层的前端,走路都带风

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值