From 5152233e4b8fb55d2a5eb99620a1dbab1bcbb337 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 21 Mar 2020 20:18:33 +0800 Subject: [PATCH 01/46] docs: edit web share api --- docs/web-share-api.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) 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 From c9a2aa1613cd50b479afce025d319868d7bfe06f Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 2 May 2020 16:22:36 +0800 Subject: [PATCH 02/46] docs: edit web audio api --- docs/webaudio.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) 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); From 0d73bf24ebfa21e28aa68139aef430ba3b1cd841 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 2 May 2020 21:38:46 +0800 Subject: [PATCH 03/46] refactor: edit .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7ce50d5..7ad8318 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: -- '8' +- 'node' branches: only: From 3cc02dee52c7faca0cf3ee1e0119fbeddf13d865 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Fri, 16 Oct 2020 20:36:34 +0800 Subject: [PATCH 04/46] docs(svg): fix typo --- docs/svg.md | 2 +- package.json | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/svg.md b/docs/svg.md index df00a39..62d7b79 100644 --- a/docs/svg.md +++ b/docs/svg.md @@ -18,7 +18,7 @@ SVG 文件可以直接插入网页,成为 DOM 的一部分,然后用 JavaScr preserveAspectRatio="xMidYMid meet" > - + ``` diff --git a/package.json b/package.json index b5edd51..224d39e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,11 @@ "chapter": "loppo chapter", "test": "echo \"Error: no test specified\" && exit 1" }, + "husky": { + "hooks": { + "pre-push": "npm update" + } + }, "repository": { "type": "git", "url": "https://github.com/wangdoc/webapi-tutorial.git" @@ -27,7 +32,7 @@ "devDependencies": {}, "dependencies": { "gh-pages": "latest", - "husky": "^0.14.3", + "husky": "^4.3.0", "loppo": "latest", "loppo-theme-wangdoc": "latest" } From 1eca48a3c4709d462d8991768af12f194538daf1 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 16 Nov 2020 21:54:15 +0800 Subject: [PATCH 05/46] docs: edit IntersectionObserver --- docs/intersectionObserver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 参数 From be500721bc76a523ef6d26c6354e5d36c9b28a8a Mon Sep 17 00:00:00 2001 From: ruanyf Date: Tue, 29 Dec 2020 00:02:17 +0800 Subject: [PATCH 06/46] docs: add fetch API --- .github/workflows/wangdoc.yml | 36 ++ .travis.yml => .travis.yml.bak | 0 chapters.yml | 1 + docs/fetch.md | 739 +++++++++++++++++++-------------- package.json | 2 +- 5 files changed, 470 insertions(+), 308 deletions(-) create mode 100644 .github/workflows/wangdoc.yml rename .travis.yml => .travis.yml.bak (100%) diff --git a/.github/workflows/wangdoc.yml b/.github/workflows/wangdoc.yml new file mode 100644 index 0000000..8ba69af --- /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@v2 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@main + with: + node-version: '14' + - name: Install dependencies + run: npm install + - name: Build pages + run: npm run build + - name: Deploy to website + uses: JamesIves/github-pages-deploy-action@3.7.1 + with: + GIT_CONFIG_NAME: wangdoc-bot + GIT_CONFIG_EMAIL: yifeng.ruan@gmail.com + REPOSITORY_NAME: wangdoc/website + ACCESS_TOKEN: ${{ secrets.WANGDOC_BOT_TOKEN }} + BASE_BRANCH: master + 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 Web API tutorial diff --git a/.travis.yml b/.travis.yml.bak similarity index 100% rename from .travis.yml rename to .travis.yml.bak diff --git a/chapters.yml b/chapters.yml index 170bdd7..a3c9d07 100644 --- a/chapters.yml +++ b/chapters.yml @@ -1,4 +1,5 @@ - canvas.md: Canvas API +- fetch.md: Fetch API - fontface.md: FontFace API - geolocation.md: Geolocation API - intersectionObserver.md: IntersectionObserver diff --git a/docs/fetch.md b/docs/fetch.md index bff1130..0677d88 100644 --- a/docs/fetch.md +++ b/docs/fetch.md @@ -1,470 +1,595 @@ -# Fetch API +# Fetch API 教程 + +`fetch()`是 XMLHttpRequest 的升级版,用于在 JavaScript 脚本里面发出 HTTP 请求。 + +浏览器原生提供这个对象。本文详细介绍它的用法。 ## 基本用法 -Ajax 操作使用的`XMLHttpRequest`对象,已经有十多年的历史了,它的API设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。Fetch API 是一种新规范,用来取代`XMLHttpRequest`对象。 +`fetch()`的功能与 XMLHttpRequest 基本相同,但有三个主要的差异。 -它主要有两个特点,一是接口合理化,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),`response.json()`是一个异步操作,取出所有内容,并将其转为 JSON 对象。 -除了返回`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`里面,这样才能捕捉异步操作中可能发生的错误。 + +后文都采用`await`的写法,不使用`.then()`的写法。 -Fetch API 提供以下五个数据流读取器。 +## esponse 对象:处理 HTTP 回应 -- `.text()`:返回字符串 -- `.json()`:返回JSON对象 -- `.formData()`:返回`FormData`对象 -- `.blob()`:返回`blob`对象 -- `.arrayBuffer()`:返回二进制数组`ArrayBuffer`对象 +### Response 对象的同步属性 -数据流只能读取一次,一旦读取,数据流就空了。再次读取就不会得到结果。解决方法是在读取之前,先使用`.clone()`方法,复制一份一模一样的副本。 +`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() +**Response.ok** -`fetch`方法的第一个参数可以是 URL 字符串,也可以是后文要讲到的`Request`对象实例。`Fetch`方法返回一个`Promise`对象,并将一个`response`对象传给回调函数。 +`Response.ok`属性返回一个布尔值,表示请求是否成功,`true`对应 HTTP 请求的状态码 200 到 299,`false`对应其他的状态码。 -`response`对象有一个`ok`属性,如果返回的状态码在200到299之间(即请求成功),这个属性为`true`,否则为`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 +`Headers.forEach()`方法也可以遍历所有的键值和键名。 -Fetch API 引入三个新的对象(也是构造函数):`Headers`, `Request`和`Response`。其中,`Headers`对象用来构造/读取 HTTP 数据包的头信息。 +```javascript +let response = await fetch(url); +response.headers.forEach( + (value, key) => console.log(key, ':', value) +); +``` + +### 读取内容的方法 + +`Response`对象根据服务器返回的不同类型的数据,提供了不同的读取方法。 + +> - `response.text()`:得到文本字符串。 +> - `response.json()`:得到 JSON 对象。 +> - `response.blob()`:得到二进制 Blob 对象。 +> - `response.formData()`:得到 FormData 表单对象。 +> - `response.arrayBuffer()`:得到二进制 ArrayBuffer 对象。 + +上面5个读取方法都是异步的,返回的都是 Promise 对象。必须等到异步操作结束,才能得到服务器返回的完整数据。 + +**response.text()** + +`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(); -reqHeaders.get('Content-Length') // 11 -reqHeaders.getAll('X-Custom-Header') // ["ProcessThisImmediately", "AnotherValue"] +const response = await fetch('song.ogg'); +const buffer = await response.arrayBuffer(); -reqHeaders.delete('X-Custom-Header') -reqHeaders.getAll('X-Custom-Header') // [] +const decodeData = await audioCtx.decodeAudioData(buffer); +source.buffer = buffer; +source.connect(audioCtx.destination); +source.loop = true; ``` -生成 Header 实例以后,可以将它作为第二个参数,传入`Request`方法。 +上面示例是`response.arrayBuffer()`获取音频文件`song.ogg`,然后在线播放的例子。 + +### Response.clone() + +Stream 对象只能读取一次,读取完就没了。这意味着,前一节的五个读取方法,只能使用一个,否则会报错。 ```javascript -var headers = new Headers(); -headers.append('Accept', 'application/json'); -var request = new Request(URL, {headers: headers}); +let text = await response.text(); +let json = await response.json(); // 报错 +``` -fetch(request).then(function(response) { - console.log(response.headers); -}); +上面示例先使用了`response.text()`,就把 Stream 读完了。后面再调用`response.json()`,就没有内容可读了,所以报错。 + +Response 对象提供`Response.clone()`方法,创建`Response`对象的副本,实现多次读取。 + +```javascript +const response1 = await fetch('flowers.jpg'); +const response2 = response1.clone(); + +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()`方法每次返回一个对象,表示本次读取的内容块。 + +这个对象的`done`属性是一个布尔值,用来判断有没有读完;`value`属性是一个 arrayBuffer 数组,表示内容块的内容,而`value.length`属性是当前块的大小。 -## Request对象 +## `fetch()`的第二个参数:定制 HTTP 请求 -`Request`对象用来构造 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`,就与默认的浏览器行为没有不同,类似` + +``` + +### 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()`方法释放资源。 + +## 实例方法 + +### toString() + +URL 实例对象的`toString()`返回`URL.href`属性,即整个网址。 + From 47f33a8ea3e5c680594f6a83a6a52b47d635bbc5 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Tue, 12 Sep 2023 19:38:41 +0800 Subject: [PATCH 27/46] docs: add chapter URLSearchParams --- chapters.yml | 1 + docs/urlsearchparams.md | 298 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 docs/urlsearchparams.md diff --git a/chapters.yml b/chapters.yml index 4696bca..c2e4a9f 100644 --- a/chapters.yml +++ b/chapters.yml @@ -13,5 +13,6 @@ - server-sent-events.md: Server-Sent Events - svg.md: SVG 图像 - url.md: URL 对象 +- urlsearchparams.md: URLSearchParams 对象 - websocket.md: WebSocket - web-share-api.md: Web Share API diff --git a/docs/urlsearchparams.md b/docs/urlsearchparams.md new file mode 100644 index 0000000..f874547 --- /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(params); +``` + +上面示例中,`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 +``` + +这个方法也可以用来将所有键值,转成一个数组。 + +```javascritp +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 有查询字符串"); +} +``` + From 10ea6b227e4c2b07e22910b64987e27fb6558013 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 25 Sep 2023 23:10:14 +0800 Subject: [PATCH 28/46] docs(response): add example --- docs/response.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/response.md b/docs/response.md index 4d6becc..aa975ea 100644 --- a/docs/response.md +++ b/docs/response.md @@ -20,7 +20,7 @@ new Response(body, options) 它带有两个参数,都是可选的。 -第一个参数`body`代表服务器返回的数据体,必须是下面类型之一:ArrayBuffer、ArrayBufferView、Blob、FormData、ReadableStream、String、URLSearchParams。也就是说,它可以是对象或字符串。 +第一个参数`body`代表服务器返回的数据体,必须是下面类型之一:ArrayBuffer、ArrayBufferView、Blob、FormData、ReadableStream、String、URLSearchParams。 第二个参数`init`是一个对象,代表服务器返回的数据头,类型描述如下。 @@ -40,6 +40,24 @@ 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 From 0bd4cefbd4e21b2df1ff5b7965b3d8248d4dff2d Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 30 Sep 2023 00:03:39 +0800 Subject: [PATCH 29/46] docs(FormData): add FormData --- docs/formdata.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/formdata.md diff --git a/docs/formdata.md b/docs/formdata.md new file mode 100644 index 0000000..bdfc8bd --- /dev/null +++ b/docs/formdata.md @@ -0,0 +1,136 @@ +# FormData 对象 + +## 简介 + +FormData 代表表单数据,是浏览器的原生对象。 + +它可以当作构造函数使用,构造一个表单实例。 + +```javascript +const formData = new FormData(); +``` + +上面示例中,`FormData()`当作构造函数使用,返回一个空的表单实例对象。 + +它也可以接受一个表单的 DOM 节点当作参数,将表单的所有元素及其值,转换成一个个键值对,包含在返回的实例对象里面。 + +```javascript +const formData = new FormData(form); +``` + +上面示例中,`FormData()`的参数`form`就是一个表单的 DOM 节点对象。 + +下面是用法示例。 + +```javascript +const form = document.querySelector('#subscription'); + +try { + let response = await fetch('subscribe.php', { + method: 'POST', + body: new FormData(form), + }); + + const result = await response.json(); + + console.log(result); +} catch (error) { + console.log(error); +} +``` + +浏览器向服务器发送 FormData 对象时,不管是用户点击 Submit 按钮发送,还是使用脚本发送,都会自动将其编码,并以`Content-Type: multipart/form-data`的形式发送。 + +## 实例方法 + +### append() + +`append()`用于添加一个键值对,即添加一个表单元素。它有两种使用形式。 + +```javascript +FormData.append(name, value) +FormData.append(name, value, file) +``` + +它的第一个参数是键名,第二个参数是键值。 + +如果键名已经存在,它会为其添加新的键值。 + +### 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); +``` + +### get() + +`get()`用于获取指定键名的键值,它的参数为键名。 + +```javascript +FormData.get(name) +``` + +如果该键名有多个键值,只返回第一个键值。 + +### getAll() + +`getAll()`用于获取指定键名的所有键值,它的参数为键名,返回值为一个数组。 + +```javascript +FormData.getAll(name) +``` + +### has() + +`has()`返回一个布尔值,表示是否存在指定键名,它的参数为键名。 + +```javascript +FormData.has(name) +``` + +### keys() + +`key()`返回一个键名的迭代器,用于遍历所有键名。 + +```javascript +FormData.keys() +``` + +### set() + +`set()`用于为指定键名设置新的键值。它有两种使用形式。 + +```javascript +FormData.set(name, value); +FormData.set(name, value, filename); +``` + +它的第一个参数为键名,第二个参数为键值。如果指定键名不存在,它会添加该键名。 + +### value() + +`value()`返回一个键值的迭代器,用于遍历所有键值。 + +```javascript +FormData.values() +``` + From 9668565c2b0576b8ae90287c0793743ebf6197df Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 30 Sep 2023 16:45:12 +0800 Subject: [PATCH 30/46] docs(FormData): finish FormData --- chapters.yml | 1 + docs/formdata.md | 155 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 136 insertions(+), 20 deletions(-) diff --git a/chapters.yml b/chapters.yml index c2e4a9f..863e2d0 100644 --- a/chapters.yml +++ b/chapters.yml @@ -2,6 +2,7 @@ - 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 diff --git a/docs/formdata.md b/docs/formdata.md index bdfc8bd..c72d6ed 100644 --- a/docs/formdata.md +++ b/docs/formdata.md @@ -20,26 +20,53 @@ const formData = new FormData(form); 上面示例中,`FormData()`的参数`form`就是一个表单的 DOM 节点对象。 -下面是用法示例。 +下面是用法示例,通过脚本发送表单数据。 + +```html + + + Picture: + + + + +``` + +浏览器向服务器发送表单数据时,不管是用户点击 Submit 按钮发送,还是使用脚本发送,都会自动将其编码,并以`Content-Type: multipart/form-data`的形式发送。 + +`FormData()`还有第三种用法,如果想把“提交”(Submit)按钮也加入表单的键值对,可以把按钮的 DOM 节点当作`FormData()`的第二个参数。 ```javascript -const form = document.querySelector('#subscription'); +new FormData(form, submitter) +``` -try { - let response = await fetch('subscribe.php', { - method: 'POST', - body: new FormData(form), - }); +上面代码中,`submitter`就是提交按钮的 DOM 节点。这种用法适合表单有多个提交按钮,服务端需要通过按钮的值来判断,用户到底选用了哪个按钮。 - const result = await response.json(); +```javascript +// 表单有两个提交按钮 +// +// - console.log(result); -} catch (error) { - console.log(error); -} +const form = document.getElementById("form"); +const submitter = document.querySelector("button[value=save]"); + +const formData = new FormData(form, submitter); ``` -浏览器向服务器发送 FormData 对象时,不管是用户点击 Submit 按钮发送,还是使用脚本发送,都会自动将其编码,并以`Content-Type: multipart/form-data`的形式发送。 +上面示例中,`FormData()`加入了第二个参数,实例对象`formData`就会增加一个键值对,键名为`intent`,键值为`save`。 ## 实例方法 @@ -49,12 +76,57 @@ try { ```javascript FormData.append(name, value) -FormData.append(name, value, file) +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() @@ -81,6 +153,19 @@ 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()`用于获取指定键名的键值,它的参数为键名。 @@ -89,11 +174,11 @@ console.log(values); FormData.get(name) ``` -如果该键名有多个键值,只返回第一个键值。 +如果该键名有多个键值,只返回第一个键值。如果找不到指定键名,则返回`null`。 ### getAll() -`getAll()`用于获取指定键名的所有键值,它的参数为键名,返回值为一个数组。 +`getAll()`用于获取指定键名的所有键值,它的参数为键名,返回值为一个数组。如果找不到指定键名,则返回一个空数组。 ```javascript FormData.getAll(name) @@ -115,16 +200,32 @@ FormData.has(name) 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, value, filename); +FormData.set(name, blob, fileName); ``` -它的第一个参数为键名,第二个参数为键值。如果指定键名不存在,它会添加该键名。 +它的第一个参数为键名,第二个参数为键值。上面第二种形式为上传文件,第二个参数`blob`为文件的二进制内容,第三个参数`fileName`为文件名。该方法没有返回值。 + +如果指定键名不存在,它会添加该键名,否则它会丢弃所有现有的键值,确保一个键名只有一个键值。这是它跟`append()`的主要区别。 ### value() @@ -134,3 +235,17 @@ FormData.set(name, value, filename); 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 +``` + From f0b4a1e514e0d7e7a4a64bd7dcf5ead3f48044bf Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 5 Oct 2023 18:57:12 +0800 Subject: [PATCH 31/46] docs: add Intl.Segmenter API --- chapters.yml | 1 + docs/intl-segmenter.md | 225 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 docs/intl-segmenter.md diff --git a/chapters.yml b/chapters.yml index 863e2d0..1333738 100644 --- a/chapters.yml +++ b/chapters.yml @@ -7,6 +7,7 @@ - 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 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`。 + From 124037f09734826fe8bfe18f1a4820c4504bc14c Mon Sep 17 00:00:00 2001 From: mashiguang Date: Wed, 11 Oct 2023 18:21:34 +0800 Subject: [PATCH 32/46] docs(FormData): value() -> values() --- docs/formdata.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/formdata.md b/docs/formdata.md index c72d6ed..9ede3f0 100644 --- a/docs/formdata.md +++ b/docs/formdata.md @@ -227,9 +227,9 @@ FormData.set(name, blob, fileName); 如果指定键名不存在,它会添加该键名,否则它会丢弃所有现有的键值,确保一个键名只有一个键值。这是它跟`append()`的主要区别。 -### value() +### values() -`value()`返回一个键值的迭代器,用于遍历所有键值。 +`values()`返回一个键值的迭代器,用于遍历所有键值。 ```javascript FormData.values() From ff5c861de326216b181904824b5ed47bb16fcbf1 Mon Sep 17 00:00:00 2001 From: Chavin Date: Wed, 25 Oct 2023 23:41:19 +0800 Subject: [PATCH 33/46] fixed a bad svg demo --- docs/svg.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/svg.md b/docs/svg.md index 62d7b79..a9fc228 100644 --- a/docs/svg.md +++ b/docs/svg.md @@ -452,20 +452,22 @@ Date |Amount ```xml - - - - + + + + + + - - $10 - $80 + + $10 + $80 - - January 2014 - April + + Jan. + Apr. From cbf0de0aab7d49d8609d3dc71746516efb83fa57 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sun, 3 Dec 2023 13:20:51 +0800 Subject: [PATCH 34/46] docs(url): add URL.canParse() --- docs/url.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/url.md b/docs/url.md index d2f58f5..ea04cb8 100644 --- a/docs/url.md +++ b/docs/url.md @@ -171,6 +171,52 @@ function handleFiles(files) { 上面代码中,一旦图片加载成功以后,为本地文件生成的临时网址就没用了,于是可以在`img.onload`回调函数里面,通过`URL.revokeObjectURL()`方法释放资源。 +### URL.canParse() + +`URL.canParse()`用来检测一个字符串是否为有效 URL,它返回一个布尔值。 + +```javascipt +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()`。 + ## 实例方法 ### toString() From 9195d6523e7d024faa0f0dfc0e169e3c3c03997a Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 10 Apr 2024 16:46:46 +0800 Subject: [PATCH 35/46] docs(url): fixed typo --- docs/url.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/url.md b/docs/url.md index ea04cb8..10244bd 100644 --- a/docs/url.md +++ b/docs/url.md @@ -18,10 +18,10 @@ let url = new URL('https://example.com'); ```javascript const url1 = new URL('page2.html', 'http://example.com/page1.html'); -url2.href // "http://example.com/page2.html" +url1.href // "http://example.com/page2.html" const url2 = new URL('..', 'http://example.com/a/b.html') -url3.href // "http://example.com/" +url2.href // "http://example.com/" ``` 这种写法很方便基于现有网址,构造新的 URL。 From c07d5cb2d97dd2bec773fa7ea3fc14b1d79175b7 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Thu, 11 Apr 2024 14:28:33 +0800 Subject: [PATCH 36/46] docs(URLSearchParams): fixed typo --- docs/urlsearchparams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/urlsearchparams.md b/docs/urlsearchparams.md index f874547..44c8000 100644 --- a/docs/urlsearchparams.md +++ b/docs/urlsearchparams.md @@ -63,7 +63,7 @@ fetch('https://example.com/api', { ```javascript const params1 = new URLSearchParams('?a=1&b=2'); -const params2 = new URLSearchParams(params); +const params2 = new URLSearchParams(params1); ``` 上面示例中,`params1`和`params2`是两个一模一样的实例对象,但是修改其中一个,不会影响到另一个。 From dfdd05261b7bd40f190e04650e4cd9e35d30195e Mon Sep 17 00:00:00 2001 From: ruanyf Date: Mon, 20 May 2024 11:40:22 +0800 Subject: [PATCH 37/46] docs(url): add URL.parse() --- docs/url.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/url.md b/docs/url.md index 10244bd..1cba9dc 100644 --- a/docs/url.md +++ b/docs/url.md @@ -173,7 +173,7 @@ function handleFiles(files) { ### URL.canParse() -`URL.canParse()`用来检测一个字符串是否为有效 URL,它返回一个布尔值。 +`URL()`构造函数解析非法网址时,会抛出错误,必须用`try...catch`代码块处理,这样终究不是非常方便。因此,URL 对象又引入了`URL.canParse()`方法,它返回一个布尔值,表示当前字符串是否为有效网址。 ```javascipt URL.canParse(url) @@ -217,6 +217,20 @@ function isUrlValid(string) { 上面示例中,给出了`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() From a0502cce821d25316ad8ebcc95214234aede422a Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 19 Jun 2024 10:05:48 +0800 Subject: [PATCH 38/46] refactor: update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 30c6cd9..01cf692 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "author": "Ruan Yifeng", "license": "Creative Commons Attribution-ShareAlike License", "dependencies": { - "gh-pages": "^6.0.0", + "gh-pages": "6.x", "loppo": "^0.6.25", - "loppo-theme-wangdoc": "^0.6.6" + "loppo-theme-wangdoc": "^0.7.1" } } From 9a028ab1f38f842e57dbb730754e97c7ddf0a3f0 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 19 Jun 2024 10:09:23 +0800 Subject: [PATCH 39/46] refactor: update github action script --- .github/workflows/wangdoc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wangdoc.yml b/.github/workflows/wangdoc.yml index d05b96f..5f718e6 100644 --- a/.github/workflows/wangdoc.yml +++ b/.github/workflows/wangdoc.yml @@ -10,11 +10,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@main + uses: actions/setup-node@v4 with: node-version: 'latest' - name: Install dependencies From d27b94c66687a689e5270eec56aa91c0a9cea6e0 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 19 Jun 2024 17:28:28 +0800 Subject: [PATCH 40/46] docs: add URL Pattern API --- chapters.yml | 1 + docs/urlpattern.md | 547 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 548 insertions(+) create mode 100644 docs/urlpattern.md diff --git a/chapters.yml b/chapters.yml index 1333738..c0b59cb 100644 --- a/chapters.yml +++ b/chapters.yml @@ -15,6 +15,7 @@ - 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 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") // 报错 +``` + From c9eee236955e8b1791a075654249378304d3029e Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 26 Jun 2024 17:36:05 +0800 Subject: [PATCH 41/46] docs: add window.postMessage() --- chapters.yml | 1 + docs/postmessage.md | 116 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 docs/postmessage.md diff --git a/chapters.yml b/chapters.yml index c0b59cb..39dabaa 100644 --- a/chapters.yml +++ b/chapters.yml @@ -19,3 +19,4 @@ - urlsearchparams.md: URLSearchParams 对象 - websocket.md: WebSocket - web-share-api.md: Web Share API +- postmessage.md: window.postMessage() 方法 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'); +} +``` + From e97b52a502bdbc1cc0269b093a44c6e408ff72cb Mon Sep 17 00:00:00 2001 From: ruanyf Date: Sat, 7 Dec 2024 20:33:34 +0800 Subject: [PATCH 42/46] docs(headers): fixed typo --- docs/headers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/headers.md b/docs/headers.md index dd121fa..7140c6d 100644 --- a/docs/headers.md +++ b/docs/headers.md @@ -6,7 +6,7 @@ Headers 代表 HTTP 消息的数据头。 它通过`Headers()`构造方法,生成实例对象。`Request.headers`属性和`Response.headers`属性,指向的都是 Headers 实例对象。 -Headers 实例对象内部,以键值对的形式保存 HTTP 消息头,可以用`for...of`循环进行便利,比如`for (const p of myHeaders)`。新建的 Headers 实例对象,内部是空的,需要用`append()`方法添加键值对。 +Headers 实例对象内部,以键值对的形式保存 HTTP 消息头,可以用`for...of`循环进行遍历,比如`for (const p of myHeaders)`。新建的 Headers 实例对象,内部是空的,需要用`append()`方法添加键值对。 ## 构造函数 From 2354874a26a1efdad22ff473874360259a3e909f Mon Sep 17 00:00:00 2001 From: TaoZiNiGePiChi <73526234+TaoZiNiGePiChi@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:16:16 +0800 Subject: [PATCH 43/46] docs: fix typo in fontface.md --- docs/fontface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fontface.md b/docs/fontface.md index fdb7708..a69d769 100644 --- a/docs/fontface.md +++ b/docs/fontface.md @@ -31,7 +31,7 @@ fontFace.family // "Roboto" - `FontFace.display`:字符串,指定字体加载期间如何展示,等同于 CSS 的`font-display`属性。它有五个可能的值:`auto`(由浏览器决定)、`block`(字体加载期间,前3秒会显示不出内容,然后只要还没完成加载,就一直显示后备字体)、`fallback`(前100毫秒显示不出内容,后3秒显示后备字体,然后只要字体还没完成加载,就一直显示后备字体)、`optional`(前100毫秒显示不出内容,然后只要字体还没有完成加载,就一直显示后备字体),`swap`(只要字体没有完成加载,就一直显示后备字体)。 - `FontFace.style`:字符串,等同于 CSS 的`font-style`属性。 - `FontFace.weight`:字符串,等同于 CSS 的`font-weight`属性。 -- `FontFace.stretch`:字符串,等同于 CSS 的`font-strentch`属性。 +- `FontFace.stretch`:字符串,等同于 CSS 的`font-stretch`属性。 - `FontFace.unicodeRange`:字符串,等同于`descriptors`对象的同名属性。 - `FontFace.variant`:字符串,等同于`descriptors`对象的同名属性。 - `FontFace.featureSettings`:字符串,等同于`descriptors`对象的同名属性。 From 0d4855b60b06a81faa1733ce96ccb0f9d60dd3a5 Mon Sep 17 00:00:00 2001 From: TaoZiNiGePiChi <73526234+TaoZiNiGePiChi@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:38:51 +0800 Subject: [PATCH 44/46] docs: fix typo in formdata.md --- docs/formdata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/formdata.md b/docs/formdata.md index 9ede3f0..41aa211 100644 --- a/docs/formdata.md +++ b/docs/formdata.md @@ -194,7 +194,7 @@ FormData.has(name) ### keys() -`key()`返回一个键名的迭代器,用于遍历所有键名。 +`keys()`返回一个键名的迭代器,用于遍历所有键名。 ```javascript FormData.keys() From ad6ccbccd540d0cb6766653326737af873deceb3 Mon Sep 17 00:00:00 2001 From: uphg <49513387+uphg@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:41:04 +0800 Subject: [PATCH 45/46] docs: fix code block name in urlsearchparams.md --- docs/urlsearchparams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/urlsearchparams.md b/docs/urlsearchparams.md index 44c8000..59163c1 100644 --- a/docs/urlsearchparams.md +++ b/docs/urlsearchparams.md @@ -259,7 +259,7 @@ for (const value of params.values()) { 这个方法也可以用来将所有键值,转成一个数组。 -```javascritp +```javascript Array.from(params.values()) // ['value1', 'value2'] ``` From 79180175f3db83bfd51708b9f1669fb089883771 Mon Sep 17 00:00:00 2001 From: uphg <49513387+uphg@users.noreply.github.com> Date: Mon, 13 Oct 2025 14:01:43 +0800 Subject: [PATCH 46/46] docs: fix code block name in url.md --- docs/url.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/url.md b/docs/url.md index 1cba9dc..cde7abf 100644 --- a/docs/url.md +++ b/docs/url.md @@ -175,7 +175,7 @@ function handleFiles(files) { `URL()`构造函数解析非法网址时,会抛出错误,必须用`try...catch`代码块处理,这样终究不是非常方便。因此,URL 对象又引入了`URL.canParse()`方法,它返回一个布尔值,表示当前字符串是否为有效网址。 -```javascipt +```javascript URL.canParse(url) URL.canParse(url, base) ```