关于fetch:fetch的使用记录

如何用fetch发动post申请上面是一个用fetch发动的post申请示例: fetch('/api/add', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'tomcat', }),}).then(res => res.json()).then(res => { console.log('res', res);});注意事项:如何传递post参数?设置Content-Type为application/json将post参数转换为字符串,须要用到JSON.stringify如何解析响应?须要对fetch返回的响应调用json办法。 因为fetch返回的是一个Response对象,不能间接读取数据,所以须要对其先调用一下json办法,而后能力失去冀望的数据对象。

February 21, 2024 · 1 min · jiezi

关于fetch:fetch异步上传图片附htmlJavaScriptphp代码

成果 index.html<!DOCTYPE html><html> <head> <title>图片上传示例</title> <meta charset="utf-8"> <script src="upload.js"></script> <style> *{ padding: 0; margin: 0; } #app{ width: 500px; margin: 100px auto 0; padding: 20px 20px; background: #eee; border-radius: 15px; } </style> </head><body> <div id="app"> <h1>fetch图片上传示例</h1> <!--上传表单--> <input type="file" id="imageFile" accept="image/*"> <!--上传后果--> <div id="result"></div> </div></body></html>upload.js// 确保JavaScript代码在DOM加载实现后执行document.addEventListener('DOMContentLoaded', function() { // 获取点击上传的按钮 var fileButton = document.getElementById('imageFile'); // 监听抉择文件按钮是否曾经抉择了文件 fileButton.addEventListener('change', function (){ // 获取抉择的文件 var fileSelected = fileButton.files[0]; // 执行上传函数 uploadFile(fileSelected, function(error, response) { if (error) { // 上传文件失败 console.log(error); } else { // 上传文件胜利 var jsonData = JSON.parse(response); console.log(jsonData); // 显示上传后果预览 document.getElementById('result').innerHTML = '<img src="'+jsonData.url+'" width="350" />'; } }); }); // 清空file表单的抉择 fileButton.value = '';})// 上传函数function uploadFile(file, callback) { // 获取表单数据 var formData = new FormData(); formData.append('file', file); // 申请上传服务器 fetch('upload.php', { method: 'POST', body: formData, }) .then(function(response) { return response.text(); }) .then(function(data) { callback(null, data); }) .catch(function(error) { callback(error, null); });}upload.php<?php// 编码header("Content-type:application/json"); // 获取文件$file = $_FILES["file"]["name"]; // 获取文件后缀名$hzm = substr($file,strpos($file,".")); // 设置新文件名$newfile = date("Y-m-d")."-".rand(100,999); // 容许上传的后缀$allowedExts = array("gif", "jpeg", "jpg", "png");$temp = explode(".", $file);$extension = end($temp);if ((($_FILES["file"]["type"] == "image/gif")|| ($_FILES["file"]["type"] == "image/jpeg")|| ($_FILES["file"]["type"] == "image/jpg")|| ($_FILES["file"]["type"] == "image/pjpeg")|| ($_FILES["file"]["type"] == "image/x-png")|| ($_FILES["file"]["type"] == "image/png"))&& ($_FILES["file"]["size"] < 10485760)&& in_array($extension, $allowedExts)){ // 判断上传后果 if ($_FILES["file"]["error"] > 0){ $result = array( 'code' => 201, 'msg' => '上传失败' ); }else{ // 上传文件 move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$newfile.$hzm); $file_url = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER["REQUEST_URI"]; $result = array( "code" => 200, "msg" => "上传胜利", "url" => dirname($file_url)."/upload/".$newfile.$hzm ); }}else{ $result = array( 'code' => 202, 'msg' => '此类文件不能上传' );}// 输入JSONecho json_encode($result, JSON_UNESCAPED_UNICODE);?>留神,须要在同一目录下建设upload目录以寄存上传的文件。 ...

June 27, 2023 · 2 min · jiezi

关于fetch:Nodejs-支持-fetch-API

最新的 Node.js v17.5 引入了对 fetch API 的反对 ,对前端同学来说,十分相熟。 fetch() 是一种风行的跨平台 HTTP 客户端 API,可在浏览器和 Web/Service Workers 中运行。 尽管目前在 v17.5.0 版本为试验性反对,然而如果在今后的 LTS 版本中正式反对了,就不须要依赖第三方 HTTP 申请模块。 fetch API 提供了 WHATWG 标准接口获取资源,这是一个基于 Promise 的 HTTP 客户端,能够用来简化 HTTP 申请,在浏览器环境已反对,浏览器环境的兼容性实现参考 Web/API/fetch,应用形式与浏览器中应用 Fetch API 统一。 fetch API 次要蕴含以下四个接口: fetch():该办法是最罕用的,用于发送申请。Headers:相当于 response/request 的头信息,能够使你查问到这些头信息,或者针对不同的后果做不同的操作。Request:相当于一个资源申请。Response:相当于申请的响应。为什么要应用?在 Node.js 中应用 fetch() 的次要起因有两个: Node.js 社区中有一场强烈的探讨,探讨如何以客户端开发者相熟的形式倒退 Node 的 HTTP 堆栈,同时又能与服务器编程模型配合,如何超过目前作为外围局部的 HTTP 模型的限度,以及如何反对 HTTP/2-3 而不适度减轻用户的累赘,而 fetch() 正是这一对话的第一步。fetch() 实现基于 Undici,该我的项目位于 Node.js Github 我的项目组织下,其中的几位贡献者也是 Node.js 我的项目的贡献者,致力于为 Node.js 开发疾速、牢靠且符合规范的 HTTP 客户端,比内置的 HTTP 模块还要快,因为它摒弃了原有的 HTTP 模块,间接构建在 socket 之上。如何应用?目前 Node.js 17.5 中的 fetch() 是一项实验性的性能,运行脚本时须要须要增加 --experimental-fetch,即可在脚本内应用 fetch()。 ...

March 8, 2022 · 1 min · jiezi

关于fetch:类和类的继承的简单使用

include <iostream>using namespace std; class Animal {public: Animal(int heigh, int weight)//构造函数{// cout <<"Animal construct"<<endl; } ~Animal()//析构函数{// cout <<"Animal 析构函数 ^_^ " << endl; } void eat() { cout <<"animal eat!" <<endl; } void sleep() { cout <<"animal sleep!" <<endl; } void breakthe() { cout <<"animal breakthe!" <<endl; }}; class fish : public Animal//继承{public: fish() : Animal(400, 300), a(1)//析构函数,对a初始化{// cout <<"fish construct"<<endl; } ~fish() {// cout <<"fish deconstruct"<<endl; ...

October 21, 2021 · 1 min · jiezi

Axiosfetch

Import

October 17, 2019 · 1 min · jiezi

ES6之fetch

js原生支持,比XHR强大,易用的数据交互对象。fetch的两个步骤 基本使用fetch("url").then(res=>{},err=>{});Response(res)对象成员 ok:是否成功headers:响应头对象status/statusText:状态码/状态文本redirected:是否重定向过 fetch('../public/info.text').then(res => { console.log('res info is:',res); const {ok} = res; if(ok){ console.log('request ok!') }else{ console.log('request error'); } },(err)=>{ //网络错误、连接失败、无法解析对方数据 console.log('request error'); })运行结果:可以看到fetch的第一个then里面成功的回调响应对象的一些信息,哎,仔细观察,发现并没有我们真正需要的数据啊,这是怎么肥事儿?其实呢,对于fetch来说,第一个then,res回调函数中响应对象res里面有一个json()方法,该返回返回一个promise对象,在这个对象的then回调里面就是我们真正需要的数据了。 fetch('../public/info.json').then(res=>res.json(),err=>{ console.log(err)}).then(data=>{ console.log('data is:',data)},err1=>{ console.log(err1)})res对象的几个方法: json():json方式解析数据------适合:一般数据arrayBuffer():把数据解析成二进制数组blob():不解析,原始二进制数据-----适合:图片、视频、音频等fromData():以表单方式解析数据text():以文本方式解析数据------适合:文本内容比较方法解释用途arrayBuffer因为高级语言(包括JS)都不擅长处理二进制数据,所以arrayBuffer是一种让js能读取二进制的对象,但一般情况下,处理二进制数据都是非常复杂的,所以极少使用极少bolb性能较好,因为无需任何转换操作图片、视频、音频json绝大部分数据文件都以json和数组的方式传输数据text文本数据文本、htmlasync配合fetch(async () => { try { let res = await fetch('./info.json') let data =await res.json() console.log('data is:',data); } catch (error) { console.log(error) }})()与async配合起来,从写法上看起来就比较简介,并且使用try...catch...对错误、异常进行处理 最终的结果也是以上面输出的一样: headers的操作一般情况下,服务器端的响应头里面的信息,在实际开发中我们有时候也会获取来进行某些操作,那么使用fetch进行数据通信的适合,该怎样去获取响应头的信息呢? 以上图为例,要获取响应头里面的Content-Type: res.headers.get('Content-Type')这就拿到了。获取其他响应头里包含的信息也是一样,调用headers对象的get方法,通过传入键,就可以获取到对应的值。 与React结合使用import React from 'react';import logo from './logo.svg';import './App.css';import { thisExpression } from '@babel/types';class App extends React.Component{ constructor(){ super(); this.state = { users:[], mIcon:'' } } async componentDidMount(){ let res = await fetch('./info.json') let users = await res.json(); let res1 = await fetch('./mCat.jpg') let mBolb = await res1.blob(); this.setState({ users, mIcon:URL.createObjectURL(mBolb) }) } render(){ return( <div> <ul> { this.state.users.map((item,index)=>( <li key={index}> <div>姓名:{item.name}</div> <div>年龄:{item.age}</div> <img src={this.state.mIcon}/> </li> )) } </ul> </div> ) }}export default App;

August 8, 2019 · 1 min · jiezi

fetch使用ajax替代方案

fetch简介Fetch 提供了一个 JavaScript接口,用于访问和操纵HTTP管道的部分,例如请求和响应。它还提供了一个全局 fetch()方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。 这种功能以前是使用 XMLHttpRequest实现的,Fetch提供了一个更好的替代方法 Fetch API 是基于 Promise 设计,使用了Promises 来处理结果/回调。旧浏览器不支持 Promise,需要使用 polyfill es6-promise 。 简单实现 fetch("http://192.168.43.169:8099/someInfo",{ method: 'post', }) .then(res=>{ console.log(response) // 包含响应结果的promise,只是一个 HTTP 响应,而不是真的JSON return response.json(); }) .then(res=>{ console.log(res) //js格式的json对象 })async await实现更方便 const fetchRequest = async () => { let response = await fetch("http://192.168.43.169:8099/teacher/resume", { method: 'post', }) let data = await response.json() console.log(data); //js格式的json对象 } fetchRequest()Response 对象属性: status:整数(默认值为200) 为response的状态码statusText: 字符串(默认值为"OK"),该值与HTTP状态码消息对应.ok:如上所示, 该属性是来检查response的状态是否在200-299(包括200,299)这个范围内.该属性返回一个Boolean值.方法:处理包含返回结果的promise对象的数据 例如 response.json()处理包含json结果的promise对象 ...

June 28, 2019 · 2 min · jiezi

Tableau-BI工具对接-AnalyticDB-for-PostgreSQL数据源

AnalyticDB for PostgreSQL(原HybridDB for PostgreSQL)作为高性能分析型数据库,可以支持用户对其业务数据进行实时分析,能够让企业敏锐感知市场动态,做出必要决策。Tableau是一款数据分析与可视化工具,它支持连接本地或云端数据,不管是电子表格,还是数据库数据,都能进行无缝连接。本文介绍Tableau以AnalyticDB for PostgreSQL作为数据源,如何进行有效的数据分析。 使用AnalyticDB for PostgreSQLAnalyticDB for PostgreSQL基于Greenplum,所以在选择连接器的时候选择Greenplum连接器: 点开出现登录页面,填上DB的连接信息完成登录。 登录后页面: 根据指导操作,可以将任意表进行统计分析,并进行报表展示。 例如使用TPCH数据中的lineitem,点开一张工作表可以进行任意维度的数据展示了: 每从度量或者维度中选择一个字段,放到工作表区时,Tableau都会发送一个query到AnalyticDB for PostgreSQL进行数据查询,例如上述图表发送的query: BEGIN;declare "SQL_CUR0x7fdabf04ca00" cursor with hold for SELECT "lineitem"."l_linestatus" AS "l_linestatus", "lineitem"."l_shipmode" AS "l_shipmode", SUM("lineitem"."l_orderkey") AS "sum_l_orderkey_ok", ((CAST("lineitem"."l_shipdate" AS DATE) + CAST(TRUNC((-1 * (EXTRACT(DAY FROM "lineitem"."l_shipdate") - 1))) AS INTEGER) * INTERVAL '1 DAY') + CAST(TRUNC((-1 * (EXTRACT(MONTH FROM "lineitem"."l_shipdate") - 1))) AS INTEGER) * INTERVAL '1 MONTH') AS "tyr_l_shipdate_ok" FROM "public"."lineitem" "lineitem" GROUP BY 1, 2, 4;fetch 10000 in "SQL_CUR0x7fdabf04ca00一些注意事项关掉cursor默认情况下Tableau使用cursor模式从AnalyticDB for PostgreSQL拉取数据: ...

June 26, 2019 · 1 min · jiezi

全面分析前端的网络请求方式

一、前端进行网络请求的关注点大多数情况下,在前端发起一个网络请求我们只需关注下面几点:传入基本参数(url,请求方式)请求参数、请求参数类型设置请求头获取响应的方式获取响应头、响应状态、响应结果异常处理携带cookie设置跨域请求二、前端进行网络请求的方式form表单、ifream、刷新页面Ajax - 异步网络请求的开山鼻祖jQuery - 一个时代fetch - Ajax的替代者axios、request等众多开源库三、关于网络请求的疑问Ajax的出现解决了什么问题原生Ajax如何使用jQuery的网络请求方式fetch的用法以及坑点如何正确的使用fetch如何选择合适的跨域方式带着以上这些问题、关注点我们对几种网络请求进行一次全面的分析。四、Ajax的出现解决了什么问题在Ajax出现之前,web程序是这样工作的:这种交互的的缺陷是显而易见的,任何和服务器的交互都需要刷新页面,用户体验非常差,Ajax的出现解决了这个问题。Ajax全称Asynchronous JavaScript + XML(异步JavaScript和XML)使用Ajax,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面。Ajax本身不是一种新技术,而是用来描述一种使用现有技术集合实现的一个技术方案,浏览器的XMLHttpRequest是实现Ajax最重要的对象(IE6以下使用ActiveXObject)。尽管X在Ajax中代表XML, 但由于JSON的许多优势,比如更加轻量以及作为Javascript的一部分,目前JSON的使用比XML更加普遍。五、原生Ajax的用法这里主要分析XMLHttpRequest对象,下面是它的一段基础使用: var xhr = new XMLHttpRequest(); xhr.open(‘post’,‘www.xxx.com’,true) // 接收返回值 xhr.onreadystatechange = function(){ if(xhr.readyState === 4 ){ if(xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ console.log(xhr.responseText); } } } // 处理请求参数 postData = {“name1”:“value1”,“name2”:“value2”}; postData = (function(value){ var dataString = “”; for(var key in value){ dataString += key+"="+value[key]+"&"; }; return dataString; }(postData)); // 设置请求头 xhr.setRequestHeader(“Content-type”,“application/x-www-form-urlencoded”); // 异常处理 xhr.onerror = function() { console.log(‘Network request failed’) } // 跨域携带cookie xhr.withCredentials = true; // 发出请求 xhr.send(postData);下面分别对XMLHttpRequest对象常用的的函数、属性、事件进行分析。函数open用于初始化一个请求,用法:xhr.open(method, url, async);method:请求方式,如get、posturl:请求的urlasync:是否为异步请求send用于发送 HTTP 请求,即调用该方法后HTTP请求才会被真正发出,用法:xhr.send(param)param:http请求的参数,可以为string、Blob等类型。abort用于终止一个ajax请求,调用此方法后readyState将被设置为0,用法:xhr.abort()setRequestHeader用于设置HTTP请求头,此方法必须在 open() 方法和 send() 之间调用,用法:xhr.setRequestHeader(header, value);getResponseHeader用于获取http返回头,如果在返回头中有多个一样的名称,那么返回的值就会是用逗号和空格将值分隔的字符串,用法:var header = xhr.getResponseHeader(name);属性readyState用来标识当前XMLHttpRequest对象所处的状态,XMLHttpRequest对象总是位于下列状态中的一个:值状态描述0UNSENT代理被创建,但尚未调用 open() 方法。1OPENEDopen() 方法已经被调用。2HEADERS_RECEIVEDsend() 方法已经被调用,并且头部和状态已经可获得。3LOADING下载中; responseText 属性已经包含部分数据。4DONE下载操作已完成。status表示http请求的状态, 初始值为0。如果服务器没有显式地指定状态码, 那么status将被设置为默认值, 即200。responseType表示响应的数据类型,并允许我们手动设置,如果为空,默认为text类型,可以有下面的取值:值描述"“将 responseType 设为空字符串与设置为"text"相同, 是默认类型 (实际上是 DOMString)。“arraybuffer"response 是一个包含二进制数据的 JavaScript ArrayBuffer 。“blob” response 是一个包含二进制数据的 Blob 对象 。“document"response 是一个 HTML Document 或 XML XMLDocument ,这取决于接收到的数据的 MIME 类型。“json” response 是一个 JavaScript 对象。这个对象是通过将接收到的数据类型视为 JSON 解析得到的。“text” response 是包含在 DOMString 对象中的文本。response返回响应的正文,返回的类型由上面的responseType决定。withCredentialsajax请求默认会携带同源请求的cookie,而跨域请求则不会携带cookie,设置xhr的withCredentials的属性为true将允许携带跨域cookie。事件回调onreadystatechange xhr.onreadystatechange = callback;当readyState 属性发生变化时,callback会被触发。onloadstart xhr.onloadstart = callback;在ajax请求发送之前(readyState==1后, readyState==2前),callback会被触发。onprogressxhr.onprogress = function(event){ console.log(event.loaded / event.total);}回调函数可以获取资源总大小total,已经加载的资源大小loaded,用这两个值可以计算加载进度。onload xhr.onload = callback;当一个资源及其依赖资源已完成加载时,将触发callback,通常我们会在onload事件中处理返回值。异常处理onerror xhr.onerror = callback;当ajax资源加载失败时会触发callback。ontimeout xhr.ontimeout = callback;当进度由于预定时间到期而终止时,会触发callback,超时时间可使用timeout属性进行设置。六、jQuery对Ajax的封装在很长一段时间里,人们使用jQuery提供的ajax封装进行网络请求,包括$.ajax、$.get、$.post等,这几个方法放到现在,我依然觉得很实用。$.ajax({ dataType: ‘json’, // 设置返回值类型 contentType: ‘application/json’, // 设置参数类型 headers: {‘Content-Type’,‘application/json’},// 设置请求头 xhrFields: { withCredentials: true }, // 跨域携带cookie data: JSON.stringify({a: [{b:1, a:1}]}), // 传递参数 error:function(xhr,status){ // 错误处理 console.log(xhr,status); }, success: function (data,status) { // 获取结果 console.log(data,status); }})$.ajax只接收一个参数,这个参数接收一系列配置,其自己封装了一个jqXHR对象,有兴趣可以阅读一下jQuary-ajax 源码常用配置:url 当前页地址。发送请求的地址。type 类型:String 请求方式 (“POST” 或 “GET”), 默认为 “GET”。注意:其它 HTTP 请求方法,如 PUT 和 DELETE 也可以使用,但仅部分浏览器支持。timeout 类型:Number 设置请求超时时间(毫秒)。此设置将覆盖全局设置。success 类型:Function 请求成功后的回调函数。jsonp在一个jsonp请求中重写回调函数的名字。这个值用来替代在"callback=?“这种GET或POST请求中URL参数里的"callback"部分。error 类型:Function 。请求失败时调用此函数。注意:源码里对错误的判定:isSuccess = status >= 200 && status < 300 || status === 304;返回值除了这几个状态码都会进error回调。dataType"xml”: 返回 XML 文档,可用 jQuery 处理。“html”: 返回纯文本 HTML 信息;包含的 script 标签会在插入 dom 时执行。“script”: 返回纯文本 JavaScript 代码。不会自动缓存结果。除非设置了 “cache” 参数。注意:在远程请求时(不在同一个域下),所有 POST 请求都将转为 GET 请求。(因为将使用 DOM 的 script标签来加载)“json”: 返回 JSON 数据 。“jsonp”: JSONP 格式。使用 JSONP 形式调用函数时,如 “myurl?callback=?” jQuery 将自动替换 ? 为正确的函数名,以执行回调函数。“text”: 返回纯文本字符串data 类型:String 使用JSON.stringify转码complete类型:Function 请求完成后回调函数 (请求成功或失败之后均调用)。async 类型:Boolean 默认值: true。默认设置下,所有请求均为异步请求。如果需要发送同步请求,请将此选项设置为 false。contentType 类型:String 默认值: “application/x-www-form-urlencoded”。发送信息至服务器时内容编码类型。键值对这样组织在一般的情况下是没有什么问题的,这里说的一般是,不带嵌套类型JSON,也就是 简单的JSON,形如这样:{ a: 1, b: 2, c: 3}但是在一些复杂的情况下就有问题了。 例如在 Ajax 中你要传一个复杂的 json 对像,也就说是对象嵌数组,数组中包括对象,你这样传: application/x-www-form-urlencoded 这种形式是没有办法将复杂的 JSON 组织成键值对形式。{ data: { a: [{ x: 2 }] }}可以用如下方式传递复杂的json对象$.ajax({ dataType: ‘json’, contentType: ‘application/json’, data: JSON.stringify({a: [{b:1, a:1}]})})七、jQuery的替代者近年来前端MV的发展壮大,人们越来越少的使用jQuery,我们不可能单独为了使用jQuery的Ajax api来单独引入他,无可避免的,我们需要寻找新的技术方案。尤雨溪在他的文档中推荐大家用axios进行网络请求。axios基于Promise对原生的XHR进行了非常全面的封装,使用方式也非常的优雅。另外,axios同样提供了在node环境下的支持,可谓是网络请求的首选方案。未来必定还会出现更优秀的封装,他们有非常周全的考虑以及详细的文档,这里我们不多做考究,我们把关注的重点放在更底层的APIfetch。Fetch API 是一个用用于访问和操纵HTTP管道的强大的原生 API。这种功能以前是使用 XMLHttpRequest实现的。Fetch提供了一个更好的替代方法,可以很容易地被其他技术使用,例如 Service Workers。Fetch还提供了单个逻辑位置来定义其他HTTP相关概念,例如CORS和HTTP的扩展。可见fetch是作为XMLHttpRequest的替代品出现的。使用fetch,你不需要再额外加载一个外部资源。但它还没有被浏览器完全支持,所以你仍然需要一个 polyfill。八、fetch的使用一个基本的 fetch请求:const options = { method: “POST”, // 请求参数 headers: { “Content-Type”: “application/json”}, // 设置请求头 body: JSON.stringify({name:‘123’}), // 请求参数 credentials: “same-origin”, // cookie设置 mode: “cors”, // 跨域}fetch(‘http://www.xxx.com’) .then(function(response) { return response.json(); }) .then(function(myJson) { console.log(myJson); // 响应数据 }) .catch(function(err){ console.log(err); // 异常处理 })Fetch API提供了一个全局的fetch()方法,以及几个辅助对象来发起一个网络请求。fetch()fetch()方法用于发起获取资源的请求。它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。Headers可以通过 Headers() 构造函数来创建一个你自己的 headers 对象,相当于 response/request 的头信息,可以使你查询到这些头信息,或者针对不同的结果做不同的操作。var myHeaders = new Headers();myHeaders.append(“Content-Type”, “text/plain”);Request通过 Request() 构造函数可以创建一个Request 对象,这个对象可以作为fetch函数的第二个参数。Response在fetch()处理完promises之后返回一个Response 实例,也可以手动创建一个Response实例。九、fetch polyfill源码分析由于fetch是一个非常底层的API,所以我们无法进一步的探究它的底层,但是我们可以借助它的polyfill探究它的基本原理,并找出其中的坑点。代码结构由代码可见,polyfill主要对Fetch API提供的四大对象进行了封装:fetch 封装代码非常清晰:构造一个Promise对象并返回创建一个Request对象创建一个XMLHttpRequest对象取出Request对象中的请求url,请求方发,open一个xhr请求,并将Request对象中存储的headers取出赋给xhrxhr onload后取出response的status、headers、body封装Response对象,调用resolve。异常处理可以发现,调用reject有三种可能:1.请求超时2.请求失败注意:当和服务器建立简介,并收到服务器的异常状态码如404、500等并不能触发onerror。当网络故障时或请求被阻止时,才会标记为 reject,如跨域、url不存在,网络异常等会触发onerror。所以使用fetch当接收到异常状态码都是会进入then而不是catch。这些错误请求往往要手动处理。3.手动终止可以在request参数中传入signal对象,并对signal对象添加abort事件监听,当xhr.readyState变为4(响应内容解析完成)后将signal对象的abort事件监听移除掉。这表示,在一个fetch请求结束之前可以调用signal.abort将其终止。在浏览器中可以使用AbortController()构造函数创建一个控制器,然后使用AbortController.signal属性这是一个实验中的功能,此功能某些浏览器尚在开发中Headers封装在header对象中维护了一个map对象,构造函数中可以传入Header对象、数组、普通对象类型的header,并将所有的值维护到map中。之前在fetch函数中看到调用了header的forEach方法,下面是它的实现:可见header的遍历即其内部map的遍历。另外Header还提供了append、delete、get、set等方法,都是对其内部的map对象进行操作。Request对象Request对象接收的两个参数即fetch函数接收的两个参数,第一个参数可以直接传递url,也可以传递一个构造好的request对象。第二个参数即控制不同配置的option对象。可以传入credentials、headers、method、mode、signal、referrer等属性。这里注意:传入的headers被当作Headers构造函数的参数来构造header对象。cookie处理fetch函数中还有如下的代码: if (request.credentials === ‘include’) { xhr.withCredentials = true } else if (request.credentials === ‘omit’) { xhr.withCredentials = false }默认的credentials类型为same-origin,即可携带同源请求的coodkie。然后我发现这里polyfill的实现和MDN-使用Fetch以及很多资料是不一致的:mdn: 默认情况下,fetch 不会从服务端发送或接收任何 cookies于是我分别实验了下使用polyfill和使用原生fetch携带cookie的情况,发现在不设置credentials的情况下居然都是默认携带同源cookie的,这和文档的说明说不一致的,查阅了许多资料后都是说fetch默认不会携带cookie,下面是使用原生fetch在浏览器进行请求的情况:然后我发现在MDN-Fetch-Request已经指出新版浏览器credentials默认值已更改为same-origin,旧版依然是omit。确实MDN-使用Fetch这里的文档更新的有些不及时,误人子弟了…Response对象Response对象是fetch调用成功后的返回值:回顾下fetch中对Response的操作: xhr.onload = function () { var options = { status: xhr.status, statusText: xhr.statusText, headers: parseHeaders(xhr.getAllResponseHeaders() || '') } options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') var body = 'response' in xhr ? xhr.response : xhr.responseText resolve(new Response(body, options)) }Response构造函数:可见在构造函数中主要对options中的status、statusText、headers、url等分别做了处理并挂载到Response对象上。构造函数里面并没有对responseText的明确处理,最后交给了_initBody函数处理,而Response并没有主动声明_initBody属性,代码最后使用Response调用了Body函数,实际上_initBody函数是通过Body函数挂载到Response身上的,先来看看_initBody函数:可见,_initBody函数根据xhr.response的类型(Blob、FormData、String...),为不同的参数进行赋值,这些参数在Body方法中得到不同的应用,下面具体看看Body函数还做了哪些其他的操作:Body函数中还为Response对象挂载了四个函数,text、json、blob、formData,这些函数中的操作就是将_initBody中得到的不同类型的返回值返回。这也说明了,在fetch执行完毕后,不能直接在response中获取到返回值而必须调用text()、json()等函数才能获取到返回值。这里还有一点需要说明:几个函数中都有类似下面的逻辑: var rejected = consumed(this) if (rejected) { return rejected }consumed函数:function consumed(body) { if (body.bodyUsed) { return Promise.reject(new TypeError('Already read')) } body.bodyUsed = true}每次调用text()、json()等函数后会将bodyUsed变量变为true,用来标识返回值已经读取过了,下一次再读取直接抛出TypeError('Already read')。这也遵循了原生fetch的原则:因为Responses对象被设置为了 stream 的方式,所以它们只能被读取一次十、fetch的坑点VUE的文档中对fetch有下面的描述:使用fetch还有很多别的注意事项,这也是为什么大家现阶段还是更喜欢 axios 多一些。当然这个事情在未来可能会发生改变。由于fetch是一个非常底层的API,它并没有被进行很多封装,还有许多问题需要处理:不能直接传递JavaScript对象作为参数需要自己判断返回值类型,并执行响应获取返回值的方法获取返回值方法只能调用一次,不能多次调用无法正常的捕获异常老版浏览器不会默认携带cookie不支持jsonp十一、对fetch的封装请求参数处理支持传入不同的参数类型:function stringify(url, data) { var dataString = url.indexOf('?') == -1 ? '?' : '&amp;'; for (var key in data) { dataString += key + '=' + data[key] + '&amp;'; }; return dataString;}if (request.formData) { request.body = request.data;} else if (/^get$/i.test(request.method)) { request.url = ${request.url}${stringify(request.url, request.data)};} else if (request.form) { request.headers.set('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8'); request.body = stringify(request.data);} else { request.headers.set('Content-Type', 'application/json;charset=UTF-8'); request.body = JSON.stringify(request.data);}cookie携带fetch在新版浏览器已经开始默认携带同源cookie,但在老版浏览器中不会默认携带,我们需要对他进行统一设置: request.credentials = 'same-origin'; // 同源携带 request.credentials = 'include'; // 可跨域携带异常处理当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。因此我们要对fetch的异常进行统一处理.then(response =&gt; { if (response.ok) { return Promise.resolve(response); }else{ const error = new Error(请求失败! 状态码: ${response.status}, 失败信息: ${response.statusText}`); error.response = response; return Promise.reject(error); }});返回值处理对不同的返回值类型调用不同的函数接收,这里必须提前判断好类型,不能多次调用获取返回值的方法:.then(response => { let contentType = response.headers.get(‘content-type’); if (contentType.includes(‘application/json’)) { return response.json(); } else { return response.text(); }});jsonpfetch本身没有提供对jsonp的支持,jsonp本身也不属于一种非常好的解决跨域的方式,推荐使用cors或者nginx解决跨域,具体请看下面的章节。fetch封装好了,可以愉快的使用了。嗯,axios真好用…十二、跨域总结谈到网络请求,就不得不提跨域。浏览器的同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。通常不允许不同源间的读操作。跨域条件:协议,域名,端口,有一个不同就算跨域。下面是解决跨域的几种方式:nginx使用nginx反向代理实现跨域,参考我这篇文章:前端开发者必备的nginx知识corsCORS是一个W3C标准,全称是"跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求。服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。app.all(’’, function (req, res, next) { res.header(“Access-Control-Allow-Origin”, “*”); res.header(“Access-Control-Allow-Headers”, “X-Requested-With”); res.header(“Access-Control-Allow-Methods”, “PUT,POST,GET,DELETE,OPTIONS”); next();});jsonpscript标签的src属性中的链接可以访问跨域的js脚本,利用这个特性,服务端不再返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中进行了调用,这样实现了跨域。jquery对jsonp的支持: $.ajax({ type : “get”, url : “http://xxxx” dataType: “jsonp”, jsonp:“callback”, jsonpCallback: “doo”, success : function(data) { console.log(data); } });fetch、axios等并没有直接提供对jsonp的支持,如果需要使用这种方式,我们可以尝试进行手动封装:(function (window,document) { “use strict”; var jsonp = function (url,data,callback) { // 1.将传入的data数据转化为url字符串形式 // {id:1,name:‘jack’} => id=1&name=jack var dataString = url.indexof(’?’) == -1? ‘?’: ‘&’; for(var key in data){ dataString += key + ‘=’ + data[key] + ‘&’; }; // 2 处理url中的回调函数 // cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉) var cbFuncName = ‘my_json_cb_’ + Math.random().toString().replace(’.’,’’); dataString += ‘callback=’ + cbFuncName; // 3.创建一个script标签并插入到页面中 var scriptEle = document.createElement(‘script’); scriptEle.src = url + dataString; // 4.挂载回调函数 window[cbFuncName] = function (data) { callback(data); // 处理完回调函数的数据之后,删除jsonp的script标签 document.body.removeChild(scriptEle); } document.body.appendChild(scriptEle); } window.$jsonp = jsonp;})(window,document)postMessage跨域postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。//捕获iframevar domain = ‘http://scriptandstyle.com’;var iframe = document.getElementById(‘myIFrame’).contentWindow;//发送消息setInterval(function(){ var message = ‘Hello! The time is: ’ + (new Date().getTime()); console.log(‘blog.local: sending message: ’ + message); //send the message and target URI iframe.postMessage(message,domain); },6000);//响应事件window.addEventListener(‘message’,function(event) { if(event.origin !== ‘http://davidwalsh.name’) return; console.log(‘message received: ’ + event.data,event); event.source.postMessage(‘holla back youngin!’,event.origin);},false);postMessage跨域适用于以下场景:同浏览器多窗口间跨域通信、iframe间跨域通信。WebSocketWebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据而不受同源策略的限制。 function WebSocketTest(){ if (“WebSocket” in window){ alert(“您的浏览器支持 WebSocket!”); // 打开一个 web socket var ws = new WebSocket(“ws://localhost:3000/abcd”); ws.onopen = function(){ // Web Socket 已连接上,使用 send() 方法发送数据 ws.send(“发送数据”); alert(“数据发送中…”); }; ws.onmessage = function (evt) { var received_msg = evt.data; alert(“数据已接收…”); }; ws.onclose = function(){ // 关闭 websocket alert(“连接已关闭…”); }; } else{ // 浏览器不支持 WebSocket alert(“您的浏览器不支持 WebSocket!”); } }文中如有错误,欢迎在评论区指正,谢谢阅读。 ...

March 27, 2019 · 4 min · jiezi

在 React Hooks 中如何请求数据?

通过这个教程,我想告诉你在 React 中如何使用 state 和 effect 这两种 hooks 去请求数据。我们将使用众所周知的 Hacker News API 来获取一些热门文章。你将定义属于你自己的数据请求的 Hooks ,并且可以在你所有的应用中复用,也可以发布到 npm 。如果你不了解 React 的这些新特性,可以查看我的另一篇文章 introduction to React Hooks。如果你想直接查看文章的示例,可以直接 checkout 这个 Github 仓库。注意:在 React 未来的版本中,Hooks 将不会用了获取数据,取而代之的是一种叫做 Suspense 的东西。尽管如此,下面的方法依然是了解 state 和 effect 两种 Hooks 的好方法。使用 React Hooks 进行数据请求如果你没有过在 React 中进行数据请求的经验,可以阅读我的文章:How to fetch data in React。文章讲解了如何使用 Class components 获取数据,如何使用可重用的 Render Props Components 和 Higher Order Components ,以及如何进行错误处理和 loading 状态。在本文中,我想用 Function components 和 React Hooks 来重现这一切。import React, { useState } from ‘react’;function App() { const [data, setData] = useState({ hits: [] }); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> );}export default App;App 组件将展示一个列表,列表信息来自 Hacker News articles 。状态和状态更新函数将通过被称为 useState 的状态钩子来生成,它负责管理通过请求得到的 App 组件的本地状态。初始状态是一个空数组,目前没有任何地方给它设置新的状态。我们将使用 axios 来获取数据,当然也可以使用你熟悉的请求库,或者浏览器自带的 fetch API。如果你还没有安装过 axios ,可以通过 npm install axios 进行安装。import React, { useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); useEffect(async () => { const result = await axios( ‘http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> );}export default App;我们在 useEffect 这个 effect hook 中,通过 axios 从 API 中获取数据,并使用 state hook 的更新函数,将数据存入到本地 state 中。并且使用 async/await 来解析promise。然而,当你运行上面的代码的时候,你会陷入到该死的死循环中。effect hook 在组件 mount 和 update 的时候都会执行。因为我们每次获取数据后,都会更新 state,所以组件会更新,并再次运行 effect,这会一次又一次的请求数据。很明显我们需要避免这样的bug产生,我们只想在组件 mount 的时候请求数据。你可以在 effect hook 提供的第二个参数中,传入一个空数组,这样做可以避免组件更新的时候执行 effect hook ,但是组件在 mount 依然会执行它。import React, { useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); useEffect(async () => { const result = await axios( ‘http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> );}export default App;第二个参数是用来定义 hook 所以依赖的变量的。如果其中一个变量发生变化,hook 将自动运行。如果第二个参数是一个空数组,那么 hook 将不会在组件更新是运行,因为它没有监控任何的变量。还有一个需要特别注意的点,在代码中,我们使用了 async/await 来获取第三方 API 提供的数据。根据文档,每一个 async 函数都将返回一个隐式的 promise:“The async function declaration defines an asynchronous function, which returns an AsyncFunction object. An asynchronous function is a function which operates asynchronously via the event loop, using an implicit Promise to return its result. ““async 函数定义了一个异步函数,它返回的是一个异步函数对象,异步函数是一个通过事件循环进行操作的函数,使用隐式的 Promise 返回最终的结果。”然而,effect hook 应该是什么也不返回的,或者返回一个 clean up 函数的。这就是为什么你会在控制台看到一个错误信息。index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.这意味着我们不能直接在 useEffect 函数使用async。让我们来实现一个解决方案,能够在 effect hook 中使用 async 函数。import React, { useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData = async () => { const result = await axios( ‘http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> );}export default App;这就是一个使用 React Hooks 进行数据请求的小案例。但是,如果你对错误处理、loading 态、如何触发表单数据获取以及如何复用出具处理 hook 感兴趣,那我们接着往下看。如何手动或者自动触发一个 hook?现在我们已经能够在组件 mount 之后获取到数据,但是,如何使用输入框动态告诉 API 选择一个感兴趣的话题呢?可以看到之前的代码,我们默认将 “Redux” 作为查询参数(‘http://hn.algolia.com/api/v1/...'),但是我们怎么查询关于 React 相关的话题呢?让我们实现一个 input 输入框,可以获得除了 “Redux” 之外的其他的话题。现在,让我们为输入框引入一个新的 state。import React, { Fragment, useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); useEffect(() => { const fetchData = async () => { const result = await axios( ‘http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> );}export default App;现在,请求数据和查询参数两个 state 相互独立,但是我们需要像一个办法希望他们耦合起来,只获取输入框输入的参数指定的话题文章。通过以下修改,组件应该在 mount 之后按照查询获取相应文章。…function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); useEffect(() => { const fetchData = async () => { const result = await axios( http://hn.algolia.com/api/v1/search?query=${query}, ); setData(result.data); }; fetchData(); }, []); return ( … );}export default App;实际上,我们还缺少部分代码。你会发现当你在输入框输入内容后,并没有获取到新的数据。这是因为 useEffect 的第二个参数只是一个空数组,此时的 effect 不依赖于任何的变量,所以这只会在 mount 只会触发一次。但是,现在我们需要依赖查询条件,一旦查询发送改变,数据请求就应该再次触发。…function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); useEffect(() => { const fetchData = async () => { const result = await axios( http://hn.algolia.com/api/v1/search?query=${query}, ); setData(result.data); }; fetchData(); }, [query]); return ( … );}export default App;好了,现在一旦你改变输入框内容,数据就会重新获取。但是现在又要另外一个问题:每次输入一个新字符,就会触发 effect 进行一次新的请求。那么我们提供一个按钮来手动触发数据请求呢?function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); const [search, setSearch] = useState(‘redux’); useEffect(() => { const fetchData = async () => { const result = await axios( http://hn.algolia.com/api/v1/search?query=${search}, ); setData(result.data); }; fetchData(); }, [search]); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“button” onClick={() => setSearch(query)}> Search </button> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> );}此外,search state 的初始状态也是设置成了与 query state 相同的状态,因为组件在 mount 的时候会请求一次数据,此时的结果也应该是反应的是输入框中的搜索条件。然而, search state 和 query state 具有类似的值,这看起来比较困惑。为什么不将真实的 URL 设置到 search state 中呢?function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); const [url, setUrl] = useState( ‘http://hn.algolia.com/api/v1/search?query=redux', ); useEffect(() => { const fetchData = async () => { const result = await axios(url); setData(result.data); }; fetchData(); }, [url]); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“button” onClick={() => setUrl(http://hn.algolia.com/api/v1/search?query=${query}) } > Search </button> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> );}这就是通过 effect hook 获取数据的案例,你可以决定 effect 取决于哪个 state。在这个案例中,如果 URL 的 state 发生改变,则再次运行该 effect 通过 API 重新获取主题文章。Loading 态 与 React Hooks让我们在数据的加载过程中引入一个 Loading 状态。它只是另一个由 state hook 管理的状态。Loading state 用于在 App 组件中呈现 Loading 状态。import React, { Fragment, useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); const [url, setUrl] = useState( ‘http://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchData = async () => { setIsLoading(true); const result = await axios(url); setData(result.data); setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“button” onClick={() => setUrl(http://hn.algolia.com/api/v1/search?query=${query}) } > Search </button> {isLoading ? ( <div>Loading …</div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> );}export default App;现在当组件处于 mount 状态或者 URL state 被修改时,调用 effect 获取数据,Loading 状态就会变成 true。一旦请求完成,Loading 状态就会再次被设置为 false。错误处理与 React Hooks通过 React Hooks 进行数据请求时,如何进行错误处理呢? 错误只是另一个使用 state hook 初始化的另一种状态。一旦出现错误状态,App 组件就可以反馈给用户。当使用 async/await 函数时,通常使用 try/catch 来进行错误捕获,你可以在 effect 中进行下面操作:…const [isError, setIsError] = useState(false);useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData();}, [url]);return ( <Fragment> … {isError && <div>Something went wrong …</div>} … <Fragment>);effect 每次运行都会重置 error state 的状态,这很有用,因为每次请求失败后,用户可能重新尝试,这样就能够重置错误。为了观察代码是否生效,你可以填写一个无用的 URL ,然后检查错误信息是否会出现。使用表单进行数据获取什么才是获取数据的正确形式呢?现在我们只有输入框和按钮进行组合,一旦引入更多的 input 元素,你可能想要使用表单来进行包装。此外表单还能够触发键盘的 “Enter” 事件。function App() { … const doFetch = (evt) => { evt.preventDefault(); setUrl(http://hn.algolia.com/api/v1/search?query=${query}); } return ( <Fragment> <form onSubmit={ doFetch } > <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“submit”>Search</button> </form> {isError && <div>Something went wrong …</div>} … </Fragment> );}自定义 hook 获取数据我们可以定义一个自定义的 hook,提取出所有与数据请求相关的东西,除了输入框的 query state,除此之外还有 Loading 状态、错误处理。还要确保返回组件中需要用到的变量。const useHackerNewsApi = () => { const [data, setData] = useState({ hits: [] }); const [url, setUrl] = useState( ‘http://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); const doFetch = () => { setUrl(http://hn.algolia.com/api/v1/search?query=${query}); }; return { data, isLoading, isError, doFetch };}现在,我们在 App 组件中使用我们的新 hook 。function App() { const [query, setQuery] = useState(‘redux’); const { data, isLoading, isError, doFetch } = useHackerNewsApi(); return ( <Fragment> … </Fragment> );}接下来,在外部传递 URL 给 DoFetch 方法。const useHackerNewsApi = () => { … useEffect( … ); const doFetch = url => { setUrl(url); }; return { data, isLoading, isError, doFetch };};function App() { const [query, setQuery] = useState(‘redux’); const { data, isLoading, isError, doFetch } = useHackerNewsApi(); return ( <Fragment> <form onSubmit={event => { doFetch( http://hn.algolia.com/api/v1/search?query=${query}, ); event.preventDefault(); }} > <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“submit”>Search</button> </form> … </Fragment> );}初始的 state 也是通用的,可以通过参数简单的传递到自定义的 hook 中:import React, { Fragment, useState, useEffect } from ‘react’;import axios from ‘axios’;const useDataApi = (initialUrl, initialData) => { const [data, setData] = useState(initialData); const [url, setUrl] = useState(initialUrl); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); const doFetch = url => { setUrl(url); }; return { data, isLoading, isError, doFetch };};function App() { const [query, setQuery] = useState(‘redux’); const { data, isLoading, isError, doFetch } = useDataApi( ‘http://hn.algolia.com/api/v1/search?query=redux', { hits: [] }, ); return ( <Fragment> <form onSubmit={event => { doFetch( http://hn.algolia.com/api/v1/search?query=${query}, ); event.preventDefault(); }} > <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“submit”>Search</button> </form> {isError && <div>Something went wrong …</div>} {isLoading ? ( <div>Loading …</div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> );}export default App;这就是使用自定义 hook 获取数据的方法,hook 本身对API一无所知,它从外部获取参数,只管理必要的 state ,如数据、 Loading 和错误相关的 state ,并且执行请求并将数据通过 hook 返回给组件。用于数据获取的 Reducer Hook目前为止,我们已经使用 state hooks 来管理了我们获取到的数据数据、Loading 状态、错误状态。然而,所有的状态都有属于自己的 state hook,但是他们又都连接在一起,关心的是同样的事情。如你所见,所有的它们都在数据获取函数中被使用。它们一个接一个的被调用(比如:setIsError、setIsLoading),这才是将它们连接在一起的正确用法。让我们用一个 Reducer Hook 将这三者连接在一起。Reducer Hook 返回一个 state 对象和一个函数(用来改变 state 对象)。这个函数被称为分发函数(dispatch function),它分发一个 action,action 具有 type 和 payload 两个属性。所有的这些信息都在 reducer 函数中被接收,根据之前的状态提取一个新的状态。让我们看看在代码中是如何工作的:import React, { Fragment, useState, useEffect, useReducer,} from ‘react’;import axios from ‘axios’;const dataFetchReducer = (state, action) => { …};const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); …};Reducer Hook 以 reducer 函数和一个初始状态对象作为参数。在我们的案例中,加载的数据、Loading 状态、错误状态都是作为初始状态参数,且不会发生改变,但是他们被聚合到一个状态对象中,由 reducer hook 管理,而不是单个 state hooks。const dataFetchReducer = (state, action) => { …};const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { const fetchData = async () => { dispatch({ type: ‘FETCH_INIT’ }); try { const result = await axios(url); dispatch({ type: ‘FETCH_SUCCESS’, payload: result.data }); } catch (error) { dispatch({ type: ‘FETCH_FAILURE’ }); } }; fetchData(); }, [url]); …};现在,在获取数据时,可以使用 dispatch 函数向 reducer 函数发送信息。使用 dispatch 函数发送的对象具有一个必填的 type 属性和一个可选的 payload 属性。type 属性告诉 reducer 函数需要转换的 state 是哪个,还可以从 payload 中提取新的 state。在这里只有三个状态转换:初始化数据过程,通知数据请求成功的结果,以及通知数据请求失败的结果。在自定义 hook 的末尾,state 像以前一样返回,但是因为我们所有的 state 都在一个对象中,而不再是独立的 state ,所以 state 对象进行解构返回。这样,调用 useDataApi 自定义 hook 的人仍然可以 data 、isLoading和isError:const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); … const doFetch = url => { setUrl(url); }; return { …state, doFetch };};最后我们还缺少 reducer 函数的实现。它需要处理三个不同的状态转换,分被称为 FEATCH_INIT、FEATCH_SUCCESS、FEATCH_FAILURE。每个状态转换都需要返回一个新的状态。让我们看看使用 switch case 如何实现这个逻辑:const dataFetchReducer = (state, action) => { switch (action.type) { case ‘FETCH_INIT’: return { …state }; case ‘FETCH_SUCCESS’: return { …state }; case ‘FETCH_FAILURE’: return { …state }; default: throw new Error(); }};reducer 函数可以通过其参数访问当前状态和 dispatch 传入的 action。到目前为止,在 switch case 语句中,每个状态转换只返回前一个状态,析构语句用于保持 state 对象不可变(即状态永远不会被直接更改)。现在让我们重写一些当前 state 返回的属性,以便在每次转换时更改 一些 state:const dataFetchReducer = (state, action) => { switch (action.type) { case ‘FETCH_INIT’: return { …state, isLoading: true, isError: false }; case ‘FETCH_SUCCESS’: return { …state, isLoading: false, isError: false, data: action.payload, }; case ‘FETCH_FAILURE’: return { …state, isLoading: false, isError: true, }; default: throw new Error(); }};现在,每个状态转换(action.type决定)都返回一个基于先前 state 和可选 payload 的新状态。例如,在请求成功的情况下,payload 用于设置新 state 对象的 data 属性。总之,reducer hook 确保使用自己的逻辑封装状态管理的这一部分。通过提供 action type 和可选 payload ,总是会得到可预测的状态更改。此外,永远不会遇到无效状态。例如,以前可能会意外地将 isLoading 和 isError 设置为true。在这种情况下,UI中应该显示什么? 现在,由 reducer 函数定义的每个 state 转换都指向一个有效的 state 对象。在 Effect Hook 中中断数据请求在React中,即使组件已经卸载,组件 state 仍然会被被赋值,这是一个常见的问题。我在之前的文章中写过这个问题,它描述了如何防止在各种场景中为未挂载组件设置状态。让我们看看在自定义 hook 中,请求数据时如何防止设置状态:const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { let didCancel = false; const fetchData = async () => { dispatch({ type: ‘FETCH_INIT’ }); try { const result = await axios(url); if (!didCancel) { dispatch({ type: ‘FETCH_SUCCESS’, payload: result.data }); } } catch (error) { if (!didCancel) { dispatch({ type: ‘FETCH_FAILURE’ }); } } }; fetchData(); return () => { didCancel = true; }; }, [url]); const doFetch = url => { setUrl(url); }; return { …state, doFetch };};每个Effect Hook都带有一个clean up函数,它在组件卸载时运行。clean up 函数是 hook 返回的一个函数。在该案例中,我们使用 didCancel 变量来让 fetchData 知道组件的状态(挂载/卸载)。如果组件确实被卸载了,则应该将标志设置为 true,从而防止在最终异步解析数据获取之后设置组件状态。注意:实际上并没有中止数据获取(不过可以通过Axios取消来实现),但是不再为卸载的组件执行状态转换。由于 Axios 取消在我看来并不是最好的API,所以这个防止设置状态的布尔标志也可以完成这项工作。原文链接 ...

March 26, 2019 · 9 min · jiezi

Ajax Fetch 和 Axios(持续更新中...)

Ajax Fetch 和 Axios(持续更新中…)知识点梳理AJAX 不是 JavaScript 的规范,它只是一个哥们“发明”的缩写:Asynchronous JavaScript and XML,意思就是用 JavaScript 执行异步网络请求。JavaScript 代码都是单线程执行的,由于这个“缺陷”,导致 JavaScript 的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现,回调函数不好看,不利于代码复用,而链式写法好处 逻辑统一、利于复用,所以出现 Primose Promise 有各种开源实现,在 ES6 中被统一规范,由浏览器直接支持。async await 是 Promise 语法糖,使异步的逻辑书写标准的同步函数Generator原生 XHRAjaxAJAX 不是 JavaScript 的规范,它只是一个哥们“发明”的缩写:Asynchronous JavaScript and XML,意思就是用 JavaScript 执行异步网络请求。属性描述onreadystatechange每当 readyState 属性改变时,就会调用该函数。readyState存有 XMLHttpRequest 的状态。从 0 到 4 发生变化。0: 请求未初始化 1: 服务器连接已建立,open()方法已调用,但是 send()方法未调用 2: 请求已连接 send()方法已调用,HTTP 请求已发送到 Web 服务器。未接收到相应3: 请求处理中 4: 请求已完成,且响应已就绪status200: “OK"404: 未找到页面;(function() { var xmlhttp if (window.XMLHttpRequest) { // IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码 xmlhttp = new XMLHttpRequest() } else { // IE6, IE5 浏览器执行代码 xmlhttp = new ActiveXObject(‘Microsoft.XMLHTTP’) } xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { console.log(xmlhttp.responseText) } } xmlhttp.open(‘GET’, ‘http://www.runoob.com/try/ajax/ajax_info.txt', true) xmlhttp.send()})()Ajax 安全限制浏览器的同源策略导致的。默认情况下,JavaScript 在发送 AJAX 请求时,URL 的域名必须和当前页面完全一致跨域请求通过 Flash 插件发送 HTTP 请求,这种方式可以绕过浏览器的安全限制,但必须安装 Flash,并且跟 Flash 交互。不过 Flash 用起来麻烦,而且现在用得也越来越少了。通过在同源域名下架设一个代理服务器来转发,JavaScript 负责把请求发送到代理服务器JSONP 它有个限制,只能用 GET 请求,并且要求返回 JavaScript。这种方式跨域实际上是利用了浏览器允许跨域引用 JavaScript 资源CORS CORS 全称 Cross-Origin Resource Sharing,是 HTML5 规范定义的如何跨域访问资源。面这种跨域请求,称之为“简单请求”。简单请求包括 GET、HEAD 和 POST(POST 的 Content-Type 类型仅限 application/x-www-form-urlencoded、multipart/form-data 和 text/plain),并且不能出现任何自定义头(例如,X-Custom: 12345),通常能满足 90%的需求Promise在 JavaScript 的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致 JavaScript 的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现function callback() { console.log(‘Done’)}console.log(‘before setTimeout()’)setTimeout(callback, 1000) // 1秒钟后调用callback函数console.log(‘after setTimeout()’)链式写法的好处在于,先统一执行 AJAX 逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用 success 函数或 fail 函数。古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在 JavaScript 中称为 Promise 对象。使用 Promise 封装 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) })}var p = ajax(‘GET’, ‘/api/categories’)p.then(function(text) { // 如果AJAX成功,获得响应内容}).catch(function(status) { // 如果AJAX失败,获得响应代码})Promise 使用方法;(function() { console.time(‘doIt’) const time1 = 300 step1(time1) .then(time2 => step2(time2)) .then(time3 => step3(time3)) .then(result => { console.log(result is ${result}) console.timeEnd(‘doIt’) })})()function takeLongTime(n) { return new Promise(resolve => { setTimeout(() => resolve(n + 200), n) })}function step1(n) { console.log(step1 with ${n}) return takeLongTime(n)}function step2(n) { console.log(step2 with ${n}) return takeLongTime(n)}function step3(n) { console.log(step3 with ${n}) return takeLongTime(n)}Promise.all()两个任务是可以并行执行的,用 Promise.all()实现var p1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, ‘P1’)})var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 6000, ‘P2’)})// 同时执行p1和p2,并在它们都完成后执行then:Promise.all([p1, p2]).then(function(results) { console.log(results) // 获得一个Array: [‘P1’, ‘P2’]})Promise.race()有些时候,多个异步任务是为了容错,只需要获得先返回的结果即可var p1 = new Promise(function(resolve, reject) { setTimeout(resolve, 3000, ‘P1’)})var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 6000, ‘P2’)})// 同时执行p1和p2,其中一个完成后执行then:Promise.race([p1, p2]).then(function(results) { console.log(results) // // ‘P1’})async awaitawaitawait 操作符用于等待一个 Promise 对象。它只能在异步函数 async function 中使用await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的 resolve 函数参数作为 await 表达式的值,继续执行 async function。若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。// 如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果。function resolveAfter2Seconds(x) { return new Promise(resolve => { setTimeout(() => { resolve(x) }, 2000) })}async function f1() { var x = await resolveAfter2Seconds(10) console.log(x) // 10}f1()// 如果 Promise 处理异常,则异常值被抛出。async function f3() { try { var z = await Promise.reject(30) } catch (e) { console.log(e) // 30 }}f3()asyncasync function 声明用于定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。一个返回的 Promise 对象会以 async function 的返回值进行解析(resolved),或者以该函数抛出的异常进行回绝(rejected)。async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待 Promise 的结果出来,然后恢复 async 函数的执行并返回解析值(resolved)。function resolveAfter2Seconds() { return new Promise(resolve => { setTimeout(() => { resolve(‘resolved’) }, 2000) })}async function asyncCall() { console.log(‘calling’) var result = await resolveAfter2Seconds() console.log(result) // expected output: ‘resolved’}asyncCall()GeneratorFetchFetch 是一个现代的概念, 等同于 XMLHttpRequest。它提供了许多与 XMLHttpRequest 相同的功能,但被设计成更具可扩展性和高效性。Fetch 是挂在在 window 下的Fetch 的核心在于对 HTTP 接口的抽象,包括 Request,Response,Headers,Body,以及用于初始化异步请求的 global fetch。Fetch 还利用到了请求的异步特性——它是基于 Promise 的。fetch(‘http://example.com/movies.json') .then(function(response) { return response.json() }) .then(function(myJson) { console.log(myJson) })postData(‘http://example.com/answer', { answer: 42 }) .then(data => console.log(data)) // JSON from response.json() call .catch(error => console.error(error))function postData(url, data) { // Default options are marked with * return fetch(url, { body: JSON.stringify(data), // must match ‘Content-Type’ header cache: ’no-cache’, // *default, no-cache, reload, force-cache, only-if-cached credentials: ‘same-origin’, // include, same-origin, *omit headers: { ‘user-agent’: ‘Mozilla/4.0 MDN Example’, ‘content-type’: ‘application/json’ }, method: ‘POST’, // *GET, POST, PUT, DELETE, etc. mode: ‘cors’, // no-cors, cors, *same-origin redirect: ‘follow’, // manual, *follow, error referrer: ’no-referrer’ // *client, no-referrer }).then(response => response.json()) // parses response to JSON}参考AJAXPromiseasync await 语法描述Fetch 语法描述 ...

February 21, 2019 · 3 min · jiezi

深度介绍:也许你对 Fetch 了解得不是那么多

编者按:除创宇前端与作者博客外,本文还在语雀发布。前言本篇主要讲述 Fetch 的一些基本知识点以及我们在生产开发中怎么去使用。为了能够更好的了解 Fetch,我们希望你对以下知识点有所了解,如果有相关的开发经验,那是最好不过的了。XHRPromiseHTTP本文中对有些关键词提供了相应的链接,如果你对该关键词不够了解或想要了解更多,你可以通过点击它充实自己。文中有些知识点在 MDN Fetch 上已经写的很详细,因此有略过,希望同学们在阅读本文章时能够同时对照阅读。本文行文思路首先从规范入手,目的是让大家了解的更透彻,达到知其然知其所以然。为了更好的掌握 Fetch,文章中还提供了一些示例代码供大家学习使用。在使用该示例代码前,我们希望你对 node.js 有一些了解,如果没有的话,你可以根据示例中的友情提示完成你的这次学习体验。读完本篇文章后你将了解到以下内容:什么是 FetchFetch 的一些基本概念如何使用 FetchFetch 的一些不足以及我们如何“优雅”的使用它希望你通过读完本篇文章后,对 Fetch 有一个基本的了解。Fetch 简介Fetch 是一种新的用于获取资源的技术,它被用来代替我们已经吐槽了很久的技术(XHR)。Fetch 使用起来很简单,它返回的是一个 Promise,即使你没有 XHR 的开发经验也能快速上手。说了那么多,我们还是先睹为快吧,让我们快快下面的示例代码。fetch(‘https://github.com/frontend9/fe9-library', {method: ‘get’}).then(function(response) {}).catch(function(err) {// Error});是不是简单的不能再简单了?好,既然我们 Fetch 有了简单的认识之后,那我们再来了解下 Fetch 的基本概念。Fetch 基本概念在 Fetch 中有四个基本概念,他们分别是 Headers、Request 、Response 和 Body。为了更好的理解 Fetch,我们需要对这些概念做一个简单的了解。在一个完整的 HTTP 请求中,其实就已经包含了这四个概念。请求中有请求头和请求体,响应中有响应头和响应体。所以我们有必要了解这些概念。Headers为了实现头部的灵活性,能够对头部进行修改是一个非常重要的能力。Headers 属于 HTTP 中首部的一份子,它是一个抽象的接口,利用它可以对 HTTP 的请求头和响应头做出添加、修改和删除的操作。下面我们先看一下它具有哪些接口:typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;[Constructor(optional HeadersInit init), Exposed=(Window,Worker)]interface Headers { void append(ByteString name, ByteString value); void delete(ByteString name); ByteString? get(ByteString name); boolean has(ByteString name); void set(ByteString name, ByteString value); iterable<ByteString, ByteString>;};interface Headers { void append(ByteString name, ByteString value); void delete(ByteString name); ByteString? get(ByteString name); boolean has(ByteString name); void set(ByteString name, ByteString value); iterable<ByteString, ByteString>;};// 来自 https://fetch.spec.whatwg.org/#headers-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看看看它有哪些方法供我们使用。这里我们对 Headers 的构造参数做个解释。首先参数类型为 HeadersInit,我们再看下这个类型支持哪些类型的值。我们从规范中可以看到的定义是:typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;这里我们对应到 JavaScript 这门语言,意思就是说这个对象可以是数组或者是键值对(即对象)。关于如何初始化这些参数,我们可以看下规范中定义的流程。To fill a Headers object (headers) with a given object (object), run these steps:If object is a sequence, then for each header in object:If header does not contain exactly two items, then throw a TypeError.Append header’s first item/header’s second item to headers.Otherwise, object is a record, then for each key → value in object, append key/value to headers.这里我需要对这个做个说明,后面对 fetch 的用法会涉及到一点以及我们看 polyfill 都会有所帮助。第一种:即数组,当数据每项如果不包含两项时,直接抛出错误。然后数组第一项是 header 名,第二项是值。,最后直接通过 append 方法添加。第二种:即键值对(这里指对象),我们通过循环直接取到键值对,然后通过 append 方法添加。示例示例代码地址:https://github.com/GoDotDotDo…打开浏览器输入:http://127.0.0.1:4000/headers那么我们该如何使用它呢?首先我们需要通过 new Headers() 来实例化一个 Headers 对象,该对象返回的是一个空的列表。在有了对象实例后,我们就可以通过接口来完成我们想要的操作,我们来一起看看下面的示例: function printHeaders(headers) { let str = ‘’; for (let header of headers.entries()) { str += &lt;li&gt;${header[0]}: ${header[1]}&lt;/li&gt; ; console.log(header[0] + ‘: ’ + header[1]); } return &lt;ul&gt; ${str} &lt;/ul&gt;; } const headers = new Headers(); // 我们打印下看看是否返回的是一个空的列表 const before = printHeaders(headers); // 发现这里没有任何输出 document.getElementById(‘headers-before’).innerHTML = before; // 我们添加一个请求头 headers.append(‘Content-Type’, ’text/plain’); headers.append(‘Content-Type’, ’text/html’); headers.set(‘Content-Type’, [‘a’, ‘b’]); const headers2 = new Headers({ ‘Content-Type’: ’text/plain’, ‘X-Token’: ‘abcdefg’, }); const after = printHeaders(headers); // 输出:content-type: 如果你觉得每次都要 append 麻烦的话,你也可以通过在构造函数中传入指定的头部,例如:const headers2 = new Headers({ ‘Content-Type’: ’text/plain’,‘X-Token’: ‘abcdefg’});printHeaders(headers2);// 输出:// content-type: text/plain// x-token: abcdefg这里我添加了一个自定义头部 X-Token,这在实际开发中很常见也很有实际意义。但是切记在 CORS 中需要满足相关规范,否则会产生跨域错误。你可以通过append 、 delete 、set 、get 和has 方法修改请求头。这里对 set 和 append 方法做个特殊的说明:set: 如果对一个已经存在的头部进行操作的话,会将新值替换掉旧值,旧值将不会存在。如果头部不存在则直接添加这个新的头部。append:如果已经存在该头部,则直接将新值追加到后面,还会保留旧值。为了方便记忆,你只需要记住 set 会覆盖,而 append 会追加。GuardGuard 是 Headers 的一个特性,他是一个守卫者。它影响着一些方法(像 append 、 set 、delete)是否可以改变 header 头。它可以有以下取值:immutable、request、request-no-cors、response 或 none。这里你无需关心它,只是为你让你了解有这样个东西在影响着我们设置一些 Headers。你也无法去操作它,这是代理的事情。举个简单的例子,我们无法在 Response Headers 中插入一个 Set-Cookie。如果你想要了解更过的细节,具体的规范请参考 concept-headers-guard 和 MDN Guard注意我们在给头部赋值的时候需要满足可接受的首部字段集合否则将会报 TypeError 。BodyBody 准确来说这里只是 mixin,代表着请求体或响应体,具体由 Response 和 Request 来实现。下面我们来看看它具有哪些接口:interface mixin Body { readonly attribute ReadableStream? body; readonly attribute boolean bodyUsed; [NewObject] Promise<ArrayBuffer> arrayBuffer(); [NewObject] Promise<Blob> blob(); [NewObject] Promise<FormData> formData(); [NewObject] Promise<any> json(); [NewObject] Promise<USVString> text();};// 来自 https://fetch.spec.whatwg.org/#body规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用。这里需要注意看这些方法返回的都是 Promise,记住这在基于 fetch 进行接口请求中很重要。记住了这个,有利于我们在后面的文章中理解 fetch 的用法。示例范例将在 Response 中体现。RequestRequest 表示一个请求类,需要通过实例化来生成一个请求对象。通过该对象可以描述一个 HTTP 请求中的请求(一般含有请求头和请求体)。既然是用来描述请求对象,那么该请求对象应该具有修改请求头(Headers)和请求体(Body)的方式。下面我们先来看下规范中 Request 具有哪些接口:typedef (Request or USVString) RequestInfo;[Constructor(RequestInfo input, optional RequestInit init), Exposed=(Window,Worker)]interface Request { readonly attribute ByteString method; readonly attribute USVString url; [SameObject] readonly attribute Headers headers; readonly attribute RequestDestination destination; readonly attribute USVString referrer; readonly attribute ReferrerPolicy referrerPolicy; readonly attribute RequestMode mode; readonly attribute RequestCredentials credentials; readonly attribute RequestCache cache; readonly attribute RequestRedirect redirect; readonly attribute DOMString integrity; readonly attribute boolean keepalive; readonly attribute boolean isReloadNavigation; readonly attribute boolean isHistoryNavigation; readonly attribute AbortSignal signal; [NewObject] Request clone();};Request includes Body;dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null};enum RequestDestination { “”, “audio”, “audioworklet”, “document”, “embed”, “font”, “image”, “manifest”, “object”, “paintworklet”, “report”, “script”, “sharedworker”, “style”, “track”, “video”, “worker”, “xslt” };enum RequestMode { “navigate”, “same-origin”, “no-cors”, “cors” };enum RequestCredentials { “omit”, “same-origin”, “include” };enum RequestCache { “default”, “no-store”, “reload”, “no-cache”, “force-cache”, “only-if-cached” };enum RequestRedirect { “follow”, “error”, “manual” };// 来自 https://fetch.spec.whatwg.org/#request-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用,这里不做一一解释。注意这里的属性都是只读的,规范中我们可以看到构造函数的第一个参数为 Request 对象或字符串,我们一般采取字符串,即需要访问的资源地址( HTTP 接口地址)。第二个参数接收一个 RequestInit 可选对象,而这个对象是一个字典。在 javascript 中,我们可以理解为一个对象({})。RequestInit 里面我们可以配置初始属性,告诉 Request 我们这个请求的一些配置信息。这里我们需要对以下几个属性特别注意下。mode 是一个 RequestMode 枚举类型,可取的值有 navigate, same-origin, no-cors, cors。它表示的是一个请求时否使用 CORS,还是使用严格同源模式。当处于跨域情况下,你应当设置为 cors。该值的默认值在使用 Request 初始化时,默认为 cors。当使用标记启动的嵌入式资源,例如 <link>、 <script>标签(未手动修改 crossorigin 属性),默认为 no-cors。详细信息请参考 whatwg 规范或 MDN 。credentials 是一个 RequestCredentials 枚举类型,可取的值有 omit, same-origin, include。它表示的是请求是否在跨域情况下发送 cookie。看到这,如果对 XHR 了解的同学应该很熟悉。这和 XHR 中的 withCredentials 很相似。但是 credentials 有三个可选值,它的默认值为 same-origin。当你需要跨域传递 cookie 凭证信息时,请设置它为 include。注意这里有一个细节,当设置为 include 时,请确保 Response Header 中 Access-Control-Allow-Origin 不能为 ,需要指定源(例如:http://127.0.0.1:4001),否则会你将会在控制台看到如下错误信息。详细信息请参考 whatwg 规范或 MDN 。The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘’ when the request’s credentials mode is ‘include’.你可以使用文章中提供的代码中启动 cors 示例代码,然后在浏览器中输入 http://127.0.0.1:4001/request,如果不出意外的话,你可以在控制台中看到上面的错误提示。body 是一个 BodyInit 类型。它可取的值有 Blob,BufferSource , FormData , URLSearchParams , ReadableStream , USVString。细心的同学不知道有没有发现,我们常见的 json 对象却不在其中。因此,我们如果需要传递 json 的话,需要调用 JSON.stringify 函数来帮助我们转换成字符串。下面将给出一段示例代码。示例示例代码地址:https://github.com/GoDotDotDo…打开浏览器输入:http://127.0.0.1:4000/request // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9’, }); const request = new Request(’/api/request’, { method: ‘GET’, headers, }); console.log(request); // Request {method: “GET”, url: “http://127.0.0.1:4000/api/request”, headers: Headers, destination: “”, referrer: “about:client”, …} console.log(request.method); // GET console.log(request.mode); // cors console.log(request.credentials); // same-origin // 如果你想打印headers信息,可以调用 printHeaders(request.headers)这里我们先以 GET 简单请求作为示例,我们传递了一个自定义的 Headers,指定了请求方法 method 为 GET(默认为 GET)。在上面的接口规范中,我们可以通过 Request 对象拿到一些常用的属性,比如 method、url、headers 、body 等等只读属性。ResponseResponse 和 Request 类似,表示的是一次请求返回的响应数据。下面我们先看下规范中定义了哪些接口。[Constructor(optional BodyInit? body = null, optional ResponseInit init), Exposed=(Window,Worker)]interface Response { [NewObject] static Response error(); [NewObject] static Response redirect(USVString url, optional unsigned short status = 302); readonly attribute ResponseType type; readonly attribute USVString url; readonly attribute boolean redirected; readonly attribute unsigned short status; readonly attribute boolean ok; readonly attribute ByteString statusText; [SameObject] readonly attribute Headers headers; readonly attribute Promise<Headers> trailer; [NewObject] Response clone();};Response includes Body;dictionary ResponseInit { unsigned short status = 200; ByteString statusText = “”; HeadersInit headers;};enum ResponseType { “basic”, “cors”, “default”, “error”, “opaque”, “opaqueredirect” };// 来自 https://fetch.spec.whatwg.org/#response-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用,这里不做一一解释。其中 status, headers 属性最为常用。通过 status 状态码我们可以判断出服务端请求处理的结果,像 200, 403 等等常见状态码。这里举个例子,当 status 为 401 时,可以在前端进行拦截跳转到登录页面,这在现如今 SPA(单页面应用程序)中尤为常见。我们也可以利用 headers 来获取一些服务端返回给前端的信息,比如 token。仔细看上面的接口的同学可以发现 Response includes Body; 这样的标识。在前面我们说过 Body 由 Request 和 Response 实现。所以 Body 具有的方法,在 Response 实例中都可以使用,而这也是非常重要的一部分,我们通过 Body 提供的方法(这里准确来说是由 Response 实现的)对服务端返回的数据进行处理。下面我们将通过一个示例来了解下简单用法:示例示例代码位置:https://github.com/GoDotDotDo… // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9-token-from-frontend’, }); const request = new Request(’/api/response’, { method: ‘GET’, headers, }); // 这里我们先发起一个请求试一试 fetch(request) .then(response => { const { status, headers } = response; document.getElementById(‘status’).innerHTML = ${status}; document.getElementById(‘headers’).innerHTML = headersToString(headers); return response.json(); }) .then(resData => { const { status, data } = resData; if (!status) { window.alert(‘发生了一个错误!’); return; } document.getElementById(‘fetch’).innerHTML = data; });这里我们先忽略 fetch 用法,后面的章节中会进行详细介绍。我们先关注第一个 then 方法回调里面的东西。可以看到返回了一个 response 对象,这个对象就是我们的 Response 的实例。示例中拿了 status 和 headers ,为了方便,这里我将其放到 html 中。再看看该回调中最后一行,我们调用了一个 response.json() 方法(这里后端返的数据是一个 JSON 对象,为了方便直接调用 json()),该方法返回一个 Promise,我们将处理结果返给最后一个 then 回调,这样就可以获得最终处理过后的数据。打开浏览器,输入 http://127.0.0.1:4000/response,如果你的示例代运行正常,你将会看到以下页面:(查看 Response 返回的数据)Fetch 与 XHR 比较Fetch 相对 XHR 来说具有简洁、易用、声明式、天生基于 Promise 等特点。XHR 使用方式复杂,接口繁多,最重要的一点个人觉得是它的回调设计,对于实现 try…catch 比较繁琐。但是 Fetch 也有它的不足,相对于 XHR 来说,目前它具有以下劣势:不能取消(虽然 AbortController 能实现,但是目前兼容性基本不能使用,可以使用 polyfill )不能获取进度不能设置超时(可以通过简单的封装来模拟实现)兼容性目前比较差(可以使用 polyfill 间接使用 XHR 来优雅降级,这里推荐使用 isomorphic-fetch )在了解 Fetch 和 XHR 的一些不同后,还是需要根据自身的业务需求来选择合适的技术,因为技术没有永远的好坏,只有合不合适。下面章节我们将介绍如何“优雅”的使用 Fetch 以及如何尽量避免掉劣势。如何使用Fetch前面了解了这么多基础知识,现在终于到了介绍如何使用 Fetch 了。老规矩,我们先来看下规范定义的接口。partial interface mixin WindowOrWorkerGlobalScope { [NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init);};规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它的用法。从规范中我们可以看到 fetch 属于 WindowOrWorkerGlobalScope 的一部分,暴露在 Window 或 WorkerGlobalScope 对象上。所以在浏览器中,你可以直接调用 fetch。规范中定义了 fetch 返回一个 Promise,它最多可接收两个参数( input 和 init )。为了能够对它的使用方法有个更全面的了解,下面来讲一下这两个参数。input 参数类型为 RequestInfo,我们可以回到前面的 Request 部分,来回顾一下它的定义。typedef (Request or USVString) RequestInfo;发现它是一个 Request 对象或者是一个字符串,因此你可以传 Request 实例或者资源地址字符串,这里一般我们推荐使用字符串。init 参数类型为 RequestInit,我们回顾前面 Requst 部分,它是一个字典类型。在 JavaScript 中你需要传递一个 Object 对象。dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null };在本小节之前我们都没有介绍 fetch 的使用方式,但是在其他章节中或多或少出现过它的容貌。现在,我们终于可以在这里正式介绍它的使用方式了。fetch 它返回一个 Promise,意味着我们可以通过 then 来获取它的返回值,这样我们可以链式调用。如果配合 async/await 使用,我们的代码可读性会更高。下面我们先通过一个简单的示例来熟悉下它的使用。示例示例代码位置:https://github.com/GoDotDotDo… // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9’, }); setTimeout(() => { fetch(’/data?name=fe’, { method: ‘GET’, // 默认为 GET,不写也可以 headers, }) .then(response => response.json()) .then(resData => { const { status, data } = resData; if (!status) { window.alert(‘发生了一个错误!’); return; } document.getElementById(‘fetch’).innerHTML = data; }); }, 1000);上面的示例中,我们自定义了一个 headers 。为了演示方便,这里我们设定了一个定时器。在请求成功时,服务器端会返回相应的数据,我们通过 Response 实例的 json 方法来解析数据。细心的同学会发现,这里 fetch 的第一个参数我们采用的是字符串,在第二个参数我们提供了一些 RequestInit 配置信息,这里我们指定了请求方法(method)和自定义请求头(headers)。当然你也可以传递一个 Request 实例对象,下面我们也给出一个示例。代码位置:https://github.com/GoDotDotDo… const headers = new Headers({ ‘X-Token’: ‘fe9’, }); const request = new Request(’/api/request’, { method: ‘GET’, headers, }); setTimeout(() => { fetch(request) .then(res => res.json()) .then(res => { const { status, data } = res; if (!status) { alert(‘服务器处理失败’); return; } document.getElementById(‘fetch-req’).innerHTML = data; }); }, 1200);在浏览器中打开:http://127.0.0.1:4000/, 如果上面的示例运行成功,你将会看到如下界面:好,在运行完示例后,相信你应该对如何使用 fetch 有个基本的掌握。在上一章节,我们讲过 fetch 有一定的缺点,下面我们针对部分缺点来尝试着处理下。解决超时当网络出现异常,请求可能已经超时,为了使我们的程序更健壮,提供一个较好的用户 体验,我们需要提供一个超时机制。然而,fetch 并不支持,这在上一小节中我们也聊到过。庆幸的是,我们有 Promise ,这使得我们有机可趁。我们可以通过自定义封装来达到支持超时机制。下面我们尝试封装下。const defaultOptions = { headers: { ‘Content-Type’: ‘application/json’, },};function request(url, options = {}) { return new Promise((resolve, reject) => { const headers = { …defaultOptions.headers, …options.headers }; let abortId; let timeout = false; if (options.timeout) { abortId = setTimeout(() => { timeout = true; reject(new Error(’timeout!’)); }, options.timeout || 6000); } fetch(url, { …defaultOptions, …options, headers }) .then((res) => { if (timeout) throw new Error(’timeout!’); return res; }) .then(checkStatus) .then(parseJSON) .then((res) => { clearTimeout(abortId); resolve(res); }) .catch((e) => { clearTimeout(abortId); reject(e); }); });}上面的代码中,我们需要注意下。就是我们手动根据超时时间来 reject 并不会阻止后续的请求,由于我们并没有关闭掉此次连接,属于是伪取消。fetch 中如果后续接受到服务器的响应,依然会继续处理后续的处理。所以这里我们在 fetch 的第一个 then 中进行了超时判断。取消 const controller = new AbortController(); const signal = controller.signal; fetch(’/data?name=fe’, { method: ‘GET’, signal, }) .then(response => response.json()) .then(resData => { const { status, data } = resData; if (!status) { window.alert(‘发生了一个错误!’); return; } document.getElementById(‘fetch-str’).innerHTML = data; }); controller.abort();我们回过头看下 fetch 的接口,发现有一个属性 signal, 类型为AbortSignal,表示一个信号对象( signal object ),它允许你通过 AbortController 对象与DOM请求进行通信并在需要时将其中止。你可以通过调用 AbortController.abort 方法完成取消操作。当我们需要取消时,fetch 会 reject 一个错误( AbortError DOMException ),中断你的后续处理逻辑。具体可以看规范中的解释。由于目前 AbortController 兼容性极差,基本不能使用,但是社区有人帮我们提供了 polyfill(这里我不提供链接,因为目前来说还不适合生产使用,会出现下面所述的问题),我们可以通过使用它来帮助我们提前感受新技术带来的快乐。但是你可能会在原生支持 Fetch 但是又不支持 AbortController 的情况下,部分浏览器可能会报如下错误:Chrome: “Failed to execute ‘fetch’ on ‘Window’: member signal is not of type AbortSignal.“Firefox: “‘signal’ member of RequestInit does not implement interface AbortSignal.“如果出现以上问题,我们也无能为力,可能原因是浏览器内部做了严格验证,对比发现我们提供的 signal 类型不对。但是我们可以通过手动 reject 的方式达到取消,但是这种属于伪取消,实际上连接并没有关闭。我们可以通过自定义配置,例如在 options 中增加配置,暴露出 reject,这样我们就可以在外面来取消掉。这里本人暂时不提供代码。有兴趣的同学可以尝试一下,也可以在下面的评论区评论。前面提到过的获取进度目前我们还无法实现。拦截器示例代码位置:https://github.com/GoDotDotDo…下面我们讲一讲如何做一个简单的拦截器,这里的拦截器指对响应做拦截。假设我们需要对接口返回的状态码进行解析,例如 403 或者 401 需要跳转到登录页面,200 正常放行,其他报错。由于 fetch 返回一个 Promise ,这就使得我们可以在后续的 then 中做些简单的拦截。我们看一下示例代码:function parseJSON(response) { const { status } = response; if (status === 204 || status === 205) { return null; } return response.json();}function checkStatus(response) { const { status } = response; if (status >= 200 && status < 300) { return response; } // 权限不允许则跳转到登陆页面 if (status === 403 || status === 401) { window ? (window.location = ‘/login.html’) : null; } const error = new Error(response.statusText); error.response = response; throw error;}/** * @description 默认配置 * 设置请求头为json /const defaultOptions = { headers: { ‘Content-Type’: ‘application/json’, }, // credentials: ‘include’, // 跨域传递cookie};/* * Requests a URL, returning a promise * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to “fetch” * * @return {object} The response data */function request(url, options = {}) { return new Promise((resolve, reject) => { const headers = { …defaultOptions.headers, …options.headers }; let abortId; let timeout = false; if (options.timeout) { abortId = setTimeout(() => { timeout = true; reject(new Error(’timeout!’)); }, options.timeout || 6000); } fetch(url, { …defaultOptions, …options, headers }) .then((res) => { if (timeout) throw new Error(’timeout!’); return res; }) .then(checkStatus) .then(parseJSON) .then((res) => { clearTimeout(abortId); resolve(res); }) .catch((e) => { clearTimeout(abortId); reject(e); }); });}从上面的 checkStatus 代码中我们可以看到,我们首先检查了状态码。当状态码为 403 或 401 时,我们将页面跳转到了 login 登录页面。细心的同学还会发现,我们多了一个处理方法就是 parseJSON,这里由于我们的后端统一返回 json 数据,为了方便,我们就直接统一处理了 json 数据。总结本系列文章整体阐述了 fetch 的基本概念、和 XHR 的差异、如何使用 fetch 以及我们常见的解决方案。希望同学们在读完整篇文章能够对 fetch 的认识有所加深。建议:在整体了解了 fetch 之后,希望同学们能够读一下 github polyfill 源码。在读代码的同时,可以同时参考 Fetch 规范。参考:MDN FetchFetch 规范示例代码文 / GoDotDotDotLess is more.编 / 荧声作者其他文章:优秀前端必知的话题:我们应该做些力所能及的优化本文由创宇前端作者授权发布,版权属于作者,创宇前端出品。欢迎注明出处转载本文。本文链接:https://blog.godotdotdot.com/…想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,我们会尽可能回复。欢迎点赞、收藏、留言评论、转发分享和打赏支持我们。打赏将被完全转交给文章作者。感谢您的阅读。 ...

January 3, 2019 · 8 min · jiezi

也许你对 Fetch 了解得不是那么多(上)

编者按:除创宇前端与作者博客外,本文还在语雀发布。前言本篇主要讲述 Fetch 的一些基本知识点以及我们在生产开发中怎么去使用。为了能够更好的了解 Fetch,我们希望你对以下知识点有所了解,如果有相关的开发经验,那是最好不过的了。XHRPromiseHTTP本文中对有些关键词提供了相应的链接,如果你对该关键词不够了解或想要了解更多,你可以通过点击它充实自己。文中有些知识点在 MDN Fetch 上已经写的很详细,因此有略过,希望同学们在阅读本文章时能够同时对照阅读。本文行文思路首先从规范入手,目的是让大家了解的更透彻,达到知其然知其所以然。为了更好的掌握 Fetch,文章中还提供了一些示例代码供大家学习使用。在使用该示例代码前,我们希望你对 node.js 有一些了解,如果没有的话,你可以根据示例中的友情提示完成你的这次学习体验。读完本篇文章后你将了解到以下内容:什么是 FetchFetch 的一些基本概念如何使用 FetchFetch 的一些不足以及我们如何“优雅”的使用它希望你通过读完本篇文章后,对 Fetch 有一个基本的了解。Fetch 简介Fetch 是一种新的用于获取资源的技术,它被用来代替我们已经吐槽了很久的技术(XHR)。Fetch 使用起来很简单,它返回的是一个 Promise,即使你没有 XHR 的开发经验也能快速上手。说了那么多,我们还是先睹为快吧,让我们快快下面的示例代码。fetch(‘https://github.com/frontend9/fe9-library', {method: ‘get’}).then(function(response) {}).catch(function(err) {// Error});是不是简单的不能再简单了?好,既然我们 Fetch 有了简单的认识之后,那我们再来了解下 Fetch 的基本概念。Fetch 基本概念在 Fetch 中有四个基本概念,他们分别是 Headers、Request 、Response 和 Body。为了更好的理解 Fetch,我们需要对这些概念做一个简单的了解。在一个完整的 HTTP 请求中,其实就已经包含了这四个概念。请求中有请求头和请求体,响应中有响应头和响应体。所以我们有必要了解这些概念。Headers为了实现头部的灵活性,能够对头部进行修改是一个非常重要的能力。Headers 属于 HTTP 中首部的一份子,它是一个抽象的接口,利用它可以对 HTTP 的请求头和响应头做出添加、修改和删除的操作。下面我们先看一下它具有哪些接口:typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;[Constructor(optional HeadersInit init), Exposed=(Window,Worker)]interface Headers { void append(ByteString name, ByteString value); void delete(ByteString name); ByteString? get(ByteString name); boolean has(ByteString name); void set(ByteString name, ByteString value); iterable<ByteString, ByteString>;};interface Headers { void append(ByteString name, ByteString value); void delete(ByteString name); ByteString? get(ByteString name); boolean has(ByteString name); void set(ByteString name, ByteString value); iterable<ByteString, ByteString>;};// 来自 https://fetch.spec.whatwg.org/#headers-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看看看它有哪些方法供我们使用。这里我们对 Headers 的构造参数做个解释。首先参数类型为 HeadersInit,我们再看下这个类型支持哪些类型的值。我们从规范中可以看到的定义是:typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;这里我们对应到 JavaScript 这门语言,意思就是说这个对象可以是数组或者是键值对(即对象)。关于如何初始化这些参数,我们可以看下规范中定义的流程。To fill a Headers object (headers) with a given object (object), run these steps:If object is a sequence, then for each header in object:If header does not contain exactly two items, then throw a TypeError.Append header’s first item/header’s second item to headers.Otherwise, object is a record, then for each key → value in object, append key/value to headers.这里我需要对这个做个说明,后面对 fetch 的用法会涉及到一点以及我们看 polyfill 都会有所帮助。第一种:即数组,当数据每项如果不包含两项时,直接抛出错误。然后数组第一项是 header 名,第二项是值。,最后直接通过 append 方法添加。第二种:即键值对(这里指对象),我们通过循环直接取到键值对,然后通过 append 方法添加。示例示例代码地址:https://github.com/GoDotDotDo…打开浏览器输入:http://127.0.0.1:4000/headers那么我们该如何使用它呢?首先我们需要通过 new Headers() 来实例化一个 Headers 对象,该对象返回的是一个空的列表。在有了对象实例后,我们就可以通过接口来完成我们想要的操作,我们来一起看看下面的示例: function printHeaders(headers) { let str = ‘’; for (let header of headers.entries()) { str += &lt;li&gt;${header[0]}: ${header[1]}&lt;/li&gt; ; console.log(header[0] + ‘: ’ + header[1]); } return &lt;ul&gt; ${str} &lt;/ul&gt;; } const headers = new Headers(); // 我们打印下看看是否返回的是一个空的列表 const before = printHeaders(headers); // 发现这里没有任何输出 document.getElementById(‘headers-before’).innerHTML = before; // 我们添加一个请求头 headers.append(‘Content-Type’, ’text/plain’); headers.append(‘Content-Type’, ’text/html’); headers.set(‘Content-Type’, [‘a’, ‘b’]); const headers2 = new Headers({ ‘Content-Type’: ’text/plain’, ‘X-Token’: ‘abcdefg’, }); const after = printHeaders(headers); // 输出:content-type: 如果你觉得每次都要 append 麻烦的话,你也可以通过在构造函数中传入指定的头部,例如:const headers2 = new Headers({ ‘Content-Type’: ’text/plain’,‘X-Token’: ‘abcdefg’});printHeaders(headers2);// 输出:// content-type: text/plain// x-token: abcdefg这里我添加了一个自定义头部 X-Token,这在实际开发中很常见也很有实际意义。但是切记在 CORS 中需要满足相关规范,否则会产生跨域错误。你可以通过append 、 delete 、set 、get 和has 方法修改请求头。这里对 set 和 append 方法做个特殊的说明:set: 如果对一个已经存在的头部进行操作的话,会将新值替换掉旧值,旧值将不会存在。如果头部不存在则直接添加这个新的头部。append:如果已经存在该头部,则直接将新值追加到后面,还会保留旧值。为了方便记忆,你只需要记住 set 会覆盖,而 append 会追加。GuardGuard 是 Headers 的一个特性,他是一个守卫者。它影响着一些方法(像 append 、 set 、delete)是否可以改变 header 头。它可以有以下取值:immutable、request、request-no-cors、response 或 none。这里你无需关心它,只是为你让你了解有这样个东西在影响着我们设置一些 Headers。你也无法去操作它,这是代理的事情。举个简单的例子,我们无法在 Response Headers 中插入一个 Set-Cookie。如果你想要了解更过的细节,具体的规范请参考 concept-headers-guard 和 MDN Guard注意我们在给头部赋值的时候需要满足可接受的首部字段集合否则将会报 TypeError 。BodyBody 准确来说这里只是 mixin,代表着请求体或响应体,具体由 Response 和 Request 来实现。下面我们来看看它具有哪些接口:interface mixin Body { readonly attribute ReadableStream? body; readonly attribute boolean bodyUsed; [NewObject] Promise<ArrayBuffer> arrayBuffer(); [NewObject] Promise<Blob> blob(); [NewObject] Promise<FormData> formData(); [NewObject] Promise<any> json(); [NewObject] Promise<USVString> text();};// 来自 https://fetch.spec.whatwg.org/#body规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用。这里需要注意看这些方法返回的都是 Promise,记住这在基于 fetch 进行接口请求中很重要。记住了这个,有利于我们在后面的文章中理解 fetch 的用法。示例范例将在 Response 中体现。RequestRequest 表示一个请求类,需要通过实例化来生成一个请求对象。通过该对象可以描述一个 HTTP 请求中的请求(一般含有请求头和请求体)。既然是用来描述请求对象,那么该请求对象应该具有修改请求头(Headers)和请求体(Body)的方式。下面我们先来看下规范中 Request 具有哪些接口:typedef (Request or USVString) RequestInfo;[Constructor(RequestInfo input, optional RequestInit init), Exposed=(Window,Worker)]interface Request { readonly attribute ByteString method; readonly attribute USVString url; [SameObject] readonly attribute Headers headers; readonly attribute RequestDestination destination; readonly attribute USVString referrer; readonly attribute ReferrerPolicy referrerPolicy; readonly attribute RequestMode mode; readonly attribute RequestCredentials credentials; readonly attribute RequestCache cache; readonly attribute RequestRedirect redirect; readonly attribute DOMString integrity; readonly attribute boolean keepalive; readonly attribute boolean isReloadNavigation; readonly attribute boolean isHistoryNavigation; readonly attribute AbortSignal signal; [NewObject] Request clone();};Request includes Body;dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null};enum RequestDestination { “”, “audio”, “audioworklet”, “document”, “embed”, “font”, “image”, “manifest”, “object”, “paintworklet”, “report”, “script”, “sharedworker”, “style”, “track”, “video”, “worker”, “xslt” };enum RequestMode { “navigate”, “same-origin”, “no-cors”, “cors” };enum RequestCredentials { “omit”, “same-origin”, “include” };enum RequestCache { “default”, “no-store”, “reload”, “no-cache”, “force-cache”, “only-if-cached” };enum RequestRedirect { “follow”, “error”, “manual” };// 来自 https://fetch.spec.whatwg.org/#request-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用,这里不做一一解释。注意这里的属性都是只读的,规范中我们可以看到构造函数的第一个参数为 Request 对象或字符串,我们一般采取字符串,即需要访问的资源地址( HTTP 接口地址)。第二个参数接收一个 RequestInit 可选对象,而这个对象是一个字典。在 javascript 中,我们可以理解为一个对象({})。RequestInit 里面我们可以配置初始属性,告诉 Request 我们这个请求的一些配置信息。这里我们需要对以下几个属性特别注意下。mode 是一个 RequestMode 枚举类型,可取的值有 navigate, same-origin, no-cors, cors。它表示的是一个请求时否使用 CORS,还是使用严格同源模式。当处于跨域情况下,你应当设置为 cors。该值的默认值在使用 Request 初始化时,默认为 cors。当使用标记启动的嵌入式资源,例如 <link>、 <script>标签(未手动修改 crossorigin 属性),默认为 no-cors。详细信息请参考 whatwg 规范或 MDN 。credentials 是一个 RequestCredentials 枚举类型,可取的值有 omit, same-origin, include。它表示的是请求是否在跨域情况下发送 cookie。看到这,如果对 XHR 了解的同学应该很熟悉。这和 XHR 中的 withCredentials 很相似。但是 credentials 有三个可选值,它的默认值为 same-origin。当你需要跨域传递 cookie 凭证信息时,请设置它为 include。注意这里有一个细节,当设置为 include 时,请确保 Response Header 中 Access-Control-Allow-Origin 不能为 ,需要指定源(例如:http://127.0.0.1:4001),否则会你将会在控制台看到如下错误信息。详细信息请参考 whatwg 规范或 MDN 。The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘’ when the request’s credentials mode is ‘include’.你可以使用文章中提供的代码中启动 cors 示例代码,然后在浏览器中输入 http://127.0.0.1:4001/request,如果不出意外的话,你可以在控制台中看到上面的错误提示。body 是一个 BodyInit 类型。它可取的值有 Blob,BufferSource , FormData , URLSearchParams , ReadableStream , USVString。细心的同学不知道有没有发现,我们常见的 json 对象却不在其中。因此,我们如果需要传递 json 的话,需要调用 JSON.stringify 函数来帮助我们转换成字符串。下面将给出一段示例代码。示例示例代码地址:https://github.com/GoDotDotDo…打开浏览器输入:http://127.0.0.1:4000/request // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9’, }); const request = new Request(’/api/request’, { method: ‘GET’, headers, }); console.log(request); // Request {method: “GET”, url: “http://127.0.0.1:4000/api/request”, headers: Headers, destination: “”, referrer: “about:client”, …} console.log(request.method); // GET console.log(request.mode); // cors console.log(request.credentials); // same-origin // 如果你想打印headers信息,可以调用 printHeaders(request.headers)这里我们先以 GET 简单请求作为示例,我们传递了一个自定义的 Headers,指定了请求方法 method 为 GET(默认为 GET)。在上面的接口规范中,我们可以通过 Request 对象拿到一些常用的属性,比如 method、url、headers 、body 等等只读属性。ResponseResponse 和 Request 类似,表示的是一次请求返回的响应数据。下面我们先看下规范中定义了哪些接口。[Constructor(optional BodyInit? body = null, optional ResponseInit init), Exposed=(Window,Worker)]interface Response { [NewObject] static Response error(); [NewObject] static Response redirect(USVString url, optional unsigned short status = 302); readonly attribute ResponseType type; readonly attribute USVString url; readonly attribute boolean redirected; readonly attribute unsigned short status; readonly attribute boolean ok; readonly attribute ByteString statusText; [SameObject] readonly attribute Headers headers; readonly attribute Promise<Headers> trailer; [NewObject] Response clone();};Response includes Body;dictionary ResponseInit { unsigned short status = 200; ByteString statusText = “”; HeadersInit headers;};enum ResponseType { “basic”, “cors”, “default”, “error”, “opaque”, “opaqueredirect” };// 来自 https://fetch.spec.whatwg.org/#response-class规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它有哪些属性和方法供我们使用,这里不做一一解释。其中 status, headers 属性最为常用。通过 status 状态码我们可以判断出服务端请求处理的结果,像 200, 403 等等常见状态码。这里举个例子,当 status 为 401 时,可以在前端进行拦截跳转到登录页面,这在现如今 SPA(单页面应用程序)中尤为常见。我们也可以利用 headers 来获取一些服务端返回给前端的信息,比如 token。仔细看上面的接口的同学可以发现 Response includes Body; 这样的标识。在前面我们说过 Body 由 Request 和 Response 实现。所以 Body 具有的方法,在 Response 实例中都可以使用,而这也是非常重要的一部分,我们通过 Body 提供的方法(这里准确来说是由 Response 实现的)对服务端返回的数据进行处理。下面我们将通过一个示例来了解下简单用法:示例示例代码位置:https://github.com/GoDotDotDo… // 客户端 const headers = new Headers({ ‘X-Token’: ‘fe9-token-from-frontend’, }); const request = new Request(’/api/response’, { method: ‘GET’, headers, }); // 这里我们先发起一个请求试一试 fetch(request) .then(response => { const { status, headers } = response; document.getElementById(‘status’).innerHTML = ${status}; document.getElementById(‘headers’).innerHTML = headersToString(headers); return response.json(); }) .then(resData => { const { status, data } = resData; if (!status) { window.alert(‘发生了一个错误!’); return; } document.getElementById(‘fetch’).innerHTML = data; });这里我们先忽略 fetch 用法,后面的章节中会进行详细介绍。我们先关注第一个 then 方法回调里面的东西。可以看到返回了一个 response 对象,这个对象就是我们的 Response 的实例。示例中拿了 status 和 headers ,为了方便,这里我将其放到 html 中。再看看该回调中最后一行,我们调用了一个 response.json() 方法(这里后端返的数据是一个 JSON 对象,为了方便直接调用 json()),该方法返回一个 Promise,我们将处理结果返给最后一个 then 回调,这样就可以获得最终处理过后的数据。打开浏览器,输入 http://127.0.0.1:4000/response,如果你的示例代运行正常,你将会看到以下页面:(查看 Response 返回的数据)编者注:本文未完待续。文 / GoDotDotDotLess is more.编 / 荧声作者其他文章:优秀前端必知的话题:我们应该做些力所能及的优化本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:https://blog.godotdotdot.com/…想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,我们会尽可能回复。欢迎点赞、收藏、留言评论、转发分享和打赏支持我们。打赏将被完全转交给文章作者。感谢您的阅读。新年快乐 :) ...

December 29, 2018 · 5 min · jiezi

React Native Fetch封装那点事...

每一门语言都离不开网络请求,有自己的一套Networking Api。React Native使用的是Fetch。 今天我们来谈谈与Fetch相关的一些事情。purpose通过这篇文章,你将了解到以下几点关于Fetch的独家报道Fetch的简单运用Fetch的主要ApiFetch使用注意事项Fetch的Promise封装fetchfetch的使用非常简单,只需传入请求的urlfetch(‘https://facebook.github.io/react-native/movies.json');当然是否请求成功与数据的处理,我们还需处理成功与失败的回调function getMoviesFromApiAsync() { return fetch(‘https://facebook.github.io/react-native/movies.json') .then((response) => response.json()) .then((responseJson) => { return responseJson.movies; }) .catch((error) => { console.error(error); });}通过response.json()将请求的返回数据转化成json数据以便使用。通过.then来对数据进行转化处理或最终暴露给调用者;.catch对异常的处理。以上就是一个简单的网络请求,该请求默认是get方式。那么post又该如何请求呢?Api & Note在fetch中我们直接传入url进行请求,其实内部本质是使用了Request对象,只是将url出入到了Request对象中。const myRequest = new Request(‘https://facebook.github.io/react-native/movies.json'); const myURL = myRequest.url; // https://facebook.github.io/react-native/movies.jsonflowers.jpgconst myMethod = myRequest.method; // GET fetch(myRequest) .then(response => response.json()) .then(responseJson => { //todo });如果我们需要请求post,需要改变Request的method属性。fetch(‘https://mywebsite.com/endpoint/', { method: ‘POST’, headers: { Accept: ‘application/json’, ‘Content-Type’: ‘application/json’, }, body: JSON.stringify({ firstParam: ‘yourValue’, secondParam: ‘yourOtherValue’, }),});非常简单,在url后直接传入{}对象,其中指定method使用post。相信大家应该都知道get与post的一个主要区别是get可以在url上直接添加参数,而post为了安全都不采用直接将参数追加到url上,而是使用body来传给service端。在使用body前,这里还需知道headers。下面某个post请求的headers信息需要注意的是Content-Type字段,它代表的是service端接收的数据类型,图片中使用的是application/x-www-form-urlencoded。这对于我们的body来说是非常重要的。只有匹配Content-Type的类型才能正确的传递参数信息。示例的代码使用的是application/json,所以body使用Json.stringify()进行参数转换,而对于Content-Type为application/x-www-form-urlencoded,需要使用queryString.stringify()。Request中除了method、headers与body,还有以下属性Request.cache: 请求的缓存模式(default/reload/no-cache)Request.context: 请求的上下文(audio/image/iframe)Request.credentials: 请求的证书(omit/same-origin/include)Request.destination: 请求的内容描述类型Request.integrity: 请求的 subresource integrityRequest.mode: 请求的模式(cors/no-cors/same-origin/navigate)Request.redirect: 请求的重定向方式(follow/error/manual)Request.referrer: 请求的来源(client)Request.referrerPolicy: 请求的来源政策(no-referrer)Request.bodyUsed: 声明body是否使用在response中请求成功之后,使用.then来转换数据,使用最多的是Body.json(),当然你也可以使用以下的几种数据转换类型Body.arrayBufferBody.blobBody.formDataBody.text以上是fetch请求相关的属性与方法。如果你已经有所了解,那么恭喜你对fetch的基本使用已经过关了,下面对fetch的使用进行封装。封装在实际开发中,url的host都是相同的,不同的是请求的方法名与参数。而对于不同的环境(debug|release)请求的方式也可能不同。例如:在debug环境中为了方便调试查看请求的参数是否正确,我们会使用get来进行请求。所以在封装之前要明确什么是不变的,什么是变化的,成功与失败的响应处理。经过上面的分析,罗列一下封装需要做的事情。不变的: host,headers,body.json()变化的: url,method,body响应方式: Promise(resolve/reject)function convertUrl(url, params) { let realUrl = ApiModule.isDebug? url + “?” + queryString.stringify(Object.assign({}, params, commonParams)) : url; if (ApiModule.isDebug) { console.log(“request: " + realUrl); } return realUrl;}首先对url与参数params进行拼接,如果为debug模式将params拼接到url后。这里使用到了Object.assign()将params与commonParams组合成一个{}对象。最终通过queryString.stringify转化成string。ApiModule.isDebug是原生传递过来的值,对于Android/IOS只需传递自己的ApiModule即可。function getMethod() { return ApiModule.isDebug? “get”: “post”;}上述提到的get与post的请求时机。const headers = { Accept: ‘application/json’, “Content-Type”: “application/x-www-form-urlencoded;charset=UTF-8”};在headers中Content-Type类型为application/x-www-form-urlencodefunction convertBody(params) { return ApiModule.isDebug? undefined : queryString.stringify(Object.assign({}, params, commonParams));}由于debug模式使用的是get方式,但get规定是不能有body的,所以这里使用了undefined来标识。同时为了匹配headers中的Content-Type,params的转化必须使用queryString.stringify;如果接受的是json,可以使用JSON.stringify。定义完之后fetch对外只需接受params参数即可。async function fetchRequest(params){ let body = convertBody(params); fetch(convertUrl(baseUrl, params),{ method: method, headers: headers, body: body }) .then((response) => response.json()) .then((responseJson) => { //todo success }) .catch((error) => { if (ApiModule.isDebug) { console.error(“request error: " + error); }; //todo error });}fetch的请求封装完成,但我们的成功与失败的状态并没有通知给调用者,所以还需要一个回调机制。Promise是一个异步操作最终完成或者失败的对象。它可以接受两个函数resolve、rejectconst p = new Promise((resolve, reject){ … //success resolve(‘success’) //error reject(’error’)});//usep.then(success => { console.log(success); }, error => { console.log(error) });将fetch请求放入到Promise的异步操作中,这样一旦数据成功返回就调用resolve函数回调给调用者;失败调用reject函数,返回失败信息。而调用者只需使用Promise的.then方法等候数据的回调通知。下面来看下完整的fetch封装。async function fetchRequest(params){ let body = convertBody(params); return new Promise(function(resolve, reject){ fetch(convertUrl(baseUrl, params),{ method: method, headers: headers, body: body }) .then((response) => response.json()) .then((responseJson) => { resolve(responseJson); }) .catch((error) => { if (ApiModule.isDebug) { console.error(“request error: " + error); }; reject(error); }); });}之后对fetch的使用就非常简单了,只需传入需要的参数即可。fetchRequest({method: “goods.getInfo”, goodsId: 27021599158370074}).then(res =>{ this.setState({ shareInfo: res.data.shareInfo });});以上是有关fetch的全部内容,当然在React Native中还有其它的第三方请求库:XMLHttpRequest,同时也支持WebSockets。感兴趣的也可以去了解一下,相信会有不错的收获。精选文章5分钟吃透React Native FlexboxViewDragHelper之手势操作神器tensorflow-梯度下降,有这一篇就足够了七大排序算法总结(java)拉粉环节:感觉不错的可以来一波关注,扫描下方二维码,关注公众号,及时获取最新知识技巧。 ...

August 30, 2018 · 2 min · jiezi