本想用channing-cyan这个主题,可惜代码块风格看着难受,写完了我自己都不想看。还是换成mk-cute了。
今天(指3天)本想把 MDN 的 JS 教程全部过完,结果光 ajax 就看了 2 天半。
1. DOM
客户端 API,讲的都是基础用法没什么好说的。不过写测验的时候有两个用法忘了。
input = document.querySelector("input");
console.log(input.value);
input.focus();
- input.value, 可以得到输入框的值,主要坑在于,一开始我写对了,但是看到 vscode 没有补全提示我还以为错了。
- input.focus(),聚焦于输入框, 和上面一样, vscode 和开发者工具都没有补全提示。
2. AJAX
全称是 Asynchronous JavaScript And XML
以及虽然名字带有 XML, 但更多的是被用来传 JSON。
主要指 XMLHttpRequest 或者/和 fetch api。
2.1 XHR 使用
这里 MDN 写的实在太简略了,参考的是《JS 高级程序设计》
// XHR Level 1
// 新建一个 XHR 对象。此时 readyState 为 0。
xhr = new XMLHttpRequest();
// 第一个传入 http 请求类型,第二个是 url ,第三个是否异步,默认 true。此时 readyState 为 1。
// 以 GET 方法为例,url 参数需要手动设置,JS 从 dom 中读取数据后,手动转换成 "/xxx?xx=xx&xxx=xxx" 的形式
// 转换的时候,除了注意?和&,键值对两边都要用 encodeURIComponent() 进行 URL 编码
// 《JS 高级程序设计》P715 页有一个自定义函数 addURLParam
xhr.open("GET", url, true);
// 自定义头部,必须在 open 之后,send 之前。让你的服务器读取。第一个参数要和浏览器默认头部的名字们区分开。
xhr.setRequestHeader("Myheader", "MyheaderValue");
// 以 POST 为例,要加上以下 header(不加的话服务器就无法通过普通的方法收到)
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
// 发送请求。刚运行时 readyState 为 2,收到一部分响应后为 3,全部收到后为 4。
xhr.send();
// 定义当 readyState 改变的时候调用的函数。
xhr.onreadystatechange = function () {
// 不要用 this,部分浏览器会出现问题
// 一般只有在 readyState 为 4 的时候会用到
if (xhr.readyState === 4) {
// HTTP 正常返回的时候才处理,304 为缓存
if ( (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 ) {
xhr.responseText; // 响应体的文本。
xhr.resonseXML; // 如果响应体是 XML 格式则返回 XML,不是则为 null。
// 相比之下 responseText 始终保存响应文本。
xhr.status; // 404 等状态码
xhr.statusText; // 状态码的描述
// 返回 HTTP 错误时进行处理
} else {
alert("HTTP error" + xhr.status + ": 一些描述啥的");
}
}
}
// XHR Level 2 相比 Level 1 增加了以下这些东西
// 1. FormData,用于编码表单数据(只能通过 POST 方法发送)
let data = new FormData();
// 1.1 可以用来对某个数据进行编码
data.apend('账号', '魔理沙');
// 1.2 也可以直接传入表单数据(在初始化阶段)
let data = new ForamData(document.forms[0]);
// 1.3. 然后作为 send 的参数发送
xhr.send(data)
// 还有个好处:使用FormData的话,就不需要额外给请求头加编码信息了,会自动判断
// 2. 除了 FormData,Level 2 还提供了 timeout 属性用来判断请求超时的时间
xhr.timeout = 1000; // 单位是毫秒
设置了这个之后,如果请求超过这个时间,就会执行 ontimeout
// 2.1 ontimeout 方法,在 timeout 时候执行的函数
xhr.ontimeout = funtion () {/*balabala*/}
// 2.2 即使触发了 timeout 后,onreadtstatechange 仍然会工作,此时如果访问xhr.status 会报错.
// 因此需要用 try...catch... 捕获(P717页)。
// 因为本来那里是不会报错的(http错误js不会报错),不会和本来的 http 错误产生捕获冲突。
xhr.onload // 代替了判断 reasyState === 4
xhr.onprogress // 在发送信息后反复触发的事件,其event带有3个额外属性。
//①进度信息是否可用②收到的字节数③总字节数,可以用来画进度条。
// 为保证正确执行,必须在 open 前添加
xhr.onprogress = (event) => {
// 第一个属性名字叫 lengthComputable
if (event.lengthComputable){
// 后两个属性名字可能是 loaded 和 total,也可能是 position 和 totalSize。
let receive = event.loaded ||event.position;
let total = event.total || event.totalSize;
console.log("进度百分比:", receive/total)
}
}
发送文件
如果用FormData那当然很简单,数据都帮你处理好了,请求头都帮你加上了。
如果一定要手动发送,那MDN 的这篇文章后面有提到。
2.2 fetch 的使用
现代的 ajax,使用 Promise
2.2.1 fetch 发送以及常用的几种发送类型
fetch 的第二个参数接受一个 init 对象参数(见下代码),设置各种参数
2.2.1.1 JSON 数据
let data = JSON.stringify({
"name": "who know"
})
// 因为我们没用 FormData,需要手动设置 content-type 请求头
let jsonHeaders = new Headers({
"Content-Type": "application/json;"
})
// fetch 的第二个参数接受 init 对象,设置各种参数
fetch("/", {
method: "POST",
body: data,
headers: jsonHeaders
}).then( (response) => {/*do something*/})
//服务器端 flask 接受数据代码,接收函数和一般不一样,且需要 json 处理
@app.route('/process', methods=["GET", "POST"])
def process():
print(json.loads(request.get_data()))
return ""
2.2.1.2 URL 编码参数数据
let data = "name=123&value=234"
// 改一下编码类型
let urlHeaders = new Headers({
"Content-Type": "application/x-www-form-urlencoded;"
})
fetch("/", {
method: "POST",
body: data,
headers: urlHeaders
}).then( (response) => {/*do something*/})
// 简单用 request.values 即可
@app.route('/process', methods=["GET", "POST"])
def process():
print(request.values)
return ""
2.2.1.3 文件数据
使用 FormData,无需手动加请求头(既然能用 fetch,就无须担心 FormData 的兼容了)
let data = new FormData();
let filesInput = document.querySelector("input[type="file"]")
// filesInput 只是 DOM 元素,需要用 files 属性取出文件
data.append("file01", filesInput.files[0]);
// 可以加多个文件
while (很多次) {
data.append("file0X", files[x])
}
fetch("/", {
method: "POST",
body: data,
}).then( (response) => {/*do something*/})
2.2.1.4 blob 数据
MDN 传统艺能例子就不写了
2.2.2 中断请求
大文件传输的时候可能会想中断传输
// 先实例化一个对象
let abortController = new AbortController();
fetch("POST", {
signal: abortController.signal
})
setTimeout( () => {abortController.abort()}, 10)
2.2.3 Request 对象
2.2.3.1 创建和克隆
接受的参数和 fetch 一样 第一个为 url, 第二个为 init 对象。
let request = new Request("/", {method: "POST", headers: myHeaders});
关键在于 Request 对象的克隆,有两种方式
- 用 new Request(),
let request2 = new Request(request1, {method: "GET"}),这个方法的好处是可以接受第二个参数,新的 init 对象里会把旧对象里相同的属性覆盖掉。以及,如果旧对象具有 body 属性(请求体),新对象创建的同时,旧对象会被标明已使用,不能在 fetch 里用了(后面讲和 fetch 的关系),也不能用来创建新的 request 对象。
let r1 = new Request("/", {method: "GET"})
let r2 = new Request(r1)
console.log(r1.bodyUsed) // 输出 false,没问题可以用于 fetch,因为不包含 body(请求体)
let r1 = new Request("/", {method: "GET", body: myBody})
let r2 = new Request(r1)
console.log(r1.bodyUsed) // 输出 true,不能用于 fetch 了。
- 用 clone,
let request2 = request1.clone(), 好处就是不会标记 bodyUsed,纯粹的克隆。
2.2.3.2 和 fetch 一起使用
用于当做 fetch 的参数即可使用,而且可以继续传入 init 对象,和克隆 request 的第一种方式一样,也是覆盖相同属性。
let request = new Request("/", {method: "POST", body: myBody})
fetch(request, {body: myOtherBody, headers: myHeader})
.then()
依然是和 request 的第一种克隆方式一样,request 带有 body 属性(请求体)的时候,使用 fetch 会将其标记为已使用,不能再用了。 不带可以再次用,因此可以设置一个不带 body 的经常用到的参数设置,之后只要调用这个加上 body 即可。
2.2.4 Response 对象
该对象主要为 fetch 产生,也可自己 new 一个出来。 第一个参数为 body (响应体),第二个为 init 对象,属性可包含 headers,status,statusText。
let response = new Response("123", {status: 200, statusText: "are you ok?"});
console.log(response.status) // 输出 200
console.log(response.statusText) // 输出 "are you ok?"
response.text().then(console.log) // 输出 "123"
克隆方法和 request 一致,有 body(响应体)的只能被读取一次会被标记已使用。 response.clone() 不会改变使用状态。 如果用 new 方法克隆,①要把 body 取出 ②共享 body
let r1 = new Response("asd", {status: 200, statusText: "no one"});
let r2 = new Response(r1.body) // 取出 body 克隆(伪克隆)
r2.text().then(console.log) // 正常输出 "asd"
r1.text().then(console.log) // 报错 body 已使用,因为共享 body 被 r2 使用了
2.2.5 body 混入
- fetch,Request,Response 都使用了 body 混入(用 body 属性表示请求体和响应体而不是值本身,body 属性如果 console.log 出来是 ReadableStream),并通过 bodyUsed 表示是否已读。
- 这样做好处是流(ReadableStream)在处理有效载荷(Payload)方面是有好处的。 由于以上原因,返回的response不能直接读取值,还要通过以下几种方法之一将流转存到内存。 (以下 Body 指的是 request 或 response 对象(fetch 的结果也是 response 对象别忘了))
- Body.text()
- Body.json()
- Body.formData()
- Body.arrayBuffer() // 如果需要用原始二进制格式查看和修改主体。
- Body.blob() // 如果需要用原始二进制格式使用主体,无需查看和修改。
①流只能被读取一次,每次调用的时候回给流加锁,再次读取时会报错(报错 locked 不是 bodyUsed,bodyUsed报错一般是在克隆或者使用fetch的时候,locked 报错是再调用流转内存的时候)。
②流只要被读取就会上锁,不一定完全读取完。bodyUsed 也一样,只要上了锁就会表示使用过,不一定说明读取完了。
2.2.6 ReadableStream 主体
《JS 高级程序设计》 p743 页。
2.3 Web Socket 协议
《JS 高级程序设计》 p747 页。
2.3.1 介绍
- Web Socket 协议可以建立长久链接。先发送一个 http 请求建立初始连接,然后通过请求头部的 upgrade 升级到 websocket 协议(
upgrade: websocket)。服务器也会返回一个 http 响应,响应头也包含upgrade: websocket。(状态码是101 switching protocol),在这之后就和 http 无关了,全是 ws 来发送接收数据。(注:传输层还是 tcp 三次握手,切换的是应用层) - 因此,需要建立支持 ws 的专有服务器。
- 两边都可以随时给对方发送消息,对等存在。比起 http 协议来说,服务器也可以主动给客户端推送消息了。
2.3.2 客户端建立连接
- url方案 以 ws:// 或者 wss:// 开头,后者是安全连接。
- ws 的 url 必须是绝对 URL, 且必须包含 url 方案。
let socket = new WebSocket("ws://www.example.com/balabala");
socket.close() // 调用这个用来关闭连接
2.3.3 客户端发送和接收数据
- ws 可以发送 ①字符串 ②ArrayBuffer ③Blob 三种数据
- 直接 send() 即可,异步方式和 XHR 一样,通过回调函数完成
- 接受数据通过监听事件 onmessage 完成(ws 不支持 addEventListener),数据可通过 event.data 访问
let socket = new WebSocket("ws://www.example.com/balabala");
let s = "zaima";
let ab = Uint8Array.from(['b', 'u', 'z', 'a', 'i']);
let blob = new blob(['o']);
// 发送消息,直接 send 即可
socket.send(s);
socket.send(ab);
socket.send(blob);
// 接收消息,通过监听完成, 数据通过event.data访问
socket.onmessage = function (event) {
let data = event.data;
// 处理数据balabala
}
2.3.4 其他事件
- open:连接成功建立时触发
- error:发生错误时触发
- close:连接关闭时触发,只有这个事件的 event 上包含一些额外属性
socket.onopen = () => {};
socket.onerror = () => {};
socket.onclose = () => {};