Node.js v21

Node.js 21

  • 内置 Websocket 客户端, 使用 --experimental-websocket 标志启用

  • writeFile 文件系统函数添加 flush 可选项。

    • 启用该选项,会在成功写入数据时强制同步刷新数据到永久存储(之前不会立即刷新到永久储存,从而使读取操作获取旧数据)
    • 已添加 flush 选项的函数
      • filehandle.createWriteStream
      • fsPromises.writeFile
      • fs.createWriteStream
      • fs.writeFile
      • fs.writeFileSync
  • 添加全局 navigator, 目前 navigator.hardwareConcurrency 这一个对象

  • Object 和 Map 添加了静态方法 groupBy()

  • 使用 --experimental-default-type 将默认模块类型设置为 ESM

Node.js 21.1.0

  • 新增 --experimental-detect-module 检测没有在 packge.json 标明 type 同时文件后缀不是 .mjs/.cjs 的普通 js 文件是否是 ES 模块(推荐所有项目都添加 type 字段,即使是 commonjs 模块)
  • filesystem appendFile 添加 flush option

Next.js App Router

Next.js 13.4

  • App Router (Stable)

    • 使用文件系统作为 API

      • ```js
        // app/layout.js

        // New: App Router ✨
        // The root layout is shared for the entire application
        export default function RootLayout({ children }) {
        return (

        <html lang="en">
          <body>{children}</body>
        </html>
        

        );
        }

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16

        - 自由导入任何 CSS 文件

        - 与 React Suspense 深度集成,getServerSideProps 不再阻止交互

        - 默认所有组件都是 React Server 组件

        - ```js
        export default async function Page() {
        const res = await fetch('https://api.example.com/...');
        // The return value is *not* serialized
        // You can use Date, Map, Set, etc.
        const data = res.json();

        return '...';
        }
    • 默认将页面标记为 transitions,使路由转换能够中断

    • 自动代码分割

      • ```js
        import { getUser } from ‘./auth’;
        import { Dashboard, Landing } from ‘./components’;

        export default async function Layout() {
        const isLoggedIn = await getUser();
        // isLoggedIn 是 false 时, 不会被传输到客户端
        return isLoggedIn ? : ;
        }

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25

        - Turbopack (Beta): 使用 `next dev --turbo` 启用

        - Server Actions (Alpha)

        - [Server Action 的背后原理,没有魔法,只是简单函数](https://zenn.dev/cybozu_frontend/articles/server-actions-deep-dive)

        - 在服务端执行任何操作,和客户端代码轻松集成,不需要再写 api 代码了

        ```js
        import kv from './kv';

        // 集成到 form
        export default function Page({ params }) {
        async function increment() {
        'use server';
        await kv.incr(`post:id:${params.id}`);
        }

        return (
        <form action={increment}>
        <button type="submit">Like</button>
        </form>
        );
        }
    • 自定义调用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      'use client';

      import { useTransition } from 'react';
      import { myAction } from './action';

      export function MyComponent() {
      const [_, startTransition] = useTransition();

      const handleClick = () => {
      startTransition(async () => {
      await myAction();
      });
      };

      return <button onClick={handleClick}>Custom Invocation</button>;
      }

在 Web 中渲染 Word 文档

在 Web 中渲染 Word 文档

  • 使用 office 厂商提供的服务,微软 office online, WPS 等
  • 转为 PDF
  • 前端解析直接解析 .docx 文件并渲染

文件解析

文档格式转换

版式布局

  • 跟随 word 展示效果,采用分页渲染
  • word 单元格转为 html table
  • 形状使用 svg 模拟
  • 公式使用 XSLTProcessor 将 Office Math 转为 MathML

bfcache

往返缓存 bfcache

bfcache 是内存缓存,可在用户离开网页时存储网页(包括 JavaScript 堆)的完整快照。将整个页面保存在内存中后,当用户决定返回时,浏览器就可以快速轻松地恢复该页面。

  • 暂停执行 JavaScript 任务队列中的所有待处理任务,从 bfcache 中恢复时继续执行
  • 监听 bfcache
    • 使用 pageshow 和 pagehide 事件
  • 阻止浏览器进行 bfcache 优化
    • 使用 unload 事件
    • 使用 Cache-Control: no-store, 可以使用 Cache-Control: no-cacheCache-Control: max-age=0 替代
    • 使用 window.opener 或 window.postMessage() 进行跨页面引用
  • 推荐用法
    • 在 pagehide 时,关闭 fetch/xhr 请求,indexedDB 连接,WebSocket/WebRTC 连接,在 pageshow 时重新连接
    • 确实部署前在 devtools 中测试

CSS colors

CSS colors

css 支持使用多种语法描述颜色

  • keywords
  • hex codes
  • 使用 16 进制表示 #RRGGBB
  • color function
    • rgb/rgba
      • rgb() 前三个参数分别指定 red, green, blue 通道,最后一个参数指定 alpha 通道
    • hsl/hsla
      • hsl() 的三个参数分别是色调、饱和度和明度,最后一个参数指定 alpha 通道
    • hwb
      • hwb() 的三个参数分别是色调、白度和黑度,最后一个参数指定 alpha 通道
    • lch
    • lab
    • color
      • 指定预定义的颜色空间
      • color(srgb …)
      • color(display-p3 …)

sRGB 色域可以用 16 进制, rgb/rgba, hsl/hsla, hwb 直接指定

1
2
3
4
5
// 不同用法表示同一个颜色
color: #ff0000;
color: rgb(255 0 0 / 0.5);
color: hsl(0 100% 50%);
color: hwb(0 0% 0%);

将 HSL 颜色转为 sRGB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @param {number} hue - Hue as degrees 0..360
* @param {number} sat - Saturation in reference range [0,100]
* @param {number} light - Lightness in reference range [0,100]
* @return {number[]} Array of RGB components 0..1
*/
function hslToRgb(hue, sat, light) {
hue = hue % 360;

if (hue < 0) {
hue += 360;
}

sat /= 100;
light /= 100;

function f(n) {
let k = (n + hue / 30) % 12;
let a = sat * Math.min(light, 1 - light);
return light - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
}

return [f(0), f(8), f(4)];
}

将 sRGB 颜色转为 HSL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @param {number} red - Red component 0..1
* @param {number} green - Green component 0..1
* @param {number} blue - Blue component 0..1
* @return {number[]} Array of HSL values: Hue as degrees 0..360, Saturation and Lightness in reference range [0,100]
*/
function rgbToHsl(red, green, blue) {
let max = Math.max(red, green, blue);
let min = Math.min(red, green, blue);
let [hue, sat, light] = [NaN, 0, (min + max) / 2];
let d = max - min;

if (d !== 0) {
sat = light === 0 || light === 1 ? 0 : (max - light) / Math.min(light, 1 - light);

switch (max) {
case red:
hue = (green - blue) / d + (green < blue ? 6 : 0);
break;
case green:
hue = (blue - red) / d + 2;
break;
case blue:
hue = (red - green) / d + 4;
}

hue = hue * 60;
}

return [hue, sat * 100, light * 100];
}

monorepo & turbo

将多个项目存放在同一个 git 仓库中,将项目放入不同的工作区,各个工作区之间互相引用模块,实现代码共享。

monorepo 文件结构

前端项目使用包管理器管理 monorepo。

1
2
3
4
5
6
7
8
9
my-monorepo
├─ docs
├─ apps
│ ├─ pc
│ └─ mobile
├─ packages
│ ├─ tsconfig
│ └─ shared-utils
└─ root-files

如果将docsappspackages 下的所有目录设为工作区,需要在根目录如下设置

npm

在根目录的 package.json 文件中配置 workspaces 字段。在workspaces 中配置工作区列表。

1
2
3
4
5
{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": ["docs", "apps/*", "packages/*"]
}

yarn

在根目录的 package.json 文件中配置 workspaces 字段。在workspaces 中配置工作区列表。

1
2
3
4
5
{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": ["docs", "apps/*", "packages/*"]
}

pnpm

在根目录的 pnpm-workspace.yaml 文件中配置工作区列表。

1
2
3
4
packages:
- 'docs'
- 'apps/*'
- 'packages/*'

添加工作区依赖

在某个工作区将另一个工作区作为依赖项,需要在其package.json 指明:

npm

1
2
3
4
5
{
"dependencies": {
"shared-utils": "*"
}
}

yarn

1
2
3
4
5
{
"dependencies": {
"shared-utils": "*"
}
}

pnpm

1
2
3
4
5
{
"dependencies": {
"shared-utils": "workspace:*"
}
}

原理解释

在根目录执行npm install 后,npm 会在根目录和每个工作区下载依赖包到 node_modules , 当遇到工作区依赖后,会将工作区符号链接node_modules, 所以可以将其当做普通依赖一样正常导入。

使用 Turborepo 管理 monorepo

Turborepo 是一个在包管理器提供的monorepo 管理功能之上,提供了更方便并且性能更好的构建系统。

使用 Turborepo只要在根目录安装turbo, 同时在根目录下添加turbo.json 即可开始使用

1
2
3
4
5
6
7
8
{
"pipeline": {
"build": {
"dependsOn": ["^build"]
},
"lint": {}
}
}

当运行  turbo lint  时,Turborepo 会查看每个工作区中的每个  lint  脚本并运行它。

turbo.json 主要配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
// 全局环境变量
"globalEnv": ["GITHUB_TOKEN"],
// 任务管道
"pipeline": {
// 任务名称,会去匹配工作区的同名任务
"build": {
// 此任务所依赖的任务列表, 执行此任务前,会先执行依赖的任务
// ^ 前缀表示,此任务取决于工作区的依赖关系
// ^build 表示依赖的工作区都执行完 build 之后,再执行此任务
// 下面的配置表示,执行 turbo build 时,会先执行 turbo lint
// 和依赖项中的 build 任务
"dependsOn": ["lint", "^build"],
// 缓存输出文件
"outputs":["dist/**"],
// 是否缓存 outputs, 默认 true
"cache": true,
// 只在匹配的文件发生更改时重新运行任务
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"],
// 标记长时间运行的任务,比如 dev
"persistent": true,
}
}
}

浏览器存储

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com; path=/; secure
Other-header: other-header-value

这里创建的 cookie 对所有 wrox.com 的子域及该域中的所有页面有效(通过 path=/ 指定)。不过,这个 cookie 只能在 SSL 连接上发送,因为设置了 secure 标志。 要知道,域、路径、过期时间和 secure 标志用于告诉浏览器什么情况下应该在请求中包含 cookie。 这些参数并不会随请求发送给服务器,实际发送的只有 cookie 的名/值对。

SameSite

  • Non**e**。浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写。

  • **Strict****。**浏览器将只在访问相同站点时发送 cookie。(在原有 Cookies 的限制条件上的加强,如上文 “Cookie 的作用域” 所述)。

  • **Lax**与 **Strict** 类似,但用户从外部站点导航至 URL 时(例如通过链接)除外。 在新版本浏览器中,为默认选项,Same-site cookies 将会为一些跨站子请求保留,如图片加载或者 frames 的调用,但只有当用户从外部站点导航到 URL 时才会发送。如 link 链接。

未设置 SameSite, 主流浏览器默认设为 Lax。

JS 想要操作 cookie 只能通过 BOM 接口 document.cookie 操作,并且需要 URL 编码(可以用 decodeURIComponent),一般都是封装下方法再使用。

如果 cookie 设置了 HttpOnly, 则前端无法操作。

限制

  • 不超过 300 个 cookie;

  • 每个 cookie 不超过 4096 字节;

  • 每个域不超过 20 个 cookie;

  • 每个域不超过 81920 字节。

Storage

方法

  • clear():删除所有值。

  • getItem(name):取得给定 name 的值。

  • key(index):取得给定数值位置的名称。

  • removeItem(name):删除给定 name 的名/值对。

  • setItem(name, value):设置给定 name 的值。

sessionStorage

使用 sessionStorage 存储的数据只在当前会话有效。有效的页面:

  • 设置 sessionStorage 的页面页面1
  • 页面1打开的页面

如果在地址栏敲入新的页面,即使是同一个页面,也无法获取到 sessionStorage。

localStorage

满足域、端口和协议都相同的页面可以获取到同一个 localSrtorage。

区别

localStorage: 存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览器而丢失。

sessionStorage: sessionStorage 中的数据不受页面刷新影响,会在关闭标签页后丢失。

IndexedDB

todo

Cache Storage API

Cache 这个 API 是针对 Request Response 的。Cache 一般结合 Service Worker 使用,因为请求级别的缓存与具有页面拦截功能的 Service Worker 最配。

创建或打开一个命名空间

// 不存在则创建
const myCache = await caches.open(‘myCache’);

添加缓存

通过 add/addAll 方法添加,调用 add 会类似 fetch 一样发送请求,并将响应放到 Cache Storage 里面。

在 put 方法里传一个 Response 也可以实现添加缓存。

// 参数同 fetch
myCache.add(‘/test-url’);

// 使用 put
fetch(url).then(function (response) {
if (!response.ok) {
throw new TypeError(‘bad response status’);
}
return cache.put(url, response);
})

读取

通过 match 或 matchAll 方法读取。

// 参数同 fetch, 可以是 URL 地址,也可以是 Request 对象
const res = await myCache.match(“/subscribe”);
// const res = await myCache.matchAll(“/subscribe”);

更新

通过 add 或 put 方法更新。

const request = new Request(“/subscribe”);
const fetchResponse = await fetch(request);
myCache.put(request, fetchResponse);

销毁

使用 delete 方法删除某个路径的缓存或者直接删除整个命名空间。

myChache.delete(‘/subscribe’);
caches.delete(‘myChache’);

使用场景

在 service worker (在全局环境里也能用) 中用来缓存 js/css/img 文件或者不经常更新的接口,加快请求或者提供离线访问能力。

babel plugin-transform-runtime 配置和编译结果差异

准备源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// module.js
const foo = () => {
return new Promise(resolve => resolve(1));
};
export const bar = async () => {
await foo();
};
export const far = (...rest) => {
console.log(rest.map(item => item?.aa));
};

// index.js
import { bar, far } from './module';

function* gen() {
const y1 = yield 1;
return y1;
}
const bar1 = async () => {
await far();
};
const p1 = (...rest) => {
return new Promise(resolve => {
resolve(...rest);
});
};

配置 1 : 仅使用 @babel/env

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}

编译结果 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// module.js
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.far = exports.bar = void 0;

require("regenerator-runtime/runtime.js");

require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

require("core-js/modules/es.array.map.js");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

var foo = function foo() {
...
};

var bar = /*#__PURE__*/function () {
...
}();

exports.bar = bar;

var far = function far() {
...
};

exports.far = far;

// index.js
"use strict";

require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

require("regenerator-runtime/runtime.js");

var _module = require("./module");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

var _marked = /*#__PURE__*/regeneratorRuntime.mark(gen);

function gen() {
...
}

var bar1 = /*#__PURE__*/function () {
...
}();

var p1 = function p1() {
...
};

从结果来看,module.js 和 index.js 都从 core-js 引入了自身需要的 polyfill (全局变量),针对 async/await 分别生成了同样的 asyncGeneratorStep 和 _asyncToGenerator

配置 2 : 使用 @babel/env 和 @babel/plugin-transform-runtime (默认配置)

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
],
"plugins": [["@babel/plugin-transform-runtime"]]
}

编译结果 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// module.js
"use strict";

var \_interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "\_\_esModule", {
value: true
});
exports.far = exports.bar = void 0;

var \_regenerator = \_interopRequireDefault(require("@babel/runtime/regenerator"));

var \_asyncToGenerator2 = \_interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

require("core-js/modules/es.array.map.js");

var foo = function foo() {
...
};

var bar = /_#**PURE**_/function () {
...
}();

exports.bar = bar;

var far = function far() {
};

exports.far = far;

// index.js
"use strict";

var \_interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

var \_asyncToGenerator2 = \_interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

var \_regenerator = \_interopRequireDefault(require("@babel/runtime/regenerator"));

var \_module = require("./module");

var \_marked = /_#**PURE**_/\_regenerator.default.mark(gen);

function gen() {
...
}

var bar1 = /_#**PURE**_/function () {
...
}();

var p1 = function p1() {
...
};

添加 @babel/plugin-transform-runtime 后,对于 async/await 不再重复生成 asyncGeneratorStep 和 _asyncToGenerator,改为从 @babel/runtime 中引入, 其余 polyfill 依然从 core-js 引入

配置 3 使用 @babel/env 和 @babel/plugin-transform-runtime (配置 corejs, 需要安装 @babel/runtime-corejs3 依赖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}

编译结果 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// module.js
"use strict";

var \_interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

Object.defineProperty(exports, "\_\_esModule", {
value: true
});
exports.far = exports.bar = void 0;

var \_regenerator = \_interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var \_asyncToGenerator2 = \_interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));

var \_promise = \_interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var \_map = \_interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/map"));

var foo = function foo() {
return new \_promise.default(function (resolve) {
return resolve(1);
});
};

var bar = /_#**PURE**_/function () {
...
}();

exports.bar = bar;

var far = function far() {
...
};

exports.far = far;

// index.js
"use strict";

var \_interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var \_promise = \_interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var \_asyncToGenerator2 = \_interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));

var \_regenerator = \_interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var \_module = require("./module");

var \_marked = /_#**PURE**_/\_regenerator.default.mark(gen);

function gen() {
...
}

var bar1 = /_#**PURE**_/function () {
...
}();

var p1 = function p1() {
...
};

@babel/plugin-transform-runtime 添加配置 corejs: 3 后,所有的 polyfill 都改为从 @babel/runtime-corejs3 导入,同时由全局覆盖变为了局部变量,不会污染全局

结论:

@babel/presets 的 『”useBuiltIns”: “usage”』会引入模块中需要的 polyfill 并且是全局覆盖的,但是每个模块都会重复生成辅助函数
添加插件 『@babel/plugin-transform-runtime』 后,辅助函数不会在每个模块中重复生成,而是从 『@babel/runtime』引入
@babel/plugin-transform-runtime 进一步配置 『 “corejs”: 3 』后,所有的 polyfill 都从 『@babel/runtime-corejs3』引入,并且是局部变量,不会污染全局

HTTP 速览

HTTP 的标准是由 IETF 组织制定,跟它相关的标准:

HTTP Documentation (httpwg.org)

HTTP/1.1 相关

中文翻译: duoani/HTTP-RFCs.zh-cn: 翻译 HTTP 相关的 RFC (中英文对照) (github.com)

HTTP/2 相关

中文翻译: abbshr/rfc7540-translation-zh_cn: RFC 7540 - HTTP/2 中文翻译版 (github.com)

HTTP 协议是基于 TCP 协议实现的,在 TCP 的基础上规定了一个 Request-Response 的模式。这个模式就是客户端发送消息给服务端,服务端才能返回消息给客户端,服务端不能主动发送消息。

HTTP 协议格式

HTTP 协议大概可以划分成 Request 和 Response。

Request

Request 是客户端需要发送到服务端的数据,可以分成下面这三部分:

  • Request-Line(请求行)
  • method(方法)
  • URL
  • version(版本)
  • request-header(头)
  • message-body(实体)

具体的数据格式:

GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

sp 是空格,cr 是 \r , lf 是 \n

Response

Response 是服务端返回给客户端的数据,对应的也可以分成三部分:

  • Response Line(响应行)

  • version(版本)

  • status code(状态码)

  • status text(状态文本)

  • Response Header(响应头)

  • message-body(实体)

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: “34aa387-d-1568eb00”
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

Hello World! My payload includes a trailing CRLF.

响应状态码

HTTP 2

HTTP2 是 HTTP1.1 的升级版,HTTP2 相对于 HTTP1.1 来说主要有下面几点的改进:

  1. 对 HTTP 的头进行压缩,对于相同的头只需要发送索引表中的索引。解决了 HTTP1.1 每次都要将 header 来回传送。
  2. 支持服务端推送。能够在客户端发送第一个请求时,提前把一部分内容推送给客户端。这可以避免客户端请求顺序带来的并行度不高,从而导致的问题。
  3. 支持 TCP 的连接复用。使用同一个 TCP 连接来传输多个 HTTP 请求,避免了 TCP 连接时的三次握手开销,和初建 TCP 连接时传输窗口小的问题。

Hooks 的 Capture Value 特性

Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

函数组件容易阅读和测试,没有状态或生命周期。因此可以让我们快速的写一个渲染 UI 的组件,这也与 React 推崇函数式编程的思想契合。

在 React v16.8 推出之前函数组件只能简单的渲染组件,没有自身的状态,Hooks 的到来改变了这一现状,它赋予函数组件更多的能力。

通过 useState 让函数组件拥有了自身的状态。useEffect 让函数组件能够执行包含副作用的行为。

Capture Value

首先说明 class 组件是不具备 Capture Value 特性的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ClassCount extends Component {
state = {
count: 0,
};

timer() {
setTimeout(() => {
console.log(`点击 3 秒后的 count: ${this.state.count}`);
}, 3000);
}

render() {
return (
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
this.timer();
}}
>
class 组件点击了 {this.state.count} 次
</button>
);
}
}

上面的代码中,每次点击后 timer 输出的值和当前组件 state 的值是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function FunctionCount() {
const [count, setCount] = useState(0);

const timer = () => {
setTimeout(() => {
// timer 打印的 count 永远是上一次 render 中的值
console.log(`function 组件 3 秒后的 count: ${count}`);
}, 3000);
};

return (
<button
onClick={() => {
setCount(count + 1);
timer();
}}
>
function 组件点击了 {count} 次
</button>
);
}

在函数组件里,每次点击后 timer 输出的值和当前组件中的 state 不一致,这是因为函数组件他就是一个普通函数,react 每次渲染都会重新执行一遍这个函数,这样就导致,对于函数内部来说每次渲染都有自己的 Props 与 State。

每次渲染都有自己的 Props 与 State

可以认为每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Counter() {
const [count, setCount] = useState(0);

return (
<button
onClick={() => {
setCount(count + 1);
}}
>
function 组件点击了 {count} 次
</button>
);
}

当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
// react 第一次渲染时
function Counter() {
const count = 0;
return <div>{count}</div>;
}

// 点击触发 react 重新渲染,整个函数组件被重新调用了
function Counter() {
const count = 1;
return <div>{count}</div>;
}

// ...

React 仅仅只是在渲染输出中插入了 count 这个数字, 这个数字由 React 提供。当 setCount 的时候,React 会带着一个不同的 count 值再次调用组件。然后,React 会更新 DOM 以保持和渲染输出一致。

这里关键的点在于任意一次渲染中的 count 常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的 count 值独立于其他渲染。

每一次渲染都有它自己的事件处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}

当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量。对于 handleAlertClick 也是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//  react 第一次渲染
function Counter() {
const count = 0;

function handleAlertClick() {
setTimeout(() => {
// count === 0
alert('You clicked on: ' + count);
}, 3000);
}

return (...);
}

// 点击触发 react 重新渲染,整个函数组件被重新调用了
function Counter() {
const count = 01;

// 创建了新的 handleAlertClick
function handleAlertClick() {
setTimeout(() => {
// count === 1
alert('You clicked on: ' + count);
}, 3000);
}

return (...);
}

// ...

React 每次渲染都会创建新的 handleAlertClick, 而对于 handleAlertClick 来讲,每次他访问到的 count 都是一个全新的常量。

在任意一次渲染中,props 和 state 是始终保持不变的。如果 props 和 state 在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的 count 值。

每次渲染都有它自己的 Effects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(`useEffect 执行, count: ${count}`);
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

useEffect 和事件处理函数一样,每次渲染都被重新执行,传入 useEffect 的回调函数也被重新创建,所有内部引用的所有 state 都是独属于本次渲染的。

错误设置依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Counter4() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
// count 永远是 0
console.log('Counter4 setInterval', count);
setCount(count + 1);
}, 1000);

return () => clearInterval(id);
}, []);

return <h1>count: {count}</h1>;
}

上面的代码中的 setInterval 会持续执行,但是 count 的值保持不变,原因就是第一次执行时 setCount 触发重新渲染,但是由于 useEffect 的依赖是[],effect 不会再重新运行,它后面每一秒都会调用 setCount(0 + 1)

设置依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter5() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log('Counter5 useEffect');

const id = setInterval(() => {
// 每次都重新生成新的定时器
console.log('Counter5 setInterval');
setCount(count + 1);
}, 1000);

return () => clearInterval(id);
}, [count]);

return <h1>count: {count}</h1>;
}

上面的代码已经可以正确运行了,缺点是 count 每次改变都会重置 setInterval

仅指定行为,不依赖具体的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter6() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log('Counter6 useEffect');

const id = setInterval(() => {
// 仅指定行为,不依赖具体的值
console.log('Counter6 setInterval');
setCount(c => c + 1);
}, 1000);

return () => clearInterval(id);
}, []);

return <h1>count: {count}</h1>;
}

附 useState, useEffect 的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { render } from '../index';

// hooks 存放在这个数组
let memoizedState = [];
// 当前 memoizedState 下标
let cursor = 0;

export function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;

const currentCursor = cursor;
function setState(newState) {
if (typeof newState === 'function') {
memoizedState[currentCursor] = newState(memoizedState[currentCursor]);
} else {
memoizedState[currentCursor] = newState;
}

render();
// 每更新一次都需要将_index归零,才不会不断重复增加 memoizedState
cursor = 0;
}

// cursor 加 1
cursor++;

// 返回当前 state
return [memoizedState[currentCursor], setState];
}

export function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true;
if (hasNoDeps || hasChangedDeps) {
setTimeout(callback);
memoizedState[cursor] = depArray;
}
cursor++;
}