JSONP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>JSONP</title>
</head>
<body>
<input type="text" id="input" />
<ul id="list"></ul>
<script>
function jsonp(url, callback, success) {
let script = document.createElement('script')
script.src = url
script.async = true
script.type = 'text/javascript'
// window[callback] = function(data) {
// success && success(data)
// }
document.body.appendChild(script)
}

function show(json) {
let list = document.getElementById('list')
list.innerHTML = ''

console.log(json)

json.s.forEach(str => {
let li = document.createElement('li')
li.innerHTML = str
list.appendChild(li)
})
}

let input = document.getElementById('input')
input.oninput = function () {
let url = `https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=${encodeURIComponent(this.value)}&cb=show`
jsonp(url, 'callback', show)
}
</script>
</body>
</html>

postMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// <iframe id="iframe1" src=""></iframe>

/* 发送消息端 */
window.parent.postMessage('hello', 'http://test.com') // iframe 向父页面发送消息
window.iframe1.contentWindow.postMessage('world', '*') // 父页面向 iframe 发送消息

/* 接收消息端 */
var mc = new MessageChannel()
mc.addEventListener('message', event => {
var origin = event.origin || event.originalEvent.origin
if (origin === 'http://test.com') {
console.log('验证通过')
}
})

window.addEventListener('message', event => {
console.log(event.origin)
console.log(event.data)
})

WebSocket

  • 和 HTTP 同属于应用层
  • 先发起一个 HTTP 请求,和服务端建立连接;连接成功之后再升级为 WebSocket 协议,然后再通讯
  • 应用场景消息通知,直播聊天,协同编辑
1
2
3
4
5
6
7
8
9
import { createServer } from 'https'
import { readFileSync } from 'fs'
import { WebSocketServer } from 'ws'

const server = createServer({
cert: readFileSync('./cert.pem'),
key: readFileSync('./key.pem')
})
const wss = new WebSocketServer({ server })

请问 WebSocket 和 HTTP 的区别?

  • 协议名称不同 wshttp
  • HTTP 一般只能由浏览器发起请求,WebSocket 双端通讯
  • WebSocket 无跨域限制
  • WebSocket 通过 sendonmessage 进行通讯,HTTP 通过 reqres 通讯

ws 可以升级为 wss 协议,像 http 升级到 https 一样,增加 SSL/TLS 安全协议

Ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
function ajax(options) {
let opts = Object.assign({
method: 'get',
url: '',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: null,
success: function () {}
}, options)

let xhr = new XMLHttpRequest()

if (opts.method.toUpperCase() === 'GET' && data) {
const query = o2u(opts.data)
if (queryStr) {
const index = url.indexOf('?')
if (index === -1) url += '?'
else if (index !== url.length - 1) url += '&'
opts.url += queryStr
}
}

xhr.open(opts.method, opts.url, true)
// xhr.withCredentials = true
// xhr.setRequestHeader('Content-type', 'application/json')

for (let i in opts.headers) {
xhr.setRequestHeader(i, opts.headers[i])
}
/*
重写 Response 里的 Content-Type
xhr.overrideMimeType('text/xml; charset=utf-8')
*/

xhr.onload = function () {
// console.log(xhr.readyState, xhr.status)
let responseData
if (xhr.getResponseHeader('Content-Type').includes('xml')) {
responseData = xhr.responseXML
} else {
responseData = JSON.parse(xhr.responseText)
}
opts.success(responseData)
}

if (opts.method.toUpperCase() === 'GET') {
xhr.send()
} else {
let sendData
switch (opts.headers['Content-Type']) {
case 'application/x-www-form-urlencoded':
sendData = o2u(opts.data)
break
case 'application/json':
sendData = JSON.stringify(opts.data)
break
}
xhr.send(sendData)
}

function o2u(obj) {
let keys = Object.keys(obj)
return keys.map(i => {
return `${encodeURIComponent(i)}=${encodeURIComponent(obj[i])}`
}).join('&')
}
}

/* 上传文件 */
let fileInput = document.getElementById('file')
fileInput.onchange = function(e) {
/*
let fileReader = new FileReader()
fileReader.onload = function(e) {
console.log(e.target.result, this.result)
}
fileReader.readAsDataURL(file)
*/

/* new URLSearchParams() */
let data = new FormData()
data.set('user', 'Bobo')
Array.from(this.files).forEach(file => {
data.append('myfile[]', file)
})

let xhr = new XMLHttpRequest()
let sTime
let sloaded
xhr.open('POST', 'http://localhost/upload', true)
/*
FormData 默认 multipart/form-data
xhr.setRequestHeader('Content-Type', 'multipart/form-data')

ajax1.0 不会发送 origin 头
xhr.setRequestHeader('X-My-Origin-Bobo', 'https://www.5207.fun/')
*/
xhr.upload.addEventListener('loadstart', () => {
stime = new Date().getTime()
sloaded = 0
}, false)
xhr.upload.addEventListener('progress', (e) => {
let cTime = new Date().getTime()
let dTime = (cTime - stime) / 1000
let dloaded = e.loaded - sloaded
let speed = dloaded / dTime
stime = cTime
sloaded = e.loaded
let unit = 'b/s'
if (speed / 1024 > 1) {
unit = 'kb/s'
speed = speed / 1024
}
if (speed / 1024 > 1) {
unit = 'mb/s'
speed = speed / 1024
}

console.log('进度: ' + (e.loaded / e.total * 100).toFixed(2) + '%', '速度: ' + speed.toFixed(2) + unit)
}, false)

xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log('Success.')
}
}
xhr.send(data)
/* router.post('/upload', ctx => {
console.log(ctx.request.files)
let fileData = fs.readFileSync(ctx.request.files.myfile.path)
fs.writeFileSync('static/' + ctx.request.files.myfile.name, fileData)
ctx.body = "上传成功"
}) */
}

请问 ajax fetch axios 的区别?

  • ajax 是一种技术称呼(异步的 JavaScript 和 XML),不是具体的 API 和 库
  • fetch 是新的异步请求API,可代替 XMLHttpRequest,原生支持 promise
  • axios 是第三方,随着 Vue框架 一起崛起

Fetch

  • 兼容性
  • 不能监控文件进度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let myHeader = new Headers()
// https://developer.mozilla.org/zh-CN/docs/Web/API/Headers/Headers
myHeader.append('Content-Type', 'application/json')
myHeader.append('X-Custom-Header', 'somevalue')

fetch('/fetch', {
method: 'post',
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
// },
headers: myHeader,
body: JSON.stringify({name: 'Bobo', age: 23}),
}).then(res => {
// return res.text()
let result = res.clone(res)
return result.json()
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})

/* res.setHeader('content-Disposition', 'attachment; filename=download.png'); */

const urlArr = 'oss/xxx.m3u8?token'.split('?')[0].split('/')
const filename = urlArr[urlArr.length - 1]
fetch('oss/xxx.m3u8?token').then(res => res.blob()).then(blob => {})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fetchWithTimeout(url, options, timeout = 5000) {
const controller = new AbortController()
const signal = controller.signal

const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
controller.abort()
reject(new Error('Request timed out'))
}, timeout)
})

const fetchPromise = fetch(url, { ...options, signal })

return Promise.race([fetchPromise, timeoutPromise])
}

Axios

CORS 跨域资源共享

协议 域名 端口 有一个不同就是跨域

CORS(Cross-Origin Resource Sharing)跨域资源共享,是一份浏览器技术的规范,用来避开浏览器的同源策略

1
2
3
4
5
6
7
8
9
10
11
12
13
app.use(, ctx => {
ctx.set('Access-Control-Allow-Origin', 'https://www.5207.fun/')
/**
* 允许携带凭证
* Access-Control-Allow-Origin 不能是 *
* 同时 xhr.withCredentials = true
*/
ctx.set('Access-Control-Allow-Credentials', true)
ctx.set('Access-Control-Allow-Headers', 'Origin, Content-Type, Content-Length, Authorization, Accept, X-Requested-With')
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
ctx.set('Access-Control-Expose-Headers', 'Content-Disposition') // 白名单
ctx.set('Access-Control-Max-Age', 3600) // 预检请求的有效期
})

简单请求

  • 请求方式(HTTP1.0) GET POST HEAD
  • Headers不超出以下字段
    • Accept Accept-Language Content-Language Last-Event-ID Content-Type
  • Content-Type
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

满足以上条件的,就是简单请求。浏览器在头信息之中,增加一个Origin字段

Origin: https://www.5207.fun/

Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口),服务器根据这个值,决定是否同意这次请求

  1. 同意: 服务器返回的响应,会多出一个头信息字段

Access-Control-Allow-Origin: https://www.5207.fun/

  1. 不同意: 如果 Origin 指定的域名,不在许可范围内,服务器仍会正常返回 HTTP 响应

浏览器发现头信息没有包含 Access-Control-Allow-Origin 字段,从而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获

预检请求

  • 请求方式(HTTP1.1) PUT PATCH DELETE CONNECT OPTIONS TRACE
  • Content-Typeapplication/json

有一些请求对服务器有着特殊的要求,会在正式通信之前,增加一次预检请求(preflight)

  • 确认当前网页所在的域名是否在服务器的许可名单之中,明确可以使用的 HTTP 请求方法和头信息字段
  • 只有在这个请求返回成功的情况下,浏览器才会发出正式的请求
  • 这样做的目的是为了避免“无用功”。一般来说,正式请求要携带一些信息,它体积可能比较大。如果我们背着这么大一个包袱到了服务端那边,却发现对方根本不接受你,那岂不是白费力气了嘛^_^

Proxy 代理

跨域是浏览器规范,通过服务器的请求转发,也能解决浏览器限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const Koa = require('koa')
const Router = require('koa-router')
const static = require('koa-static')
const koaBody = require('koa-body')
const http = require('http')
const url = require('url')
const koaServerHttpProxy = require('koa-server-http-proxy')

let app = new Koa()

let router = new Router()
app.use(router.routes())

app.use(koaBody({
multipart:true
}))
app.use(static(__dirname + '/static'))

/* 中间件 */
app.use(koaServerHttpProxy('/api', {
target: 'https://www.5207.fun/',
pathRewrite: {'^/api': ''}
}))

router.get('/', ctx => {
ctx.body = 'Hello World!'
})

/* 原生方法 */
router.all('/proxy', async ctx => {
// const options = url.parse('localhost:3000/api')
const options = {
host: 'localhost',
port: 3000,
method: 'get',
path: '/api'
}
const res = await new Promise(resolve => {
const request = http.request(options, res => {
let str = ''
res.on('data', chunk => {
str += chunk
})
res.on('end', () => {
resolve(str.toString())
})
})
request.end()
})
ctx.body = res
})

app.listen(3000)

devServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const proxy = {
'/api': {
target: 'https://api.wawotv.com',
pathRewrite: { '^/api': '/api' },
changeOrigin: true,
secure: true,
headers: {
host: 'https://www.wawotv.com',
origin: 'https://www.wawotv.com'
},
cookieDomainRewrite: {
'wawotv.com': ''
}
}
}

多标签页通信

  1. WebSocket 需要服务端参与,但不限制跨域

  2. localStorage 简单易用

    1
    2
    3
    4
    window.addEventListener('storage', event => {
    console.log(event.key)
    console.log(event.newValue)
    })
  3. SharedWorker 本地调试不方便,考虑浏览器兼容性,不支持 IE 11

    • WebWorker 专用于 JS 计算,不支持 DOM 操作
    • WebWorker 是当前页面专有的,不得在多个页面、iframe 共享
    • SharedWorker 可以被同域的多个页面共享

Restful API

  • GET 向服务器获取资源
  • POST 发送数据给服务器
  • HEAD 请求资源的头部信息,并且这些头部与 HTTP GET 方法请求时返回的一致。该请求方法的一个使用场景:下载一个大文件前先获取其大小再决定是否要下载,以此可以节约带宽资源
  • PUT 用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
  • PATCH 用于对资源进行部分修改
  • DELETE 用于删除指定的资源
  • CONNECT HTTP1.1 协议中预留给能够将连接改为管道方式的代理服务器
  • OPTIONS 用于获取目的资源所支持的通信选项
  • TRACE 回显服务器收到的请求,主要用于测试或诊断

以一个博客项目为例,实现“增删改查”功能,使用 Restful API 的接口设计

新增博客

  • urlhttp://xxx.com/api/blog
  • methodPOST

删除博客

  • urlhttp://xxx.com/api/blog/100
  • methodDELETE

修改博客

  • urlhttp://xxx.com/api/blog/100
  • methodPATCH

PUT替换全部内容
PATCH更新部分内容

查询博客

  1. 查询单个博客
    • urlhttp://xxx.com/api/blog/100
    • methodGET
  2. 查询博客列表
    • urlhttp://xxx.com/api/blog
    • methodGET

Get 用于无副作用幂等的场景,例如 查询
Post 用于有副作用,不幂等的场景,例如 注册