fetch总结

What fetch

官方描述:

Fetch API 提供了一个 JavaScript接口,用于访问和操纵HTTP管道的部分,例如请求和响应。它还提供了一个全局 fetch()方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。这种功能以前是使用  XMLHttpRequest实现的。Fetch提供了一个更好的替代方法,可以很容易地被其他技术使用,例如 Service WorkersFetch还提供了单个逻辑位置来定义其他HTTP相关概念,例如CORSHTTP的扩展。

人话:

      fetch是一种HTTP数据请求的方式,是XMLHttpRequest的一种替代方案。fetch不是ajax的进一步封装,而是原生js。Fetch函数就是原生js,没有使用XMLHttpRequest对象。

Why Fetch

1.与xhr对比:

XMLHttpRequest 是一个设计粗糙的 API,如下缺点:

a. 不符合关注分离(Separation of Concerns)的原则,

b. 配置和调用方式非常混乱,

c. 基于事件的异步模型写起来也没有现代的 Promise,generator/yield,async/await 友好。

例如:

var xhr = new XMLHttpRequest();
xhr.open('GET',url);
xhr.responseType = 'json';
xhr.onload = function(){
    console.log(xhr.response);
}
xhr.onerror = function(){
    console.log('xhr error');
}
xhr.send();

Fetch 的出现就是为了解决 XHR 的问题.

使用fetch做请求后:

fetch(url).then(function(response){
    return response.json();
}).then(function(data){
    console.log(data);
}).catch(function(e){
    console.log('error' + e);
});

es6写法:

fetch(url).then(response=>response.json())
    .then(data=>console.log(data))
    .catch(e=>console.log('error' + e));

处理text/html响应:

fetch(url).then(response=>response.text())
    .then(data=>console.log(data))
    .catch(e=>console.log('error' + e));

获取头信息:

fetch(url).then((response)=>{
    console.log(response.status);
    console.log(response.statusText);
    console.log(response.headers.get('Content-Type'));
    console.log(response.headers.get('Date'));
    return response.json();
}).then(data=>console.log(data))
  .catch(e=>console.log('error' + e);

设置头信息

fetch(url,{
    headers:{
        'Accept': 'application/json',
        'Content-Type': 'application/json'
    }
}).then(response=>response.json())
  .then(data=>console.log(data))
  .catch(e=>console.log('error' + e);

提交表单

fetch(url,{
    method: 'post',
    body: new FormData(document.getElementById('form'))
}).then(response=>response.json())
  .then(data=>console.log(data))
  .catch(e=>console.log('error' + e);

提交json数据

fetch(url,{
    method: 'post',
    body: JSON.stringify({
        username: document.getElementById('username').value,
        password: document.getElementById('password').value
    })
}).then(response=>response.json())
  .then(data=>console.log(data))
  .catch(e=>console.log('error' + e);

 

fetch有一项xhr比不上的能力:跨域的处理。

fetch中可以设置mode为"no-cors"(不跨域),如下所示:

fetch('/users.json', {
    method: 'post',
    mode: 'no-cors',
    data: {}
}).then(function() { /* handle response */ });

这样之后我们会得到一个type为“opaque”的返回。需要指出的是,这个请求是真正抵达过后台的,所以我们可以使用这种方法来进行信息上报,在我们之前的image.src方法中多出了一种选择,另外,我们在network中可以看到这个请求后台设置跨域头之后的实际返回,有助于我们提前调试接口(当然,通过chrome插件我们也可以做的到)。

2.与jQuery的ajax对比:

a. 本身是针对MVC的编程,不符合现在前端MVVM的浪潮

b. 基于原生的XHR开发,XHR本身的架构不清晰,已经有了fetch的替代方案

c. JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理(采取个性化打包的方案又不能享受CDN服务)

3.与Axios对比:

axios({
    method: 'post',
    url: '/user/12345',
    data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
    }
})
.then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});

Vue2.0之后,尤雨溪推荐大家用axios替换JQuery ajax。Axios本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范,从它的官网上可以看到它有以下几条特性:

1.从浏览器中创建 XMLHttpRequest
2.支持 Promise API
3.客户端支持防止CSRF
4.提供了一些并发请求的接口(重要,方便了很多的操作)
5.从 node.js 创建 http 请求
6.拦截请求和响应
7.转换请求和响应数据
8.取消请求
9.自动转换JSON数据

 

PS:防止CSRF:就是让你的每个请求都带一个从cookie中拿到的key, 根据浏览器同源策略,假冒的网站是拿不到你cookie中得key的,这样,后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略。

优点:

  1. Axios既提供了并发的封装
  2. 没有下文会提到的fetch的各种问题
  3. 体积也较小,当之无愧现在最应该选用的请求的方式。

 

fetch相比Axios的优点:

  1. 更加底层,提供的API丰富(request, response)
  2. 脱离了XHR,是ES规范里新的实现方式

Fetch 优点主要有:

  1. 语法简洁,更加语义化
  2. 基于标准 Promise 实现,支持 async/await
  3. 同构方便,使用 isomorphic-fetch

Fetch 的兼容问题:

fetch原生支持性不高,引入下面这些 polyfill 后可以完美支持 IE8+ :

  1. 由于 IE8 是 ES3,需要引入 ES5 的 polyfill: es5-shim, es5-sham
  2. 引入 Promise[z1]  的 polyfill: es6-promise
  3. 引入 fetch 探测库:fetch-detector
  4. 引入 fetch 的 polyfill: fetch-ie8
  5. 可选:如果你还使用了 jsonp,引入 fetch-jsonp
  6. 可选:开启 Babel 的 runtime 模式,现在就使用 async/await

Fetch polyfill 的基本原理[z2] 是探测是否存在 window.fetch 方法,如果没有则用 XHR 实现。这也是 github/fetch 的做法,但是有些浏览器(Chrome 45)原生支持 Fetch,但响应中有中文时会乱码,老外又不太关心这种问题,所以我自己才封装了 fetch-detector 和 fetch-ie8 只在浏览器稳定支持 Fetch 情况下才使用原生 Fetch。这些库现在每天有几千万个请求都在使用,绝对靠谱

例子:

使用isomorphic-fetch,兼容Node.js和浏览器两种环境运行。

npm install --save isomorphic-fetch es6-promise
require('es6-promise').polyfill();
require('isomorphic-fetch');

 

Fetch 常见坑

1、Fetch 请求默认是不带 cookie 的

  • 需要设置 fetch(url, {credentials: 'include'})

2、fetch请求对某些错误http状态不会reject

这主要是由fetch返回promise导致的,因为fetch返回的promise在某些错误的http状态下如400、500等不会reject,相反它会被resolve;只有网络错误会导致请求不能完成时,fetch 才会被 reject;所以一般会对fetch请求做一层封装,例如下面代码所示:

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}
function parseJSON(response) {
  return response.json();
}
export default function request(url, options) {
  let opt = options||{};
  return fetch(url, {credentials: 'include', ...opt})
    .then(checkStatus)
    .then(parseJSON)
    .then((data) => ( data ))
    .catch((err) => ( err ));
}

3、不可以取消:

1.fetch不支持abort,不支持超时控制,使用setTimeoutPromise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费。

2.fetch没有办法原生监测请求的进度,而XHR可以

Ps: xhr send完之后 本质请求已经发送了 进入网络传输了 流量浪费已经发生了 abort只是在你未来某个时机调用 不想要这个请求了 那么即使远端服务器返回数据了 我浏览器拿到了 也不执行回调了 因为你已经abort了这个请求 所以我想说。。。。流量浪费已经发生了 只是abort可以更好地控制你要不要执行请求的回调

 

4.fetch不支持超时timeout处理

用过fetch的都知道,fetch不像大多数ajax库那样对请求设置超时timeout,它没有有关请求超时的feature,这一点比较蛋疼。所以在fetch标准添加超时feature之前,都需要polyfill该特性。

实际上,我们真正需要的是abort(), timeout可以通过timeout+abort方式来实现,起到真正超时丢弃当前的请求。

而在目前的fetch指导规范中,fetch并不是一个具体实例,而只是一个方法;其返回的promise实例根据Promise指导规范标准是不能abort的,也不能手动改变promise实例的状态,只能由内部来根据请求结果来改变promise的状态。

既然不能手动控制fetch方法执行后返回的promise实例状态,那么是不是可以创建一个可以手动控制状态的新Promise实例呢。所以:

实现fetch的timeout功能,其思想就是新创建一个可以手动控制promise状态的实例,根据不同情况来对新promise实例进行resolve或者reject,从而达到实现timeout的功能;

根据github上timeout handling上的讨论,目前可以有两种不同的解决方法:

方法一:单纯setTimeout方式

var oldFetchfn = fetch; //拦截原始的fetch方法
window.fetch = function(input, opts){//定义新的fetch方法,封装原有的fetch方法
    return new Promise(function(resolve, reject){
        var timeoutId = setTimeout(function(){
            reject(new Error("fetch timeout"))
        }, opts.timeout);
        oldFetchfn(input, opts).then(
            res=>{
                clearTimeout(timeoutId);
                resolve(res)
            },
            err=>{
                clearTimeout(timeoutId);
                reject(err)
            }
        )
    })
}

当然在上面基础上可以模拟类似XHR的abort功能:

var oldFetchfn = fetch; 
window.fetch = function(input, opts){
    return new Promise(function(resolve, reject){
        var abort_promise = function(){
            reject(new Error("fetch abort"))
        };
        var p = oldFetchfn(input, opts).then(resolve, reject);
        p.abort = abort_promise;
        return p;
    })
}

方法二:利用Promise.race方法

Promise.race方法接受一个promise实例数组参数,表示多个promise实例中任何一个最先改变状态,那么race方法返回的promise实例状态就跟着改变,具体可以参考这里

var oldFetchfn = fetch; //拦截原始的fetch方法
window.fetch = function(input, opts){//定义新的fetch方法,封装原有的fetch方法
    var fetchPromise = oldFetchfn(input, opts);
    var timeoutPromise = new Promise(function(resolve, reject){
        setTimeout(()=>{
             reject(new Error("fetch timeout"))
        }, opts.timeout)
    });
    retrun Promise.race([fetchPromise, timeoutPromise])
}

最后,对fetch的timeout的上述实现方式补充几点:

timeout不是请求连接超时的含义,它表示请求的response时间,包括请求的连接、服务器处理及服务器响应回来的时间;

fetch的timeout即使超时发生了,本次请求也不会被abort丢弃掉,它在后台仍然会发送到服务器端,只是本次请求的响应内容被丢弃而已;

5.fetch不支持JSONP

fetch是与服务器端进行异步交互的,而JSONP是外链一个javascript资源,并不是真正ajax,所以fetch与JSONP没有什么直接关联,当然至少目前是不支持JSONP的。

这里我们把JSONP与fetch关联在一起有点差强人意,fetch只是一个ajax库,我们不可能使fetch支持JSONP;只是我们要实现一个JSONP,只不过这个JSONP的实现要与fetch的实现类似,即基于Promise来实现一个JSONP;而其外在表现给人感觉是fetch支持JSONP一样;

目前比较成熟的开源JSONP实现fetch-jsonp给我们提供了解决方案,想了解可以自行前往。不过再次想唠叨一下其JSONP的实现步骤,因为在本人面试的前端候选人中大部分人对JSONP的实现语焉不详;

使用它非常简单,首先需要用npm安装fetch-jsonp

 npm install fetch-jsonp --save-dev

然后在像下面一样使用:

fetchJsonp('/users.jsonp', {
    timeout: 3000,
    jsonpCallback: 'custom_callback'
  })
  .then(function(response) {
    return response.json()
  }).catch(function(ex) {
    console.log('parsing failed', ex)
  })

6.fetch不支持progress事件

XHR是原生支持progress事件的,例如下面代码这样:

var xhr = new XMLHttpRequest()
xhr.open('POST', '/uploads')
xhr.onload = function() {}
xhr.onerror = function() {}
function updateProgress (event) {
  if (event.lengthComputable) {
    var percent = Math.round((event.loaded / event.total) * 100)
    console.log(percent)
  }
xhr.upload.onprogress =updateProgress; //上传的progress事件
xhr.onprogress = updateProgress; //下载的progress事件
}
xhr.send();

但是fetch是不支持有关progress事件的;不过可喜的是,根据fetch的指导规范标准,其内部设计实现了RequestResponse类;其中Response封装一些方法和属性,通过Response实例可以访问这些方法和属性,例如response.json()response.body等等;

值得关注的地方是,response.body是一个可读字节流对象,其实现了一个getRender()方法,其具体作用是:

getRender()方法用于读取响应的原始字节流,该字节流是可以循环读取的,直至body内容传输完成;

因此,利用到这点可以模拟出fetch的progress,具体可以参考这篇文章2016 - the year of web streams

代码实现如下,在线demo请参考fetch progress demo

// fetch() returns a promise that resolves once headers have been received
fetch(url).then(response => {
  // response.body is a readable stream.
  // Calling getReader() gives us exclusive access to the stream's content
  var reader = response.body.getReader();
  var bytesReceived = 0;

  // read() returns a promise that resolves when a value has been received
  reader.read().then(function processResult(result) {
    // Result objects contain two properties:
    // done  - true if the stream has already given you all its data.
    // value - some data. Always undefined when done is true.
    if (result.done) {
      console.log("Fetch complete");
      return;
    }

    // result.value for fetch streams is a Uint8Array
    bytesReceived += result.value.length;
    console.log('Received', bytesReceived, 'bytes of data so far');

    // Read some more, and call this function again
    return reader.read().then(processResult);
  });
});

另外,github上也有使用Promise+XHR结合的方式实现类fetch的progress效果(当然这跟fetch完全不搭边)可以参考这里,具体代码如下:

function fetchProgress(url, opts={}, onProgress){
    return new Promise(funciton(resolve, reject){
        var xhr = new XMLHttpRequest();
        xhr.open(opts.method || 'get', url);
        for(var key in opts.headers || {}){
            xhr.setRequestHeader(key, opts.headers[key]);
        }

        xhr.onload = e => resolve(e.target.responseText)
        xhr.onerror = reject;
        if (xhr.upload && onProgress){
            xhr.upload.onprogress = onProgress; //上传
        }
        if ('onprogerss' in xhr && onProgress){
            xhr.onprogress = onProgress; //下载
        }
        xhr.send(opts.body)
    })
}
fetchProgress('/upload').then(console.log)

7.fetch跨域问题

既然是ajax库,就不可避免与跨域扯上关系;XHR2是支持跨域请求的,只不过要满足浏览器端支持CORS,服务器通过Access-Control-Allow-Origin来允许指定的源进行跨域,仅此一种方式。

与XHR2一样,fetch也是支持跨域请求的,只不过其跨域请求做法与XHR2一样,需要客户端与服务端支持;另外,fetch还支持一种跨域,不需要服务器支持的形式,具体可以通过其mode的配置项来说明。

fetch的mode配置项有3个值,如下:

  • same-origin:该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个error告知不能跨域;其对应的response type为basic

  • cors: 该模式支持跨域请求,顾名思义它是以CORS的形式跨域;当然该模式也可以同域请求不需要后端额外的CORS支持;其对应的response type为cors

  • no-cors: 该模式用于跨域请求但是服务器不带CORS响应头,也就是服务端不支持CORS;这也是fetch的特殊跨域请求方式;其对应的response type为opaque

针对跨域请求,cors模式是常见跨域请求实现,但是fetch自带的no-cors跨域请求模式则较为陌生,该模式有一个比较明显的特点:

该模式允许浏览器发送本次跨域请求,但是不能访问响应返回的内容,这也是其response type为opaque透明的原因。

这与<img/>发送的请求类似,只是该模式不能访问响应的内容信息;但是它可以被其他APIs进行处理,例如ServiceWorker。另外,该模式返回的repsonse可以在Cache API中被存储起来以便后续的对它的使用,这点对script、css和图片的CDN资源是非常合适的,因为这些资源响应头中都没有CORS头。

总的来说,fetch的跨域请求是使用CORS方式,需要浏览器和服务端的支持。

简单封装fetch

实现GET请求和POST请求参数的统一。

/**
obj:{{"a", "1"},{"b", "2"}}
arr:[["a", "1"],["b", "2"]]
 * 将对象转成 a=1&b=2的形式
 * @param obj 对象
 */

function obj2String(obj, arr = [], idx = 0) {
  for (let item in obj) {
    arr[idx++] = [item, obj[item]]
  }
  return new URLSearchParams[z3] (arr).toString()
}
/**
 * 真正的请求
 * @param url 请求地址
 * @param options 请求参数
 * @param method 请求方式
 */

function commonFetcdh(url, options, method = 'GET') {
  const searchStr = obj2String(options)
  let initObj = {}
  if (method === 'GET') { // 如果是GET请求,拼接url
    url += '?' + searchStr
    initObj = {
      method: method,
      credentials: 'include'
    }
  } else {
    initObj = {
      method: method,
      credentials: 'include',
      headers: new Headers({
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded'
      }),
      body: searchStr
    }
  }
  fetch(url, initObj).then((res) => {
    return res.json()[z4] 
  }).then((res) => {
    return res
  })
}



/**
 * GET请求
 * @param url 请求地址
 * @param options 请求参数
 */

function GET(url, options) {
  return commonFetcdh(url, options, 'GET')
}

/**
 * POST请求
 * @param url 请求地址
 * @param options 请求参数
 */

function POST(url, options) {
  return commonFetcdh(url, options, 'POST')
}

GET('https://www.baidu.com/search/error.html', {a:1,b:2})
POST('https://www.baidu.com/search/error.html', {a:1,b:2})

Fetch封装JSONP

fetch是与服务器端进行异步交互的,而JSONP是外链一个javascript资源,并不是真正ajax,所以fetch与JSONP没有什么直接关联,当然至少目前是不支持JSONP的。

这里我们把JSONP与fetch关联在一起有点差强人意,fetch只是一个ajax库,我们不可能使fetch支持JSONP;只是我们要实现一个JSONP,只不过这个JSONP的实现要与fetch的实现类似,即基于Promise来实现一个JSONP;而其外在表现给人感觉是fetch支持JSONP一样;

目前比较成熟的开源JSONP实现fetch-jsonp给我们提供了解决方案,想了解可以自行前往。不过再次想唠叨一下其JSONP的实现步骤,因为在本人面试的前端候选人中大部分人对JSONP的实现语焉不详;

使用它非常简单,首先需要用npm安装fetch-jsonp

npm install fetch-jsonp --save-dev

然后在像下面一样使用:

fetchJsonp('/users.jsonp', {
    timeout: 3000,
    jsonpCallback: 'custom_callback'
  })
  .then(function(response) {
    return response.json()
  }).catch(function(ex) {
    console.log('parsing failed', ex)
  })

 

axios拦截设置和错误处理

现在vue的官方包已经不更新vue-resource了,转而推荐axios,下面是项目实战是总结的axios插件设置:

/**
 * @file Axios的Vue插件(添加全局请求/响应拦截器)
 */
// https://github.com/mzabriskie/axios
import axios from 'axios'

// 拦截request,设置全局请求为ajax请求
axios.interceptors.request.use((config) => {
  config.headers['X-Requested-With'] = 'XMLHttpRequest'

  return config
})

// 拦截响应response,并做一些错误处理
axios.interceptors.response.use((response) => {
  const data = response.data

// 根据返回的code值来做不同的处理(和后端约定)
  switch (data.code) {
    case '0':
      // 举例
      // exp: 修复iPhone 6+ 微信点击返回出现页面空白的问题
      if (isIOS()) {
        // 异步以保证数据已渲染到页面上
        setTimeout(() => {
          // 通过滚动强制浏览器进行页面重绘
          document.body.scrollTop += 1
        }, 0)
      }
      // 这一步保证数据返回,如果没有return则会走接下来的代码,不是未登录就是报错
      return data

    // 需要重新登录
    case 'SHIRO_E5001':
      // 微信生产环境下授权登录
      if (isWeChat() && IS_PRODUCTION) {
        axios.get(apis.common.wechat.authorizeUrl).then(({ result }) => {
          location.replace(global.decodeURIComponent(result))
        })
      } else {
        // 否则跳转到h5登录并带上跳转路由
        const search = encodeSearchParams({
          next: location.href,
        })
        location.replace(`/user/login?${search}`)
      }
      // 不显示提示消息
      data.description = ''
      break
    default:
  }
  // 若不是正确的返回code,且已经登录,就抛出错误
  const err = new Error(data.description)
  err.data = data
  err.response = response
  throw err
}, (err) => { // 这里是返回状态码不为200时候的错误处理
  if (err && err.response) {
    switch (err.response.status) {
      case 400:
        err.message = '请求错误'
        break

      case 401:
        err.message = '未授权,请登录'
        break

      case 403:
        err.message = '拒绝访问'
        break

      case 404:
        err.message = `请求地址出错: ${err.response.config.url}`
        break

      case 408:
        err.message = '请求超时'
        break

      case 500:
        err.message = '服务器内部错误'
        break

      case 501:
        err.message = '服务未实现'
        break

      case 502:
        err.message = '网关错误'
        break

      case 503:
        err.message = '服务不可用'
        break

      case 504:
        err.message = '网关超时'
        break

      case 505:
        err.message = 'HTTP版本不受支持'
        break

      default:
    }
  }

  return Promise.reject(err)
})

axios.install = (Vue) => {
  Vue.prototype.$axios = axios
}

export default axios

Http拦截和路由拦截,token验证

第一步: 路由 多添加一个自定义字段 requireAuth
 path: '/repository',
        name: 'repository',
        meta: {
            requireAuth: true,  // 添加该字段,表示进入这个路由是需要登录的
        },
        component: Repository
第二步: 
router.beforeEach((to, from, next) => {
    if (to.meta.requireAuth) {  // 判断该路由是否需要登录权限
        if (store.state.token) {  // 通过vuex state获取当前的token是否存在
            next();
        }
        else {
            next({
                path: '/login',
                query: {redirect: to.fullPath}  // 将跳转的路由path作为参数,登录成功后跳转到该路由
            })
        }
    }
    else {
        next();
    }

登录拦截到这里就结束了吗?并没有。

这种方式只是简单的前端路由控制,并不能真正阻止用户访问需要登录权限的路由。(可手动在浏览器地址栏输入没有权限的路由)还有一种情况便是:当前token失效了,但是token依然保存在本地。这时候你去访问需要登录权限的路由时,实际上应该让用户重新登录。这时候就需要结合 http 拦截器 + 后端接口返回的http 状态码来判断。

第三步:  拦截器 (要想统一处理所有http请求和响应,就得用上 axios 的拦截器。)

每次跳页面, 都要获取新路由对应的html页面, 这时候可以用axioshttp拦截每次路由跳转, 都先让后台验证一下token是否有效, http头添加token, 当后端接口返回 401 Unauthorized(未授权) ,让用户重新登录。关于Autorization     使用之后会忽略cookietoken,  削弱了安全性, 可以配合https

// http request 拦截器
axios.interceptors.request.use(
    config => {
        if (store.state.token) {  // 判断是否存在token,如果存在的话,则每个http header都加上token
            config.headers.Authorization = `token ${store.state.token}`;
        }
        return config;
    },
    err => {
        return Promise.reject(err);
    });
// http response 拦截器
axios.interceptors.response.use(
    response => {
        return response;
    },
    error => {
        if (error.response) {
            switch (error.response.status) {
                case 401:  401  旌旗  灵医 , 只用[授权] 旌旗的医生 才是 灵医

                    // 返回 401 清除token信息并跳转到登录页面
                    store.commit(types.LOGOUT);
                    router.replace({
                        path: 'login',
                        query: {redirect: router.currentRoute.fullPath}
                    })
            }
        }
        return Promise.reject(error.response.data)   // 返回接口返回的错误信息
    });

完整的方法见 /src/http.js .

通过上面这几步,就可以在前端实现登录拦截了。 登出 功能也就很简单,只需要把当前token清除,再跳转到首页即可。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值