From 044e9a1c8889190ca04a270c34ed47661731e5eb Mon Sep 17 00:00:00 2001 From: linhieng <790728740@qq.com> Date: Tue, 9 Apr 2024 22:23:54 +0800 Subject: [PATCH 01/12] migrate lim-note-web repo to here --- web/Ajv/README.md | 9 + web/CSS/scss.md | 123 ++ web/Docker/draft.md | 85 ++ web/ES6/ES_Version.md | 237 +++ web/ES6/README.md | 226 +++ web/ES6/basic.md2 | 226 +++ web/ES6/draft.md | 71 + web/ES6/promise-basic.md | 720 ++++++++++ web/ES6/promise-glossary.md | 170 +++ web/ESLint/README.md | 113 ++ web/Express/draft.md | 296 ++++ web/Express/middleware.md | 221 +++ web/Jest/README.md | 175 +++ ...15\347\275\256\346\226\207\344\273\266.md" | 52 + web/Nginx/draft.md | 136 ++ web/Nginx/old.conf | 140 ++ web/Nodejs/README.md | 5 + web/Prettier/.prettierrc | 5 + web/Prettier/README.md | 23 + web/Rollup/README.md | 10 + web/Rollup/base-use.md | 218 +++ web/Svelte/README.md | 7 + web/Svelte/SvelteKit-base-use.md | 5 + web/Svelte/base-use.md | 1247 ++++++++++++++++ web/Svelte/draft.md | 41 + web/Typescript/README.md | 172 +++ .../Typescript/TS\347\211\271\346\200\247.md" | 19 + web/Typescript/draft.md | 487 +++++++ web/Typescript/how.md | 34 + ...15\347\275\256\346\226\207\344\273\266.md" | 16 + .../README.md" | 21 + ...20\347\240\201\350\247\243\346\236\220.md" | 1267 +++++++++++++++++ .../draft.md" | 120 ++ web/webpack/README.md | 22 + .../README.md" | 144 ++ .../commitlint.md" | 61 + 36 files changed, 6924 insertions(+) create mode 100644 web/Ajv/README.md create mode 100644 web/CSS/scss.md create mode 100644 web/Docker/draft.md create mode 100644 web/ES6/ES_Version.md create mode 100644 web/ES6/README.md create mode 100644 web/ES6/basic.md2 create mode 100644 web/ES6/draft.md create mode 100644 web/ES6/promise-basic.md create mode 100644 web/ES6/promise-glossary.md create mode 100644 web/ESLint/README.md create mode 100644 web/Express/draft.md create mode 100644 web/Express/middleware.md create mode 100644 web/Jest/README.md create mode 100644 "web/Jest/\351\205\215\347\275\256\346\226\207\344\273\266.md" create mode 100644 web/Nginx/draft.md create mode 100644 web/Nginx/old.conf create mode 100644 web/Nodejs/README.md create mode 100644 web/Prettier/.prettierrc create mode 100644 web/Prettier/README.md create mode 100644 web/Rollup/README.md create mode 100644 web/Rollup/base-use.md create mode 100644 web/Svelte/README.md create mode 100644 web/Svelte/SvelteKit-base-use.md create mode 100644 web/Svelte/base-use.md create mode 100644 web/Svelte/draft.md create mode 100644 web/Typescript/README.md create mode 100644 "web/Typescript/TS\347\211\271\346\200\247.md" create mode 100644 web/Typescript/draft.md create mode 100644 web/Typescript/how.md create mode 100644 "web/Typescript/\351\205\215\347\275\256\346\226\207\344\273\266.md" create mode 100644 "web/Vue/\345\223\215\345\272\224\345\274\217/README.md" create mode 100644 "web/Vue/\345\223\215\345\272\224\345\274\217/reactive\346\272\220\347\240\201\350\247\243\346\236\220.md" create mode 100644 "web/Vue/\350\204\232\346\211\213\346\236\266/draft.md" create mode 100644 web/webpack/README.md create mode 100644 "web/\346\265\201\347\250\213\347\256\241\347\220\206/README.md" create mode 100644 "web/\346\265\201\347\250\213\347\256\241\347\220\206/commitlint.md" diff --git a/web/Ajv/README.md b/web/Ajv/README.md new file mode 100644 index 0000000..1eb8f2e --- /dev/null +++ b/web/Ajv/README.md @@ -0,0 +1,9 @@ +# JSON-Schema 校验 + +Ajv 可以用于校验 json 的数据类型。(相见恨晚呀,很多开发的痛点,都是有对应库支持的!) + +## 简介 + +1. 自定义 format +2. 自定义 keywords +3. 自定义错误提示信息,需要安装 ajv-errors 库 diff --git a/web/CSS/scss.md b/web/CSS/scss.md new file mode 100644 index 0000000..d674963 --- /dev/null +++ b/web/CSS/scss.md @@ -0,0 +1,123 @@ +# scss + +sass 语法更简洁,可以省略大括号、分号。类似于 python。 + +- `$var` +- `#{express}` 插值表达式,用于将变量的值插入到字符串中 +- `@mixin`, `@include` +- `@extend` +- `@for` +- `@function`, `@return` + - `random()` + - `floor()` + +## draft + +sass 导入 mixin + 需要使用 as * + @use './mixin' as *; + 或者使用 + @import './mixin'; + 可以统一在 index.scss 中使用 @import + + +## 案例 + +```scss +// 封装媒体查询 +@mixin sm { + @media screen and (max-height: 799px) { // 媒体查询可不止能查宽度 + @content; + } +} +.wrapper { + @include sm { + li:nth-child(1) { + width: 100px; + } + } +} + + + + +// 定义: +@mixin important-text { + color: red; + font-weight: bold; +} +@mixin transform($property) { + -webkit-transform: $property; + -ms-transform: $property; + transform: $property; +} +// 使用: +.danger { + @include important-text; + @include transform(scale(10)); +} + + + +// 变量 +{ + $accent: #ed2553; + color: $accent; +} + + + +// 继承 +.button-block { + display: block; + width: 100%; +} +.button { + @extend .button-block; +} + + + + +// through 代表包含 33,如果将 through 换成 to 则不包含 33 +@for $i from 1 through 33 { + // 使用#{$i}引用该变量 + .shard-wrap:nth-child(#{$i}) .shard { + $m: 1+$i/10; + transition-duration: #{$m}s; + } +} + +@for $i from 0 to 3 { + .avatar:nth-of-type(#{$i+1}) { + $x: 40 + 100 * $i + '%'; + transform: translateX(-#{$x}); + } +} + + +@for $i from 0 to 5 { + #Polygon-#{$i + 1} { + $x: 0.2 * $i + 's'; + animation-delay: #{$x}; + } +} + + +// 变量是可以是一个类似数组,然后配合 @for 和 length() 使用 +$slide-bgs: url(''), url(''), url(''); +@for $i from 1 through length($slide-bgs) { + div:nth-child(#{$i}) { + background: nth($slide-bgs, $i); + } +} + + + +// 可以自定义一个函数,比如生成随机数函数 +@function random_range($min, $max) { + $rand: random(); + $random_range: $min + floor($rand * (($max - $min) + 1)); + @return $random_range; +} +``` diff --git a/web/Docker/draft.md b/web/Docker/draft.md new file mode 100644 index 0000000..cd82746 --- /dev/null +++ b/web/Docker/draft.md @@ -0,0 +1,85 @@ +# docker 草稿 + +## docker 网络问题 + +docker 容器中的 git 使用的依旧是本机中的 git,所以相关配置也是使用本机的配置。 +而本机为 git 配置了 http.proxy 为 127.0.0.1:7890,但在容器中的 127.0.0.1:7890 +并不是本机的地址,这一点要注意!同理,其他相关网络问题也是如此! + +所以,可以单独为容器中的项目进行相关配置,比如 `git config --local http.proxy ''`。 +或者可以考虑端口转发,但这个比较麻烦,以后再弄。 + + +## 开始:在 container 学习 rust + +安装 vscode 插件: + +- remote.remote-containers +- remote.vscode-remote-extensionpack + +```sh +git clone https://github.com/microsoft/vscode-remote-try-rust.git +``` + +1. vscode 中关闭 http.proxy 代理配置。 +2. Windows 中启动系统全局代理。 +3. 打开 docker。 +4. vscode 中运行 `Dev Containers: Open Folder in Containers...`,打开刚刚克隆的仓库。 +5. 然后 show log,可以看到 vscode 在下载一系列依赖 +6. 此时 docker 中会多了一个名为 vscode 的 volume +7. 下载完成后,docker 中会多出一个名为 mcr.microsoft.com/devcontainers/rust 的 images (大小为 2.2GB) +8. 同时也会多出一个名为 gallant_ganguly 的 containers,它的 images 就是 mcr.microsoft.com/devcontainers/rust +9. 此时 vscode 应该处于容器里面了,目录就是刚刚克隆的仓库。 + +在目录中执行以下命令 + +```sh +# 当前目录是 /workspaces/vscode-remote-try-rust + +# 初次进入容器时,不是 root 用户,并且此时默认没有 root 密码 +# 所以需要设置初始密码 +sudo passwd root + +# 默认的用户是 vscode +whoami + +cd .. +# 进入 /workspaces + +sudo git clone https://github.com/Linhieng/learn-rust.git +# 刚刚克隆的 vscode-remote-try-rust 项目没有用,它只是用来帮助下载 rust 镜像的 +# 实际学习中,使用自己的仓库。 + +sudo chown vscode:vscode learn-rust/ +# 将所有者改为 vscode + +cd learn-rust +code . +# 打开新窗口,注意此时还是使用的系统全局代理,而不是 vscode 中的 http.proxy +``` + +需要注意的是,vscode 的本机 workspace 不支持容器中的目录! + +## docker 中的 container、images 和 volumes + +在Docker中,有三个核心概念:容器(Container)、镜像(Image)和卷(Volume)。这些概念是 Docker 的基础,了解它们可以帮助您更好地使用 Docker 来构建、部署和管理应用程序。 + +1. 容器(Container): + + - 容器是 Docker 运行时的实体,它封装了应用程序及其所有的依赖项(包括运行时、库、环境变量等)。 + - 容器基于镜像启动,每个容器都是一个独立的、隔离的环境,可以在其中运行应用程序。 + - 容器可以被创建、启动、停止、删除和暂停等操作。 + +1. 镜像(Image): + + - 镜像是容器的模板。它是一个只读的文件,包含了运行应用程序所需的所有文件系统内容、运行时、库、环境变量等信息。 + - 镜像可以看作是一个类比于面向对象编程中的类,而容器则是类的实例。 + - 通过 Dockerfile 或者拉取已有的镜像(从 Docker Hub 等镜像仓库)来创建镜像。 + +1. 卷(Volume): + + - 卷是持久化存储的一种方式,它提供了容器之间共享数据或者将数据持久化到主机上的方法。 + - 使用卷可以将容器内部的文件系统与主机文件系统进行关联,以实现数据的持久化、共享和备份等操作。 + - 卷可以在容器创建时被挂载,也可以在容器运行时进行挂载。 + +总的来说,容器是运行时的实例,镜像是容器的模板,而卷则用于持久化存储和数据共享。使用这些概念,您可以轻松地创建、管理和部署 Docker 应用程序,并确保它们的可靠性和可移植性。 diff --git a/web/ES6/ES_Version.md b/web/ES6/ES_Version.md new file mode 100644 index 0000000..ffa388d --- /dev/null +++ b/web/ES6/ES_Version.md @@ -0,0 +1,237 @@ +# ES 版本历史 + +内容摘录自 [w3schools](https://www.w3schools.com/js/js_versions.asp)。 +写在这里仅做复习参考 + +## ES5 (2009) 新特性 + +- "use strict" +- 字符串 + - String[number] access + - Multiline strings + - trim() +- 数组 + - Array.isArray() + - forEach() + - map() + - filter() + - reduce() + - reduceRight() + - every() + - some() + - indexOf() + - lastIndexOf() +- JSON + - JSON.parse() + - JSON.stringify() +- Date + - Date.now() + - toISOString() + - toJSON() +- 对象 + - Property getters and setters. + - Reserved words as property names + - + - Object.create(parent, donor) + - Object.defineProperty(object, property, descriptor) + - Object.defineProperties(object, descriptors) + - Object.getOwnPropertyDescriptor(object, property) + - Object.getOwnPropertyNames(object) + - Object.getPrototypeOf(object) + - Object.keys(object) + - + - Object.preventExtensions(object) + - Object.isExtensible(object) + - Object.seal(object) + - Object.isSealed(object) + - Object.freeze(object) + - Object.isFrozen(object) +- Function bind() +- Trailing commas + - ES5 allows trailing commas in object and array definitions. + +对象描述符(descriptor) +- value +- writable +- enumerable +- configurable + +## ES6 (2015) 新特性 + +- 变量声明关键字 + - The let keyword + - The const keyword +- Arrow Functions + - 箭头函数内的 this 永远保持为闭合词法上下文的值 +- The Spread (...) Operator +- for..of + - 注意,for..in 在 ES1 (1997) 的时候就出现了 +- Map Objects 和 Set Objects + - Map 最重要的特性就是支持将对象作为 key +- Classes 注意 js 中的类不是对象,而是对象的模版 +- Promises +- Symbol +- Default Parameters 支持函数定义默认参数值 +- Function Rest Parameter 函数参数支持使用 ... 获取剩余参数 +- 字符串 + - String.includes() + - String.startsWith() + - String.endsWith() +- 数组 + - Array.from() + - Array.of() + - keys() + - find() + - findIndex() + - entries() +- New Math Methods + - Math.trunc() + - Math.sign() + - Math.cbrt() + - Math.log2() + - Math.log10() +- New Number Properties + - Number.EPSILON + - Number.MIN_SAFE_INTEGER + - Number.MAX_SAFE_INTEGER +- New Number Methods + - Number.isInteger() + - Number.isSafeInteger() 范围是 `1 - 2**53` 到 `2**53 - 1` (均包含) +- New Global Methods + - isFinite() + - isNaN() +- JavaScript Modules 浏览器支持模块 + +## ES 2016 新特性 + +- JavaScript Exponentiation (**) +- JavaScript Exponentiation assignment (**=) +- JavaScript Array includes() + +## ES 2017 新特性 + +- 字符串 + - padStart() + - padEnd() +- 对象 + - Object.entries() + - 可以更方便的转换对象为 Map。`new Map(Object.entries(obj))` + - Object.values() + - Object.getOwnPropertyDescriptors +- async 和 await +- Trailing Commas in Functions + +## ES 2018 新特性 + +- Asynchronous Iteration +- Promise.prototype.finally() +- 支持对象结构赋值 `const {x, y, z} = {x:1, y:2, z:3}` +- New RegExp Features + - Unicode Property Escapes (`\p{...}`) + - Lookbehind Assertions (`?<=` ) and (`? new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'success') }) +const p2 = () => new Promise((resolve, reject) => { setTimeout(reject, 1000, 'failure') }) +const arr = [p1(), p2()] +Promise.allSettled(arr).then(val => { + console.log(val) + // [ + // { status: 'fulfilled', value: 'success' }, + // { status: 'rejected', reason: 'failure' } + // ] +}) + +// 如果使用 all,由于其中一个异步任务返回 reject,所以最终会报错 +Promise.all(arr).then(val => { + console.log(val) +}).catch(reason => { + console.log(reason) + // 只输出 failure +}) + +``` + + +## ES2021 + +- 新增 Promise.any() + - 只返回第一个兑现的值。如果全部都被拒绝,则也返回拒绝 +- 新增 String.prototype.replaceAll() +- 新增 JavaScript Numeric Separator (_) + - 注意只能用于中间,比如 `1_000_000_000` + +```js +const p1 = () => new Promise((resolve, reject) => { setTimeout(reject, 1000, 'failure1') }) +const p2 = () => new Promise((resolve, reject) => { setTimeout(reject, 1000, 'failure2') }) +Promise.any([p1(), p2()]) + .then(res => console.log(res)) + .catch(reasons => console.log(reasons.errors)) + // [ 'failure1', 'failure2' ] +``` + +## ES2022 + +- 数组 + - at() 用于实现负数反向索引 +- 字符串 + - at() +- 正则 `d` 标识 +- 对象 + - Object.hasOwn() +- 支持 Error Cause +- 支持 await import +- 类 + - 支持 Class Field Declarations + - 支持 Private Methods and Fields。使用 `#` 前缀标识 + +## ES2023 + +- 数组 + - findLast() + - findLastIndex() + - toReversed() 返回新数组,而不修改原数组 + - toSorted() 返回新数组,而不修改原数组 + - toSpliced() 返回新数组,而不修改原数组 + - splice 早在 chrome 1 就支持 +- `#!/usr/bin/env node` diff --git a/web/ES6/README.md b/web/ES6/README.md new file mode 100644 index 0000000..439b851 --- /dev/null +++ b/web/ES6/README.md @@ -0,0 +1,226 @@ +# + +## [元编程]:代理和反射 + +反射和代理是一对双胞胎,代理支持的所以捕获器,都可以在反射上面找到。 + +代理可以让你定义陷阱(traps),用于捕获相关操作。但它仅仅只是捕获,它并不会为你提供捕获到的操作的原始行为。这个时候就可以使用反射了。反射提供的 api 和代理相同,通过反射调用的 api,和直接操作 target 没有什么不同。那我们为什么不直接操作 target 呢?还专门创建了一个反射?我的理解就是这就是为了方便开发。你想想,当你用代理捕获 get 行为时,如果你要直接操作 target,那么你编写的代码就是 return 一个 `target[property]`。这是简单的情况,因为你很清楚 get 代理的是获取属性这一行为,但如果你是代理一个 has 行为呢?是不是突然想不起来 has 代理的是什么行为了?这个时候如果有反射,那么你就直接调用 Reflect.has。也正是因为有了这个反射,所以你代理一个对象时,你需要编写 get,而不需要再次实现 set 等操作,因为这些操作内部都帮你调用了 Reflect。 + +此外,反射还可以帮你将抛出错误转换为 false,比如下面案例: + +不使用反射时: + +```js +const obj = Object.defineProperty({}, 'foo', { + value: 'bar', + configurable: false +}) + +try { + // 定义一个属性时,可能抛出错误,所以我们需要使用 try-catch + Object.defineProperty(obj, 'foo', {value: 'baz'}) + console.log('success') +} catch (error) { + console.log('failure') +} +``` + +使用反射重构上面代码: + +```js +const obj = Object.defineProperty({}, 'foo', { + value: 'bar', + configurable: false +}) + +if (Reflect.defineProperty(obj, 'foo', {value: 'baz'})) { + console.log('success') +} else { + console.log('failure') +} +``` + +可以看到,比起 trycatch,反射可以让我们简单的使用 if else,这不是更方便吗? + +### api 参考 + +只对应 Object.* 和 Reflect.* 的: +- [handler.setPrototypeOf()] + - 拦截 [Object.setPrototypeOf()] + - 拦截 [Reflect.setPrototypeOf()] +- [handler.isExtensible()] + - 拦截 [Object.isExtensible()] + - 拦截 [Reflect.isExtensible()] +- [handler.preventExtensions()] + - 拦截 [Object.preventExtensions()] + - 拦截 [Reflect.preventExtensions()] +- [handler.getOwnPropertyDescriptor()] + - 拦截 [Object.getOwnPropertyDescriptor()] + - 拦截 [Reflect.getOwnPropertyDescriptor()] +- [handler.defineProperty()] + - 拦截 [Object.defineProperty()] + - 拦截 [Reflect.defineProperty()] + +- [handler.get()] + - 拦截 `proxy[foo]` 括号表示法(bracket notation)访问属性 + - 拦截 `proxy.bar` 点表示法(dot notation)访问属性 + - 拦截 `Object.create(proxy)[foo]` 通过原型链访问属性 + - 拦截 [Reflect.get()] + +- [handler.set()] + - 拦截 `proxy[foo] = bar` + - 拦截 `proxy.foo = bar` + - 拦截 `Object.create(proxy)[foo] = bar` + - 拦截 [Reflect.set()] + +- [handler.has()] + - 拦截 `foo in proxy` + - 拦截 `foo in Object.create(proxy)` + - 拦截 [Reflect.has()] + +- [handler.getPrototypeOf()] + - 拦截 [Object.getPrototypeOf()] + - 拦截 [Reflect.getPrototypeOf()] + - 拦截 [`__proto__`] + - 拦截 [Object.prototype.isPrototypeOf()] + - 拦截 [instanceof] + - 拦截 [Reflect.getPrototypeOf()] + +- [handler.deleteProperty()] + - 拦截 `delete proxy[foo]` + - 拦截 `delete proxy.foo` + - 拦截 [Reflect.deleteProperty()] + - 拦截 [Reflect.deleteProperty()] + +- [handler.ownKeys()] + - 返回一个数组,数组中的元素只能是 string 或 symbol 类型 + - 必须包含 target 中的所以 **不可配置** 的 **自有属性** + - 如果 target 对象不可扩展,则必须包含所有 **自有属性** + - 拦截 [Object.getOwnPropertyNames()] + - 拦截 [Object.getOwnPropertySymbols()] + - 拦截 [Object.keys()] + - 拦截 [Reflect.ownKeys()] + - 拦截 [Reflect.ownKeys()] + +- [handler.apply()] + - 拦截 [proxy(..args)] + - 拦截 [Function.prototype.apply()] + - 拦截 [Function.prototype.call()] + - 拦截 [Reflect.apply()] + - 拦截 [Reflect.apply()] + +- [handler.construct()] + - 拦截 `new proxy(...args)` + - 拦截 [Reflect.construct()] + - 拦截 [Reflect.construct()] + + + +get 案例 + +```js +const rawObj = { + foo: 'bar' +} +const proxyObj = new Proxy(rawObj, { + get() { + return 'handler override' + } +}) + +console.log(rawObj.foo) // bar +console.log(proxyObj.foo) // handler override +console.log(rawObj['foo']) // bar +console.log(proxyObj['foo']) // handler override +console.log(Object.create(rawObj)['foo']) // bar +console.log(Object.create(proxyObj)['foo']) // handler override +``` + +案例二: + +```js + +const obj = {} +const e = Symbol('a symbol') +Object.defineProperty(obj, 'a', { configurable: true }) +Object.defineProperty(obj, 'b', { enumerable: true }) +Object.defineProperty(obj, 'c', { value: true }) +Object.defineProperty(obj, 'd', { writable: true }) +Object.defineProperty(obj, e, {}) +const proxy = new Proxy(obj, { + ownKeys(target) { + // return Reflect.ownKeys(target) + return ['b', 'c', 'd', e, 'other'] // 至少要前面四个属性,不能会报错。多余的参数不会报错 + } +}) +for (let key in proxy) { + console.log(key) // 只输出 'b' 因为只有 b 是可枚举的 +} +console.log(Reflect.ownKeys(obj)) // [ 'a', 'b', 'c', 'd', Symbol(a symbol) ] +console.log(Reflect.ownKeys(proxy)) // [ 'b', 'c', 'd', Symbol(a symbol) ] +console.log(Object.keys(proxy)) // [ 'b' ] +console.log(Object.getOwnPropertyNames(proxy)) // [ 'b', 'c', 'd', 'other' ] +console.log(Object.getOwnPropertySymbols(proxy)) // [ Symbol(a symbol) ] + +``` + +--- +[元编程]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming + + +[handler.getPrototypeOf()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getPrototypeOf +[handler.setPrototypeOf()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/setPrototypeOf +[handler.isExtensible()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/isExtensible +[handler.preventExtensions()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/preventExtensions +[handler.getOwnPropertyDescriptor()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor +[handler.defineProperty()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/defineProperty +[handler.has()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/has +[handler.get()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get +[handler.set()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set +[handler.deleteProperty()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/deleteProperty +[handler.ownKeys()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/ownKeys +[handler.apply()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply +[handler.construct()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/construct + +[Reflect.setPrototypeOf()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/setPrototypeOf +[Reflect.isExtensible()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/isExtensible +[Reflect.preventExtensions()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/preventExtensions +[Reflect.getOwnPropertyDescriptor()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor +[Reflect.defineProperty()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/defineProperty +[Reflect.construct()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct +[Reflect.getPrototypeOf()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getPrototypeOf +[Reflect.deleteProperty()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/deleteProperty +[Reflect.has()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/has +[Reflect.get()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get +[Reflect.set()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set +[Reflect.apply()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply +[Reflect.ownKeys()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys + + +[Object.setPrototypeOf()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf +[Object.isExtensible()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible +[Object.preventExtensions()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions +[Object.getOwnPropertyDescriptor()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor +[Object.defineProperty()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty +[Object.getPrototypeOf()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf +[Object.getOwnPropertyNames()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames +[Object.getOwnPropertySymbols()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols +[Object.keys()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +[Object.prototype.isPrototypeOf()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf + + +[Function.prototype.apply()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply +[Function.prototype.call()]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call +[`__proto__`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto +[instanceof]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof +[TypeError]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError +[String]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String +[Symbol]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol + +[Property query]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming#property_query +[Property access]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming#property_access +[Property assignment]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming#property_assignment +[Property deletion]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming#property_deletion +[Inherited property quer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming#inherited_property_query +[Inherited property acces]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming#inherited_property_access +[Inherited property assignmen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming#inherited_property_assignment diff --git a/web/ES6/basic.md2 b/web/ES6/basic.md2 new file mode 100644 index 0000000..65b217c --- /dev/null +++ b/web/ES6/basic.md2 @@ -0,0 +1,226 @@ +# 基础,但易忘(笔试面试) + + + + + + + + + + + +> 注意: +> 虽然 ECMAScript 中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位,而是先把数值转换为 32 位整数,再进行位操作,操作完成后再把结果转换为 64 位。所以,对我们(开发者)而言,就好像只有 32 位整数一样。 + +```js + let n1 = 0b1_0000_0000_0000 + console.log(n1.toString(2)) // 1_0000_0000_0000 + n1 <<= 1 + console.log(n1.toString(2)) // 10_0000_0000_0000 + + + let n2 = 0b1_0000_0000_0000_0000_0000_0000_0000_0000 // 33 位 + console.log(n2.toString(2)) // 1_0000_0000_0000_0000_0000_0000_0000_0000 + n2 <<= 1 + console.log(n2.toString(2)) // 0 + // 因为位运算只能操作 32 位,所以在 64 --> 32 --> 64 过程中精度丢失了! +``` + + + +## 二进制的有符号和无符号右移 + + 有符号右移:除了符号位(第32位),所有位都右移。空出来的位置填充对应符号位。所以正数右移永远成正数,负数右移永远是负数 + +## 负数的二进制存储方式 + + 在计算机中以补码的形式存储负数。 + 补码的计算方式是:对正数的二进制进行所有位取反,然后加一 + 比如 -27 的二进制(补码)就是 27 取反加一 + +```js + 0000 0000 0000 0000 0000 0000 0001 1011 (27) + ~ 1111 1111 1111 1111 1111 1111 1110 0100 + ++ 1111 1111 1111 1111 1111 1111 1110 0101 (-27) +``` + 对于负数,计算其二进制值的方式也是一样的:取反,加一 + +```js + 1111 1111 1111 1111 1111 1111 1110 0101 (-27) + ~ 0000 0000 0000 0000 0000 0000 0001 1010 + ++ 0000 0000 0000 0000 0000 0000 0001 1011 (27) + 然后在前面加个负号 +``` + + JS 打印负数的二进制时,并不是以补码形式打印出来的,而是以人类易读的方式打印出来的,比如 +```js + Number(-27).toString(2) // 打印 -1 1011 + Number.parseInt('-11011', 2) // 得到负数 -27 +``` + +## JS 中的相关位运算 + + & 与运算(全 1) + + | 或运算(有 1) + + ~ 非运算(取反) + + ^ 异或运算(相加) + + << 左移(补零) + + >> 右移(补符号位) + + >>> 无符号右移(补零) + + + ~ 非运算可以用于对数字进行向下取整: + console.log(~~1.1); + console.log(~~1.9); + console.log(~~'1.9'); + console.log(~~'1.1'); + + 为什么说右移是补符号位呢?我们还是拿前面的 -27 举例。 + 首先,-27 在计算机中的二进制存储是这样的: + 1111 1111 1111 1111 1111 1111 1110 0101 + 而当我们对 -27 进行有符号左移时,它的最右侧的 1 会消失,同时最左侧会空出一个位置 + _111 1111 1111 1111 1111 1111 1111 0010 ~1~ + 那么这个位置需要补什么呢?答案是符号位,由于我们是负数,所以它会补上 1 + 所以结果就是 + 1111 1111 1111 1111 1111 1111 1111 0010 + 这个数字转换为十进制就是 -14。为什么这个二进制是 -14 呢?当然是我们算出来的。 + 计算方法前面已经讲过了,不过这里会再讲一次: + +```js + 1111 1111 1111 1111 1111 1111 1111 0010 (某个数字的补码形式) + ~ 0000 0000 0000 0000 0000 0000 0000 1101 (对其取反) + ++ 0000 0000 0000 0000 0000 0000 0000 1110 (然后加一,得到 14) + 在前面加个负号 + 就得到 -14 了 + 所以我们知道这个补码 + 的十进制是 -14 +``` + 既然有符号右移都说了,那再说说无符号右移的。 + 还是拿 -27 举例,步骤是一样的。 + 首先,-27 在计算机中的二进制存储是这样的: + 1111 1111 1111 1111 1111 1111 1110 0101 + 而当我们对 -27 进行有符号左移时,它的最右侧的 1 会消失,同时最左侧会空出一个位置 + _111 1111 1111 1111 1111 1111 1111 0010 ~1~ + 那么这个位置需要补什么呢?答案是 0,无论是正数还是负数,都会补上 0 + 所以,结果会是 + 0111 1111 1111 1111 1111 1111 1111 0010 + 而这个数字自己计算一下,就知道是二进制的 2147483634 + 自己在 js 中运行一下,就可以知道答案 + +```js + console.log(-27 >>> 1) // 输出 2147483634 +``` + +## 以 & 运算为例 + + 正数 & 正数 +```js + 00000000000000000000000000000011 (3) + & 00000000000000000000000000011001 (25) + ----------------------------------- + 00000000000000000000000000000001 (1) +``` + 正数 & 负数 +```js + 00000000000000000000000000000011 (3) + & 11111111111111111111111111100111 (-25) + ----------------------------------- + 00000000000000000000000000000011 (3) +``` + 负数 & 正数 +```js + 11111111111111111111111111111101 (-3) + & 00000000000000000000000000011001 (25) + ----------------------------------- + 00000000000000000000000000011001 (25) +``` + 负数 & 负数 +```js + 11111111111111111111111111111101 (-3) + & 11111111111111111111111111100111 (-25) + ----------------------------------- + 11111111111111111111111111100101 (-27) +``` + + + + + + + + + + + +上下文/执行上下文/作用域、作用域链 + + +## 上下文 + + - 变量或函数的上下文决定了它们可以访问哪些属性 + - 每个上下文都有一个关联的变量对象,虽然无法通过变量访问到它,但后台处理数据时会用到它。 + - 每个函数都有自己的上下文,调用一个函数时,其上下文就会被推入到一个上下文栈中。 + - 浏览器中的全局上下文是 window + - 上下文中的所有代码执行完毕后,上下文会被销毁(包括在该上下文中定义的所有变量和函数) + - 全局上下文 window 只有当应用程序退出时才会报销毁(比如关闭网页或浏览器) + - 函数作用域中默认有一个定义变量 arguments(全局上下文中没有) + +## 作用域有: + + - 全局作用域 (window) + - 函数作用域 (var) + - 块级作用域 (let,const) + +## 作用域链 + + - 因为作用域可以嵌套,所以就有了作用域链这个概念。 + - 作用域链指的是在词法作用域中变量查找的机制。 + - 子级可以访问父级,父级不能访问子级。 + - 函数上下文中可以访问到全局上下文中的变量,是因为可以在作用域链中找到它。 + + +--- + +虽然执行上下文主要有两种 + + - 全局上下文 + - 函数上下文。eval() 调用内部存在第三种上下文 + +但某些语句可以增加作用域链 + + - try/catch 中的 catch 块:会创建一个 err,err 对象中会包含要抛出的错误对象。 + - with 语句:可以在作用域前面添加临时添加一个上下文 + +```js + (() => { + var protocol = 'ftp' + // 通过 with 临时添加一个 location 上下文 + with(location) { + // 这样就可以通过上下文找到 protocol, hostname, port 变量。 + console.log(protocol, hostname, port) // http + } + console.log(protocol) // ftp + })(); +``` + +```js + (() => { + var protocol = 'ftp' + with(location) { + // 在代码在 firefox 和 chrome 中都是无效果的 + // 但在 edge 中将会刷新页面。原因是 edge 都支持修改 location.protocol, + // 但 firefox 和 chrome 不支持修改为 hello 这个无效值 + // 如果更改为 var protocol = 'http' ,则 firefox 和 chrome 也会刷新 + var protocol = 'hello' + console.log(protocol, hostname, port) // https + } + console.log(protocol) // ftp + })(); +``` diff --git a/web/ES6/draft.md b/web/ES6/draft.md new file mode 100644 index 0000000..4475625 --- /dev/null +++ b/web/ES6/draft.md @@ -0,0 +1,71 @@ +# 草稿 + +## __dirname + +```ts +import { fileURLToPath } from 'node:url' +// import.meta.url 是当前文件 url,需要通过 fileURLToPath 转换为路径 +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +console.log(import.meta.url) +console.log(__dirname) // 获取当前文件所在文件夹 +``` + +或者借助 dirname 获取所在文件夹 + +```ts +import { dirname } from "node:path" +import { fileURLToPath } from "node:url" + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +console.log(__dirname); +``` + +## 异步模块 + +异步导出: + +```js +// b.mjs +export default await new Promise((resolve, reject) => { + setTimeout(() => resolve('data'), 3000) + setTimeout(() => reject('err'), 100) +}) +``` + +导入时也需要异步导入,而不能是同步导入 + +```js +// ❌bad 不要这样导入,因为这样不仅会阻塞同步代码的运行,而且无法捕获错误 +import b from './b.mjs' +console.log(b) +``` + +```js +try { + const b = await import('./b.mjs') +} catch (error) { + console.error('Error importing module.', error) +} +``` + +如果可以自己处理 b.mjs 文件,则可以只导出一个 promise: + +```js +// b.mjs +export default new Promise((resolve, reject) => { + setTimeout(() => resolve('data'), 3000) + setTimeout(() => reject('err'), 100) +}) +``` + +```js +import b from './b.mjs' +try { + const moduleB = await b + console.log(moduleB) +} catch (error) { + console.error('Error importing module:', error) +} +``` diff --git a/web/ES6/promise-basic.md b/web/ES6/promise-basic.md new file mode 100644 index 0000000..7b4484c --- /dev/null +++ b/web/ES6/promise-basic.md @@ -0,0 +1,720 @@ +- [Promise 对象](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise) +- [使用 Promise(en-US)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) +- [使用 Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises) +- [博客 - Promise 基本概念](https://thenewtoys.dev/blog/2021/02/08/lets-talk-about-how-to-talk-about-promises/) + +## 认识 Promise + +- Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。 + +- Promise 本质上是一个函数返回的对象。我们把回调函数绑在 Promise 链上,而不是像旧的异步函数那样,直接将回调函数作为参数传入。 + + - 旧写法 + ```js + function successCallback(result) { + console.log(`Audio file ready at URL: ${result}`); + } + + function failureCallback(error) { + console.error(`Error generating audio file: ${error}`); + } + + createAudioFileAsync(audioSettings, successCallback, failureCallback); + ``` + + - Promise 链式绑定回调函数的写法 + ```js + createAudioFileAsync(audioSettings).then(successCallback, failureCallback); + ``` + +## 链式调用的好处 + +按序执行多个异步函数是很常见的需求。按照以前的写法,这种需求会写成下面这样——回调地狱 +```js +doSomething(function (result) { + doSomethingElse(result, function (newResult) { + doThirdThing(newResult, function (finalResult) { + console.log(`Got the final result: ${finalResult}`); + }, failureCallback); + }, failureCallback); +}, failureCallback); +``` + +但使用 Promise 可以写成下面这样——链式调用,而且只需要利用 .catch 就能捕获前面所有异步函数的错误。 +```js +doSomething() + .then((result) => doSomethingElse(result)) + .then((newResult) => doThirdThing(newResult)) + .then((finalResult) => { + console.log(`Got the final result: ${finalResult}`); + }) + .catch(failureCallback); +``` + +**注意** :链式调用中,多个异步函数之间是通过返回值和参数进行传递数据的,所以一定要记得在then() 中写返回值。没有返回值时被称为 floating。 +下面就是一个忘记写返回值的错误案例:由于你忘记写返回值,所以本来是一条链,现在链条上多了一段浮动(floating)的链 —— fetch() 部分 + +```js +// ❌ +const listOfIngredients = []; + +doSomething() + .then((url) => { + // I forgot to return this + fetch(url) + .then((res) => res.json()) + .then((data) => { + listOfIngredients.push(data); + }); + }) + .then(() => { + console.log(listOfIngredients); + // Always [], because the fetch request hasn't completed yet. + }); +``` + +此外,虽然你可以简单的将 fetch() 作为返回值,从而解决 floating 问题,就像下面这样: +```js +// ❌ +const listOfIngredients = []; + +doSomething() + .then((url) => + fetch(url) + .then((res) => res.json()) + .then((data) => { + listOfIngredients.push(data); + }), + ) + .then(() => { + console.log(listOfIngredients); + }); +``` + +但这种嵌套链的方式违背了我们的初衷。将链条扁平化处理无疑是更好的做法: +```js +// ✔️ +const listOfIngredients = []; + +doSomething() + .then((url) => fetch(url)) + .then((res) => res.json()) + .then((data) => { + listOfIngredients.push(data); + }) + .then(() => { + console.log(listOfIngredients); + }); +``` + +## 捕获错误 + +嵌套链式调用是不被推荐的,但是在某些特定情景下,正确的使用嵌套能够让我们更精准的捕获错误。 +比如,将我们不在乎的可能会报错的异步操作嵌套起来,然后单独 catch,这样就能只捕获我们在乎的异步操作的错误。 +```js +doSomethingCritical() + .then((result) => + doSomethingOptional(result) + .then((optionalResult) => doSomethingExtraNice(optionalResult)) + .catch((e) => {}), // 内部的 catch:忽略错误(error-silencing)。 + ) // Ignore if optional stuff fails; proceed. + .then(() => moreCriticalStuff()) + .catch((e) => console.error(`Critical failure: ${e.message}`)); // 外部的 catch:能够捕获 doSomethingCritical 的错误。 +``` + +当 catch 到错误后,不会影响后续链条的执行,比如下面这样: +```js +new Promise((resolve, reject) => { + console.log("Initial"); + + resolve(); +}) + .then(() => { + throw new Error("Something failed"); + + console.log("Do something"); + }) + .catch(() => { + console.error("something wrong"); + }) + .then(() => { + console.log("Do something, no matter what happened before"); + }); +``` + +## 编写 Promise 链时容易出现的错误案例 + +```js +// ❌ 错误案例: 下面代码中有三处错误 + +doSomething() + .then(function (result) { + // Forgot to return promise from inner chain + unnecessary nesting + doSomethingElse(result).then((newResult) => doThirdThing(newResult)); // 此处两个错误 + }) + .then(() => doFourthThing()); +// Forgot to terminate chain with a catch! 此处一个错误 +``` + +- 第一个错误:在 then 中创建了 promise 对象却没有将其返回,这导致了 Promise 链的断裂。 带来的后果是:出现两条独立的链条并行执行,即 doFuorthThing 函数不会等待 doSomethingElse() 或 doThirdThing() 函数的完成。这违背了我们期待的按序执行。 +- 第二个错误:错误使用嵌套,使用了没必要嵌套。因为该程序并没有在嵌套内捕获错误,也没有说明嵌套内的异步操作是可选操作。 当内部出现错误时,我们将捕获到未知的错误。 + > tips: 捕获错误是捕获我们知道的错误, 比如用户填写手机号时, 我们会捕获 “输入不是合法手机号” 这一个错误,但如果捕获到了未知错误,则不是我们期待的。比如程序员不怕 404,怕 500。 +- 第三个错误:在链条最后没有使用 catch。 这将导致 reject 状态的 Promise 被忽略。 + +正确使用案例: +```js +// ✔️ +doSomething() + .then(function (result) { + // 当 doSomethingElse() 返回一个 Promise 对象时, 一定要记得 return doSomethingElse() + return doSomethingElse(result); + }) + // 如果使用箭头函数, 记得省略花括号 + .then((newResult) => doThirdThing(newResult)) + // 前面几个 then() 中返回 Promise 对象, 是为了维持 Promise 链条。 + // 即使下一个 then() 并不需要用到这个值 + .then((/* result ignored */) => doFourthThing()) + // 永远记得在 Promise 的最后添加 catch, 以避免任何未处理的 rejections + .catch((error) => console.error(error)); +``` + +如果你使用 `async`/`await`, 那么上面大多数问题都可以得到解决。 但是当你使用 `async`/`await` 时, 最常见的错误就是忘记添加 `await` 关键字! + +## `async`/`await` 捕获错误 + +我们使用 Promise 链式捕获错误只需要在尾部调用 catch: +```js +doSomething() + .then((result) => doSomethingElse(result)) + .then((newResult) => doThirdThing(newResult)) + .then((finalResult) => console.log(`Got the final result: ${finalResult}`)) + .catch(failureCallback); +``` + +同步函数捕获错误是使用 `try`/`catch`: +```js +try { + const result = syncDoSomething(); + const newResult = syncDoSomethingElse(result); + const finalResult = syncDoThirdThing(newResult); + console.log(`Got the final result: ${finalResult}`); +} catch (error) { + failureCallback(error); +} +``` + +`async`/`await` 捕获错误的方式和同步代码一样 —— `try`/`catch`: +```js +async function foo() { + try { + const result = await doSomething(); + const newResult = await doSomethingElse(result); + const finalResult = await doThirdThing(newResult); + console.log(`Got the final result: ${finalResult}`); + } catch (error) { + failureCallback(error); + } +} +``` + +这意味着, 利用 `async`/`await` 和 Promise 编写异步代码, 能够让我们像编写同步代码一样! + +## Promise 的 rejection 事件 + +在浏览器的环境下, 当 Promise 对象被拒绝时, 下面两个事件之一被发送到全局作用域(一般是 window): +- `unhandledrejection` + 当 Promise 被拒绝,并且没有 reject handled 处理该 rejection 时,会派发此事件。 +- `rejectionhandled` + 当 `unhandledrejection` 事件触发后, reject handled 才处理该 rejection 时,会派发此事件。 + +比如这样: +```js +window.addEventListener('unhandledrejection', (event) => { + console.log('❌unhandledrejection', event.reason, event.promise) + event.preventDefault(); // 可以添加该行代码来阻止浏览器的默认打印行为。 需要控制台输出显示的级别包含"详细(verbose)"或"所有级别" +}) +window.addEventListener('rejectionhandled', (event) => { + console.log('❌rejectionhandled', event.reason, event.promise) +}) + +// 创建一个未处理的 Promise +const promise = Promise.reject('Promise 被拒绝(rejected)') + +// 500ms 后才添加 reject handled 来处理 rejection +setTimeout(() => { + promise.catch(() => { }) +}, 500); +``` + +上面的两个事件(事件类型为 PromiseRejectionEvent)都有两个属性 +- `PromiseRejectionEvent.promise` + 被拒绝的 Promise 对象 +- `PromiseRejectionEvent.reason` + Promise 被拒绝时携带的文本信息 + +当你导入别人的代码, 而别人代码中存储未处理的 rejection 时, 上面两个事件能够帮助你处理别人的 rejection。 + +## 并行处理多个 Promise + +Promise 提供了有四个工具函数能够帮我们并行处理 Promise。 + +- [`Promise.all(iterable)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) +- [`Promise.allSettled(iterable)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) +- [`Promise.any(iterable)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any) +- [`Promise.race(iterable)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) + +他们都会返回 Promise 对象, 并且都可以接受空的可迭代对象(比如空数组)。 +当接受空的可迭代对象时, 前三个返回的 Promise 对象都会同步地敲定状态, 只有 `race` 函数返回的 Promise 依旧是待定(pending)状态。 +除此之外的任何情况, 四个函数返回的 Promise 对象都是异步敲定状态的! +> Promise 对象被敲定(settled), 表示该 Promise 对象被兑现(fulfilled)或者被拒绝(rejected)。 + +```js +const p1 = Promise.all([]) +const p2 = Promise.allSettled([]) +const p3 = Promise.any([]) +const p4 = Promise.race([]) +console.log(p1) // Promise {: Array(0)} +console.log(p2) // Promise {: Array(0)} +console.log(p3) // Promise {: AggregateError: All promises were rejected} +console.log(p4) // Promise {} +setTimeout(() => { // 利用 setTimeout 函数我们可以在堆栈为空后执行代码 + console.log("堆栈现在为空") + console.log(p1) // Promise {: Array(0)} + console.log(p2) // Promise {: Array(0)} + console.log(p3) // Promise {: AggregateError: All promises were rejected} + console.log(p4) // Promise {} +}) +// p3.catch(r=>r) // 如果是 Nodejs 环境下, 需要在堆栈空之前先捕获被拒绝的 Promise, 不然堆栈空后, rejection 冒泡到堆栈外后会直接导致程序终止。 从而看不到堆栈空后的效果 +``` + +此外, 还可以利用数组的 `reduce()` 组合 Promise 运行: +```js +[func1, func2, func3] + .reduce((p, f) => p.then(f), Promise.resolve()) + .then((result3) => { + /* use result3 */ + }); +``` +这种递归调用一个由异步函数组成的数组时,相当于一个 Promise 链: +```js +Promise.resolve() + .then(func1) + .then(func2) + .then(func3) + .then((result3) => { + /* use result3 */ + }); +``` +利用数组方法递归调用时, 我们可以改成函数式编程, 比如下面这样: +```js +const applyAsync = (acc, val) => acc.then(val); +const composeAsync = + (...funcs) => + (x) => + funcs.reduce(applyAsync, Promise.resolve(x)); +``` +`composeAsync` 函数使用方式为: +```js +const transformData = composeAsync(func1, func2, func3); // 允许任意数量异步函数 +const result3 = transformData(data); +``` + +利用 `for of` 和 `async`/`await` 也可以实现 Promise 的组合: +```js +let result; +for (const f of [func1, func2, func3]) { + result = await f(result); +} +/* use last result (i.e. result3) */ +``` +但使用这种同步阻塞的方式组合 Promise 之前, 最好仔细思考一下是否有必要 —— 并行处理 Promise 是个更优的选择, 因为他们不会相互阻塞。 + +## 为 setTimeout 创建 Promise + +可以通过 Promise 的构造器的方式从零开始创建 Promise。 但这种方式应当 **只** 在封装旧 API 的时候用到。 + +理想状态下, 所以异步函数都应该返回 Promise, 但有一些 API 仍然使用旧方式来传入回调。典型的例子就是 setTimeout() 函数。 + +混用旧方式和 Promise 方式可能会造成运行时序问题, 比如 传递给 setTimeout() 的回调函数有错误, 则只能在内部捕获它。 + +这个时候, 可以使用 Promise 封装来封装它(其他使用旧方法的 API 同理): +```js +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +wait(10 * 1000) + .then(() => saySomething("10 seconds")) + .catch(failureCallback); +``` + +## 时序 + +基于 callback 的 API ,如何调用 callback 取悦于具体的实现。 比如下面这种, 即可能是同步调用, 也可能是异步调用 的 API 设计是绝对不可取的: +```js +// ❌ +function doSomething(callback) { + if (Math.random() > 0.5) { + callback(); + } else { + setTimeout(() => callback(), 1000); + } +} + +``` + +Promise 是 loC 的一种形式 —— 我们(作为 API implementor)不需要控制 callback 何时执行。 相反, callback 队列的维护和 callback 的调用由 Promise 负责。 这样以来, 大家就能获得强有力的语义保证: +- 使用 then() 来接收 callback, 并且保证在 [JS事件循环](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop) 的一个 message 运行完成之前, 该 callback 永远不会被调用。 + + > loC(Inversion of Control): 控制反转, 是一种软件设计原则。 它是一种将程序的控制权从调用方转移到框架或容器中的设计原则。 + + > [message(消息)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop#queue): JS 运行时有一个消息队列, 每一个消息队列都关联着一个回调函数, 当消息被处理完成时, 该消息将会被作为对应的回调函数的参数, 传递给回调函数。 所以, 如果一个消息未运行完成就调用 callback 的话, 该 callback 的输入参数将会是空。 比如我们为一个点击事件绑定了回调函数 callback, 此时消息队列中会有一个消息与其关联, 当点击事件发生时, 该消息将被处理, 然后将其作为参数 event 传递给 callback, 最终确保我们能够在 callback 中正确的获取 event 参数, 从而得到我们想要的信息(比如鼠标位置)。 + ```js + function callback(event) { } + div.addEventListener('click', callback) + ``` +- 可以通过 then() 的串联, 实现多个 callback 的按序执行 +- 即使是在 Promise 被敲定后, 才将 callback 添加到 then() 中, 也能保证该 callback 被执行。 + - 为了防止歧义, 任何被添加到 then() 中的 callback , 他们的调用都会是异步调用的。 比如下面这样: + ```js + promise = Promise.resolve() + // 虽然此时的 promise 已经是敲定状态, 但是下面 then 中的回调函数也不会立马执行, 而是会异步执行 + promise.then(()=> {console.log(2)}) + console.log(1) // 这就是为什么 1 会比 2 先输出。 + ``` + +传递到 then() 中的 callback 都会被异步调用, 因为这些 callback 会被添加到微任务队列中。 +具体的说,这个调用时机是在 JS 的执行栈为空之后, 控制权返回给 event loop 之前。 比如下面的代码 +```js +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + +wait(0).then(() => console.log(4)) +Promise.resolve() + .then(() => console.log(2)) + .then(() => console.log(3)) +console.log(1) // 1, 2, 3, 4 +// 在 JS 的执行栈为空之后, 所以会先输出 1 +// 在控制权返回给 event loop 之前, 所以只有当 2, 3 输出后, 控制权才会回到 event loop 上, 此时 event loop 才会调用 setTimeout 此时 wait(0) 返回的 Promise 对象才会是敲定状态, wait(0).then() 的回调函数才会被添加到微任务队列中, 然后执行。 +``` + +### 任务队列(Task queues) vs 微任务(microtasks) + +Promise callback 被当成微任务处理, 而 setTimeout 中的 callback 会被当成任务队列处理。 + +比如下面的代码 +```js +const promise = new Promise((resolve, reject) => { + console.log("1 Promise callback") + resolve() +}).then((result) => { + console.log("3 Promise callback (.then)") +}) + +setTimeout(() => { + console.log("4 event-loop cycle: Promise (fulfilled)", promise) +}, 0) + +console.log("2 Promise (pending)", promise) +``` + +有关事件循环, 任务队列, 微任务的更多信息, 请参考 [MDN 深入理解微任务和 JS 运行上下文](https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth) + +> 简单的说, 就是执行 JS 代码时, 会有多个 "代理", 每一个 "代理" 内部由很多内容构成, 比如每个代理都有自己的 "任务队列" 和 "微任务队列"。 +而 "事件循环" 就是用来驱动这些 "代理" 的。 +> +> 每个代理都是由事件循环(Event loop)驱动的,事件循环负责收集事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务,然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。 +> +> 一个任务就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务。 +> +> 任务队列和微任务队列的区别很简单: +> - 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行。 +> - 每次当一个任务退出且执行上下文栈为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,这些新的微任务将在下一个任务开始运行之前,在当前事件循环迭代结束之前执行。 +> +> 由于我们的代码和浏览器的用户界面运行在同一个线程中,共享同一个事件循环,所以当我们的代码阻塞时浏览器将没有时间来渲染和绘制网站和 UI、处理用户事件等,从而导致卡顿。 +> 解决方案有 +> - 使用 web worker 可以让主线程另起新的线程来运行脚本,新的线程属于另外的事件循环。 +> - 使用像 promise 这样的异步 JavaScript 技术可以使得主线程在等待请求返回结果的同时继续往下执行。 +> - 微任务是另一种解决该问题的方案,通过将代码安排在下一次事件循环开始之前运行,而不是必须要等到下一次开始之后才执行。 +> + +其他参考 +> - [微任务](https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide) +> - [异步 JavaScript](https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous) +> - [事件循环](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop#run-to-completion) +> - [Nodejs 事件循环](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick#what-is-the-event-loop) +> - [Promise 测试工具](https://tech.io/playgrounds/11107/tools-for-promises-unittesting/introduction) +> - [大佬博客 13年提出js异步](https://blog.izs.me/2013/08/designing-apis-for-asynchrony/) + +## 组合 Promise 案例 + +> Promise 对象被敲定(settled), 表示该 Promise 对象被兑现(fulfilled)或者被拒绝(rejected)。 + +一共有四个工具函数能够帮我们组合 Promise 执行。 +- `Promise.all(iterable)` 返回的 Promise 对象有以下四种状态 + - ✔️已兑现(Already fulfilled); + - `iterable` 为空(比如空数组)。 + - 兑现值为空数组 + - 待定(pending) + - `iterable`还没有出现敲定的 Promise 对象 + - ✔️异步兑现(Asynchronously fulfilled); + - `iterable` 中所有 Promise 都被兑现。 + - 兑现值是由 `iterable` 中所有兑现值按序组成的数组 + - ❌异步拒绝(Asynchronously rejected); + - `iterable` 中存在一个被拒绝的 Promise。 + - 拒绝原因和第一个被拒绝的 Promise 的一样 + +- `Promise.allSettled(iterable)` 返回的 Promise 对象没有拒绝状态 + - ✔️已兑现(Already fulfilled); + - `iterable` 为空(比如空数组)。 + - 兑现值为空数组 + - 待定(pending) + - `iterable`还没有出现敲定的 Promise 对象 + - ✔️异步兑现(Asynchronously fulfilled); + - `iterable` 中所有 Promise 都已敲定 + - 兑现值是数组, 数组的元素是一个对象, 存有敲定后的 Promise 的状态(兑现或拒绝)和值(兑现值或拒绝原因)。 + +- `Promise.any(iterable)` 返回的 Promise 对象有以下四种状态 + - ❌已拒绝(Already rejected); + - `iterable` 为空(比如空数组)。 + - 兑现值为空数组 + - 待定(pending) + - `iterable`还没有出现敲定的 Promise 对象 + - ✔️异步兑现(Asynchronously fulfilled); + - `iterable` 中的存在一个被兑现 Promise + - 兑现值和第一个兑现的 Promise 一样 + - ❌异步拒绝(Asynchronously rejected); + - `iterable` 中所有 Promise 都被拒绝 + - 拒绝原因是由 `iterable` 中所有拒绝原因按序组成的数组 + +- `Promise.race(iterable)` + - 待定(pending) + - `iterable` 为空(比如空数组) + - `iterable` 非空, 并且每个 Promise 都是敲定状态。(即 race 执行前就已经待定) + - `iterable` 中还没有出现敲定的 Promise 对象 + - ✔️异步兑现(Asynchronously fulfilled); + - race() 执行后, 第一个敲定的 Promise 是兑现状态 + - 兑现值和第一个敲定的 Promise 的一样 + - ❌异步拒绝(Asynchronously rejected); + - race() 执行后, 第一个敲定的 Promise 是拒绝状态 + - 拒绝原因和第一个敲定的 Promise 的一样 + +### 案例 + +#### `Promise.all()` 案例 + +- 空数组案例 +```js +Promise.all([]) + .then(val => { + console.log('✔️', val) // 输出 ✔️ [] + }) + .catch(reason => { + console.error('❌', reason) + }) +``` + +- 异步兑现案例 +```js +const iterableFun = [ + Promise.resolve(1), + Promise.resolve(2), + Promise.resolve(3), + Promise.reject(4).catch(()=>{}), + Promise.resolve(5), + Promise.resolve(6), +] + +Promise.all(iterableFun) + .then(val => { + console.log('✔️', val) // ✔️ [ 1, 2, 3, undefined, 5, 6 ] + }) + .catch(reason => { + console.error('❌', reason) + }) +``` + +- 异步拒绝案例 +```js +const iterableFun = [ + Promise.resolve(1), + Promise.resolve(2), + Promise.resolve(3), + Promise.reject(4), + Promise.resolve(5), + Promise.resolve(6), +] + +Promise.all(iterableFun) + .then(val => { + console.log('✔️', val) + }) + .catch(err => { + console.error('❌', err) // 输出 ❌ 4 + }) +``` + +#### `Promise.allSettled()` 案例 + +- 空数组案例 +```js +Promise.allSettled([]) + .then(val => { + console.log('✔️', val) // 输出 ✔️ [] + }) + .catch(reason => { + console.error('❌', reason) + }) +``` + +- 异步兑现案例 +```js +const iterableFun = [ + Promise.resolve(1), + Promise.resolve(2), + Promise.resolve(3), + Promise.reject(4), + Promise.resolve(5), + Promise.resolve(6), +] + +Promise.allSettled(iterableFun) + .then(val => { + console.log('✔️', val) + /* ✔️ [ + { status: 'fulfilled', value: 1 }, + { status: 'fulfilled', value: 2 }, + { status: 'fulfilled', value: 3 }, + { status: 'rejected', reason: 4 }, + { status: 'fulfilled', value: 5 }, + { status: 'fulfilled', value: 6 } + ]*/ + }) + // Promise.allSettled() 返回的 Promise 不可能被拒绝, 所以不需要 catch, 但如果你在上一个 then 中返回了新的 promise, 那就需要 catch 了 +``` + +#### `Promise.any(iterable)` + +- 空数组案例 +```js +Promise.any([]) + .then(val => { + console.log('✔️', val) + }) + .catch(reason => { + console.error('❌', reason) // ❌ AggregateError: All promises were rejected + }) +``` + +- 异步兑现案例 +```js +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) +const iterableFun = [ + wait(1000).then(()=>'1000'), + wait(30).then(()=>Promise.reject('30')), + wait(20).then(()=>Promise.reject('20')), + wait(10).then(()=>Promise.reject('10')), +] + +Promise.any(iterableFun) + .then(val => { + console.log('✔️', val) // 等待1秒后输出 ✔️ 1000 + }) + .catch(reason => { + console.error('❌', reason) + }) +``` + +- 异步拒绝案例 +```js +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) +const iterableFun = [ + wait(1000).then(()=>Promise.reject('1000')), + wait(30).then(()=>Promise.reject('30')), + wait(20).then(()=>Promise.reject('20')), + wait(10).then(()=>Promise.reject('10')), +] + +Promise.any(iterableFun) + .then(val => { + console.log('✔️', val) + }) + .catch(reason => { + console.log('❌', reason) // 等待1秒后输出 ❌ AggregateError: All promises were rejected + }) +``` + +#### `Promise.race()` 案例 + +- 空数组案例 +```js +Promise.race([]) + .then(val => { + console.log('✔️', val) + }) + .catch(reason => { + console.log('❌', reason) + }) +// 不会有任何输出, 并且返回的 Promise 为待定(pending)状态 +``` + +- 待定案例 +```js +``` + +- 异步兑现案例 +```js +const iterableFun = [ + Promise.resolve(1), + Promise.resolve(2), + new Promise((resolve) => {for(let i=0;i<9999999999;i++);resolve(3)}), + Promise.reject(4), + Promise.resolve(5), + Promise.resolve(6), +] + +Promise.race(iterableFun) + .then(val => { + console.log('✔️', val) // 等待几秒钟(取决于电脑性能)后, 输出 ✔️ 1 + }) + .catch(reason => { + console.log('❌', reason) + }) +``` + +- 异步兑现案例 +```js +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) +const iterableFun = [ + wait(1000).then(()=>'1000'), + wait(200).then(()=>'200'), + wait(10).then(()=>'10'), + wait(100).then(()=>'100'), +] + +Promise.race(iterableFun) + .then(val => { + console.log('✔️', val) // 输出 ✔️ 10 + }) + .catch(reason => { + console.log('❌', reason) + }) +``` + +- 异步拒绝案例 +```js +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) +const iterableFun = [ + wait(1000).then(()=>'1000'), + wait(200).then(()=>'200'), + wait(100).then(()=>'100'), + wait(10).then(()=>Promise.reject('10')), +] + +Promise.race(iterableFun) + .then(val => Promise.resolve(val)) + .then(val => { + console.log('✔️', val) + }) + .catch(reason => { + console.log('❌', reason) // ❌ 10 + }) +``` + diff --git a/web/ES6/promise-glossary.md b/web/ES6/promise-glossary.md new file mode 100644 index 0000000..63d7907 --- /dev/null +++ b/web/ES6/promise-glossary.md @@ -0,0 +1,170 @@ +本文的 Promise 中文术语来自 [Promise - MDN 文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise) ,术语解释依据来自 MDN 文档推荐的 [Let's talk about how to talk about promises](https://thenewtoys.dev/blog/2021/02/08/lets-talk-about-how-to-talk-about-promises/) 参考文章。 + +本文将介绍 Promise 的以下术语: +- **待定(pending)** +- **兑现(fulfilled)** +- **兑现值(fulfillment value)** +- **拒绝(rejected)** +- **拒绝理由(rejection reason)** +- **敲定(settled)** +- **解决(resolve)** +- **锁定(locked in)** + +本文仅介绍这些术语,不会介绍 Promise 的基本使用。 + +## 🍕 Promise 状态 + +一个 Promise 对象只可能有三种状态, 并且这三种状态是互斥的: +- **待定** ,这是大部分 Promise 对象的初始状态。 +- 已 **兑现** ,此时的 Promise 对象会传递一个 **兑现值** 给 `then()`。 +- 已 **拒绝** ,此时的 Promise 对象会传递一个 **拒绝理由** 给 `catch()`。 + +这几个术语是 Promise 的基础,而且也是很好懂的。示例代码如下(运行在浏览器环境下): + +```js +const pendingPromise = new Promise(resolve => {}) +console.log(pendingPromise) // 输出: Promise {} +``` +```js +const fulfillPromise = new Promise(resolve => {resolve('兑现值')}) +console.log(fulfillPromise) // 输出: Promise {: '兑现值'} +``` +```js +const rejectPromise = new Promise((resolve, reject) => {reject('拒绝理由')}) +console.log(rejectPromise) // 输出: Promise {: '拒绝理由'} +rejectPromise.catch((reason)=>{console.error(reason)}) // 被拒绝的 Promise 对象, 会抛出错误, 需要通过 catch() 获取 +``` + +## 🍕 *敲定(settled)* + +当一个 Promise 对象的状态是已 **兑现** 或者 已 **拒绝** 时, 我们称呼该 Promise 已经被 **敲定**。 + +## 🍕 *解决(resolve)* + +**解决** 这个术语最开始是来自 [Promises/A+ 规范](https://promisesaplus.com/#the-promise-resolution-procedure)。 在规范中它表示一个抽象的操作。 + +**解决** 一个 Promise 对象有下面三种方式: +- 通过调用 `new Promise()` 中提供的 `resolve()` 方法。 +- 通过调用 `Promise.resolve()` 创建一个已经 **解决** 的 Promise 对象。 +- 在 promise 机制控制的回调函数中 `return` 一个值(没有 `return` 时该值默认是 `undefined`)。 + +> 备注:所谓 promise 机制控制的回调函数,指的是该回调函数什么时候能执行是由 promise 机制控制的,典型的有 `then()` 中的回调函数。 + +## 🍕 *锁定(locked in)* + +当 Promise A 对象 与 Promise B 对象 **锁定** 时(英文为: resolve A to B ),遵循下面规则: +- 当 Promise B 对象被 **兑现** (或者已经是被 **兑现** )时, Promise A 也会被 **兑现** ,并且 **兑现值** 和 Promise B 的相同。 +- 当 Promise B 对象被 **拒绝** (或者已经是被 **拒绝** )时, Promise A 也会被 **拒绝** ,并且 **拒绝理由** 和 Promise B 的相同。 +- 当 Promise B 对象未 **敲定** 时, Promise A 也不会 **敲定**。 + +下面的场景中,将会 **锁定** 两个 Promise 对象: +- 调用 `new Promise()` 中提供的 `resolve()` 方法时,传入的参数是一个 Promise 对象, 此时构造出来的 Promise 对象与传入的参数 **锁定** 。 +- `Promise.resolve(input)` 中的 `input` 是一个 Promise 对象时, `Promise.resolve()` 返回的 Promise 对象将会与 `input` **锁定** 。 +- 在 promise 机制控制的回调函数中返回的值 `val` 是一个 Promise 对象时, 回调函数返回的 Promise 对象将会与 `val` **锁定**。 + +> 备注:promise 机制控制的回调函数始终都会返回一个 Promise 对象,无论回调函数中返回的是什么。(可能不太严谨) +> - 如果回调函数中返回的是非 Promise 对象,则它会作为 Promise 对象的 **兑现值**。 +> - 如果回调函数中返回的是 Promise 对象,则两个 Promise 对象会 **锁定**。 + +所以,当一个 Promise 被 **解决** 时,该 Promise 对象的状态不一定会改变的。(有些人可能会以为 **解决** 一个 Promise 时该 Promise 状态会变成 **兑现**) + +## 🍕 通过代码介绍 *解决(resolve)* 和 *锁定(locked in)* + +```js +function first () { + return new Promise(resolve => { + setTimeout(() => { + resolve('firstResult') // 4️⃣ 在某个时间点, Promise A 被 "解决",此时 Promise A 的状态为已 "兑现" + }, 500) + }) +} + +function second (firstResult) { + return new Promise(resolve => { + setTimeout(() => { + resolve('secondResult') // 6️⃣ 在某个时间点, Promise C 被 "解决" 了, 此时 Promise C 的状态是已 "兑现"。 + // 同时,由于 Promise B 与 Promise C 锁定,所以 Promise B 的状态也会变成已 "兑现",其 "兑现值" 和 Promise C 的一样。 + }, 500) + }) +} + +function doStuff () { + return first() // 2️⃣ 当调用 doStuff() 时, first() 执行并返回一个 Promise A + + .then(firstResult => { // 3️⃣ 当调用 Promise A 的 then() 方法时, 该 then() 方法也会创建一个 Promise B 并返回。 该 Promise B 就是 doStuff 返回的 Promise 对象 + + return second(firstResult) // 5️⃣ 当 Promise A "兑现" 时, 就会执行 second(), 这个 second() 也会返回一个 Promise C 。 + // 由于该回调函数(promise 机制控制的回调函数) 返回了一个值,所以 Promise B 被 "解决" 了 + // 同时,因为返回的值是 Promise C,所以 Promise B 与 Promise C "锁定"。 + // 此时❗ 虽然 Promise B 被 "解决" 了,但他的状态不变 —— "待定",因为 Promise C 的状态是 "待定"。 + }) +} + + +doStuff() // 1️⃣ + .then(secondResult => { + console.log(secondResult) // 7️⃣ 当 Promise B 状态变为 "兑现" 时(取决于 Promise C ),该回调函数将被调用。 + }) + .catch(error => { + console.error(error) + }) +``` + +1. 调用 `doStuff()` +2. `doStuff()` 会调用 `first()` 函数,此时 `first()` 函数返回一个 Promise A 对象。 +3. 调用 Promise A 的 `then()` 方法时,该 `then()` 会创建一个 Promise B 对象, Promise B 就是 `doStuff()` 返回的 Promise 对象。 并且该 `then()` 方法中传入了一个由一个回调函数 callback A ,该回调函数的执行时机是由 promise 机制控制的。 +4. 在某个时间点, Promise A 被通过调用 `resolve()` 的方式 **解决** ,此时 Promise A 的状态会变成 **兑现** ,并且携带 **兑现值** +5. 当 Promise A 被 **兑现** 后,由 promise 机制控制的 callback A 执行, `second()` 被调用, 因为 `second()` 返回一个 Promise C 对象,所以此时 Promise B 被 **解决** 了,但是❗ Promise B 的状态还是 **待定** ,因为 Promise B 与 Promise C **锁定** 了。 +6. 在某个时间点, Promise C **兑现** 了, 此时 Promise B 状态也会是 **兑现** ,其 **兑现值** 和 Promise C 一致。 +7. 当 Promise B **兑现** 时, Promise B 对应的 `then()` 中的回调函数将被执行 + + +上面的解释也适用 `async/await` ,比如下面代码: +```js +async function first() {/* ... */} +async function second(firstResult) {/* ... */} + +async function doStuff() { + const firstResult = await first() + return second(firstResult) +} +``` +1. `doStuff()` 函数被调用时,会调用 `first()` 函数。 +2. 当 `first()` 函数返回的 Promise A 的状态变为 **兑现** 时, doStuff 调用 `second()` ,同时创建并返回一个已经 **解决** 了的 Promise B 对象,该 Promise B 对象会与 `second()` 返回的 Promise C 对象 **锁定**。在 Promise C 对象 **敲定** 之前, Promise B 对象的状态始终是 **待定**。 + +## 🍕 总结 + +- Promise 对象只有三种互斥状态 **待定**、**兑现** 和 **拒绝** +- Promise 对象被 **兑现** 时会有一个 **兑现值** 传递给 `then()` +- Promise 对象被 **拒绝** 时会有一个 **拒绝理由** 传递 `catch()` +- 当 Promise 对象的状态不是 **待定** 时,我们称呼该 Promise 被 **敲定** 了。 +- 当 **解决** 一个 Promise 对象时,它的状态不一定会改变。 +- 多个 Promise 对象可以通过 **锁定** 的方式确保他们按序执行。(这也就是 Promise 链) + +在实际项目中, **解决** 一个 Promise 对象时,它的状态往往是不变的(依旧是 **待定** )。 +因为在设计某个需求函数为异步函数时,该异步函数往往是需要多个异步任务按序执行的。 +所以异步函数返回的 Promise 对象,经常与其他的 Promise 对象 **锁定**,而状态保持 **待定**。 +比如下面代码: + +```JS +fetch(url) + .then(response => response.text()) + .then( data => { + console.log(data.slice(0, 20)) + }) + .catch(reason => { + console.error(reason) + }) +``` +```js +async function send(url) { + const response = await fetch(url) + return response.json() +} +try { + const data = await send(url) + console.log(data) +} catch (error) { + console.error(error) +} +``` \ No newline at end of file diff --git a/web/ESLint/README.md b/web/ESLint/README.md new file mode 100644 index 0000000..4fb045e --- /dev/null +++ b/web/ESLint/README.md @@ -0,0 +1,113 @@ +# ESLint 基本使用 + +## draft + +非默认,但有用的规则: + +- require-await + - async 函数中必须使用 await + +个人习惯: + +```js +const rules = { + 'semi': ['error', 'never'], + 'quotes': ['error', 'single'], + 'comma-dangle': ['error', 'always-multiline'], +}, +``` + +## 参考网站: + +- [eslint](https://eslint.org/docs/latest/use/getting-started) +- [ts eslint](https://typescript-eslint.io/getting-started/) +- [eslint 所有规则](https://eslint.org/docs/latest/rules/#possible-problems) +- [eslint 推荐的规则](https://github.com/eslint/eslint/blob/main/packages/js/src/configs/eslint-recommended.js) +- [eslint 扁平配置](https://allalmohamedlamine.medium.com/eslint-flat-config-and-new-system-an-ultimate-deep-dive-2023-46aa151cbf2b) +- [eslint 支持的参数](https://eslint.org/docs/latest/use/command-line-interface) +- [eslint 配置规则、注释语法](https://zh-hans.eslint.org/docs/latest/use/configure/rules) + - 即将废弃的选项 (eslintrc Mode Only): + - --no-eslintrc + - --env + - --ext + - --resolve-plugins-relative-to + - --ignore-path + - `/* eslint-env jest */` + +## 注释语法(内联注释) + +- 块注释 + - `/* eslint-disable */` + - `/* eslint-enable */` +- 禁用/启用特定规则 + - `/* eslint-disable no-alert, no-console */` + - `/* eslint-enable no-alert, no-console */` + - `/* eslint no-alert: "off" */` + - `/* eslint quotes: ["error", "double"], curly: 2 */` +- 行尾注释 + - `// eslint-disable-line` + - `/* eslint-disable-line */` + - `// eslint-disable-line no-alert, quotes, semi` + - `/* eslint-disable-line no-alert, quotes, semi */` +- 下一行注释 + - `// eslint-disable-next-line` + - `/* eslint-disable-next-line */` + - `// eslint-disable-next-line no-alert` + - `/* eslint-disable-next-line no-alert */` + + ```js + /* eslint-disable-next-line + no-alert, + quotes, + semi + */ + ``` + +- 注释描述 + + ```js + // eslint-disable-next-line semi -- 因为不喜欢分号,所以禁用 semi 规则 + + /* eslint semi: "off", curly: "error" + -------- + 因为不喜欢分号,所以禁用 semi 规则 */ + + /* eslint-disable-next-line semi -- + * 因为不喜欢分号, + * 所以禁用 semi 规则 + **/ + ``` + +## 解决 eslint 提示的错误(旧) + +### App.vue 文件提示 `Parsing error: '>' expected` + +```js +module.exports = { + // 之前: + 'parser': '@typescript-eslint/parser', + // 之后: + 'parser': 'vue-eslint-parser', +} +``` + +### HelloWorld.vue 文件提示 `Parsing error: Unexpected token )` + +```js +// 在 parserOptions 参数中添加 ts 解析: +module.exports = { + 'parserOptions': { + 'parser': '@typescript-eslint/parser', + }, +} +``` + +### `.eslintrc.cjs` 提示 `'module' is not defined.eslintno-undef` + +```js +module.exports = { + 'env': { + 'node': true, + }, +} +``` diff --git a/web/Express/draft.md b/web/Express/draft.md new file mode 100644 index 0000000..7bf2a70 --- /dev/null +++ b/web/Express/draft.md @@ -0,0 +1,296 @@ +# 草稿(迁移) + +## 基础 + +express 的使用非常简单,核心就是链。需要注意的就是,链式每个节点(中间件、路由、错误处理)都有一个 next 函数,每个节点都是通过 next 连接成链的。所以,如果每个节点没有调用 next 函数,则必须在该节点中响应数据,否则该请求会被挂起(hang),此时垃圾回收将无法对其处理。 + +### 托管静态文件 + +```js +app.use(express.static('public')) +// 将 public 文件夹设置为静态文件,用户通过 /xx 可以直接访问 public/xx 的内容 + +app.use('/static', express.static('public')) +// 为静态文件提供虚拟的前缀路径。用户通过访问 /static/xx 可以获取 /public/xx 的内容 +// 注意 static 要有前缀,不能写成 static +``` + +### 基本路由 + +基本格式: + +```js +app[method]('/xx', (req, res) => { + // .. +}) + +// 使用多个处理程序,通过 next 连接 +app[method]('/xx', (req, res, next) => { + // .. +}, (req, res, next) => { + // .. +}) +``` + +> 注意,如果没有调用 next 参数,那么这个程序就必须负责返回响应的数据,否则该请求将会被挂起(hang),而且不会被垃圾回收, + +常见的 method 有 get, post, put, delete, 或者也可以通过 all 指定所有的 method。完整的 method 请参考 [app.METHOD]。 + +第一个参数指的是匹配路由字符串,该字符串同时也是正则表达式字符串,使用 [path-to-regexp] 模块进行正则的解析。比如 `app.get('/')`。你也可以显式的传递一个 RegExp 字面量。 + +```js +// 匹配 http://localhost:3000/abcd +// 匹配 http://localhost:3000/acd +app.get('/ab?cd', (req, res) => { + res.send('ab?cd') +}) + +// 匹配 http://localhost:3000/foobarfly +app.get(/.*fly$/, (req, res) => { + res.send('/.*fly$/') +}) +``` + +通过冒号可以配置路径参数(params)下面是一个简单的案例: + +```js +app.get('/users/:userId/books/:bookId', (req, res) => { + res.send(req.params) // { userId: '123', bookId: '123333' } +}) +``` + +> 注意,查询字符串(search)并不不要写在路由字符串中,而是通过 `req.query` 获取 + +参数名称只能是一个 word(`[A-Za-z0-9_]`),其中并不包含 hyphen (-) 和 dot (.)。这样一来,hyphen 和 dot 就可以使用在路径中,下面是一个简单的案例: + +```js +// 用户访问 http://localhost:3000/flights/深圳-广州 +app.get('/flights/:from-:to', (req, res) => { + console.log(req.params) // { from: '深圳', to: '广州' } +}) + +// 用户访问 http://localhost:3000/something/你好.世界 +app.get('/something/:foo.:bar', (req, res) => { + console.log(req.params) // { foo: '你好', bar: '世界' } +}) +``` + +匹配参数也可以结合字符串实现更精确的匹配: + +```js +// 匹配 http://localhost:3000/user/123 +// 但不匹配 http://localhost:3000/user/a7b7 +app.get('/user/:userId(\\d+)', (req, res) => { + console.log(req.params) // { userId: '123' } +}) +``` + +> js 字符串中的 `\` 是转义字符,所以字符串中需要使用 `\\d` 才能得到字符串 `\d`。 + +注意,express 4 中,不推荐使用 * 进行匹配,请用 {0,} 代替 + +```js +// 能给匹配 http://localhost:3000/baz/123/321 +app.get('/baz/:foo(\\d{0,})/:bar(\\d{0,})', (req, res) => { + console.log(req.params) // { foo: '123', bar: '321' } +}) + +// 使用 * 会导致一些问题 +app.get('/baz/:foo(\\d*)/:bar(\\d*)', (req, res) => { + console.log(req.params) // { '0': '23', '1': '21', foo: '123', bar: '321' } + res.send('ok') +}) +``` + +其他一些用法: + +- 链式路由 + + ```js + app.route('/book') + .get((req, res) => { + res.send('Get a random book') + }) + .post((req, res) => { + res.send('Add a book') + }) + .put((req, res) => { + res.send('Update the book') + }) + /* + await (await fetch('/book', {method: 'GET'})).text() + 'Get a random book' + await (await fetch('/book', {method: 'POST'})).text() + 'Add a book' + await (await fetch('/book', {method: 'PUT'})).text() + 'Update the book' + */ + ``` + +- 路由模块 + + ```js + const router = express.Router() + router.get('/foo', (req, res) => res.send('/module/foo')) + router.post('/bar', (req, res) => res.send('/module/bar')) + app.use('/module', router) + /* + await (await fetch('/module/foo', {method: 'GET'})).text() + '/module/foo' + await (await fetch('/module/bar', {method: 'POST'})).text() + '/module/bar' + */ + ``` + +- 多个路由处理回调 + + ```js + app.get('/example/a', (req, res, next) => { + req.hello = '你好' + next() + }, (req, res) => { + res.send(req.hello + ',世界') + }) + /* + await (await fetch('/example/a', {method: 'GET'})).text() + '你好,世界' + */ + + + + const cb1 = (req, res, next) => {req.cb1 = '1'; next()} + const cb2 = (req, res, next) => {req.cb2 = '2'; next()} + const cb3 = (req, res, next) => {req.cb3 = '3'; next()} + app.get('/example/b', [cb1,cb2,cb3], (req, res) => { + res.send(req.cb1 + req.cb2 + req.cb3) + }) + /* + await (await fetch('/example/b', {method: 'GET'})).text() + '123' + */ + + + const cb1 = (req, res, next) => {req.cb1 = 'hello'; next()} + const cb2 = (req, res) => { res.send(req.cb1 + ', world') } + app.get('/example/c', [cb1,cb2]) + /* + await (await fetch('/example/c', {method: 'GET'})).text() + 'hello, world' + */ + ``` + +### 全局错误处理 + +express 在链的末端默认提供了一个全局错误处理,如果链中任何一部分错误没有被捕获,则最终会被这个默认错处理捕获。 + +> 需要注意的是,在 express 4 中,对于链中的异步错误处理需要自行捕获 + +在开发环境(默认)下,express 默认错误处理所捕获到的错误会直接返回给客户端。在生产环境下则不会。可以通过设置环境变量 NODE_ENV 为 production 来让程序运行在生产环境下。 + +> 可以是在命令行中指定环境变量,比如使用 pwsh 终端运行应用时,通过 `$env:NODE_ENV = "production"` 则可以设置环境变量。 + +编写自己的错误处理程序,和编写中间件一样,直接添加在链上。不同的是错误处理程序接收四个参数。我们可以根据数据的流向,在链的最后定义我们自己的默认错误处理程序。 + +```js +// ... + +app.use((err, req, res, next) => { + console.error(err.stack) + res.status(500).send('Something broke!') +}) +``` + +注意,如果没有调用 next 参数,那么这个节点就必须负责返回响应的数据,否则该请求将会被挂起(hang),而且不会被垃圾回收, + +### response 对象 + +通过调用 response 对象上的以下方法,可以响应内容给客户端: + +| 方法 | 描述 | +| ---------------------------- | ------------------------------- | +| `res.sendStatus(statusCode)` | 发送对应响应码以及默认消息 | +| `res.send(any)` | 发送任意类型响应体 | +| `res.json(obj)` | 发送 JSON 格式响应体 | +| `res.jsonp(obj)` | 发送 JSONP 格式响应体 | +| `res.download()` | Prompt a file to be downloaded. | +| `res.end()` | End the response process. | +| `res.redirect()` | 重定向请求 | +| `res.render()` | Render a view template. | +| `res.sendFile()` | Send a file as an octet stream. | + +此外,常用的方法还有以下这些: + +| 方法 | 简单描述 | +| ----------------- | ---------------- | +| `res.cookie()` | 设置 cookie | +| `res.setHeader()` | 添加一条响应标头 | +| `res.status()` | 设置响应码 | + +### request 对象 + +request 通常用于获取用户传递数据,所以常用的属性和方法有: + +| property | 简单说明 | +| -------------- | ------------------------------------------------------------------------ | +| `req.params` | 获取路由参数,具体值由路由匹配字符串决定,比如 .get('/params/:foo/:bar') | +| `req.body` | 需要使用中间件(如 [body-parser] 或 [multer] )进行填充 | +| `req.cookies` | 使用 [cookie-parser] 进行填充 | +| `req.query` | 获取查询(search)参数,比如 ?foo=bar&baz=qux | +| `req.path` | 获取请求路径(不包含 origin 和 search) | +| `req.file` | 需要使用中间件(如 [multer])进行填充 | +| `req.files` | 需要使用中间件(如 [multer])进行填充 | +| `req.ip` | 获取请求 ip | +| `req.header()` | 获取特定请求标头 | + +> 特殊地,可以通过 req.rawHeaders 获取所有请求标头 + +注意,如果服务器上使用了反向代理,则 express 默认不信任这些反向代理的请求头。 +比如通过 nginx 设置反向代理后,通过 `req.ip` 只会获取到 127.0.0.1。 +如果想要信任反向代理,需要将 trust proxy 设置为 true + +```js +app.enable('trust proxy') +``` + +此外,设置反向代理时,也要让它将实际的 ip 转发给 node。比如 nginx 中需要配置: + +```nginx +location / { + # $proxy_add_x_forwarded_for 和 $remote_addr 都是实际 ip + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; +} +``` + +## 使用 node:https 模块部署 https 站点 + +```js +const express = require('express') +const https = require('https') +const http = require('http') +const fs = require('fs') +const path = require('path') + +const app = express() + +app.get('/', (req, res) => { + res.send('hello, world') +}) + +const certPath = path.resolve('c:/cert/localhost.pem') +const keyPath = path.resolve('c:/cert/localhost-key.pem') +const options = { + key: fs.readFileSync(keyPath), + cert: fs.readFileSync(certPath), +} + + +https.createServer(options, app).listen('443', () => { console.log(`https://localhost`) }) +http.createServer(app).listen('80', () => { console.log('http://localhost') }) +``` + +[app.METHOD]: https://expressjs.com/en/4x/api.html#app.METHOD +[path-to-regexp]: https://github.com/pillarjs/path-to-regexp +[body-parser]: https://www.npmjs.com/package/body-parser +[multer]: https://www.npmjs.com/package/multer +[cookie-parser]: https://www.npmjs.com/package/cookie-parser diff --git a/web/Express/middleware.md b/web/Express/middleware.md new file mode 100644 index 0000000..6a0267a --- /dev/null +++ b/web/Express/middleware.md @@ -0,0 +1,221 @@ + +常用中间件 + +- `body-parse` +- `cors` +- `multer` + +| middleware | handler (Content-Type) | +| ------------ | ----------------------------------- | +| `body-parse` | `application/json` | +| `body-parse` | `application/x-www-form-urlencoded` | +| `multer ` | `multipart/form-data` | + +## body-parse + +```js +const bodyParser = require('body-parser') + +// for parsing application/json +app.use(bodyParser.json()) + +// for parsing application/x-www-form-urlencoded +app.use(bodyParser.urlencoded({ extended: true })) +``` + +## multer 中间件 + +只处理 multipart/form-data 类型的表单数据,主要用于上传文件 + +Multer 会添加一个 body 对象 以及 file 或 files 对象 到 express 的 request 对象中。 body 对象包含表单的文本域信息,file 或 files 对象包含对象表单上传的文件信息。 + + +multer 会填充 body, file 和 files 字段到 req 对象中 + body 字段:表单文本值 + file 或 files 字段:表单上传的文件 +multer 的核心在于处理文件,有以下几种方式: + Multer.any() 处理表单中包含的所有文件 + Multer.none() 告诉 multer 不需要处理文件 + 如果表单中有文件会报错 + Multer.single() 告诉 multer 处理单文件并填充进 file 字段 + Multer.array() 告诉 multer 处理多个文件并填充进 files 字段(此时是数组) + 如果没提供参数,等同 none(),此时如果有文件也会报错 + Multer.fields() 告诉 multer 按照特定结构处理多个文件并填充进 files 字段(此时是对象) + files 字段的结构会和 fields 所定义的结构相同。 + + +### 文件包含的信息 + + +| Key | Description | Note | +| ------------ | ------------------------------ | ------------- | +| fieldname | Field name 由表单指定 | | +| originalname | 用户计算机上的文件的名称 | | +| encoding | 文件编码 | | +| mimetype | 文件的 MIME 类型 | | +| size | 文件大小(字节单位) | | +| destination | 保存路径 | DiskStorage | +| filename | 保存在 destination 中的文件名 | DiskStorage | +| path | 已上传文件的完整路径 | DiskStorage | +| buffer | 一个存放了整个文件的 Buffer | MemoryStorage | + + + +### multer(options) 中的 options 参数 + +Multer 接受一个 options 对象,其中最基本的是 dest 属性,这将告诉 Multer 将上传文件保存在哪。如果你省略 options 对象,这些文件将保存在内存中,永远不会写入磁盘。 + +为了避免命名冲突,multer 会对文件进行重命名,该功能可自己定制 + +options 可选的配置 + + +| Key | Description | +| -------------- | ---------------------------------- | +| dest / storage | 将文件存储在哪里 | +| fileFilter | 文件过滤器,控制哪些文件可以被接受 | +| limits | 限制上传的数据 | +| preservePath | 是否保存包含文件名的完整文件路径 | + + + +dest 值为一个路径字符串(若不存在会自动创建) +storage 代表存储在内存中,值为 multer.memoryStorage() + +### multer 对象上的方法 + +```js +const upload = multer({ dest: 'uploads/' }) +app.post('/profile', upload.single('avatar'), function (req, res, next) { + // req.file 是 `avatar` 文件的信息 + // req.body 将具有文本域数据,如果存在的话 +}) + +``` + +* .single(fieldname) + + 接受一个以 fieldname 命名的文件。这个文件的信息保存在 req.file。这个 filedname 会和表单中的 name 匹配 + +* .array(fieldname[, maxCount]) + + 接受一个以 fieldname 命名的文件数组。可以配置 maxCount 来限制上传的最大数量。这些文件的信息保存在 req.files。 + +* .fields(fields) + + 接受指定 fields 的混合文件。这些文件的信息保存在 req.files。 + + fields 应该是一个对象数组,应该具有 name 和可选的 maxCount 属性。 + +```js +upload.fields([ + { name: 'avatar', maxCount: 1 }, + { name: 'gallery', maxCount: 8 } +]) +``` + +* .none() + + 只接受文本域。如果任何文件上传到这个模式,将发生 "LIMIT_UNEXPECTED_FILE" 错误。这和 upload.fields([]) 的效果一样。 + +* .any() + + 接受一切上传的文件。文件数组将保存在 req.files。 + + +------------------------------------------------------------- + + +### `storage` + +#### 磁盘存储引擎 (`DiskStorage`) + +磁盘存储引擎可以让你控制文件的存储。 + +**注意:** cb 是回调函数,第一个参数代表是否是否有错误,若无则传入 null + +```javascript +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, '/tmp/my-uploads') + }, + filename: function (req, file, cb) { + cb(null, file.fieldname + '-' + Date.now()) + } +}) +const upload = multer({ storage }) + +``` + +有两个选项可用,`destination` 和 `filename`。他们都是用来确定文件存储位置的函数。 + +`destination` 是用来确定上传的文件应该存储在哪个文件夹中。 +可以使用一个函数,也可以提供一个 `string` (例如 `'/tmp/uploads'`)。如果没有设置 `destination`,则使用操作系统默认的临时文件夹。 + +**注意**: 如果你提供的 `destination` 是一个函数,你需要负责创建文件夹。当提供一个字符串,multer 会自动创建该文件夹(若不存在) + +`filename` 用于确定文件夹中的文件名的确定。 +如果没有设置 `filename`,每个文件将设置为一个随机文件名,并且是没有扩展名的。 + +**注意**: Multer 不会为你添加任何扩展名,你的程序应该返回一个完整的文件名。 + +每个函数都传递了请求对象 (`req`) 和一些关于这个文件的信息 (`file`),有助于你的决定。 + +注意 `req.body` 可能还没有完全填充,这取决于向客户端发送字段和文件到服务器的顺序。 + + +------------------------ + +#### 内存存储引擎 (`MemoryStorage`) + +内存存储引擎将文件存储在内存中的 `Buffer` 对象,它没有任何选项。 + +```javascript +const storage = multer.memoryStorage() +const upload = multer({ storage: storage }) +``` + +当使用内存存储引擎,文件信息将包含一个 `buffer` 字段,里面包含了整个文件数据。 + +**警告**: 当你使用内存存储,上传非常大的文件,或者非常多的小文件,会导致你的应用程序内存溢出。 + + +### limits 对文件进行限制 + +limits 是一个对象 + + + +| Key | Description(Number 类型) | Default | +| ------------- | ---------------------------------------------------------- | --------- | +| fieldNameSize | field 名字最大长度(单位 bytes) | 100 bytes | +| fieldSize | field 值的最大长度(单位 bytes) | 1MB | +| fields | 非文件 field 的最大数量 | 无限 | +| fileSize | 在 multipart 表单中,文件最大长度 (字节单位) | 无限 | +| files | 在 multipart 表单中,文件最大数量 | 无限 | +| parts | 在 multipart 表单中,part 传输的最大数量(fields + files) | 无限 | +| headerPairs | 在 multipart 表单中,键值对最大组数 | 2000 | + +设置 limits 可以帮助保护你的站点抵御拒绝服务 (DoS) 攻击。 + +### fileFilter + +设置一个函数来控制可以上传什么类型的文件,那些文件会被跳过 + +示例: + +```js +function fileFilter (req, file, cb) { + // cd 函数的第二个参数:指示是否应接受该文件 + // 例如:接受这个文件,使用`true` + cb(null, true) + // 如果有问题,你可以总是这样发送一个错误: + cb(new Error(`I don't have a clue!`)) +} + + +``` + + +> [查看官方文档]( https://github.com/expressjs/multer) +> [如何自定义存储引擎](https://github.com/expressjs/multer/blob/master/StorageEngine.md) diff --git a/web/Jest/README.md b/web/Jest/README.md new file mode 100644 index 0000000..a7fdaf3 --- /dev/null +++ b/web/Jest/README.md @@ -0,0 +1,175 @@ +# [Jest](https://jestjs.io/zh-Hans/) 使用 + +安装 `@types/jest` 可获得 API 提示。 + +```sh +npm install --save-dev @types/jest +``` + +Jest 主要还是提供了工具,具体的判断还是得自己手写测试逻辑逻辑,不然也不会有测试工程师这个职业了。 + +测试的代码为的是清晰,所以有一些重复的代码是允许的。不过注意,共有变量需要在 beforeEach 中声明,而不是直接写在全局作用域中,这样才能为每一个测试单元提供不同的变量。 + +## 问题 + +- 测试文件名要有 `.test.`,不然 jest 找不到 + +## 覆盖率报告 + +通过 `--coverage` 参数可以很便捷的生成测试覆盖率报告。 + +覆盖率有四个指标: + +- Stmts: 表示语句覆盖率,一个完整的可能有多行,比如将一个三元表达式分成多行。 +- Branch: 分支覆盖率。也就是 if else 等分支执行情况(排列组合) +- Funcs: 函数覆盖率,有多少函数被调用了 +- Lines: 行覆盖率,比 Stmts 更精细,精确到了行。 + +覆盖率会生成 coverage 文件夹,该文件夹一般是会上传到云端的,方便他人查看你的测试报告。 + +应用的覆盖率一般不会很高,60% 多就算不错了。但对于类库要求是比较高的,比如 [ant-design](https://github.com/ant-design/ant-design) 是 100%! + +## 全局设定 + +- `describe(name, fn)`' +- `it(name, fn)` + - `test` 是 `it` 的别名,但似乎有这么一个约定俗成的习惯:`describe` 中的测试都是使用 `it`。成型的测试文件中一般不会用到 `test`。 +- `afterAll(fn, timeout?)` +- `afterEach(fn, timeout?)` +- `beforeAll(fn, timeout?)` +- `beforeEach(fn, timeout?)` + +## Expect 断言 / 匹配 + +`expect(...)` 返回一个待匹配的内容 + +- 基础的匹配 + - `.not.` 取反 + - `toBe()` 精准匹配 + - `toEqual()` 对象匹配,会忽略 key 为 undefined 的键值对、忽略数组中的空槽、忽略对象的类型。 + - `toStrictEqual()` 更严格的对象匹配 + - `toContain()` 是否包含某个匹配项 +- 真值价值匹配: + - `toBeNull()` 只匹配 null + - `toBeUndefined()` 只匹配 undefined + - `toBeDefined()` 与 `toBeUndefined()` 相反 + - `toBeTruthy()` 匹配真值(即能让任何 if 语句为真的值) + - `toBeFalsy()` 匹配假值 +- 数字匹配: + - `toBeGreaterThan()` 大于 + - `toBeGreaterThanOrEqual()` 大于等于 + - `toBeLessThan()` 小于 + - `toBeLessThanOrEqual()` 小于等于 + - `toBeCloseTo()` 匹配浮点数的等于。`expect(0.1+0.2).toBeCloseTo(0.3);` +- 正则匹配 + - `toMatch()` +- 抛出错误 + - `toThrow(..)`,比如 `toThrow()`, `toThrow(Error)`, `toThrow('wrong')`, `toThrow(/^wrong.*$/)` + +### 数组断言 + +要判断两个数组相同还真不简单。 + +```js +test('两个数组元素相同,长度相同。', () => { + const received = [1, 2, 3, 1] + const excepted = [3, 2, 1, 2] + expect(received.length).toBe(excepted.length) + expect(received).toEqual(expect.arrayContaining(excepted)) + expect(excepted).toEqual(expect.arrayContaining(received)) +}) +``` + +## 异步测试 + +- 错误案例 + + ```js + function fetchData(data) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(data) + }, 1000) + }) + } + test('测试异步函数', () => { + const data = 2 + fetchData(data).then((data) => { + expect(data).toBe('2') + }) + }) + ``` + + 运行上面代码,会发现 jest 测试结果是通过的。(虽然后面会提示错误,但那并不是我们期待的报错方式) + +- 正确案例 1: 返回一个 Promise + + ```js + function fetchData(data) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(data) + }, 1000) + }) + } + test('测试异步函数', () => { + const data = 2 + return fetchData(data).then((data) => { + expect(data).toBe('2') + }) + }) + ``` + +- 正确案例 2: 使用 async/await + + ```js + function fetchData(data) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(data) + }, 1000) + }) + } + test('测试异步函数', async () => { + const data = 2 + await fetchData(data).then((data) => { + expect(data).toBe('2') + }) + }) + ``` + + 这种使用方式,其实是返回一个 Promise 的语法糖。 + +- 正确案例 3: 通过调用 `done()` 来测试异步函数 + + ```js + function fetchData(data) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(data) + }, 1000) + }) + } + test('测试异步函数', (done) => { + const data = 2 + fetchData(data).then((data) => { + console.log(done) + try { + expect(data).toBe('2') + done() + } catch (error) { + done(error) + } + }) + }) + ``` + + 当使用了 `done` 参数后,测试将会等待 done 函数的调用,如果超过一定时间(6s)还没有调用 done,则会提出测试超时错误。 + + 使用 done 时注意要通过 trycatch 包裹起来,因为测试出现错误时,将会抛出一个错误,这意味着后面的 done 将不会运行,所以最终只会提示超时错误,而不是具体的测试错误。 + +## Mock 模拟函数 + +## CLI 选项 + +- `-t=` / `--testNamePattern=` 匹配对应 `it` 的测试名称 diff --git "a/web/Jest/\351\205\215\347\275\256\346\226\207\344\273\266.md" "b/web/Jest/\351\205\215\347\275\256\346\226\207\344\273\266.md" new file mode 100644 index 0000000..9db4572 --- /dev/null +++ "b/web/Jest/\351\205\215\347\275\256\346\226\207\344\273\266.md" @@ -0,0 +1,52 @@ +# Jest 配置文件 + +## 案例,来自 [vue-cli](https://github1s.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-plugin-unit-jest/presets/default/jest-preset.js#L18-L54) + +- `moduleFileExtensions` 自动猜测模块后缀名,注意有优先级 +- `transform` 转换代码,比如让 vueJest 编译 vue 文件。 +- `transformIgnorePatterns` 与 `transform` 对应 +- `moduleNameMapper` 配置路径的映射,比如将 `@` 映射为 `src` 根目录 + +```js +module.exports = { + moduleFileExtensions: [ // 导入模块时,会根据该数组自动猜测后缀名,注意是有顺序要求的,如果已经找到 a.js 那么就不会找 a.jsx 了。 + 'js', 'jsx', 'json', 'vue', + ], + transform: { // 转换代码,比如让 vueJest 编译 vue 文件。, + // process *.vue files with vue-jest + '^.+\\.vue$': vueJest, + '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|avif)$': + require.resolve('jest-transform-stub'), + '^.+\\.jsx?$': require.resolve('babel-jest'), + }, + transformIgnorePatterns: ['/node_modules/'], + // support the same @ -> src alias mapping in source code + moduleNameMapper: { // 配置路径的映射,比如将 `@` 映射为 `src` 根目录 + '^@/(.*)$': '/src/$1', + }, +} +``` + +当想要依赖其他配置时,可以借助 `deepmerge` 来进行合并。比如下面案例: + +```js +const deepmerge = require('deepmerge') +const defaultPreset = require('../default/jest-preset') + +let tsJest = null +try { + tsJest = require.resolve('ts-jest') +} catch (e) { + throw new Error('Cannot resolve "ts-jest" module. Typescript preset requires "ts-jest" to be installed.') +} + +module.exports = deepmerge( + defaultPreset, + { + moduleFileExtensions: ['ts', 'tsx'], + transform: { + '^.+\\.tsx?$': tsJest + } + } +) +``` diff --git a/web/Nginx/draft.md b/web/Nginx/draft.md new file mode 100644 index 0000000..9e38fc3 --- /dev/null +++ b/web/Nginx/draft.md @@ -0,0 +1,136 @@ +# nginx 草稿 + +## 安装和基本使用 + +进入 https://nginx.org/en/download.html + +下载 Mainline version 版本,然后解压 + +> 注意:不管配不配置环境变量,对 nginx 的所有操作都应该在对应文件夹中。 + +```powershell +[Environment]::SetEnvironmentVariable("NGINX_HOME", "C:\nginx-1.25.4\", "User") +# 添加 nginx 目录为环境变量,方便以后进入该文件夹 + +cd $env:NGINX_HOME +# 一定要先进入文件 + +start nginx +# 启动 nginx + +nginx -t +# 测试配置文件是否有效 + +nginx -s reload +# 应用新的配置文件(平滑关闭旧进程) + +nginx -s quit +# 平滑关闭 graceful shutdown +nginx -s stop +# 快速关闭 fast shutdown +``` + +其他辅助命令 + +```sh +tasklist | findstr "nginx.exe" +# 查看 nginx 状态 + +taskkill /F /PID xx /PID xx +# 强制关闭对应 PID +``` + +## 本地搭建 https + +安装 [mkcert-v1.4.4-windows-amd64.exe] + +执行命令 + +```sh +$ .\mkcert-v1.4.4-windows-amd64.exe -install +# 安装 CA 根证书 + +$ .\mkcert-v1.4.4-windows-amd64.exe -CAROOT +# 查看根证书位置 +# 或者运行 certmgr.msc,点击 “受信任的根证书颁发机构”,可以找到 mkcert xx@xx + +$ .\mkcert-v1.4.4-windows-amd64.exe localhost 127.0.0.1 +# 为 localhost 和 127.0.0.1 生成证书: +# c:\Users\keety\Downloads\localhost+1.pem +# c:\Users\keety\Downloads\localhost+1-key.pem +``` + +配置 nginx: + +```nginx +# C:\nginx-1.25.4\conf\nginx.conf +events { + worker_connections 1024; +} + +http { + + server { + listen 80; + listen [::]:80; + # html 文件夹,指的是 nginx 安装目录中的 html 文件夹 + # 比如 C:\nginx-1.25.4\html + root html; + index index.html; + } + + server { + listen 443 ssl; + listen [::]:443 ssl; + server_name localhost; + + location / { + root html; + index index.html; + } + + ssl_certificate c:\Users\keety\Downloads\localhost+1.pem; + ssl_certificate_key c:\Users\keety\Downloads\localhost+1-key.pem; + } + + server { + listen 8080 ssl; + listen [::]:8080 ssl; + server_name localhost; + + ssl_certificate c:\Users\keety\Downloads\localhost+1.pem; + ssl_certificate_key c:\Users\keety\Downloads\localhost+1-key.pem; + + location / { + # 反向代理,需要运行一个网页在 6449 端口。然后通过 https://localhost:8080 就可以访问到 6449 端口的内容 + proxy_pass http://localhost:6449; + } + } + +} +``` + +可以将其添加到环境变量中,然后为其创建一个软连接 + +```sh +$ New-Item -ItemType SymbolicLink -Target mkcert-v1.4.4-windows-amd64.exe -Path C:\soft\it\mkcert\mkcert.exe +# C:\soft\it\mkcert\ 已经添加到环境变量中。其中 mkcert-v1.4.4-windows-amd64.exe 也在 C:\soft\it\mkcert\ 里面 + +$ mkcert +# 测试。 +``` + + + + [mkcert-v1.4.4-windows-amd64.exe]: https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-windows-amd64.exe diff --git a/web/Nginx/old.conf b/web/Nginx/old.conf new file mode 100644 index 0000000..147ddfd --- /dev/null +++ b/web/Nginx/old.conf @@ -0,0 +1,140 @@ +# 这个文件是作为 “学习 nginx ” 的过渡产品。有什么需要的在这里看。新学了什么也写在这里。等到积累的够多时,再重构它 +# 2022-08-26 22:49 + +# 阅读官方文档 http://nginx.org/en/docs/ + +# 这里是全局配置 + +# 不懂 nginx 是啥。 +user nginx; + +# nginx 调用的工作进程数量 +worker_processes auto; + +# 错误日志的输出位置 +error_log /var/log/nginx/error.log; + +# 指定 pid 文件存放的路径 +pid /run/nginx.pid; + +# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. +include /usr/share/nginx/modules/*.conf; + +events { + # 这里是 events 全局块。 events 块涉及的指令主要影响Nginx服务器与用户的网络连接 + # nginx 一个工作进程的最大连接数量 + worker_connections 1024; +} + +http { + # http 全局块 + + # 指定日志内容格式 + # $remote_addr: 用以记录客户端的ip地址; + # $remote_user :用来记录客户端用户名称; + # $time_local : 用来记录访问时间与时区; + # $request : 用来记录请求的url与http协议; + # $status : 用来记录请求状态;成功是200, + # $body_bytes_sent :记录发送给客户端文件主体内容大小; + # $http_referer :用来记录从那个页面链接访问过来的; + # $http_user_agent :记录客户浏览器的相关信息; + # $http_x_forwarded_for :用以记录客户端的ip地址(从请求头中获取的); + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + # 指定日志文件所在位置 + access_log /var/log/nginx/access.log main; + + # (不懂) + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # 导入其他配置文件, 文件扩展名与文件类型映射表(不太懂) + include /etc/nginx/mime.types; + + # 默认文件类型,如果不加此指令,默认值为text/plain。 + default_type application/octet-stream; + + # 从 /etc/nginx/conf.d 目录下加载配置文件 + include /etc/nginx/conf.d/*.conf; + + server { + # 监听端口号, 80 等同于 *:80 代表监听来自所有 IP 的 80 端口号 + listen 80; + # 指定了去哪里找请求的资源。(请求 itaem.cn/ 时,后面省略了 /index.html,所以默认会去找 /usr/80/index.html 文件) + root /usr/80; + } + server { + listen 443; + root /usr/443; + + # 这三项用于配置 https 证书 + ssl on; + ssl_certificate /.keys/cert.pem; + ssl_certificate_key /.keys/key.key; + + } + server { + listen 8001; + + # ~ 代表正则匹配 + location ~ /(js|css)/.* { + root /usr/8001/; + # $uri 代表 location 匹配到的内容 + try_files $uri 404; + } + location ~ /page/.* { + root /usr/8001; + # 注意 /index.html 的 / 符号不能省略 + try_files /index.html 404; + } + # 不加匹配参数,默认是 ~ + location / { + # 反向代理, 将接收到的请求转给 http://127.0.0.1:10001 进行处理 + proxy_pass http://127.0.0.1:10001; + # proxy_set_header 反向代理时, 设置请求头(添加或修改) + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + } + server { + listen 8081; + + location / { + root /usr/8081; + try_files $uri $uri/ /index.html; + } + location = /fill-in/ { + root /usr/8081; + try_files /index.html /index.html; + } + location /fill-in/ { + root /usr/8081; + try_files $uri $uri/ /fill-in.html; + } + } + server { + listen 39100; + root /usr/dist; + + # 高德地图 Web服务API 代理 + location /_AMapService/ { + # 修改 $args 变量, 往后面添加一个参数,然后转发给 https://restapi.amap.com + set $args "$args&jscode=xxxxxxxxxxxxxxxxxxxxxxxx"; + proxy_pass https://restapi.amap.com/; + } + + # 当出现 404 时, 修改 location 为 /404.html 这样就会被 location = /40x.html 识别 + error_page 404 /404.html; + location = /40x.html { } + + # 同理,当出现 500 502 503 504,跳到 location = /50x.html + error_page 500 502 503 504 /50x.html; + location = /50x.html { } + } + + +} diff --git a/web/Nodejs/README.md b/web/Nodejs/README.md new file mode 100644 index 0000000..7b750e9 --- /dev/null +++ b/web/Nodejs/README.md @@ -0,0 +1,5 @@ +# Nodejs + +- process + - `process.argv.slice(2)` 获取传入的命令行参数。(TODO: 查看 `minimist` 包的源码)。 + - `process.env` 环境变量 diff --git a/web/Prettier/.prettierrc b/web/Prettier/.prettierrc new file mode 100644 index 0000000..953c78d --- /dev/null +++ b/web/Prettier/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true, + "quoteProps": "consistent" +} diff --git a/web/Prettier/README.md b/web/Prettier/README.md new file mode 100644 index 0000000..f430e58 --- /dev/null +++ b/web/Prettier/README.md @@ -0,0 +1,23 @@ +# Prettier 基本使用 + +prettier 插件用于文件保存时自动格式化。 + +prettier 包用于 ci 流,结合 husky, lint-staged, eslint 使用 + +- 支持的代码 + - `ts`, `js`, `jsx`, `vue` + - `html`, `css`, `scss`, `less` + - `md`, `json`, `json`, `yaml` + - `flow`, `angular`, `Ember/Handlebars`, `GraphQL` +- CLI + - `prettier pattern [option]` + - `-c` / `--check` + - `-w` / `--write` +- 忽略文件 + - 通过 `.prettierignore` 文件指定 + - 通过代码注释 `/* prettier-ignore */` 指定 + - 通过命令行指定 `prettier **/*.{js,json,jsonc} !package-lock.json --check` +- 规则配置 + - 通过 `.prettierrc` 指定 + - 在 `package.json` 中的指定 `prettier` + - 命令行指定 `--single-quote` diff --git a/web/Rollup/README.md b/web/Rollup/README.md new file mode 100644 index 0000000..20d8a18 --- /dev/null +++ b/web/Rollup/README.md @@ -0,0 +1,10 @@ +# Rollup + +不仅仅是 rollup,各种打包构建工具,最重要的基本都是配置插件。学习构建,其实就是在学习插件的使用。[点击查看 rollup 插件列表](https://github.com/rollup/awesome) + +## 目前我使用过的插件 + +- `@rollup/plugin-json` 处理 json +- `@rollup/plugin-terser` 最小化代码 +- `@rollup/plugin-node-resolve` 解析模块(外部依赖)的路径(node_modules) +- `@rollup/plugin-commonjs` 将 CommonJS 转换为 ES 模块 diff --git a/web/Rollup/base-use.md b/web/Rollup/base-use.md new file mode 100644 index 0000000..558aaf8 --- /dev/null +++ b/web/Rollup/base-use.md @@ -0,0 +1,218 @@ +# rollup 基本使用(内容来自官方文档,方便对某些内容进行注释,熟悉后大概率删除) + +## 零碎说明 + +rollup 默认只会解析相对路径的模块。对于 `import { workspace } from "vscode"` 这种模块,rollup 会认为 `vscode` 是运行时需要的外部模块,所以不会去解析它。如果想要强制将外部模块打包处理,可以借助 `@rollup/plugin-node-resolve` 插件,或者告诉 rollup 如何找到它们。具体请查看[with npm package](https://cn.rollupjs.org/tools/#with-npm-packages) + +## CLI 配置 + +```sh +-f, --format 输出类型(amd、cjs、es、iife、umd、system) +-o, --file 单个输出文件(如果不存在,则打印到 stdout) +-d, --dir Directory for chunks(如果不存在,则打印到 stdout) + +-c, --config 指定配置文件。未指定时默认是 rollup.config.mjs -> rollup.config.cjs -> rollup.config.js + +--compact 缩小包装器代码 + +-m, --sourcemap 生成源映射(`-m inline` 为内联映射) +--sourcemapBaseUrl 使用给定的基本 URL 发出绝对源映射 URL,命令上格式为 C:/a/b/c 最终效果是在 bundle.js.map 文件末尾添加 //# sourceMappingURL=c:/a/b/c/bundle.js.map +--sourcemapExcludeSources 在源映射中不包括源代码 +--sourcemapFile 指定源映射的包位置。效果是指定 .map 文件中的 file 值。 +``` + +## 配置文件 + +```js +// rollup.config.js + +// 可以是数组(即多个输入源) +export default { + // 核心输入选项 + external, // 明确指定外部模块,外部模块不会被解析打包,而是会在运行时获取。比如 external: Object.keys(pkg.dependencies) + input, // 入口文件。比如 input: 'src/main.js' + plugins, + + // 进阶输入选项 + cache, + logLevel, + makeAbsoluteExternalsRelative, + maxParallelFileOps, + onLog, + onwarn, + preserveEntrySignatures, + strictDeprecations, + + // 危险区域 + acorn, + acornInjectPlugins, + context, + moduleContext, + preserveSymlinks, + shimMissingExports, + treeshake, + + // 实验性 + experimentalCacheExpiry, + experimentalLogSideEffects, + experimentalMinChunkSize, + perf, + + // 必需(可以是数组,用于描述多个输出) + output: { + // 核心输出选项 + dir, + file, + format, + globals, + name, + plugins, + + // 进阶输出选项 + assetFileNames, + banner, + chunkFileNames, + compact, + dynamicImportInCjs, + entryFileNames, + extend, + externalImportAssertions, + footer, + generatedCode, + hoistTransitiveImports, + inlineDynamicImports, + interop, + intro, + manualChunks, + minifyInternalExports, + outro, + paths, + preserveModules, + preserveModulesRoot, + sourcemap, + sourcemapBaseUrl, + sourcemapExcludeSources, + sourcemapFile, + sourcemapIgnoreList, + sourcemapPathTransform, + validate, + + // 危险区域 + amd, + esModule, + exports, + externalLiveBindings, + freeze, + indent, + noConflict, + sanitizeFileName, + strict, + systemNullSetters, + + // 实验性 + experimentalMinChunkSize + }, + + watch: { + buildDelay, + chokidar, + clearScreen, + exclude, + include, + skipWrite + } +}; +``` + +## 案例:处理 json 文件 + +1. 安装 + + ```sh + npm install --save-dev rollup @rollup/plugin-json @rollup/plugin-terser + ``` + +2. 在文件中导入 json 文件 + + ```js + // src/main.js + import foo, { add } from './foo.js' + import { name } from './a.json' // 注意不能写成 'a.json' + + export default function () { + console.log(foo) + console.log(name) + console.log(add(1, 2)) + } + ``` + +3. 配置 rollup 文件 + + ```js + import { defineConfig } from 'rollup' + import json from '@rollup/plugin-json' + import terser from '@rollup/plugin-terser' + + export default defineConfig({ // 使用 defineConfig 导出是为了获取类型提示。 + input: 'src/main.js', + plugins: [json()], + output: {}, + output: [ + { + file: 'dist/bundle.js', // 由于 output 是数组,所以无法指定 dir: 'dist' 是我不会使用吗? + format: 'cjs', + }, + { + file: 'dist/bundle.min.js', + format: 'iife', + name: 'version', + plugins: [terser()], // 打包后再处理的插件要放在这里 + }, + ], + }) + ``` + +4. 打包 + + ```sh + npx rollup --config + ``` + +5. 查看 `dist/bundle.js` 会发现 json 中的 `name` 字段自动被提取出来了,这就是 Tree Shaking + +## 案例:打包 node_modeles 依赖 + +说明:目前已经比较熟悉了,所以不会给出很详细的步骤。 + +1. `npm i -D @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup the-answer log-snapshot` + +2. 案例代码 + + ```js + // index.mjs + import answer from 'the-answer'; + import a from "log-snapshot"; + export default function () { + console.log('the answer is ' + answer); + console.log(a) + } + ``` + +3. 配置文件 + + ```js + import { defineConfig } from "rollup"; + import resolve from '@rollup/plugin-node-resolve'; + import commonjs from "@rollup/plugin-commonjs"; + + export default defineConfig({ + input: 'index.mjs', + // 不想打包的模块,需要添加到 external 中。 + external: ['the-answer'], + plugins: [resolve(), commonjs()], + output: { + format: 'cjs', + file: 'dist.js' + } + }) + ``` diff --git a/web/Svelte/README.md b/web/Svelte/README.md new file mode 100644 index 0000000..38ca9e8 --- /dev/null +++ b/web/Svelte/README.md @@ -0,0 +1,7 @@ +# Svelte + +## 构建 + +- 自己配置构建工具,可参考 [template](https://github.com/sveltejs/template),但注意它已经不再维护 +- 使用 vite 构建,运行 `npm init vite` +- 使用 SvelteKit 构建,运行 `npm create svelte@latest my-app`。 diff --git a/web/Svelte/SvelteKit-base-use.md b/web/Svelte/SvelteKit-base-use.md new file mode 100644 index 0000000..ed711a2 --- /dev/null +++ b/web/Svelte/SvelteKit-base-use.md @@ -0,0 +1,5 @@ +# SvelteKit + +## draft + +`export const trailingSlash = 'always'` 的作用,最直观的就是直接访问 `/about` 时不会 404(通过 serve 方式,而不是 vite preview)。 diff --git a/web/Svelte/base-use.md b/web/Svelte/base-use.md new file mode 100644 index 0000000..adb27f2 --- /dev/null +++ b/web/Svelte/base-use.md @@ -0,0 +1,1247 @@ +# 语法(熟悉后大概率删除) + +## `{#}` 语法 + +```svelte +{#await getRandomNumberPromise} +

...waiting

+{:then number} +

The random number is {number}

+{:catch error} +

Something wrong: {error.message}

+{/await} +``` + +## 事件 + +原生事件 + +```svelte +
+ The pointer is at {m.x} x {m.y} +
+ +
+ The pointer is at {m.x} x {m.y} +
+``` + +事件修饰符。使用语法为 `on:click|once|capture={...}`。支持的事件修饰符如下: + +- `preventDefault` 调用 `event.preventDefault()` +- `stopPropagation` 调用 `event.stopPropagation()` +- `passive` 优化了对 touch/wheel 事件的滚动表现。(Svelte will add it automatically where it's safe to do so) +- `nonpassive` 明确声明 passive 为 false +- `capture` 在事件捕获阶段触发 handler 而不是在事件冒泡阶段 +- `once` 触发 handler 后立马删除该事件 +- `self` 只有当 `event.target` 等于元素自身时才会触发 +- `trusted` 只有当 `event.isTrusted` 为 `true` 时才会触发 + +### 组件自定义事件 + +```svelte + + + + + + +``` + +```svelte + + + + +

{detail}

+``` + +### 组件自定义事件不会冒泡 + +比如 App --> Outper --> Inner + +Innert 中 `dispatch('message')`,则该事件只会传递到 Outper 组件中,如果 App 想要获取 message 事件,则需要 Outper 进行事件转发。 + +```svelte + + + +``` + +由于转发事件的操作基本都是一样的,所以 svelte 提供了快捷方式——如果没有赋予事件 handler,则默认转发该事件。 + +```svelte + + + +``` + +该语法糖同样适用于原生事件 + +```svelte + + +``` + +```svelte + + + + +``` + +## 绑定 `bind:` + +绑定使用起来非常简单,直接查看示例就懂了 + +在表单中的使用案例: + +```svelte + +{#each [1, 2, 3] as number} + +{/each} + + +{#each ['cookies and cream', 'mint choc chip', 'raspberry ripple'] as flavour} + +{/each} + + + +``` + +绑定还支持 `