diff --git a/.github/workflows/wangdoc.yml b/.github/workflows/wangdoc.yml new file mode 100644 index 0000000..5f718e6 --- /dev/null +++ b/.github/workflows/wangdoc.yml @@ -0,0 +1,36 @@ +name: Web API tutorial CI +on: + push: + branches: + - master + +jobs: + page-generator: + name: Generating pages + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'latest' + - name: Install dependencies + run: npm install + - name: Build pages + run: npm run build + - name: Deploy to website + uses: JamesIves/github-pages-deploy-action@v4 + with: + git-config-name: wangdoc-bot + git-config-email: yifeng.ruan@gmail.com + repository-name: wangdoc/website + token: ${{ secrets.WANGDOC_BOT_TOKEN }} + branch: master # The branch the action should deploy to. + folder: dist # The folder the action should deploy. + target-folder: dist/webapi + clean: true # Automatically remove deleted files from the deploy branch + commit-message: update from WebAPI tutorial + diff --git a/.travis.yml b/.travis.yml.bak similarity index 95% rename from .travis.yml rename to .travis.yml.bak index 7ce50d5..7ad8318 100644 --- a/.travis.yml +++ b/.travis.yml.bak @@ -1,6 +1,6 @@ language: node_js node_js: -- '8' +- 'node' branches: only: diff --git a/README.md b/README.md index b08393e..af6b8b9 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -Web API 教程,提供各种浏览器 API 文档,正在建设中。 +Web API 文档,记录浏览器原生的各种 API 对象,正在建设中。 diff --git a/chapters.yml b/chapters.yml index 170bdd7..39dabaa 100644 --- a/chapters.yml +++ b/chapters.yml @@ -1,11 +1,22 @@ - canvas.md: Canvas API +- clipboard.md: Clipboard API +- fetch.md: Fetch API - fontface.md: FontFace API +- formdata.md: FormData 对象 - geolocation.md: Geolocation API +- headers.md: Headers 对象 - intersectionObserver.md: IntersectionObserver - intl-relativetimeformat.md: Intl.RelativeTimeFormat +- intl-segmenter.md: Intl.Segmenter API - page-lifecycle.md: Page Lifecycle API - page-visibility.md: Page Visibility API +- request.md: Request API +- response.md: Response API - server-sent-events.md: Server-Sent Events - svg.md: SVG 图像 +- url.md: URL 对象 +- urlpattern.md: URL Pattern API +- urlsearchparams.md: URLSearchParams 对象 - websocket.md: WebSocket - web-share-api.md: Web Share API +- postmessage.md: window.postMessage() 方法 diff --git a/docs/canvas.md b/docs/canvas.md index 3594216..9c211eb 100644 --- a/docs/canvas.md +++ b/docs/canvas.md @@ -315,9 +315,10 @@ var canvas = document.getElementById('myCanvas'); var ctx = canvas.getContext('2d'); var text1 = ctx.measureText('Hello world'); -text.width // 49.46 +text1.width // 49.46 ctx.font = 'Bold 20px Arial'; +var text2 = ctx.measureText('Hello world'); text2.width // 107.78 ``` @@ -790,7 +791,7 @@ function blobToImg(blob) { var newImg = document.createElement('img'); var url = URL.createObjectURL(blob); - newImg.onload = functio () { + newImg.onload = function () { // 使用完毕,释放 URL 对象 URL.revokeObjectURL(url); }; diff --git a/docs/clipboard.md b/docs/clipboard.md new file mode 100644 index 0000000..21fb28a --- /dev/null +++ b/docs/clipboard.md @@ -0,0 +1,305 @@ +# 剪贴板操作 Clipboard API 教程 + +## 简介 + +浏览器允许 JavaScript 脚本读写剪贴板,自动复制或粘贴内容。 + +一般来说,脚本不应该改动用户的剪贴板,以免不符合用户的预期。但是,有些时候这样做确实能够带来方便,比如“一键复制”功能,用户点击一下按钮,指定的内容就自动进入剪贴板。 + +目前,一共有三种方法可以实现剪贴板操作。 + +- `Document.execCommand()`方法 +- 异步的 Clipboard API +- `copy`事件和`paste`事件 + +本文逐一介绍这三种方法。 + +## Document.execCommand() 方法 + +`Document.execCommand()`是操作剪贴板的传统方法,各种浏览器都支持。 + +它支持复制、剪切和粘贴这三个操作。 + +- `document.execCommand('copy')`(复制) +- `document.execCommand('cut')`(剪切) +- `document.execCommand('paste')`(粘贴) + +(1)复制操作 + +复制时,先选中文本,然后调用`document.execCommand('copy')`,选中的文本就会进入剪贴板。 + +```javascript +const inputElement = document.querySelector('#input'); +inputElement.select(); +document.execCommand('copy'); +``` + +上面示例中,脚本先选中输入框`inputElement`里面的文字(`inputElement.select()`),然后`document.execCommand('copy')`将其复制到剪贴板。 + +注意,复制操作最好放在事件监听函数里面,由用户触发(比如用户点击按钮)。如果脚本自主执行,某些浏览器可能会报错。 + +(2)粘贴操作 + +粘贴时,调用`document.execCommand('paste')`,就会将剪贴板里面的内容,输出到当前的焦点元素中。 + +```javascript +const pasteText = document.querySelector('#output'); +pasteText.focus(); +document.execCommand('paste'); +``` + +(3)缺点 + +`Document.execCommand()`方法虽然方便,但是有一些缺点。 + +首先,它只能将选中的内容复制到剪贴板,无法向剪贴板任意写入内容。 + +其次,它是同步操作,如果复制/粘贴大量数据,页面会出现卡顿。有些浏览器还会跳出提示框,要求用户许可,这时在用户做出选择前,页面会失去响应。 + +为了解决这些问题,浏览器厂商提出了异步的 Clipboard API。 + +## 异步 Clipboard API + +Clipboard API 是下一代的剪贴板操作方法,比传统的`document.execCommand()`方法更强大、更合理。 + +它的所有操作都是异步的,返回 Promise 对象,不会造成页面卡顿。而且,它可以将任意内容(比如图片)放入剪贴板。 + +`navigator.clipboard`属性返回 Clipboard 对象,所有操作都通过这个对象进行。 + +```javascript +const clipboardObj = navigator.clipboard; +``` + +如果`navigator.clipboard`属性返回`undefined`,就说明当前浏览器不支持这个 API。 + +由于用户可能把敏感数据(比如密码)放在剪贴板,允许脚本任意读取会产生安全风险,所以这个 API 的安全限制比较多。 + +首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(`localhost`)允许使用非加密协议。 + +其次,调用时需要明确获得用户的许可。权限的具体实现使用了 Permissions API,跟剪贴板相关的有两个权限:`clipboard-write`(写权限)和`clipboard-read`(读权限)。“写权限”自动授予脚本,而“读权限”必须用户明确同意给予。也就是说,写入剪贴板,脚本可以自动完成,但是读取剪贴板时,浏览器会弹出一个对话框,询问用户是否同意读取。 + +![](https://www.wangbase.com/blogimg/asset/202101/bg2021012004.jpg) + +另外,需要注意的是,脚本读取的总是当前页面的剪贴板。这带来的一个问题是,如果把相关的代码粘贴到开发者工具中直接运行,可能会报错,因为这时的当前页面是开发者工具的窗口,而不是网页页面。 + +```javascript +(async () => { + const text = await navigator.clipboard.readText(); + console.log(text); +})(); +``` + +如果你把上面的代码,粘贴到开发者工具里面运行,就会报错。因为代码运行的时候,开发者工具窗口是当前页,这个页面不存在 Clipboard API 依赖的 DOM 接口。一个解决方法就是,相关代码放到`setTimeout()`里面延迟运行,在调用函数之前快速点击浏览器的页面窗口,将其变成当前页。 + +```javascript +setTimeout(async () => { + const text = await navigator.clipboard.readText(); + console.log(text); +}, 2000); +``` + +上面代码粘贴到开发者工具运行后,快速点击一下网页的页面窗口,使其变为当前页,这样就不会报错了。 + +## Clipboard 对象 + +Clipboard 对象提供了四个方法,用来读写剪贴板。它们都是异步方法,返回 Promise 对象。 + +### Clipboard.readText() + +`Clipboard.readText()`方法用于复制剪贴板里面的文本数据。 + +```javascript +document.body.addEventListener( + 'click', + async (e) => { + const text = await navigator.clipboard.readText(); + console.log(text); + } +) +``` + +上面示例中,用户点击页面后,就会输出剪贴板里面的文本。注意,浏览器这时会跳出一个对话框,询问用户是否同意脚本读取剪贴板。 + +如果用户不同意,脚本就会报错。这时,可以使用`try...catch`结构,处理报错。 + +```javascript +async function getClipboardContents() { + try { + const text = await navigator.clipboard.readText(); + console.log('Pasted content: ', text); + } catch (err) { + console.error('Failed to read clipboard contents: ', err); + } +} +``` + +### Clipboard.read() + +`Clipboard.read()`方法用于复制剪贴板里面的数据,可以是文本数据,也可以是二进制数据(比如图片)。该方法需要用户明确给予许可。 + +该方法返回一个 Promise 对象。一旦该对象的状态变为 resolved,就可以获得一个数组,每个数组成员都是 ClipboardItem 对象的实例。 + +```javascript +async function getClipboardContents() { + try { + const clipboardItems = await navigator.clipboard.read(); + for (const clipboardItem of clipboardItems) { + for (const type of clipboardItem.types) { + const blob = await clipboardItem.getType(type); + console.log(URL.createObjectURL(blob)); + } + } + } catch (err) { + console.error(err.name, err.message); + } +} +``` + +ClipboardItem 对象表示一个单独的剪贴项,每个剪贴项都拥有`ClipboardItem.types`属性和`ClipboardItem.getType()`方法。 + +`ClipboardItem.types`属性返回一个数组,里面的成员是该剪贴项可用的 MIME 类型,比如某个剪贴项可以用 HTML 格式粘贴,也可以用纯文本格式粘贴,那么它就有两个 MIME 类型(`text/html`和`text/plain`)。 + +`ClipboardItem.getType(type)`方法用于读取剪贴项的数据,返回一个 Promise 对象。该方法接受剪贴项的 MIME 类型作为参数,返回该类型的数据,该参数是必需的,否则会报错。 + +### Clipboard.writeText() + +`Clipboard.writeText()`方法用于将文本内容写入剪贴板。 + +```javascript +document.body.addEventListener( + 'click', + async (e) => { + await navigator.clipboard.writeText('Yo') + } +) +``` + +上面示例是用户在网页点击后,脚本向剪贴板写入文本数据。 + +该方法不需要用户许可,但是最好也放在`try...catch`里面防止报错。 + +```javascript +async function copyPageUrl() { + try { + await navigator.clipboard.writeText(location.href); + console.log('Page URL copied to clipboard'); + } catch (err) { + console.error('Failed to copy: ', err); + } +} +``` + +### Clipboard.write() + +`Clipboard.write()`方法用于将任意数据写入剪贴板,可以是文本数据,也可以是二进制数据。 + +该方法接受一个 ClipboardItem 实例作为参数,表示写入剪贴板的数据。 + +```javascript +try { + const imgURL = 'https://dummyimage.com/300.png'; + const data = await fetch(imgURL); + const blob = await data.blob(); + await navigator.clipboard.write([ + new ClipboardItem({ + [blob.type]: blob + }) + ]); + console.log('Image copied.'); +} catch (err) { + console.error(err.name, err.message); +} +``` + +上面示例中,脚本向剪贴板写入了一张图片。注意,Chrome 浏览器目前只支持写入 PNG 格式的图片。 + +`ClipboardItem()`是浏览器原生提供的构造函数,用来生成`ClipboardItem`实例,它接受一个对象作为参数,该对象的键名是数据的 MIME 类型,键值就是数据本身。 + +下面的例子是将同一个剪贴项的多种格式的值,写入剪贴板,一种是文本数据,另一种是二进制数据,供不同的场合粘贴使用。 + +```javascript +function copy() { + const image = await fetch('kitten.png'); + const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'}); + const item = new ClipboardItem({ + 'text/plain': text, + 'image/png': image + }); + await navigator.clipboard.write([item]); +} +``` + +## copy 事件,cut 事件 + +用户向剪贴板放入数据时,将触发`copy`事件。 + +下面的示例是将用户放入剪贴板的文本,转为大写。 + +```javascript +const source = document.querySelector('.source'); + +source.addEventListener('copy', (event) => { + const selection = document.getSelection(); + event.clipboardData.setData('text/plain', selection.toString().toUpperCase()); + event.preventDefault(); +}); +``` + +上面示例中,事件对象的`clipboardData`属性包含了剪贴板数据。它是一个对象,有以下属性和方法。 + +- `Event.clipboardData.setData(type, data)`:修改剪贴板数据,需要指定数据类型。 +- `Event.clipboardData.getData(type)`:获取剪贴板数据,需要指定数据类型。 +- `Event.clipboardData.clearData([type])`:清除剪贴板数据,可以指定数据类型。如果不指定类型,将清除所有类型的数据。 +- `Event.clipboardData.items`:一个类似数组的对象,包含了所有剪贴项,不过通常只有一个剪贴项。 + +下面的示例是拦截用户的复制操作,将指定内容放入剪贴板。 + +```javascript +const clipboardItems = []; + +document.addEventListener('copy', async (e) => { + e.preventDefault(); + try { + let clipboardItems = []; + for (const item of e.clipboardData.items) { + if (!item.type.startsWith('image/')) { + continue; + } + clipboardItems.push( + new ClipboardItem({ + [item.type]: item, + }) + ); + await navigator.clipboard.write(clipboardItems); + console.log('Image copied.'); + } + } catch (err) { + console.error(err.name, err.message); + } +}); +``` + +上面示例中,先使用`e.preventDefault()`取消了剪贴板的默认操作,然后由脚本接管复制操作。 + +`cut`事件则是在用户进行剪切操作时触发,它的处理跟`copy`事件完全一样,也是从`Event.clipboardData`属性拿到剪切的数据。 + +## paste 事件 + +用户使用剪贴板数据,进行粘贴操作时,会触发`paste`事件。 + +下面的示例是拦截粘贴操作,由脚本将剪贴板里面的数据取出来。 + +```javascript +document.addEventListener('paste', async (e) => { + e.preventDefault(); + const text = await navigator.clipboard.readText(); + console.log('Pasted text: ', text); +}); +``` + +## 参考链接 + +- [Unblocking clipboard access](https://web.dev/async-clipboard/) +- [Interact with the clipboard](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard) +- [Multi-MIME Type Copying with the Async Clipboard API](https://blog.tomayac.com/2020/03/20/multi-mime-type-copying-with-the-async-clipboard-api/) + diff --git a/docs/fetch.md b/docs/fetch.md index bff1130..d1a9be0 100644 --- a/docs/fetch.md +++ b/docs/fetch.md @@ -1,470 +1,599 @@ -# Fetch API +# Fetch API 教程 + +`fetch()`是 XMLHttpRequest 的升级版,用于在 JavaScript 脚本里面发出 HTTP 请求。 + +浏览器原生提供这个对象。本章详细介绍它的用法。 ## 基本用法 -Ajax 操作使用的`XMLHttpRequest`对象,已经有十多年的历史了,它的API设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。Fetch API 是一种新规范,用来取代`XMLHttpRequest`对象。 +`fetch()`的功能与 XMLHttpRequest 基本相同,都是向服务器发出 HTTP 请求,但有三个主要的差异。 -它主要有两个特点,一是接口合理化,Ajax 将所有不同性质的接口都放在 XHR 对象上,而 Fetch 将它们分散在几个不同的对象上,设计更合理;二是 Fetch 操作返回`Promise`对象,避免了嵌套的回调函数。 +(1)`fetch()`使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁。 -下面的代码检查浏览器是否部署了 Fetch API。 +(2)`fetch()`采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。 -```javascript -if ('fetch' in window){ - // 支持 -} else { - // 不支持 -} -``` +(3)`fetch()`通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHttpRequest 对象不支持数据流,所有的数据全部放在缓存里,不支持分块读取,必须等待全部获取后,再一次性读取。 -下面是一个 Fetch API 的简单例子。 +用法上,`fetch()`接受一个 URL 字符串作为参数,默认向该网址发出 GET 请求,返回一个 Promise 对象。它的基本用法如下。 ```javascript fetch(url) -.then(function (response) { - return response.json(); -}) -.then(function (jsonData) { - console.log(jsonData); -}) -.catch(function () { - console.log('出错了'); -}); + .then(...) + .catch(...) ``` -上面代码首先向`url`发出请求,得到回应后,将其转为 JSON 格式,输出到控制台。如果出错,则输出一条提示信息。这里需要注意,`fetch`方法返回的是一个 Promise 对象。 - -作为比较,`XMLHttpRequest`的写法如下。 +下面是一个例子,从服务器获取 JSON 数据。 ```javascript -var xhr = new XMLHttpRequest(); -xhr.open('GET', url); -xhr.responseType = 'json'; -xhr.onload = function() { - console.log(xhr.response); -}; -xhr.onerror = function() { - console.log('出错了'); -}; -xhr.send(); +fetch('https://api.github.com/users/ruanyf') + .then(response => response.json()) + .then(json => console.log(json)) + .catch(err => console.log('Request Failed', err)); ``` -## stream 数据流 +上面示例中,`fetch()`接收到的`response`是一个 [Stream 对象](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API),里面的数据本例是 JSON 数据,所以使用`response.json()`方法,将其转为 JSON 对象。它是一个异步操作,返回一个 Promise 对象。 -除了返回`Promise`对象,Fetch API 还有一个特点,就是数据传送是以数据流(stream)的形式进行的。对于大文件,数据是一段一段得到的。 +Promise 可以使用 await 语法改写,使得语义更清晰。 ```javascript -response.text().then(function (responseText) { - console.log(responseText); +async function getJSON() { + let url = 'https://api.github.com/users/ruanyf'; + try { + let response = await fetch(url); + return await response.json(); + } catch (error) { + console.log('Request Failed', error); + } } ``` -上面代码中,`text()`就是一个数据流读取器。 +上面示例中,`await`语句必须放在`try...catch`里面,这样才能捕捉异步操作中可能发生的错误。 -Fetch API 提供以下五个数据流读取器。 +后文都采用`await`的写法,不再使用`.then()`的写法。 -- `.text()`:返回字符串 -- `.json()`:返回JSON对象 -- `.formData()`:返回`FormData`对象 -- `.blob()`:返回`blob`对象 -- `.arrayBuffer()`:返回二进制数组`ArrayBuffer`对象 +## Response 对象:处理 HTTP 回应 -数据流只能读取一次,一旦读取,数据流就空了。再次读取就不会得到结果。解决方法是在读取之前,先使用`.clone()`方法,复制一份一模一样的副本。 +### Response 对象的同步属性 + +`fetch()`请求成功以后,得到的是一个 [Response 对象](https://developer.mozilla.org/en-US/docs/Web/API/Response)。它对应服务器的 HTTP 回应。 ```javascript -var url = 'LargeFile.txt'; -var progress = 0; -var contentLength = 0; - -fetch(url).then(function (response) { - // 本次请求总的数据长度 - contentLength = response.headers.get('Content-Length'); - var getStream = function (reader) { - // ... - }; - return getStream(response.body.getReader()); -}) -.catch(function (error) { - console.log(error); -}); +const response = await fetch(url); ``` -上面代码中,`response.body.getReader()`返回的就是数据流之中的一段。处理数据流的`getStream`函数代码如下。 +前面说过,Response 包含的数据通过 Stream 接口异步读取,但是它还包含一些同步属性,对应 HTTP 回应的标头信息(Headers),可以立即读取。 ```javascript -var progress = 0; -var contentLength = 0; - -var getStream = function (reader) { - return reader.read().then(function (result) { - // 如果数据已经读取完毕,直接返回 - if (result.done) { - return; - } - - // 取出本段数据(二进制格式) - var chunk = result.value; - - var text = ''; - // 假定数据是UTF-8编码,前三字节是数据头, - // 而且每个字符占据一个字节(即都为英文字符) - for (var i = 3; i < chunk.byteLength; i++) { - text += String.fromCharCode(chunk[i]); - } - - // 将本段数据追加到网页之中 - document.getElementById('content').innerHTML += text; - - // 计算当前进度 - progress += chunk.byteLength; - console.log(((progress / contentLength) * 100) + '%'); - - // 递归处理下一段数据 - return getStream(reader); - }; -}; +async function fetchText() { + let response = await fetch('/readme.txt'); + console.log(response.status); + console.log(response.statusText); +} ``` -上面这样的数据流处理,可以提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。传统的`XMLHTTPRequest`对象不支持数据流,所有的数据必须放在缓存里,等到全部拿到后,再一次性吐出来。 +上面示例中,`response.status`和`response.statusText`就是 Response 的同步属性,可以立即读取。 -## fetch() +标头信息属性有下面这些。 -`fetch`方法的第一个参数可以是 URL 字符串,也可以是后文要讲到的`Request`对象实例。`Fetch`方法返回一个`Promise`对象,并将一个`response`对象传给回调函数。 +**Response.ok** -`response`对象有一个`ok`属性,如果返回的状态码在200到299之间(即请求成功),这个属性为`true`,否则为`false`。因此,判断请求是否成功的代码可以写成下面这样。 +`Response.ok`属性返回一个布尔值,表示请求是否成功,`true`对应 HTTP 请求的状态码 200 到 299,`false`对应其他的状态码。 + +**Response.status** + +`Response.status`属性返回一个数字,表示 HTTP 回应的状态码(例如200,表示成功请求)。 + +**Response.statusText** + +`Response.statusText`属性返回一个字符串,表示 HTTP 回应的状态信息(例如请求成功以后,服务器返回“OK”)。 + +**Response.url** + +`Response.url`属性返回请求的 URL。如果 URL 存在跳转,该属性返回的是最终 URL。 + +**Response.type** + +`Response.type`属性返回请求的类型。可能的值如下: + +- `basic`:普通请求,即同源请求。 +- `cors`:跨源请求。 +- `error`:网络错误,主要用于 Service Worker。 +- `opaque`:如果`fetch()`请求的`type`属性设为`no-cors`,就会返回这个值,详见请求部分。表示发出的是简单的跨源请求,类似`
`表单的那种跨源请求。 +- `opaqueredirect`:如果`fetch()`请求的`redirect`属性设为`manual`,就会返回这个值,详见请求部分。 + +**Response.redirected** + +`Response.redirected`属性返回一个布尔值,表示请求是否发生过跳转。 + +### 判断请求是否成功 + +`fetch()`发出请求以后,有一个很重要的注意点:只有网络错误,或者无法连接时,`fetch()`才会报错,其他情况都不会报错,而是认为请求成功。 + +这就是说,即使服务器返回的状态码是 4xx 或 5xx,`fetch()`也不会报错(即 Promise 不会变为 `rejected`状态)。 + +只有通过`Response.status`属性,得到 HTTP 回应的真实状态码,才能判断请求是否成功。请看下面的例子。 ```javascript -fetch('./api/some.json').then(function (response) { - if (response.ok) { - response.json().then(function (data) { - console.log(data); - }); +async function fetchText() { + let response = await fetch('/readme.txt'); + if (response.status >= 200 && response.status < 300) { + return await response.text(); } else { - console.log('请求失败,状态码为', response.status); + throw new Error(response.statusText); } -}, function(err) { - console.log('出错:', err); -}); +} ``` -`response`对象除了`json`方法,还包含了服务器 HTTP 回应的元数据。 +上面示例中,`response.status`属性只有等于 2xx (200~299),才能认定请求成功。这里不用考虑网址跳转(状态码为 3xx),因为`fetch()`会将跳转的状态码自动转为 200。 + +另一种方法是判断`response.ok`是否为`true`。 ```javascript -fetch('users.json').then(function(response) { - console.log(response.headers.get('Content-Type')); - console.log(response.headers.get('Date')); - console.log(response.status); - console.log(response.statusText); - console.log(response.type); - console.log(response.url); -}); +if (response.ok) { + // 请求成功 +} else { + // 请求失败 +} ``` -上面代码中,`response`对象有很多属性,其中的`response.type`属性比较特别,表示HTTP回应的类型,它有以下三个值。 +### Response.headers 属性 -- basic:正常的同域请求 -- cors:CORS 机制下的跨域请求 -- opaque:非 CORS 机制下的跨域请求,这时无法读取返回的数据,也无法判断是否请求成功 +Response 对象还有一个`Response.headers`属性,指向一个 [Headers 对象](https://developer.mozilla.org/en-US/docs/Web/API/Headers),对应 HTTP 回应的所有标头。 -如果需要在 CORS 机制下发出跨域请求,需要指明状态。 +Headers 对象可以使用`for...of`循环进行遍历。 ```javascript -fetch('http://some-site.com/cors-enabled/some.json', {mode: 'cors'}) - .then(function(response) { - return response.text(); - }) - .then(function(text) { - console.log('Request successful', text); - }) - .catch(function(error) { - log('Request failed', error) - }); +const response = await fetch(url); + +for (let [key, value] of response.headers) { + console.log(`${key} : ${value}`); +} + +// 或者 +for (let [key, value] of response.headers.entries()) { + console.log(`${key} : ${value}`); +} ``` -除了指定模式,fetch 方法的第二个参数还可以用来配置其他值,比如指定 cookie 连同 HTTP 请求一起发出。 +Headers 对象提供了以下方法,用来操作标头。 + +> - `Headers.get()`:根据指定的键名,返回键值。 +> - `Headers.has()`: 返回一个布尔值,表示是否包含某个标头。 +> - `Headers.set()`:将指定的键名设置为新的键值,如果该键名不存在则会添加。 +> - `Headers.append()`:添加标头。 +> - `Headers.delete()`:删除标头。 +> - `Headers.keys()`:返回一个遍历器,可以依次遍历所有键名。 +> - `Headers.values()`:返回一个遍历器,可以依次遍历所有键值。 +> - `Headers.entries()`:返回一个遍历器,可以依次遍历所有键值对(`[key, value]`)。 +> - `Headers.forEach()`:依次遍历标头,每个标头都会执行一次参数函数。 + +上面的有些方法可以修改标头,那是因为继承自 Headers 接口。对于 HTTP 回应来说,修改标头意义不大,况且很多标头是只读的,浏览器不允许修改。 + +这些方法中,最常用的是`response.headers.get()`,用于读取某个标头的值。 ```javascript -fetch(url, { - credentials: 'include' -}) +let response = await fetch(url); +response.headers.get('Content-Type') +// application/json; charset=utf-8 ``` -发出 POST 请求的写法如下。 +`Headers.keys()`和`Headers.values()`方法用来分别遍历标头的键名和键值。 ```javascript -fetch('http://www.example.org/submit.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: 'firstName=Nikhil&favColor=blue&password=easytoguess' -}).then(function(res) { - if (res.ok) { - console.log('Perfect! Your settings are saved.'); - } else if (res.status == 401) { - console.log('Oops! You are not authorized.'); - } -}, function(e) { - console.log('Error submitting form!'); -}); +// 键名 +for(let key of myHeaders.keys()) { + console.log(key); +} + +// 键值 +for(let value of myHeaders.values()) { + console.log(value); +} +``` + +`Headers.forEach()`方法也可以遍历所有的键值和键名。 + +```javascript +let response = await fetch(url); +response.headers.forEach( + (value, key) => console.log(key, ':', value) +); ``` -## Headers +### 读取内容的方法 + +`Response`对象根据服务器返回的不同类型的数据,提供了不同的读取方法。 + +> - `response.text()`:得到文本字符串。 +> - `response.json()`:得到 JSON 对象。 +> - `response.blob()`:得到二进制 Blob 对象。 +> - `response.formData()`:得到 FormData 表单对象。 +> - `response.arrayBuffer()`:得到二进制 ArrayBuffer 对象。 + +上面5个读取方法都是异步的,返回的都是 Promise 对象。必须等到异步操作结束,才能得到服务器返回的完整数据。 + +**response.text()** -Fetch API 引入三个新的对象(也是构造函数):`Headers`, `Request`和`Response`。其中,`Headers`对象用来构造/读取 HTTP 数据包的头信息。 +`response.text()`可以用于获取文本数据,比如 HTML 文件。 ```javascript -var content = 'Hello World'; -var headers = new Headers(); -headers.append('Accept', 'application/json'); -headers.append('Content-Type', 'text/plain'); -headers.append('Content-Length', content.length.toString()); -headers.append('X-Custom-Header', 'ProcessThisImmediately'); +const response = await fetch('/users.html'); +const body = await response.text(); +document.body.innerHTML = body ``` -`Headers`对象的实例,除了使用`append`方法添加属性,也可以直接通过构造函数一次性生成。 +**response.json()** + +`response.json()`主要用于获取服务器返回的 JSON 数据,前面已经举过例子了。 + +**response.formData()** + +`response.formData()`主要用在 Service Worker 里面,拦截用户提交的表单,修改某些数据以后,再提交给服务器。 + +**response.blob()** + +`response.blob()`用于获取二进制文件。 ```javascript -var reqHeaders = new Headers({ - 'Content-Type': 'text/plain', - 'Content-Length': content.length.toString(), - 'X-Custom-Header': 'ProcessThisImmediately', -}); +const response = await fetch('flower.jpg'); +const myBlob = await response.blob(); +const objectURL = URL.createObjectURL(myBlob); + +const myImage = document.querySelector('img'); +myImage.src = objectURL; ``` -Headers 对象实例还提供了一些工具方法。 + 上面示例读取图片文件`flower.jpg`,显示在网页上。 + +**response.arrayBuffer()** + +`response.arrayBuffer()`主要用于获取流媒体文件。 ```javascript -reqHeaders.has('Content-Type') // true -reqHeaders.has('Set-Cookie') // false -reqHeaders.set('Content-Type', 'text/html') -reqHeaders.append('X-Custom-Header', 'AnotherValue') +const audioCtx = new window.AudioContext(); +const source = audioCtx.createBufferSource(); + +const response = await fetch('song.ogg'); +const buffer = await response.arrayBuffer(); -reqHeaders.get('Content-Length') // 11 -reqHeaders.getAll('X-Custom-Header') // ["ProcessThisImmediately", "AnotherValue"] +const decodeData = await audioCtx.decodeAudioData(buffer); +source.buffer = buffer; +source.connect(audioCtx.destination); +source.loop = true; +``` + +上面示例是`response.arrayBuffer()`获取音频文件`song.ogg`,然后在线播放的例子。 + +### Response.clone() -reqHeaders.delete('X-Custom-Header') -reqHeaders.getAll('X-Custom-Header') // [] +Stream 对象只能读取一次,读取完就没了。这意味着,前一节的五个读取方法,只能使用一个,否则会报错。 + +```javascript +let text = await response.text(); +let json = await response.json(); // 报错 ``` -生成 Header 实例以后,可以将它作为第二个参数,传入`Request`方法。 +上面示例先使用了`response.text()`,就把 Stream 读完了。后面再调用`response.json()`,就没有内容可读了,所以报错。 + +Response 对象提供`Response.clone()`方法,创建`Response`对象的副本,实现多次读取。 ```javascript -var headers = new Headers(); -headers.append('Accept', 'application/json'); -var request = new Request(URL, {headers: headers}); +const response1 = await fetch('flowers.jpg'); +const response2 = response1.clone(); -fetch(request).then(function(response) { - console.log(response.headers); -}); +const myBlob1 = await response1.blob(); +const myBlob2 = await response2.blob(); + +image1.src = URL.createObjectURL(myBlob1); +image2.src = URL.createObjectURL(myBlob2); ``` -同样地,Headers 实例可以用来构造 Response 方法。 +上面示例中,`response.clone()`复制了一份 Response 对象,然后将同一张图片读取了两次。 + +Response 对象还有一个`Response.redirect()`方法,用于将 Response 结果重定向到指定的 URL。该方法一般只用在 Service Worker 里面,这里就不介绍了。 + +### Response.body 属性 + +`Response.body`属性是 Response 对象暴露出的底层接口,返回一个 ReadableStream 对象,供用户操作。 + +它可以用来分块读取内容,应用之一就是显示下载的进度。 ```javascript -var headers = new Headers({ - 'Content-Type': 'application/json', - 'Cache-Control': 'max-age=3600' -}); +const response = await fetch('flower.jpg'); +const reader = response.body.getReader(); -var response = new Response( - JSON.stringify({photos: {photo: []}}), - {'status': 200, headers: headers} -); +while(true) { + const {done, value} = await reader.read(); -response.json().then(function(json) { - insertPhotos(json); -}); + if (done) { + break; + } + + console.log(`Received ${value.length} bytes`) +} ``` -上面代码中,构造了一个 HTTP 回应。目前,浏览器构造 HTTP 回应没有太大用处,但是随着 Service Worker 的部署,不久浏览器就可以向 Service Worker 发出 HTTP 回应。 +上面示例中,`response.body.getReader()`方法返回一个遍历器。这个遍历器的`read()`方法每次返回一个对象,表示本次读取的内容块。 -## Request对象 +这个对象的`done`属性是一个布尔值,用来判断有没有读完;`value`属性是一个 arrayBuffer 数组,表示内容块的内容,而`value.length`属性是当前块的大小。 -`Request`对象用来构造 HTTP 请求。 +## `fetch()`的第二个参数:定制 HTTP 请求 + +`fetch()`的第一个参数是 URL,还可以接受第二个参数,作为配置对象,定制发出的 HTTP 请求。 ```javascript -var req = new Request('/index.html'); -req.method // "GET" -req.url // "http://example.com/index.html" +fetch(url, optionObj) ``` -`Request`对象的第二个参数,表示配置对象。 +上面命令的`optionObj`就是第二个参数。 + +HTTP 请求的方法、标头、数据体都在这个对象里面设置。下面是一些示例。 + +**(1)POST 请求** ```javascript -var uploadReq = new Request('/uploadImage', { +const response = await fetch(url, { method: 'POST', headers: { - 'Content-Type': 'image/png', + "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", }, - body: 'image data' + body: 'foo=bar&lorem=ipsum', }); + +const json = await response.json(); ``` -上面代码指定`Request`对象使用 POST 方法发出,并指定 HTTP 头信息和信息体。 +上面示例中,配置对象用到了三个属性。 + +> - `method`:HTTP 请求的方法,`POST`、`DELETE`、`PUT`都在这个属性设置。 +> - `headers`:一个对象,用来定制 HTTP 请求的标头。 +> - `body`:POST 请求的数据体。 -下面是另一个例子。 +注意,有些标头不能通过`headers`属性设置,比如`Content-Length`、`Cookie`、`Host`等等。它们是由浏览器自动生成,无法修改。 + +**(2)提交 JSON 数据** ```javascript -var req = new Request(URL, {method: 'GET', cache: 'reload'}); -fetch(req).then(function(response) { - return response.json(); -}).then(function(json) { - someOperator(json); +const user = { name: 'John', surname: 'Smith' }; +const response = await fetch('/article/fetch/post/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + body: JSON.stringify(user) }); ``` -上面代码中,指定请求方法为 GET,并且要求浏览器不得缓存 response。 +上面示例中,标头`Content-Type`要设成`'application/json;charset=utf-8'`。因为默认发送的是纯文本,`Content-Type`的默认值是`'text/plain;charset=UTF-8'`。 + +**(3)提交表单** + +```javascript +const form = document.querySelector('form'); + +const response = await fetch('/users', { + method: 'POST', + body: new FormData(form) +}) +``` + +**(4)文件上传** -`Request`对象实例有两个属性是只读的,不能手动设置。一个是`referrer`属性,表示请求的来源,由浏览器设置,有可能是空字符串。另一个是`context`属性,表示请求发出的上下文,如果值是`image`,表示是从``标签发出,如果值是`worker`,表示是从`worker`脚本发出,如果是`fetch`,表示是从`fetch`函数发出的。 +如果表单里面有文件选择器,可以用前一个例子的写法,上传的文件包含在整个表单里面,一起提交。 -`Request`对象实例的`mode`属性,用来设置是否跨域,合法的值有以下三种:same-origin、no-cors(默认值)、cors。当设置为`same-origin`时,只能向同域的 URL 发出请求,否则会报错。 +另一种方法是用脚本添加文件,构造出一个表单,进行上传,请看下面的例子。 ```javascript -var arbitraryUrl = document.getElementById("url-input").value; -fetch(arbitraryUrl, { mode: "same-origin" }).then(function(res) { - console.log("Response succeeded?", res.ok); -}, function(e) { - console.log("Please enter a same-origin URL!"); +const input = document.querySelector('input[type="file"]'); + +const data = new FormData(); +data.append('file', input.files[0]); +data.append('user', 'foo'); + +fetch('/avatars', { + method: 'POST', + body: data }); ``` -上面代码中,如果用户输入的URL不是同域的,将会报错,否则就会发出请求。 +上传二进制文件时,不用修改标头的`Content-Type`,浏览器会自动设置。 + +**(5)直接上传二进制数据** -如果`mode`属性为`no-cors`,就与默认的浏览器行为没有不同,类似` +``` + +浏览器向服务器发送表单数据时,不管是用户点击 Submit 按钮发送,还是使用脚本发送,都会自动将其编码,并以`Content-Type: multipart/form-data`的形式发送。 + +`FormData()`还有第三种用法,如果想把“提交”(Submit)按钮也加入表单的键值对,可以把按钮的 DOM 节点当作`FormData()`的第二个参数。 + +```javascript +new FormData(form, submitter) +``` + +上面代码中,`submitter`就是提交按钮的 DOM 节点。这种用法适合表单有多个提交按钮,服务端需要通过按钮的值来判断,用户到底选用了哪个按钮。 + +```javascript +// 表单有两个提交按钮 +// +// + +const form = document.getElementById("form"); +const submitter = document.querySelector("button[value=save]"); + +const formData = new FormData(form, submitter); +``` + +上面示例中,`FormData()`加入了第二个参数,实例对象`formData`就会增加一个键值对,键名为`intent`,键值为`save`。 + +## 实例方法 + +### append() + +`append()`用于添加一个键值对,即添加一个表单元素。它有两种使用形式。 + +```javascript +FormData.append(name, value) +FormData.append(name, blob, fileName) +``` + +它的第一个参数是键名,第二个参数是键值。上面的第二种形式`FormData.append(name, blob, fileName)`,相当于添加一个文件选择器``,第二个参数`blob`是文件的二进制内容,第三个参数`fileName`是文件名。 + +如果键名已经存在,它会为其添加新的键值,即同一个键名有多个键值。 + +下面是一个用法示例。 + +```javascript +let formData = new FormData(); +formData.append('key1', 'value1'); +formData.append('key2', 'value2'); + +for(let [name, value] of formData) { + console.log(`${name} = ${value}`); +} +// key1 = value1 +// key2 = value2 +``` + +下面是添加二进制文件的例子。 + +```javascript +// HTML 代码如下 +// + +let imageBlob = await new Promise( + resolve => canvasElem.toBlob(resolve, 'image/png') +); + +let formData = new FormData(); +formData.append('image', imageBlob, 'image.png'); + +let response = await fetch('/article/formdata/post/image-form', { + method: 'POST', + body: formData +}); + +let result = await response.json(); +console.log(result); +``` + +下面是添加 XML 文件的例子。 + +```javascript +const content = 'hey!'; +const blob = new Blob([content], { type: "text/xml" }); + +formData.append('userfile', blob); +``` + +### delete() + +`delete()`用于删除指定的键值对,它的参数为键名。 + +```javascript +FormData.delete(name); +``` + +### entries() + +`entries()`返回一个迭代器,用于遍历所有键值对。 + +```javascript +FormData.entries() +``` + +下面是一个用法示例。 + +```javascript +const form = document.querySelector('#subscription'); +const formData = new FormData(form); +const values = [...formData.entries()]; +console.log(values); +``` + +下面是使用`entries()`遍历键值对的例子。 + +```javascript +formData.append("key1", "value1"); +formData.append("key2", "value2"); + +for (const pair of formData.entries()) { + console.log(`${pair[0]}, ${pair[1]}`); +} +// key1, value1 +// key2, value2 +``` + +### get() + +`get()`用于获取指定键名的键值,它的参数为键名。 + +```javascript +FormData.get(name) +``` + +如果该键名有多个键值,只返回第一个键值。如果找不到指定键名,则返回`null`。 + +### getAll() + +`getAll()`用于获取指定键名的所有键值,它的参数为键名,返回值为一个数组。如果找不到指定键名,则返回一个空数组。 + +```javascript +FormData.getAll(name) +``` + +### has() + +`has()`返回一个布尔值,表示是否存在指定键名,它的参数为键名。 + +```javascript +FormData.has(name) +``` + +### keys() + +`keys()`返回一个键名的迭代器,用于遍历所有键名。 + +```javascript +FormData.keys() +``` + +下面是用法示例。 + +```javascript +const formData = new FormData(); +formData.append("key1", "value1"); +formData.append("key2", "value2"); + +for (const key of formData.keys()) { + console.log(key); +} +// key1 +// key2 +``` + +### set() + +`set()`用于为指定键名设置新的键值。它有两种使用形式。 + +```javascript +FormData.set(name, value); +FormData.set(name, blob, fileName); +``` + +它的第一个参数为键名,第二个参数为键值。上面第二种形式为上传文件,第二个参数`blob`为文件的二进制内容,第三个参数`fileName`为文件名。该方法没有返回值。 + +如果指定键名不存在,它会添加该键名,否则它会丢弃所有现有的键值,确保一个键名只有一个键值。这是它跟`append()`的主要区别。 + +### values() + +`values()`返回一个键值的迭代器,用于遍历所有键值。 + +```javascript +FormData.values() +``` + +下面是用法示例。 + +```javascript +const formData = new FormData(); +formData.append("key1", "value1"); +formData.append("key2", "value2"); + +for (const value of formData.values()) { + console.log(value); +} +// value1 +// value2 +``` + diff --git a/docs/headers.md b/docs/headers.md new file mode 100644 index 0000000..7140c6d --- /dev/null +++ b/docs/headers.md @@ -0,0 +1,228 @@ +# Headers 对象 + +## 简介 + +Headers 代表 HTTP 消息的数据头。 + +它通过`Headers()`构造方法,生成实例对象。`Request.headers`属性和`Response.headers`属性,指向的都是 Headers 实例对象。 + +Headers 实例对象内部,以键值对的形式保存 HTTP 消息头,可以用`for...of`循环进行遍历,比如`for (const p of myHeaders)`。新建的 Headers 实例对象,内部是空的,需要用`append()`方法添加键值对。 + +## 构造函数 + +`Headers()`构造函数用来新建 Headers 实例对象。 + +```javascript +const myHeaders = new Headers(); +``` + +它可以接受一个表示 HTTP 数据头的对象,或者另一个 Headers 实例对象,作为参数。 + +```javascript +const httpHeaders = { + "Content-Type": "image/jpeg", + "X-My-Custom-Header": "Zeke are cool", +}; +const myHeaders = new Headers(httpHeaders); +``` + +最后,它还可以接受一个键值对数组,作为参数。 + +```javascript +const headers = [ + ["Set-Cookie", "greeting=hello"], + ["Set-Cookie", "name=world"], +]; +const myHeaders = new Headers(headers); +``` + +## 实例方法 + +### append() + +`append()`方法用来添加字段。如果字段已经存在,它会将新的值添加到原有值的末端。 + +它接受两个参数,第一个是字段名,第二个是字段值。它没有返回值。 + +```javascript +append(name, value) +``` + +下面是用法示例。 + +```javascript +const myHeaders = new Headers(); +myHeaders.append("Content-Type", "image/jpeg"); +``` + +下面是同名字段已经存在的情况。 + +```javascript +myHeaders.append("Accept-Encoding", "deflate"); +myHeaders.append("Accept-Encoding", "gzip"); +myHeaders.get("Accept-Encoding"); // 'deflate, gzip' +``` + +上面示例中,`Accept-Encoding`字段已经存在,所以`append()`会将新的值添加到原有值的末尾。 + +### delete() + +`delete()`用来删除一个键值对,参数`name`指定删除的字段名。 + +```javascript +delete(name) +``` + +如果参数`name`不是合法的字段名,或者是不可删除的字段,上面的命令会抛错。 + +下面是用法示例。 + +```javascript +const myHeaders = new Headers(); +myHeaders.append("Content-Type", "image/jpeg"); +myHeaders.delete("Content-Type"); +``` + +### entries() + +`entries()`方法用来遍历所有键值对,返回一个 iterator 指针,供`for...of`循环使用。 + +```javascript +const myHeaders = new Headers(); +myHeaders.append("Content-Type", "text/xml"); +myHeaders.append("Vary", "Accept-Language"); + +for (const pair of myHeaders.entries()) { + console.log(`${pair[0]}: ${pair[1]}`); +} +``` + +### forEach() + +`forEach()`方法用来遍历所有键值对,对每个指定键值对执行一个指定函数。 + +它的第一个参数是回调函数`callbackFn`,第二个参数`thisArg`是`callbackFn`所用的 this 对象。 + +```javascript +forEach(callbackFn) +forEach(callbackFn, thisArg) +``` + +回调函数`callback`会接受到以下参数。 + +- value:当前的字段值。 +- key:当前的字段名。 +- object:当前正在执行的 Headers 对象。 + +下面是用法示例。 + +```javascript +const myHeaders = new Headers(); +myHeaders.append("Content-Type", "application/json"); +myHeaders.append("Cookie", "This is a demo cookie"); +myHeaders.append("compression", "gzip"); + +myHeaders.forEach((value, key) => { + console.log(`${key} ==> ${value}`); +}); +``` + +### get() + +`get()`方法用于取出指定字段的字段值,它的参数就是字段名。如果字段名不合法(比如包含中文字符),它会抛错;如果字段在当前 Headers 对象不存在,它返回`null`。 + +```javascript +get(name) +``` + +下面是用法示例。 + +```javascript +myHeaders.append("Content-Type", "image/jpeg"); +myHeaders.get("Content-Type"); // "image/jpeg" +``` + +如果当前字段有多个值,`get()`会返回所有值。 + +### getSetCookie() + +`getSetCookie()`返回一个数组,包含所有`Set-Cookie`设定的 Cookie 值。 + +```javascript +const headers = new Headers({ + "Set-Cookie": "name1=value1", +}); + +headers.append("Set-Cookie", "name2=value2"); + +headers.getSetCookie(); +// ["name1=value1", "name2=value2"] +``` + +### has() + +`has()`返回一个布尔值,表示 Headers 对象是否包含指定字段。 + +```javascript +has(name) +``` + +如果参数`name`不是有效的 HTTP 数据头的字段名,该方法会报错。 + +下面是用法示例。 + +```javascript +myHeaders.append("Content-Type", "image/jpeg"); +myHeaders.has("Content-Type"); // true +myHeaders.has("Accept-Encoding"); // false +``` + +### keys() + +`keys()`方法用来遍历 Headers 数据头的所有字段名。它返回的是一个 iterator 对象,供`for...of`使用。 + +```javascript +const myHeaders = new Headers(); +myHeaders.append("Content-Type", "text/xml"); +myHeaders.append("Vary", "Accept-Language"); + +for (const key of myHeaders.keys()) { + console.log(key); +} +``` + +### set() + +`set()`方法用来为指定字段添加字段值。如果字段不存在,就添加该字段;如果字段已存在,就用新的值替换老的值,这是它与`append()`方法的主要区别。 + +它的第一个参数`name`是字段名,第二个参数`value`是字段值。 + +```javascript +set(name, value) +``` + +下面是用法示例。 + +```javascript +const myHeaders = new Headers(); +myHeaders.set("Accept-Encoding", "deflate"); +myHeaders.set("Accept-Encoding", "gzip"); +myHeaders.get("Accept-Encoding"); // 'gzip' +``` + +上面示例中,连续两次使用`set()`对`Accept-Encoding`赋值,第二个值会覆盖第一个值。 + +### values() + +`values()`方法用来遍历 Headers 对象的字段值。它返回一个 iterator 对象,供`for...of`使用。 + +```javascript +const myHeaders = new Headers(); +myHeaders.append("Content-Type", "text/xml"); +myHeaders.append("Vary", "Accept-Language"); + +for (const value of myHeaders.values()) { + console.log(value); +} +``` + diff --git a/docs/intersectionObserver.md b/docs/intersectionObserver.md index a81ec46..80971ff 100644 --- a/docs/intersectionObserver.md +++ b/docs/intersectionObserver.md @@ -45,7 +45,7 @@ observer.observe(elementB); ## IntersectionObserver.observe() -`IntersectionObserver.observe()`方法用来启动对一个 DOM 元素的观察。该方法接受两个参数:回调函数`callback`和配置对象`options`。 +IntersectionObserver 实例的`observe()`方法用来启动对一个 DOM 元素的观察。该方法接受两个参数:回调函数`callback`和配置对象`options`。 ### callback 参数 diff --git a/docs/intl-segmenter.md b/docs/intl-segmenter.md new file mode 100644 index 0000000..9676c08 --- /dev/null +++ b/docs/intl-segmenter.md @@ -0,0 +1,225 @@ +# Intl segmenter API + +## 简介 + +Intl.Segmenter 是浏览器内置的用于文本分词的 API。 + +使用时,先用`Intl.Segmenter()`新建一个分词器对象。 + +```javascript +const segmenter = new Intl.Segmenter( + 'en', + { granularity: 'word' } +); +``` + +`Intl.Segmenter()`接受两个参数,第一个是所要分词的语言简称(上例是`en`),第二个参数是一个配置对象,有以下两个属性。 + +- `localeMatcher`:指定分词算法,有两个可能的值,一个是`lookup`,表示采用特定的算法(BCP 47),另一个是`best fit`(默认值),表示采用操作系统或浏览器现有的尽可能适用的算法。 +- `granularity`:表示分词的颗粒度,有三个可能的值:grapheme(字符,这是默认值),word(词语),sentence(句子)。 + +拿到分词器对象以后,就可以进行分词了。 + +```javascript +const segmenter = new Intl.Segmenter( + 'en', + { granularity: 'word' } +); + +const segments = segmenter.segment('This has four words!'); + +Array.from(segments).map((segment) => segment.segment); +// ['This', ' ', 'has', ' ', 'four', ' ', 'words', '!'] +``` + +上面示例中,变量`segmenter`是分词器对象,可以对英语进行分词,颗粒度是词语。所以,“This has four words!”被分成了8个部分,包括4个词语、3个空格和1个标点符号。 + +分词器对象的`segment()`方法是实际的分词方法,它的参数是需要分词的文本,返回值是一个具有迭代器接口的分词结果对象。`Array.from()`将这个分词结果对象转成数组,也可以采用`[...segments]`的写法。 + +下面的例子是过滤掉非词语字符。 + +```javascript +const segments = segmenter.segment('This has four words!'); + +Array.from(segments) + .filter((segment) => segment.isWordLike) + .map((segment) => segment.segment); +// ['This', 'has', 'four', 'words'] +``` + +上面示例中,`Array.from()`将分词结果对象转成一个数组,变量`segment`是数组的每个成员,它也是一个对象。该对象的`isWordLike`属性是一个布尔值,表示当前值是否为一个真正的词,而该对象的`segment`属性(上例的`segment.segment`)则是真正的分词结果。 + +Intl Segmenter 支持各种语言,下面是日语分词的例子。 + +```javascript +const segmenter = new Intl.Segmenter('ja', { granularity: 'word' }); +const segments = segmenter.segment('これは日本語のテキストです'); + +Array.from(segments).map((segment) => segment.segment); +// ['これ', 'は', '日本語', 'の', 'テキスト', 'です'] +``` + +下面是法语的例子。 + +```javascript +const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' }); +const string1 = 'Que ma joie demeure'; + +const iterator1 = segmenterFr.segment(string1)[Symbol.iterator](); + +iterator1.next().value.segment // 'Que' +iterator1.next().value.segment // ' ' +``` + +## 静态方法 + +### Intl.Segmenter.supportedLocalesOf() + +`Intl.Segmenter.supportedLocalesOf()`返回一个数组,用来检测当前环境是否支持指定语言的分词。 + +```javascript +const locales1 = ['ban', 'id-u-co-pinyin', 'de-ID']; +const options1 = { localeMatcher: 'lookup', granularity: 'string' }; + +Intl.Segmenter.supportedLocalesOf(locales1, options1) +// ["id-u-co-pinyin", "de-ID"] +``` + +它接受两个参数,第一个参数是一个数组,数组成员是需要检测的语言简称;第二个参数是配置对象,跟构造方法的第二个参数是一致的,可以省略。 + +上面示例中,需要检测的三种语言分别是巴厘岛语(ban)、印度尼西亚语(id-u-co-pinyin)、德语(de-ID)。结果显示只支持前两者,不支持巴厘岛语。 + +## 实例方法 + +### resolvedOptions() + +实例对象的`resolvedOptions()`方法,用于获取构造该实例时的参数。 + +```javascript +const segmenter1 = new Intl.Segmenter('fr-FR'); +const options1 = segmenter1.resolvedOptions(); + +options1.locale // "fr-FR" +options1.granularity // "grapheme" +``` + +上面示例中,`resolveOptions()`方法返回了一个对象,该对象的`locale`属性对应构造方法的第一个参数,`granularity`属性对应构造方法第二个参数对象的颗粒度属性。 + +### segment() + +实例对象的`segment()`方法进行实际的分词。 + +```javascript +const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' }); +const string1 = 'Que ma joie demeure'; + +const segments = segmenterFr.segment(string1); + +segments.containing(5) +// {segment: 'ma', index: 4, input: 'Que ma joie demeure', isWordLike: true} +``` + +`segment()`方法的返回结果是一个具有迭代器接口的分词结果对象,有三种方法进行处理。 + +(1)使用`Array.from()`或扩展运算符(`...`)将分词结果对象转成数组。 + +```javascript +const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' }); +const string1 = 'Que ma joie demeure'; + +const iterator1 = segmenterFr.segment(string1); + +Array.from(iterator1).map(segment => { + if (segment.segment.length > 4) { + console.log(segment.segment); + } +}) +// demeure +``` + +上面示例中,`segmenterFr.segment()`返回一个针对`string1`的分词结果对象,该对象具有迭代器接口。`Array.from()`将其转为数组,数组的每个成员是一个分词颗粒对象,该对象的`segment`属性就是分词结果。分词颗粒对象的介绍,详见后文。 + +(2)使用`for...of`循环,遍历分词结果对象。 + +```javascript +const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' }); +const string1 = 'Que ma joie demeure'; + +const iterator1 = segmenterFr.segment(string1); + +for (const segment of iterator1) { + if (segment.segment.length > 4) { + console.log(segment.segment); + } +} +// demeure +``` + +上面示例中,`for...of`默认调用分词结果对象的迭代器接口,获取每一轮的分词颗粒对象。 + +由于迭代器接口是在`Symbol.iterator`属性上面,所以实际执行的代码如下。 + +```javascript +const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' }); +const string1 = 'Que ma joie demeure'; + +const iterator1 = segmenterFr.segment(string1)[Symbol.iterator](); + +for (const segment of iterator1) { + if (segment.segment.length > 4) { + console.log(segment.segment); + } +} +// "demeure" +``` + +`for...of`循环每一轮得到的是一个分词颗粒对象,该对象的`segment`属性就是当前的分词结果,详见下文。 + +(3)使用`containing()`方法获取某个位置的分词颗粒对象。 + +```javascript +const segmenterFr = new Intl.Segmenter('fr', { granularity: 'word' }); +const string1 = 'Que ma joie demeure'; + +const segments = segmenterFr.segment(string1); + +segments.containing(5) +// {segment: 'ma', index: 4, input: 'Que ma joie demeure', isWordLike: true} +``` + +`containing()`方法的参数是一个整数,表示原始字符串的指定位置(从0开始计算)。如果省略该参数,则默认为0。 + +`containing()`的返回值是该位置的分词颗粒对象,如果参数位置超出原始字符串,则返回`undefined`。分词颗粒对象有以下属性。 + +- segment:指定位置对应的分词结果。 +- index:本次分词在原始字符串的开始位置(从0开始)。 +- input:进行分词的原始字符串。 +- isWordLike:如果分词颗粒度为`word`,该属性返回一个布尔值,表示当前值是否一个真正的词。如果分词颗粒度不为`word`,则返回`undefined`。 + +```javascript +const input = "Allons-y!"; + +const segmenter = new Intl.Segmenter("fr", { granularity: "word" }); +const segments = segmenter.segment(input); + +let current = segments.containing(); +// { index: 0, segment: "Allons", isWordLike: true } + +current = segments.containing(4); +// { index: 0, segment: "Allons", isWordLike: true } + +current = segments.containing(6); +// { index: 6, segment: "-", isWordLike: false } + +current = segments.containing(current.index + current.segment.length); +// { index: 7, segment: "y", isWordLike: true } + +current = segments.containing(current.index + current.segment.length); +// { index: 8, segment: "!", isWordLike: false } + +current = segments.containing(current.index + current.segment.length); +// undefined +``` + +上面示例中,分词结果中除了空格和标点符号,其他情况下,`isWordLike`都返回`false`。 + diff --git a/docs/postmessage.md b/docs/postmessage.md new file mode 100644 index 0000000..40bf764 --- /dev/null +++ b/docs/postmessage.md @@ -0,0 +1,116 @@ +# window.postMessage() 方法 + +## 简介 + +`window.postMessage()`用于浏览器不同窗口之间的通信,主要包括 iframe 嵌入窗口和新开窗口两种情况。它不要求两个窗口同源,所以有着广泛的应用。 + +`window.postMessage()`里面的`window`对象,是发送消息的目标窗口。比如,父窗口通过`window.open()`打开子窗口,那么子窗口可以通过`targetWindow = window.opener`获取父窗口。再比如,父窗口通过`iframe`嵌入了子窗口,那么子窗口可以通过`window.parent`获取父窗口。 + +## 参数和返回值 + +`window.postMessage()`方法有几种使用形式。 + +最简单的一种就是直接发送消息。 + +```javascript +window.postMessage(message) +``` + +上面写法中的`message`就是发送的消息,可以是字符串,也可以是对象。如果是对象,浏览器会自动将该对象序列化,以字符串形式发送。 + +由于`window.postMessage()`可以用于任意两个源(协议+域名+端口)之间的通信,为了减少安全隐患,可以使用第二个参数`targetOrigin`,指定目标窗口的源。 + +```javascript +window.postMessage(message, targetOrigin) +``` + +上面写法中的`targetOrigin`是一个字符串,表示目标窗口里面的网页的源(origin),比如`https://example.com`。如果对目标窗口不加限制,可以省略这个参数,或者写成`*`。一旦指定了该参数,只有目标窗口符合指定的源(协议+域名+端口),目标窗口才会接收到消息发送事件。 + +`window.postMessage()`还可以指定第三个参数,用于发送一些可传送物体(transferable object),比如 ArrayBuffer 对象。 + +```javascript +window.postMessage(message, targetOrigin, transfer) +``` + +上面写法中的`transfer`就是可传送物体。该物体一旦发送以后,所有权就转移到了目标窗口,当前窗口将无法再使用该物体。这样的设计是为了发送大量数据时,可以提高效率。 + +`targetOrigin`和`transfer`这两个参数,也可以写在一个对象里面,作为第二个参数。 + +```javascript +window.postMessage(message, { targetOrigin, transfer }) +``` + +下面是一个跟弹出窗口发消息的例子。 + +```javascript +const popup = window.open('http://example.com'); +popup.postMessage("hello there!", "http://example.com"); +``` + +`window.postMessage()`方法没有返回值。 + +## message 事件 + +当前窗口收到其他窗口发送的消息时,会发生 message 事件。通过监听该事件,可以接收对方发送的消息。 + +```javascript +window.addEventListener( + "message", + (event) => { + if (event.origin !== "http://example.com") return; + // ... + }, + false, +); +``` + +事件的监听函数,可以接收到一个 event 参数对象。该对象有如下属性。 + +- data:其他窗口发送的消息。 +- origin:发送该消息的窗口的源(协议+域名+端口)。 +- source:发送该消息的窗口对象的引用,使用该属性可以建立双向通信,下面是一个示例。 + +```javascript +window.addEventListener("message", (event) => { + if (event.origin !== "http://example.com:8080") return; + event.source.postMessage( + "hi there!", + event.origin, + ); +}); +``` + +## 实例 + +父页面是`origin1.com`,它打开了子页面`origin2.com`,并向其发送消息。 + +```javascript +function sendMessage() { + const otherWindow = window.open('https://origin2.com/origin2.html'); + const message = 'Hello from Origin 1!'; + const targetOrigin = 'https://origin2.com'; + otherWindow.postMessage(message, targetOrigin); +} +``` + +子页面`origin2.com`监听父页面发来的消息。 + +```javascript +window.addEventListener('message', receiveMessage, false); + +function receiveMessage(event) { + if (event.origin === 'https://origin1.com') { + console.log('Received message: ' + event.data); + } +} +``` + +下面是 iframe 嵌入窗口向父窗口`origin1.com`发送消息的例子。 + +```javascript +function sendMessage() { + const message = 'Hello from Child Window!'; + window.parent.postMessage(message, 'https://origin1.com'); +} +``` + diff --git a/docs/request.md b/docs/request.md new file mode 100644 index 0000000..afa5e27 --- /dev/null +++ b/docs/request.md @@ -0,0 +1,211 @@ +# Request API + +浏览器原生提供 Request() 构造函数,用来构造发给服务器的 HTTP 请求。它生成的 Response 实例,可以作为`fetch()`的参数。 + +注意,构造一个 Request 对象,只是构造出一个数据结构,本身并不会发出 HTTP 请求,只有将它传入`fetch()`方法才会真的发出请求。 + +## 构造方法 + +Request 作为构造函数的语法如下,返回一个 Request 实例对象。 + +```javascript +new Request(url: String, [init: Object]): Request +``` + +它的第一个参数是请求的 URL 字符串,第二个参数是一个可选的配置对象,用来构造 HTTP 请求,该对象的类型描述如下。 + +```javascript +{ + body: Object + cache: String + credentials: String + headers: Object + integrity: String + keepalive: Boolean + method: String + mode: String + redirect: String + referrer: String + referrerPolicy: String + requestMode: String + requestCredentials: String + signal: AbortSignal +} +``` + +第二个参数配置对象的各个属性的含义如下。 + +- `body`:HTTP 请求的数据体,必须是 Blob、BufferSource、FormData、String、URLSearchParams 类型之一。 +- `cache`:请求的缓存模式。 +- `credentials`:请求所用的凭证,可以设为 omit、same-origini、include。Chrome 47 之前,默认值为 same-origin;Chrome 47 之后,默认值为 include。 +- `headers`:一个代表 HTTP 请求数据头的对象,类型为 Headers 对象实例。 +- `integrity`:请求的资源的资源完整度验证值,比如`sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=`。 +- `method`:HTTP 方法,一般为`GET`、`POST`、`DELETE`,默认是`GET`。 +- `mode`:请求模式,比如 cors、no-cors、navigate,默认为 cors。 +- `redirect`:请求所用的模式,可以设为 error、follow、manual,默认为 follow。 +- `referrer`:请求的来源,默认为 about:client。 + +下面是两个示例。 + +```javascript +// 示例一 +const request = new Request('flowers.jpg'); + +// 示例二 +const myInit = { + method: "GET", + headers: { + "Content-Type": "image/jpeg", + }, + mode: "cors", + cache: "default", +}; + +const request = new Request('flowers.jpg', myInit); +``` + +`Request()`还有另一种语法,第一个参数是另一个 Request 对象,第二个参数还是一个配置对象。它返回一个新的 Request 对象,相当于对第一个参数 Request 对象进行修改。 + +```javascript +new Request(request: Request, [init: Object]): Request +``` + +## 实例属性 + +Request 实例对象的属性,大部分就是它的构造函数第二个参数配置对象的属性。 + +(1)`body` + +`body`属性返回 HTTP 请求的数据体,它的值是一个 ReadableStream 对象或 null(`GET`或`HEAD`请求时没有数据体)。 + +```javascript +const request = new Request('/myEndpoint', { + method: "POST", + body: "Hello world", +}); + +request.body; // ReadableStream 对象 +``` + +注意,Firefox 不支持该属性。 + +(2)`bodyused` + +`bodyUsed`属性是一个布尔值,表示`body`是否已经被读取了。 + +(3)`cache` + +`cache`属性是一个只读字符串,表示请求的缓存模式,可能的值有 default、force-cache、no-cache、no-store、only-if-cached、reload。 + +(4)`credentials` + +`credentials`属性是一个只读字符串,表示跨域请求时是否携带其他域的 cookie。可能的值有 omit(不携带)、 include(携带)、same-origin(只携带同源 cookie)。 + +(5)`destination` + +`destination`属性是一个字符串,表示请求内容的类型,可能的值有 ''、'audio'、'audioworklet'、'document'、'embed'、'font'、'frame'、'iframe'、'image'、'manifest'、'object'、'paintworklet'、 'report'、'script'、'sharedworker'、'style'、'track'、'video'、'worker'、'xslt' 等。 + +(6)`headers` + +`headers`属性是一个只读的 Headers 实例对象,表示请求的数据头。 + +(7)`integrity` + +`integrity`属性表示所请求资源的完整度的验证值。 + +(8)`method` + +`method`属性是一个只读字符串,表示请求的方法(GET、POST 等)。 + +(9)`mode` + +`mode`属性是一个只读字符串,用来验证是否可以有效地发出跨域请求,可能的值有 same-origin、no-cors、cors。 + +(10)`redirect` + +`redirect`属性是一个只读字符串,表示重定向时的处理模式,可能的值有 follow、error、manual。 + +(11)`referrer` + +`referrer`属性是一个只读字符串,表示请求的引荐 URL。 + +(12)`referrerPolicy` + +`referrerPolicy`属性是一个只读字符串,决定了`referrer`属性是否要包含在请求里面的处理政策。 + +(13)`signal` + +`signal`是一个只读属性,包含与当前请求相对应的中断信号 AbortSignal 对象。 + +(14)`url` + +`url`是一个只读字符串,包含了当前请求的字符串。 + +```javascript +const myRequest = new Request('flowers.jpg'); +const myURL = myRequest.url; +``` + +## 实例方法 + +### 取出数据体的方法 + +- arrayBuffer():返回一个 Promise 对象,将 Request 的数据体作为 ArrayBuffer 对象返回。 +- blob():返回一个 Promise 对象,将 Request 的数据体作为 Blob 对象返回。 +- json():返回一个 Promise 对象,将 Request 的数据体作为 JSON 对象返回。 +- text():返回一个 Promise 对象,将 Request 的数据体作为字符串返回。 +- formData():返回一个 Promise 对象,将 Request 的数据体作为表单数据 FormData 对象返回。 + +下面是`json()`方法的一个示例。 + +```javascript +const obj = { hello: "world" }; + +const request = new Request("/myEndpoint", { + method: "POST", + body: JSON.stringify(obj), +}); + +request.json().then((data) => { + // 处理 JSON 数据 +}); +``` + +`.formData()`方法返回一个 Promise 对象,最终得到的是一个 FormData 表单对象,里面是用键值对表示的各种表单元素。该方法很少使用,因为需要拦截发给服务器的请求的场景不多,一般用在 Service Worker 拦截和处理网络请求,以修改表单数据,然后再发送到服务器。 + +```javascript +self.addEventListener('fetch', event => { + // 拦截表单提交请求 + if ( + event.request.method === 'POST' && + event.request.headers.get('Content-Type') === 'application/x-www-form-urlencoded' + ) { + event.respondWith(handleFormSubmission(event.request)); + } +}); + +async function handleFormSubmission(request) { + const formData = await request.formData(); + formData.append('extra-field', 'extra-value'); + + const newRequest = new Request(request.url, { + method: request.method, + headers: request.headers, + body: new URLSearchParams(formData) + }); + + return fetch(newRequest); +} +``` + +上面示例中,Service Worker 拦截表单请求以后,添加了一个表单成员,再调用`fetch()`向服务器发出修改后的请求。 + +### clone() + +`clone()`用来复制 HTTP 请求对象。 + +```javascript +const myRequest = new Request('flowers.jpg'); +const newRequest = myRequest.clone(); +``` + diff --git a/docs/response.md b/docs/response.md new file mode 100644 index 0000000..aa975ea --- /dev/null +++ b/docs/response.md @@ -0,0 +1,263 @@ +# Response API + +浏览器原生提供`Response()`构造函数,用来构造服务器响应。 + +`fetch()`方法返回的就是一个 Response 对象。 + +## 构造方法 + +`Response()`作为构造方法调用时,返回 Response 实例。 + +```javascript +// 定义 +new Response([body:Object, [init : Object]]): Response + +// 用法 +new Response() +new Response(body) +new Response(body, options) +``` + +它带有两个参数,都是可选的。 + +第一个参数`body`代表服务器返回的数据体,必须是下面类型之一:ArrayBuffer、ArrayBufferView、Blob、FormData、ReadableStream、String、URLSearchParams。 + +第二个参数`init`是一个对象,代表服务器返回的数据头,类型描述如下。 + +```javascript +{ + status: Number + statusText: String + headers: Object +} +``` + +下面是一个例子。 + +```javascript +const myBlob = new Blob(); +const myOptions = { status: 200, statusText: "OK" }; +const myResponse = new Response(myBlob, myOptions); +``` + +注意,如果返回 JSON 数据,必须将其转成字符串返回。 + +```javascript +const data = { + hello: "world", +}; + +const json = JSON.stringify(data, null, 2); + +const result = new Response(json, { + headers: { + "content-type": "application/json;charset=UTF-8", + }, +}); +``` + +上面示例中,构造一个返回 JSON 数据的 Response 对象,就必须用`JSON.stringify()`方法,将第一个参数转为字符串。 + +## 实例属性 + +### body,bodyUsed + +`body`属性代表数据体,是一个只读的 ReadableStream 对象。 + +```javascript +const res = await fetch('/fireworks.ogv'); +const reader = res.body.getReader(); + +let result; +while (!(result = await reader.read()).done) { + console.log('chunk size:', result.value.byteLength); +} +``` + +上面示例中,先建立一个 body 的读取器,然后每次读取一段数据,输出这段数据的字段长度。 + +注意,`body`是一个 Stream 对象,只能读取一次。取出所有数据以后,第二次就读不到了。 + +`bodyUsed`属性是一个只读的布尔值,表示`body`属性是否已经读取。 + +### headers + +`headers`属性代表服务器返回的数据头,是一个只读的 Headers 对象。 + +```javascript +const res = await fetch('/flowers.jpg'); +console.log(...res.headers); +``` + +上面示例中,发出请求后,展开打印`res.headers`属性,即服务器回应的所有消息头。 + +### ok + +`ok`属性是一个布尔值,表示服务器返回的状态码是否成功(200到299),该属性只读。 + +```javascript +const res1 = await fetch('https://httpbin.org/status/200'); +console.log(res1.ok); // true + +const res2 = await fetch('https://httpbin.org/status/404'); +console.log(res2.ok); // false +``` + +### redirected + +`redirected`是一个布尔值,表示服务器返回的状态码是否跳转类型(301,302等),该属性只读。 + +```javascript +const res1 = await fetch('https://httpbin.org/status/200'); +console.log(res1.redirected); // false + +const res2 = await fetch('https://httpbin.org/status/301'); +console.log(res2.redirected); // true +``` + +### status,statusText + +`status`属性是一个数值,代表服务器返回的状态码,该属性只读。 + +```javascript +const res1 = await fetch('https://httpbin.org/status/200'); +console.log(res1.status); // 200 + +const res2 = await fetch('https://httpbin.org/status/404'); +console.log(res2.status); // 404 +``` + +`statusText`属性是一个字符串,代表服务器返回的状态码的文字描述。比如,状态码200的`statusText`一般是`OK`,也可能为空。 + +### type + +`type`属性是一个只读字符串,表示服务器回应的类型,它的值有下面几种:basic、cors、default、error、opaque、opaqueredirect。 + +### url + +`url`属性是一个字符串,代表服务器路径,该属性只读。如果请求是重定向的,该属性就是重定向后的 URL。 + +## 实例方法 + +### 数据读取 + +以下方法可以获取服务器响应的消息体,根据返回数据的不同类型,调用相应方法。 + +- .json():返回一个 Promise 对象,最终得到一个解析后的 JSON 对象。 +- .text():返回一个 Promise 对象,最终得到一个字符串。 +- .blob():返回一个 Promise 对象,最终得到一个二进制 Blob 对象,代表某个文件整体的原始数据。 +- .arrayBuffer():返回一个 Promise 对象,最终得到一个 ArrayBuffer 对象,代表一段固定长度的二进制数据。 +- .formData():返回一个 Promise 对象,最终得到一个 FormData 对象,里面是键值对形式的表单提交数据。 + +下面是从服务器获取 JSON 数据的一个例子,使用`.json()`方法,其他几个方法的用法都大同小异。 + +```javascript +async function getRedditPosts() { + try { + const response = await fetch('https://www.reddit.com/r/all/top.json?limit=10'); + const data = await response.json(); + const posts = data.data.children.map(child => child.data); + console.log(posts.map(post => post.title)); + } catch (error) { + console.error(error); + } +} +``` + +下面是从服务器获取二进制文件的例子,使用`.blob()`方法。 + +```javascript +async function displayImageAsync() { + try { + const response = await fetch('https://www.example.com/image.jpg'); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const img = document.createElement('img'); + img.src = url; + document.body.appendChild(img); + } catch (error) { + console.error(error); + } +} +``` + +下面是从服务器获取音频文件,直接解压播放的例子,使用`.arrayBuffer()`方法。 + +```javascript +async function playAudioAsync() { + try { + const response = await fetch('https://www.example.com/audio.mp3'); + const arrayBuffer = await response.arrayBuffer(); + const audioBuffer = await new AudioContext().decodeAudioData(arrayBuffer); + const source = new AudioBufferSourceNode(new AudioContext(), { buffer: audioBuffer }); + source.connect(new AudioContext().destination); + source.start(0); + } catch (error) { + console.error(error); + } +} +``` + +### clone() + +`clone()`方法用来复制 Response 对象。 + +```javascript +const res1 = await fetch('/flowers.jpg'); +const res2 = res1.clone(); +``` + +复制以后,读取一个对象的数据,不会影响到另一个对象。 + +## 静态方法 + +### Response.json() + +`Response.json()`返回一个 Response 实例,该实例对象的数据体就是作为参数的 JSON 数据,数据头的`Content-Type`字段自动设为`application/json`。 + +```javascript +Response.json(data) +Response.json(data, options) +``` + +`Response.json()`基本上就是`Response()`构造函数的变体。 + +下面是示例。 + +```javascript +const jsonResponse1 = Response.json({ my: "data" }); + +const jsonResponse2 = Response.json( + { some: "data", more: "information" }, + { status: 307, statusText: "Temporary Redirect" }, +); +``` + +### Response.error() + +`Response.error()`用来构造一个表示报错的服务器回应,主要用在 Service worker,表示拒绝发送。 + +```javascript +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + if (url.pathname === '/flowers.jpg') { + event.respondWith(Response.error()); + } +}); +``` + +### Response.redirect() + +`Response.redirect()`用来构造一个表示跳转的服务器回应,主要用在 Service worker,表示跳转到其他网址。 + +```javascript +Response.redirect(url) +Response.redirect(url, status) +``` + +这个方法的第一个参数`url`是所要跳转的目标网址,第二个参数是状态码,一般是301或302(默认值)。 + +```javascript +Response.redirect("https://www.example.com", 302); +``` + diff --git a/docs/service-worker.md b/docs/service-worker.md index faa8534..a9d8718 100644 --- a/docs/service-worker.md +++ b/docs/service-worker.md @@ -53,7 +53,7 @@ navigator.serviceWorker.register('sw.js'.then(() => { }) ``` -上面代码的`sw.js`就是需要浏览器注册的 service worker 脚本。注意,这个脚本必须与当前网址同域,service worker 不支持跨与脚本。另外,`sw.js`必须是从 HTTPS 协议加载的。 +上面代码的`sw.js`就是需要浏览器注册的 service worker 脚本。注意,这个脚本必须与当前网址同域,service worker 不支持跨域脚本。另外,`sw.js`必须是从 HTTPS 协议加载的。 默认情况下,Service worker 只对根目录`/`生效,如果要改变生效范围,可以运行下面的代码。 diff --git a/docs/svg.md b/docs/svg.md index df00a39..a9fc228 100644 --- a/docs/svg.md +++ b/docs/svg.md @@ -18,7 +18,7 @@ SVG 文件可以直接插入网页,成为 DOM 的一部分,然后用 JavaScr preserveAspectRatio="xMidYMid meet" > - + ``` @@ -452,20 +452,22 @@ Date |Amount ```xml - - - - + + + + + + - - $10 - $80 + + $10 + $80 - - January 2014 - April + + Jan. + Apr. diff --git a/docs/url.md b/docs/url.md new file mode 100644 index 0000000..cde7abf --- /dev/null +++ b/docs/url.md @@ -0,0 +1,239 @@ +# URL 对象 + +浏览器内置的 URL 对象,代表一个网址。通过这个对象,就能生成和操作网址。 + +## 构造函数 + +URL 可以当作构造函数使用,生成一个实例对象。 + +它接受一个网址字符串作为参数。 + +```javascript +let url = new URL('https://example.com'); +``` + +如果网址字符串无法解析,它会报错,所以它要放在`try...catch`代码块里面。 + +如果这个参数只是一个网站路径,比如`/foo/index.html`,那么需要提供基准网址,作为第二个参数。 + +```javascript +const url1 = new URL('page2.html', 'http://example.com/page1.html'); +url1.href // "http://example.com/page2.html" + +const url2 = new URL('..', 'http://example.com/a/b.html') +url2.href // "http://example.com/" +``` + +这种写法很方便基于现有网址,构造新的 URL。 + +`URL()`的参数也可以是另一个 URL 实例。这时,`URL()`会自动读取该实例的href属性,作为实际参数。 + +## 实例属性 + +一旦得到了 URL 实例对象,就可以从它的各种属性,方便地获取 URL 的各个组成部分。 + +- href:完整的网址 +- protocol:访问协议,带结尾冒号`:`。 +- search:查询字符串,以问号`?`开头。 +- hash:哈希字符串,以`#`开头。 +- username:需要认证的网址的用户名。 +- password:需要认证的网址的密码。 +- host:主机名,不带协议,但带有端口。 +- hostname:主机名,不带协议和端口。 +- port:端口。 +- origin:包括协议、域名和端口。 +- pathname:服务器路径,以根路径`/`开头,不带有查询字符串。 +- searchParams:指向一个 URLSearchParams 实例,方便用来构造和操作查询字符串。 + +下面是用法示例。 + +```javascript +const url = new URL('http://user:pass@example.com:8080/resource/path?q=1#hash'); + +url.href // http://user:pass@example.com:8080/resource/path?q=1#hash +url.protocol // http: +url.username // user +url.password // pass +url.host // example.com:8080 +url.hostname // example.com +url.port // 8080 +url.pathname // /resource/path +url.search // ?q=1 +url.hash // #hash +url.origin // http://example.com:8080 +``` + +这些属性里面,只有`origin`属性是只读的,其他属性都可写,并且会立即生效。 + +```javascript +const url = new URL('http://example.com/index.html#part1'); + +url.pathname = 'index2.html'; +url.href // "http://example.com/index2.html#part1" + +url.hash = '#part2'; +url.href // "http://example.com/index2.html#part2" +``` + +上面示例中,改变 URL 实例的`pathname`属性和`hash`属性,都会实时反映在 URL 实例当中。 + +下面是`searchParams`属性的用法示例,它的具体属性和方法介绍参见 《URLSearchParams》一章。 + +```javascript +const url = new URL('http://example.com/path?a=1&b=2'); + +url.searchParams.get('a') // 1 +url.searchParams.get('b') // 2 + +for (const [k, v] of url.searchParams) { + console.log(k, v); +} +// a 1 +// b 2 +``` + +## 静态方法 + +### URL.createObjectURL() + +`URL.createObjectURL()`方法用来为文件数据生成一个临时网址(URL 字符串),供那些需要网址作为参数的方法使用。该方法的参数必须是 Blob 类型(即代表文件的二进制数据)。 + +```javascript +// HTML 代码如下 +//
+// +const div = document.getElementById('display'); + +function handleFiles(files) { + for (let i = 0; i < files.length; i++) { + let img = document.createElement('img'); + img.src = window.URL.createObjectURL(files[i]); + div.appendChild(img); + } +} +``` + +上面示例中,`URL.createObjectURL()`方法用来为上传的文件生成一个临时网址,作为``元素的图片来源。 + +该方法生成的 URL 就像下面的样子。 + +```javascript +blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1 +``` + +注意,每次使用`URL.createObjectURL()`方法,都会在内存里面生成一个 URL 实例。如果不再需要该方法生成的临时网址,为了节省内存,可以使用`URL.revokeObjectURL()`方法释放这个实例。 + +下面是生成 Worker 进程的一个示例。 + +```html + + +``` + +### URL.revokeObjectURL() + +`URL.revokeObjectURL()`方法用来释放`URL.createObjectURL()`生成的临时网址。它的参数就是`URL.createObjectURL()`方法返回的 URL 字符串。 + +下面为上一小节的示例加上`URL.revokeObjectURL()`。 + +```javascript +var div = document.getElementById('display'); + +function handleFiles(files) { + for (var i = 0; i < files.length; i++) { + var img = document.createElement('img'); + img.src = window.URL.createObjectURL(files[i]); + div.appendChild(img); + img.onload = function() { + window.URL.revokeObjectURL(this.src); + } + } +} +``` + +上面代码中,一旦图片加载成功以后,为本地文件生成的临时网址就没用了,于是可以在`img.onload`回调函数里面,通过`URL.revokeObjectURL()`方法释放资源。 + +### URL.canParse() + +`URL()`构造函数解析非法网址时,会抛出错误,必须用`try...catch`代码块处理,这样终究不是非常方便。因此,URL 对象又引入了`URL.canParse()`方法,它返回一个布尔值,表示当前字符串是否为有效网址。 + +```javascript +URL.canParse(url) +URL.canParse(url, base) +``` + +`URL.canParse()`可以接受两个参数。 + +- `url`:字符串或者对象(比如``元素的 DOM 对象),表示 URL。 +- `base`:字符串或者 URL 实例对象,表示 URL 的基准位置。它是可选参数,当第一个参数`url`为相对 URL 时,会使用这个参数,计算出完整的 URL,再进行判断。 + +```javascript +URL.canParse("https://developer.mozilla.org/") // true +URL.canParse("/en-US/docs") // false +URL.canParse("/en-US/docs", "https://developer.mozilla.org/") // true +``` + +上面示例中,如果第一个参数是相对 URL,这时必须要有第二个参数,否则返回`false`。 + +下面的示例是第二个参数为 URL 实例对象。 + +```javascript +let baseUrl = new URL("https://developer.mozilla.org/"); +let url = "/en-US/docs"; + +URL.canParse(url, baseUrl) // true +``` + +该方法内部使用`URL()`构造方法相同的解析算法,因此可以用`URL()`构造方法代替。 + +```javascript +function isUrlValid(string) { + try { + new URL(string); + return true; + } catch (err) { + return false; + } +} +``` + +上面示例中,给出了`URL.canParse()`的替代实现`isUrlValid()`。 + +### URL.parse() + +`URL.parse()`是一个新添加的方法,Chromium 126 和 Firefox 126 开始支持。 + +它的主要目的就是,改变`URL()`构造函数解析非法网址抛错的问题。这个新方法不会抛错,如果参数是有效网址,则返回 URL 实例对象,否则返回`null`。 + +```javascript +const urlstring = "this is not a URL"; + +const not_a_url = URL.parse(urlstring); // null +``` + +上面示例中,`URL.parse()`的参数不是有效网址,所以返回`null`。 + +## 实例方法 + +### toString() + +URL 实例对象的`toString()`返回`URL.href`属性,即整个网址。 + diff --git a/docs/urlpattern.md b/docs/urlpattern.md new file mode 100644 index 0000000..ba69063 --- /dev/null +++ b/docs/urlpattern.md @@ -0,0 +1,547 @@ +# URL Pattern API + +## 简介 + +URL Pattern API 基于正则表达式和通配符,对 URL 进行匹配和解析。 + +它提供一个构造函数`URLPattern()`,用于新建一个 URL 模式实例。 + +```javascript +const pattern = new URLPattern(input); +``` + +有了模式实例,就可以知道某个 URL 是否符合该模式。 + +```javascript +const pattern = new URLPattern({ pathname: "/books" }); +console.log(pattern.test("https://example.com/books")); // true +``` + +上面示例中,模式实例是 包含`/books`路径的 URL,实例方法`test()`用来检测指定网址是否符合该模式,结果为`true`。 + +URL Pattern 支持多种协议,不仅是 HTTP 协议。 + +```javascript +const pattern = new URLPattern("data\\:foo*"); +``` + +上面示例中,URL Pattern 新建了一个 Data 协议的模式。 + +## 构造函数 URLPattern() + +### 基本用法 + +构造函数`URLPattern()`用于新建一个 URL 模式实例。 + +```javascript +const pattern = new URLPattern(input); +``` + +该构造函数的参数`input`是一个模式字符串或者模式对象。 + +```javascript +new URLPattern("https://example.com/books/:id") +// { +// hasRegExpGroups: false, +// hash: "*", +// hostname: "example.com", +// password: "*", +// pathname: "/books/:id", +// port: "", +// protocol: "https", +// search: "*", +// username: "*", +// ... +// } +``` + +上面示例中,参数`https://example.com/books/:id`就是一个模式字符串,执行后返回一个 URLPattern 实例对象,包含模式的各个组成部分。 + +参数`input`也可以写成一个对象,用属性指定模式 URL 的每个部分。也就是说,模式对象可以有以下属性。 + +- protocol +- username +- password +- hostname +- port +- pathname +- search +- hash +- baseURL + +上面的示例,如果参数改成模式对象,就是下面这样。 + +```javascript +new URLPattern({ + protocol: 'https', + hostname: 'example.com', + pathname: '/books/:id', +}) +``` + +模式字符串或者模式对象之中,没有定义的部分,默认为`*`,表示所有可能的字符,包括零字符的情况。 + +`URLPattern()`正常情况下将返回一个 URLPattern 实例对象,但是遇到参数无效或语法不正确,则会报错。 + +```javascript +new URLPattern(123) // 报错 +``` + +上面示例中,参数`123`不是一个有效的 URL 模式,就报错了。 + +需要注意的是,如果模式字符串为相对路径,那么`URLPattern()`还需要第二个参数,用来指定基准 URL。 + +```javascript +new URLPattern(input, baseURL) +``` + +上面代码中,第二个参数`baseURL`就是基准 URL。 + +```javascript +new URLPattern('/books/:id') // 报错 +new URLPattern('/books/:id', 'https://example.com') // 正确 +``` + +上面示例中,第一个参数`/books/:id`是一个相对路径,这时就需要第二个参数`https://example.com`,用来指定基准 URL,否则报错。 + +但是,如果参数为模式对象,则可以只指定 URL 模式的某个部分。 + +```javascript +new URLPattern({ + pathname: '/books/:id' +}) // 正确 +``` + +上面示例中,参数是一个模式对象,那么参数允许只指定 URL 的部分模式。 + +模式对象里面,也可以指定基准 URL。 + +```javascript +let pattern4 = new URLPattern({ + pathname: "/books/:id", + baseURL: "https://example.com", +}); +``` + +基准 URL 必须是合法的 URL,不能包含模式。 + +注意,如果用了模式对象,就不能使用基准 URL 作为第二个参数,这样会报错。 + +```javascript +new URLPattern({ pathname: "/foo/bar" }, "https://example.com") // 报错 +new URLPattern({ pathname: "/foo/bar" }, "https://example.com/baz") // 报错 +``` + +上面示例中,同时使用了模式对象和第二个参数,结果就报错了。 + +`URLpattern()`还可以加入配置对象参数,用于定制匹配行为。 + +```javascript +new URLPattern(input, options) +new URLPattern(input, baseURL, options) +``` + +上面代码中,参数`options`就是一个配置对象。 + +目前,这个配置对象`options`只有`ignoreCase`一个属性,如果设为`true`,将不区分大小写,默认值为`false`,表示区分大小写。 + +```javascript +new URLPattern(input, { + ignoreCase: false // 默认值,区分大小写 +}) +``` + +请看下面的例子。 + +```javascript +const pattern = new URLPattern("https://example.com/2022/feb/*"); + +pattern.test("https://example.com/2022/feb/xc44rsz") // true +pattern.test("https://example.com/2022/Feb/xc44rsz") // false +``` + +上面示例,默认匹配时,会区分`feb`和`Feb`。 + +我们可以用`ignoreCase`将其关闭。 + +```javascript +const pattern = new URLPattern( + "https://example.com/2022/feb/*", + { ignoreCase: true, } +); + +pattern.test("https://example.com/2022/feb/xc44rsz") // true +pattern.test("https://example.com/2022/Feb/xc44rsz") // true +``` + +### 模式写法 + +模式字符串基本上采用正则表达式的写法,但是不是所有的正则语法都支持,比如先行断言和后行断言就不支持。 + +(1)普通字符 + +如果都是普通字符,就表示原样匹配。 + +```javascript +const p = new URLPattern('https://example.com/abc'); +``` + +上面代码就表示确切匹配路径`https://example.com/abc`。 + +```javascript +p.test('https://example.com') // false +p.test('https://example.com/a') //false +p.test('https://example.com/abc') // true +p.test('https://example.com/abcd') //false +p.test('https://example.com/abc/') //false +p.test('https://example.com/abc?123') //true +``` + +上面示例中,URL 必须严格匹配路径`https://example.com/abc`,即使尾部多一个斜杠都不行,但是加上查询字符串是可以的。 + +(2)`?` + +量词字符`?`表示前面的字符串,可以出现0次或1次,即该部分可选。 + +```javascript +let pattern = new URLPattern({ + protocol: "http{s}?", +}); +``` + +上面示例中,`{s}?`表示字符组`s`可以出现0次或1次。 + +`?`不包括路径的分隔符`/`。 + +```javascript +const pattern = new URLPattern("/books/:id?", "https://example.com"); + +pattern.test("https://example.com/books/123") // true +pattern.test("https://example.com/books") // true +pattern.test("https://example.com/books/") // false +pattern.test("https://example.com/books/123/456") // false +pattern.test("https://example.com/books/123/456/789") // false +pattern.test("https://example.com/books/123/456/") // false +``` + +上面示例中,`?`不能匹配网址结尾的斜杠。 + +如果一定要匹配,可以把结尾的斜杠放在`{}`里面。 + +```javascript +const pattern = new URLPattern({ pathname: "/product{/}?" }); + +pattern.test({ pathname: "/product" }) // true +pattern.test({ pathname: "/product/" }) // true +``` + +上面示例中,不管网址有没有结尾的斜杠,`{/}?`都会成功匹配。 + +(3)`+` + +量词字符`+`表示前面的字符串出现1次或多次。 + +```javascript +const pattern = new URLPattern({ + pathname: "/books/(\\d+)", +}) +``` + +上面示例中,`\\d+`表示1个或多个数字,其中的`\d`是一个内置的字符类,表示0-9的数字,因为放在双引号里面,所以反斜杠前面还要再加一个反斜杠进行转义。 + +`+`可以包括`/`分隔的路径的多个部分,但不包括路径结尾的斜杠。 + +```javascript +const pattern = new URLPattern("/books/:id+", "https://example.com"); + +pattern.test("https://example.com/books/123") // true +pattern.test("https://example.com/books") // false +pattern.test("https://example.com/books/") // false +pattern.test("https://example.com/books/123/456") // true +pattern.test("https://example.com/books/123/456/789") // true +pattern.test("https://example.com/books/123/456/") // false +``` + +(4)`*` + +量词字符`*`表示出现零次或多次。 + +```javascript +const pattern = new URLPattern('https://example.com/{abc}*'); + +pattern.test('https://example.com') // true +pattern.test('https://example.com/') // true +pattern.test('https://example.com/abc') // true +pattern.test('https://example.com/abc/') // false +pattern.test('https://example.com/ab') // false +pattern.test('https://example.com/abcabc') // true +pattern.test('https://example.com/abc/abc/abc') // false +``` + +上面示例中,`{abc}*`表示`abc`出现零次或多次,也不包括路径分隔符`/`。 + +如果`*`前面没有任何字符,就表示所有字符,包括零字符的情况,也包括分隔符`/`。 + +```javascript +let pattern = new URLPattern({ + search: "*", + hash: "*", +}); +``` + +上面示例中,`*`表示匹配所有字符,包括零字符。 + +下面是另一个例子。 + +```javascript +const pattern = new URLPattern("/*.png", "https://example.com"); + +pattern.test("https://example.com/image.png") // true +pattern.test("https://example.com/image.png/123") // false +pattern.test("https://example.com/folder/image.png") // true +pattern.test("https://example.com/.png") // true +``` + +`*`匹配的部分可以从对应部分的数字属性上获取。 + +```javascript +const pattern = new URLPattern({ + hostname: "example.com", + pathname: "/foo/*" +}); + +const result = pattern.exec("/foo/bar", "https://example.com/baz"); + +result.pathname.input // '/foo/bar' +result.pathname.groups[0] // 'bar' +``` + +上面示例中,`*`的匹配结果可以从`pathname.groups[0]`获取。 + +```javascript +const pattern = new URLPattern({ hostname: "*.example.com" }); +const result = pattern.exec({ hostname: "cdn.example.com" }); + +result.hostname.groups[0] // 'cdn' +result.hostname.input // 'cdn.example.com' +``` + +上面示例中,`*`的匹配结果可以从`hostname.groups[0]`获取。 + +(5)`{}` + +特殊字符`{}`用来定义量词`?`、`+`、`+`的生效范围。 + +如果`{}`后面没有量词,那就跟没有使用的效果一样。 + +```javascript +const pattern = new URLPattern('https://example.com/{abc}'); + +pattern.test('https://example.com/') // false +pattern.test('https://example.com/abc') // true +``` + +(6)`()` + +特殊字符`()`用来定义一个组匹配,匹配结果可以按照出现顺序的编号,从`pathname.groups`对象上获取。 + +```javascript +const pattern = new URLPattern("/books/(\\d+)", "https://example.com"); +pattern.exec("https://example.com/books/123").pathname.groups +// { '0': '123' } +``` + +上面示例中,`(\\d+)`是一个组匹配,因为它是第一个组匹配,所以匹配结果放在`pathname.groups`的属性`0`。 + +(7)`|` + +特殊字符`|`表示左右两侧的字符,都可以出现,即表示逻辑`OR`。 + +```javascript +let pattern = new URLPattern({ + port: "(80|443)", +}); +``` + +上面示例中,`(80|443)`表示80或者443都可以。 + +(8)`:` + +特殊字符`:`用来定义一个具名组匹配,后面跟着变量名。 + +```javascript +let pattern = new URLPattern({ + pathname: "/:path", +}); +``` + +上面示例中,`/:path`表示斜杠后面的部分,都被捕捉放入变量`path`,可以从匹配结果的`pathname.groups`上的对应属性获取。 + +```javascript +const pattern = new URLPattern({ pathname: "/books/:id" }); + +pattern.exec("https://example.com/books/123").pathname.groups +// { id: '123' } +``` + +上面示例中,`pathname.groups`返回一个对象,该对象的属性就是所有捕捉成功的组变量,上例是`id`。 + + +下面是另一个例子。 + +```javascript +const pattern = new URLPattern({ pathname: "/:product/:user/:action" }); +const result = pattern.exec({ pathname: "/store/wanderview/view" }); + +result.pathname.groups.product // 'store' +result.pathname.groups.user // 'wanderview' +result.pathname.groups.action // 'view' +result.pathname.input // '/store/wanderview/view' +``` + +上面示例中,`:product`、`:user`、`:action`的匹配结果,都可以从`pathname.groups`的对应属性上获取。 + +组匹配可以放在模式的前面。 + +```javascript +const pattern = new URLPattern( + "/books/:id(\\d+)", + "https://example.com" +); +``` + +上面示例中,组匹配`:id`后面跟着模型定义`\\d+`,模式需要放在括号里面。 + +**(9)特殊字符转义** + +如果要将特殊字符当作普通字符使用,必须在其前面加入双重反斜杠进行转义。 + +```javascript +let pattern1 = new URLPattern({ + pathname: "/a:b", +}); + +let pattern2 = new URLPattern({ + pathname: "/a\\:b", +}); +``` + +上面示例中,`a:b`表示路径以字符`a`开头,后面的部分都放入变量`b`。而`a\\:b`表示路径本身就是`a:b`就是。 + +## 实例属性 + +URLPattern 实例的属性对应`URLPattern()`模式对象参数的各个部分。 + +```javascript +const pattern = new URLPattern({ + hostname: "{*.}?example.com", +}); + +pattern.hostname // '{*.}?example.com' +pattern.protocol // '*' +pattern.username // '*' +pattern.password // '*' +pattern.port // "" +pattern.pathname // '*' +pattern.search // '*' +pattern.hash // '*' +``` + +上面示例中,`pattern`是一个实例对象,它的属性与`URLPattern()`的参数对象的属性一致。 + +注意,`search`不包括开头的`?`,`hash`不包括开头的`#`,但是`pathname`包括开头的`/`。 + +下面是另一个例子。 + +```javascript +const pattern = new URLPattern("https://cdn-*.example.com/*.jpg"); + +pattern.protocol // 'https' +pattern.hostname // 'cdn-*.example.com' +pattern.pathname // '/*.jpg' +pattern.username // '' +pattern.password // '' +pattern.search // '' +pattern.hash // '' +``` + +## 实例方法 + +### exec() + +实例的`exec()`方法,把模式用于解析参数网址,返回匹配结果。 + +`exec()`方法的参数与`new URLPattern()`是一致的。它可以是一个 URL 字符串。 + +```javascript +pattern.exec("https://store.example.com/books/123"); +``` + +如果第一个参数是相对 URL,那么需要基准 URL,作为第二个参数。 + +```javascript +pattern.exec("/foo/bar", "https://example.com/baz"); +``` + +`exec()`方法的参数,也可以是一个对象。 + +```javascript +pattern.exec({ + protocol: "https", + hostname: "store.example.com", + pathname: "/books/123", +}); +``` + +如果匹配成功,它返回一个包括匹配结果的对象。如果匹配失败,返回`null`。 + +```javascript +const pattern = new URLPattern("http{s}?://*.example.com/books/:id"); +pattern.exec("https://example.com/books/123") // null +``` + +上面示例中,匹配失败返回`null`。 + +匹配成功返回的对象,有一个`inputs`属性,包含传入`pattern.exec()`的参数数组。其他属性的值也是一个对象,该对象的`input`属性对应传入值,`groups`属性包含各个组匹配。 + +```javascript +const pattern = new URLPattern("http{s}?://*.example.com/books/:id"); +let match = pattern.exec("https://store.example.com/books/123"); + +match.inputs // ['https://store.example.com/books/123'] +match.protocol // { input: "https", groups: {} } +match.username // { input: "", groups: {} } +match.password // { input: "", groups: {} } +match.hostname // { input: "store.example.com", groups: { "0": "store" } } +match.port // { input: "", groups: {} } +match.pathname // { input: "/books/123", groups: { "id": "123" } } +match.search // { input: "", groups: {} } +match.hash // { input: "", groups: {} } +``` + +### test() + +实例的`test()`方法,用来检测参数网址是否符合当前模式。 + +它的参数跟`URLPattern()`是一样的,可以是模式字符串,也可以是模式对象。 + +```javascript +const pattern = new URLPattern({ + hostname: "example.com", + pathname: "/foo/*" + }); + +pattern.test({ + pathname: "/foo/bar", + baseURL: "https://example.com/baz", +}) // true + +pattern.test("/foo/bar", "https://example.com/baz") // true +``` + +正常情况下,它返回一个布尔值。但是,如果语法不合法,它也会抛错。 + +```javascript +pattern.test({ pathname: "/foo/bar" }, "https://example.com/baz") // 报错 +``` + diff --git a/docs/urlsearchparams.md b/docs/urlsearchparams.md new file mode 100644 index 0000000..59163c1 --- /dev/null +++ b/docs/urlsearchparams.md @@ -0,0 +1,298 @@ +# URLSearchParams 对象 + +## 简介 + +URLSearchParams 对象表示 URL 的查询字符串(比如`?foo=bar`)。它提供一系列方法,用来操作这些键值对。URL 实例对象的`searchParams`属性,就是指向一个 URLSearchParams 实例对象。 + +URLSearchParams 实例对象可以用`for...of`进行遍历。 + +```javascript +for (const [key, value] of mySearchParams) { +} +``` + +## 构造方法 + +URLSearchParams 可以作为构造函数使用,生成一个实例对象。 + +```javascript +const params = new URLSearchParams(); +``` + +它可以接受一个查询字符串作为参数,将其转成对应的实例对象。 + +```javascript +const params = new URLSearchParams('?a=1&b=2'); +``` + +注意,它最多只能去除查询字符串的开头问号`?`,并不能解析完整的网址字符串。 + +```javascript +const paramsString = "http://example.com/search?query=%40"; +const params = new URLSearchParams(paramsString); +``` + +上面示例中,URLSearchParams 会认为键名是`http://example.com/search?query`,而不是`query`。 + +它也可以接受表示键值对的对象或数组作为参数。 + +```javascript +// 参数为数组 +const params3 = new URLSearchParams([ + ["foo", "1"], + ["bar", "2"], +]); + +// 参数为对象 +const params1 = new URLSearchParams({ foo: "1", bar: "2" }); +``` + +浏览器向服务器发送表单数据时,可以直接使用 URLSearchParams 实例作为表单数据。 + +```javascript +const params = new URLSearchParams({foo: 1, bar: 2}); +fetch('https://example.com/api', { + method: 'POST', + body: params +}).then(...) +``` + +上面示例中,fetch 向服务器发送命令时,可以直接使用 URLSearchParams 实例对象作为数据体。 + +它还可以接受另一个 URLSearchParams 实例对象作为参数,等于复制了该对象。 + +```javascript +const params1 = new URLSearchParams('?a=1&b=2'); +const params2 = new URLSearchParams(params1); +``` + +上面示例中,`params1`和`params2`是两个一模一样的实例对象,但是修改其中一个,不会影响到另一个。 + +URLSearchParams会对查询字符串自动编码。 + +```javascript +const params = new URLSearchParams({'foo': '你好'}); +params.toString() // "foo=%E4%BD%A0%E5%A5%BD" +``` + +上面示例中,`foo`的值是汉字,URLSearchParams 对其自动进行 URL 编码。 + +键名可以没有键值,这时 URLSearchParams 会认为键值等于空字符串。 + +```javascript +const params1 = new URLSearchParams("foo&bar=baz"); +const params2 = new URLSearchParams("foo=&bar=baz"); +``` + +上面示例中,`foo`是一个空键名,不管它后面有没有等号,URLSearchParams 都会认为它的值是一个空字符串。 + +## 实例方法 + +### append() + +`append()`用来添加一个查询键值对。如果同名的键值对已经存在,它依然会将新的键值对添加到查询字符串的末尾。 + +它的第一个参数是键名,第二个参数是键值,下面是用法示例。 + +```javascript +const params = new URLSearchParams('?a=1&b=2'); + +params.append('a', 3); +params.toString() // 'a=1&b=2&a=3' +``` + +上面示例中,键名`a`已经存在,但是`append()`依然会将`a=3`添加在查询字符串的末尾。 + +### delete() + +`delete()`删除给定名字的键值对。 + +### get() + +`get()`返回指定键名所对应的键值。如果存在多个同名键值对,它只返回第一个键值。 + +```javascript +const params = new URLSearchParams('?a=1&b=2'); +params.get('a') // 1 +``` + +对于不存在的键名,它会返回`null`。 + +注意,`get()`会将键值里面的加号转为空格。 + +```javascript +const params = new URLSearchParams(`c=a+b`); +params.get('c') // 'a b' +``` + +上面示例中,`get()`将`a+b`转为`a b`。如果希望避免这种行为,可以先用`encodeURIComponent()`对键值进行转义。 + +### getAll() + +`getAll()`返回一个数组,里面是指定键名所对应的所有键值。 + +```javascript +const params = new URLSearchParams('?a=1&b=2&a=3'); +params.getAll('a') // [ '1', '3' ] +``` + +### has() + +`has()`返回一个布尔值,表示指定键名是否存在。 + +```javascript +const params = new URLSearchParams('?a=1&b=2'); +params.has('a') // true +params.has('c') // false +``` + +### set() + +`set()`用来设置一个键值对。如果相同键名已经存在,则会替换当前值,这是它与`append()`的不同之处。该方法适合用来修改查询字符串。 + +```javascript +const params = new URLSearchParams('?a=1&b=2'); +params.set('a', 3); +params.toString() // 'a=3&b=2' +``` + +上面示例中,`set()`修改了键`a`。 + +如果有多个的同名键,`set()`会移除现存所有的键,再添加新的键值对。 + +```javascript +const params = new URLSearchParams('?foo=1&foo=2'); +params.set('foo', 3); +params.toString() // "foo=3" +``` + +上面示例中,有两个`foo`键,`set()`会将它们都删掉,再添加一个新的`foo`键。 + +### sort() + +`sort()`按照键名(以 Unicode 码点为序)对键值对排序。如果有同名键值对,它们的顺序不变。 + +```javascript +const params = new URLSearchParams('?a=1&b=2&a=3'); +params.sort(); +params.toString() // 'a=1&a=3&b=2' +``` + +### entries() + +`entries()`方法返回一个 iterator 对象,用来遍历键名和键值。 + +```javascript +const params = new URLSearchParams("key1=value1&key2=value2"); + +for (const [key, value] of params.entries()) { + console.log(`${key}, ${value}`); +} +// key1, value1 +// key2, value2 +``` + +如果直接对 URLSearchParams 实例进行`for...of`遍历,其实内部调用的就是`entries`接口。 + +```javascript +for (var p of params) {} +// 等同于 +for (var p of params.entries()) {} +``` + +### forEach() + +`forEach()`用来依次对每个键值对执行一个回调函数。 + +它接受两个参数,第一个参数`callback`是回调函数,第二个参数`thisArg`是可选的,用来设置`callback`里面的`this`对象。 + +```javascript +forEach(callback) +forEach(callback, thisArg) +``` + +`callback`函数可以接收到以下三个参数。 + +- value:当前键值。 +- key:当前键名。 +- searchParams:当前的 URLSearchParams 实例对象。 + +下面是用法示例。 + +```javascript +const params = new URLSearchParams("key1=value1&key2=value2"); + +params.forEach((value, key) => { + console.log(value, key); +}); +// value1 key1 +// value2 key2 +``` + +### keys() + +`keys()`返回一个 iterator 对象,用来遍历所有键名。 + +```javascript +const params = new URLSearchParams("key1=value1&key2=value2"); + +for (const key of params.keys()) { + console.log(key); +} +// key1 +// key2 +``` + +### values() + +`values()`返回一个 iterator 对象,用来遍历所有键值。 + +```javascript +const params = new URLSearchParams("key1=value1&key2=value2"); + +for (const value of params.values()) { + console.log(value); +} +// value1 +// value2 +``` + +这个方法也可以用来将所有键值,转成一个数组。 + +```javascript +Array.from(params.values()) // ['value1', 'value2'] +``` + +### toString() + +`toString()`用来将 URLSearchParams 实例对象转成一个字符串。它返回的字符串不带问号,这一点与`window.location.search`不同。 + +## 实例属性 + +### size + +`size`是一个只读属性,返回键值对的总数。 + +```javascript +const params = new URLSearchParams("c=4&a=2&b=3&a=1"); +params.size; // 4 +``` + +上面示例中,键名`a`在查询字符串里面有两个,`size`不会将它们合并。 + +如果想统计不重复的键名,可以将使用 Set 结构。 + +```javascript +[...new Set(params.keys())].length // 3 +``` + +`size`属性可以用来判别,某个网址是否有查询字符串。 + +```javascript +const url = new URL("https://example.com?foo=1&bar=2"); + +if (url.searchParams.size) { + console.log("该 URL 有查询字符串"); +} +``` + diff --git a/docs/web-share-api.md b/docs/web-share-api.md index 72d8b2b..5be2cf0 100644 --- a/docs/web-share-api.md +++ b/docs/web-share-api.md @@ -2,7 +2,7 @@ ## 概述 -网页内容如果要分享到其他应用,通常要自己实现分享接口,逐一给出目标应用的连接方式。这样很麻烦,也对网页性能有一定影响。Web Share API 就是为了解决这个问题而提出的,允许网页调用操作系统的分享接口。 +网页内容如果要分享到其他应用,通常要自己实现分享接口,逐一给出目标应用的连接方式。这样很麻烦,也对网页性能有一定影响。Web Share API 就是为了解决这个问题而提出的,允许网页调用操作系统的分享接口,实质是 Web App 与本机的应用程序交换信息的一种方式。 这个 API 不仅可以改善网页性能,而且不限制分享目标的数量和类型。社交媒体应用、电子邮件、即时消息、以及本地系统安装的、且接受分享的应用,都会出现在系统的分享弹窗,这对手机网页尤其有用。另外,使用这个接口只需要一个分享按钮,而传统的网页分享有多个分享目标,就有多少个分享按钮。 @@ -73,8 +73,37 @@ shareButton.addEventListener('click', async () => { }); ``` +## 分享文件 + +这个 API 还可以分享文件,先使用`navigator.canShare()`方法,判断一下目标文件是否可以分享。因为不是所有文件都允许分享的,目前图像,视频,音频和文本文件可以分享2。 + +```javascript +if (navigator.canShare && navigator.canShare({ files: filesArray })) { + // ... +} +``` + +上面代码中,`navigator.canShare()`方法的参数对象,就是`navigator.share()`方法的参数对象。这里的关键是`files`属性,它的值是一个`FileList`实例对象。 + +`navigator.canShare()`方法返回一个布尔值,如果为`true`,就可以使用`navigator.share()`方法分享文件了。 + +```javascript +if (navigator.canShare && navigator.canShare({ files: filesArray })) { + navigator.share({ + files: filesArray, + title: 'Vacation Pictures', + text: 'Photos from September 27 to October 14.', + }) + .then(() => console.log('Share was successful.')) + .catch((error) => console.log('Sharing failed', error)); +} +``` + + + ## 参考链接 - [How to Use the Web Share API](https://css-tricks.com/how-to-use-the-web-share-api/), Ayooluwa Isaiah - [Web Share API - Level 1](https://wicg.github.io/web-share/), W3C - [Introducing the Web Share API](https://developers.google.com/web/updates/2016/09/navigator-share), Paul Kinlan, Sam Thorogood +- [Share like a native app with the Web Share API](https://web.dev/web-share/), Joe Medley diff --git a/docs/webaudio.md b/docs/webaudio.md index 6b59b69..9fdaea5 100644 --- a/docs/webaudio.md +++ b/docs/webaudio.md @@ -4,13 +4,32 @@ Web Audio API 用于操作声音。这个 API 可以让网页发出声音。 ## 基本用法 -浏览器原生提供`AudioContext`对象,该对象用于生成一个声音的上下文。 +浏览器原生提供`AudioContext`对象,该对象用于生成一个声音的上下文,与扬声器相连。 ```javascript const audioContext = new AudioContext(); ``` -然后,`context.createBuffer()`方法生成一个内存的操作视图,用于存放数据。 +然后,获取音源文件,将其在内存中解码,就可以播放声音了。 + +```javascript +const context = new AudioContext(); + +fetch('sound.mp4') + .then(response => response.arrayBuffer()) + .then(arrayBuffer => context.decodeAudioData(arrayBuffer)) + .then(audioBuffer => { + // 播放声音 + const source = context.createBufferSource(); + source.buffer = audioBuffer; + source.connect(context.destination); + source.start(); + }); +``` + +## context.createBuffer() + +`context.createBuffer()`方法生成一个内存的操作视图,用于存放数据。 ```javascript const buffer = audioContext.createBuffer(channels, signalLength, sampleRate); diff --git a/package.json b/package.json index b5edd51..01cf692 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build-and-commit": "npm run build && npm run commit", "commit": "gh-pages --dist dist --dest dist/webapi --branch master --add --repo git@github.com:wangdoc/website.git", "chapter": "loppo chapter", + "server": "loppo server", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -24,11 +25,9 @@ ], "author": "Ruan Yifeng", "license": "Creative Commons Attribution-ShareAlike License", - "devDependencies": {}, "dependencies": { - "gh-pages": "latest", - "husky": "^0.14.3", - "loppo": "latest", - "loppo-theme-wangdoc": "latest" + "gh-pages": "6.x", + "loppo": "^0.6.25", + "loppo-theme-wangdoc": "^0.7.1" } }