VMWare 配置桥接模式

桥接模式配置

  • 管理员模式启动VMware

  • VMware虚拟网络管理中,选择桥接模式并选择当前网卡

  • 虚拟机网络配置,选择桥接模式

  • 虚拟机网络链接配置,选择手动模式
    ip 地址前三位相同,最后一位保证在当前网段唯一
    其他信息与宿主机保持一致

编码思路-工程化

which/whereis

  • which

    搜索范围:仅在当前用户的 PATH 环境变量指定的目录中搜索。

    输出内容:仅返回第一个匹配的可执行文件路径。

  • whereis

    搜索范围:在预定义的标准系统目录(如 /bin, /usr/include, /usr/share/man 等)中搜索,不依赖 PATH。

    输出内容:返回所有相关文件路径(二进制、手册页、源代码等)。

npm/yarn/pnpm

  • npm

    嵌套依赖结构:早期版本采用嵌套的 node_modules 结构,导致依赖重复和路径过长问题。

    确定性依赖:在 npm@5 后引入 package-lock.json,锁定依赖版本(解决早期版本依赖不确定性)。

    早期有重复依赖的问题,扁平化处理后可能导致幽灵依赖。

  • yarn

    确定性依赖:引入 yarn.lock 文件(早于 npm 的 package-lock.json),锁定依赖树。

    并行下载:利用并行请求提升安装速度。

    离线缓存:全局缓存已下载的依赖包,减少重复下载。

    扁平化结构:将嵌套依赖提升到 node_modules 顶层,减少重复安装(但可能引发依赖冲突)

    PnP 模式: 劫持 Node.js 的模块解析逻辑,使其不再依赖物理的 node_modules 目录,而是通过映射表(.pnp.cjs)直接定位到 .zip 文件中的代码。

  • pnpm

    硬链接 + 符号链接:所有依赖包存储在全局存储目录(类似缓存)。通过硬链接共享相同版本的依赖,减少磁盘占用。使用符号链接在项目中按需链接依赖。

    严格依赖隔离:每个包的依赖在独立的 node_modules 中,避免幽灵依赖。

本地依赖

通常使用 lerna, nx 等工具,原生实现可以使用 file 协议。

1
2
3
4
5
{
"dependencies": {
"@my/cli-util": "file:/mnt/d/Workspace/my-cli/packages/util"
}
}

使用 pnpm

1
2
3
4
// pnpm-workspace.yaml
packages:
- "packages/*"

1
2
3
4
5
{
"dependencies": {
"@my/cli-util": "workspace:*"
}
}

路径处理

  • import-local 优先使用自己的本地安装版本

  • pkg-dir 查找项目的根目录

  • resolve-cwd 从 CWD 目录解析模块的绝对路径。

  • which 在 path 环境变量中查找第一个匹配

  • node 默认处理 .js .json .node 文件, 其他文件格式当作 js 文件处理

    1
    2
    3
    4
    5
    //a.txt
    console.log(1);

    // index.js
    const a = require("./a.txt"); // 不会报错,可以加载

root 检查

root-check, 如果使用 root 用户启动,尝试降级

参数处理

minimist 解析命令行参数

yargs commander 提供交互式的命令行

检查包是否安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let dir = __dirname;

do {
if (fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()) {
return true;
}
} while (dir !== (dir = path.dirname(dir)));

//require("module") 用于管理模块的接口
for (const internalPath of require("module").globalPaths) {
if (fs.statSync(path.join(internalPath, packageName)).isDirectory()) {
return true;
}
}
return false;

区分使用的是那种包管理器

1
2
3
4
5
6
7
if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
packageManager = "yarn";
} else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
packageManager = "pnpm";
} else {
packageManager = "npm";
}

原生的命令行交互 readLine

1
2
3
4
5
6
7
8
9
10
11
const readLine = require("readline");

const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr,
});

questionInterface.question("question one (yes/no):", (answer) => {
if (answer.startsWith("y")) {
}
});

子进程的 spawn 和 exec 函数之间的区别

Node.js 的子进程模块(child_process)有两个函数 spawn 和 exec,使用这两个函数,我们可以启动一个子进程来执行系统中的其他程序。刚接触 child_process 的人可能会问,为什么做同一件事会有两个函数,以及应该使用哪个函数。我将解释 spawn 和 exec 之间的区别,以帮助你决定何时使用哪个函数。

child_process.spawn 和 child_process.exec 的最大区别在于它们的返回值–spawn 返回一个流,而 exec 返回一个缓冲区。

child_process.spawn 返回一个包含 stdout 和 stderr 流的对象。您可以点击 stdout 流来读取子进程发回 Node 的数据。作为一个流,stdout 具有流所具有的 “data”(数据)、”end”(结束)和其他事件。当您希望子进程向 Node 返回大量数据(如图像处理、读取二进制数据等)时,最好使用 spawn。

child_process.spawn 是 “异步 asynchronous”(异步不同步)的,这意味着一旦子进程开始执行,它就会以流的形式从子进程发回数据。

这里有一个例子,我用 spawn 读取了 Node 的 curl 请求结果。

child_process.exec 返回子进程输出的整个缓冲区。默认情况下,缓冲区大小为 200k。如果子进程返回的数据超过该值,程序就会崩溃,并显示错误信息 “Error: maxBuffer exceeded”(错误:超过最大缓冲区)。你可以在执行选项中设置更大的缓冲区大小来解决这个问题。但你不应该这样做,因为 exec 并不适合向 Node 返回巨大缓冲区的进程。你应该使用 spawn 来解决这个问题。那么,exec 用来做什么呢?用它来运行返回结果状态而不是数据的程序。

child_process.exec 是 “同步异步 “的,也就是说,虽然 exec 是异步的,但它会等待子进程结束,并尝试一次性返回所有缓冲数据。如果 exec 的缓冲区大小设置得不够大,就会出现 “maxBuffer exceeded”(超过最大缓冲区)错误,导致执行失败。

请看这里的一个示例,我使用 exec 执行 wget 下载文件,并向 Node 更新执行状态。

这就是 Node 的子进程 span 和 exec 之间的区别。当你希望子进程向 Node 返回大量二进制数据时,请使用 spawn;当你希望子进程返回简单的状态信息时,请使用 exec。

1
2
3
4
5
6
7
8
9
10
11
12
13
const cp = require("child_process");
new Promise((resolve, reject) => {
const executedCommand = cp.spawn("echo 1", [], {
stdio: "inherit",
shell: true,
});

executedCommand.on("error", reject);

executedCommand.on("exit", (code) => {
if (code === 0) resolve();
});
}).then(() => {});

区分包的模块化方案

1
2
3
4
5
6
7
const pkgPath = require.resolve(`${packageName}/package.json`);
const pkg = require(pkgPath);
if (pkg.type === "module" || /\.mjs/i.test(pkg.bin[name])) {
import(path.resolve(path.dirname(pkgPath), pkg.bin[name])).catch();
} else {
require(path.resolve(path.dirname(pkgPath), pkg.bin[name]));
}

检查两个命令是否相似

(莱文斯坦距离)[https://zh.wikipedia.org/wiki/%E8%90%8A%E6%96%87%E6%96%AF%E5%9D%A6%E8%B7%9D%E9%9B%A2] (fastest-levenshtein)[https://github.com/ka-weihe/fastest-levenshtein]

获取某个包的最新版本

推荐使用 cross-spawn

1
2
3
4
5
6
7
8
9
10
const cp = require("child_process");

const { output } = cp.spawnSync(
"npm.cmd",
["view", "react@latest", "version"],
{
stdio: "pipe",
}
);
console.log(output.toString());

或者通过 api 拉取 https://registry.npmjs.com/lodash

是否是浏览器环境

1
2
// 使用 typeof, 对于不存在的变量会返回 undefined
const hasDocument = typeof document !== "undefined";

检查 IE 浏览器

1
2
3
var isIE11 =
typeof navigator !== "undefined" &&
navigator.userAgent.indexOf("Trident") !== -1;

版本号对比

1
2
3
4
5
6
7
8
const semver = require("semver");

const version1 = "1.2.3";
const version2 = "2.0.0";

if (semver.gt(version1, version2)) {
console.log(`${version1} is greater than ${version2}`);
}

commander 基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Command } from "commander";
const program = new Command();

program
.name("sun-cli")
.description("这是一个描述")
.option("--targetCommand", "参数的描述", "team1/command")
.action((options) => {
console.log(options);
});

program
.command("clone <source> [destination]")
.description("子命令的描述")
.action((source, destination) => {
console.log("clone command called");
});

program.parse();

React v16 源码分析 ⑬ 性能优化相关

对于以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Child() {
console.log("child");
return <div>2</div>;
}
function App() {
const [state, setState] = useState(1);
console.log("parent");
return (
<div>
<div onClick={() => setState(2)}>click</div>
<Child></Child>
</div>
);
}

首次挂载打印 parent => child;
第一次点击 click 打印 parent=>child
第二次点击 click 打印 parent
第三次点击 click,不打印任何内容

第二次点击的时候没有打印 child 实际上是命中了 bailout 策略,命中该策略的子组件会跳过 reconcile 过程,因此不会进入 render 阶段。

第三次点击没有任何打印,说明父组件和子组件都没有进入 render 阶段,实际上是命中了 eagerState 策略,这是一种发生于触发状态更新时的优化策略,如果命中了该策略不会进入 schedule 阶段,更不会进入 render 阶段。

eagerState

如果某个状态更新前后没有变化就可以跳过更新流程。

state 是基于 update 计算出来的,计算过程发生在 render 的 beginWork, 而 eagerState 则是将计算过程提到了 schedule 之前执行。

前提条件是该 fiberNode 不存在等待执行的更新,如果不存在更新那么就可以作为第一个更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 如果新状态与当前状态相同,我们可以完全摆脱副作用
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState; // 就是 memoizedState
const eagerState = lastRenderedReducer(currentState, action); // 基于 action 提前计算 state
// 如果在我们进入渲染阶段时 reducer 没有改变,那我们可以使用 eager 状态而无需再次调用 reducer。
update.hasEagerState = lastRenderedReducer; // 标记该 update 存在 eagerState
update.eagerState = eagerState; // 存储 eagerState 的值
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// ...
} finally {
// ...
}
}
}

上面的代码中通过 lastRenderedReducer 提前计算 State,如果前后状态没有变化就会命中 eagerState 策略,如果没有命中但是当前更新是 fiberNode 的第一个更新,也可以作为后续更新的 baseState。

为什么第二次点击的时候,父组件还是要渲染一次,因为进入 eagerState 的条件是

1
fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes);

current.lanes 和 workInProcess.lanes 都需要 NoLanes, 第一次点击时 beginWork 执行结束后 workInProcess.lanes 会被设置为 NoLanes 但此时还没有交换 fiberTree 需要等到 commit 之后执行,因此第一次点击之后,只有 current 被设置为了 NoLanes。
虽然没有命令 eagerState 但是会命中 bailout 这时会被设置为 NoLanes。

1
2
3
4
5
function bailoutHooks(current: Fiber, workInProgress: Fiber, lanes: Lanes) {
workInProgress.updateQueue = current.updateQueue;
// ...
current.lanes = removeLanes(current.lanes, lanes);
}

微前端 ③ systemJs

什么是 systemJs

它可以加载不同模块格式的代码,包括 ES 模块、CommonJS 和 AMD 模块,提供了一致的模块加载体验。

SystemJS 支持将现代 ES 模块代码转译为 System.register 格式(通常需要配合 webpack 等编译工具),以便在不支持 ES 模块的旧版浏览器中运行。这意味着开发者可以编写现代的、基于标准的模块代码,并确保它在旧版浏览器(如 IE11)中也能正常运行。

通过使用接近原生模块加载速度的 System.register 格式,SystemJS 在旧版浏览器中提供了高性能的模块加载能力。

另外还有一下的高级特性:

  • SystemJS 支持顶级 await、动态导入(dynamic import)、循环引用和实时绑定(live bindings),这些都是现代 JavaScript 模块的重要特性。
  • 它还支持 import.meta.url,这是 ES 模块的一部分,允许模块访问其自身的 URL。
  • 支持模块类型(module types)和导入映射(import maps),使得开发者可以更灵活地管理模块依赖关系。
  • 提供对内容安全策略(Content Security Policy, CSP)和完整性检查(integrity)的支持,增强了模块加载的安全性。

它也是 import 提案的解决方案, 在浏览器中原生的 esModule 不允许通过 import React from 'react' 的方式引入依赖,必须使用相对或绝对路径 import React from 'https://xxx/cdn/react.js'import React from './react' ,但配合 webpack, 可以将代码编译成 systemJs 模块化方案的代码,用于加载外部依赖。

systemJs 加载 react 应用

创建 index.html 模板, systemJS 会接管有 type='systemjs-importmap' script 标签中资源的加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<head>
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react/umd/react.development.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js"
}
}
</script>
<script src="https://cdn.staticfile.net/systemjs/6.14.3/system.js"></script>
</head>

<body>
<div id="root"></div>
</body>

配合 webpack 将 scripts 处理成 systemJs 能识别的格式, libraryTarget 配置为 system, 设置 HtmlWebpackPlugin 插件的 scriptLoading: "systemjs-module" 可以为 script 标签自动添加 type 类型。externals 配置是可选的,对于多个子应用有不同版本的相同第三方依赖,可以直接将依赖打包在子应用中,不需要单独在主应用中加载。(对于这个多版本依赖的问题,并没有明确的最佳实践,官方推荐将比较重的第三方依赖变成共享资源, 如果共享资源有依赖关系例如 react react-dom,可以使用 systemJS 的 amd 扩展,或者将依赖放置到 depcache 字段中,推荐使用 amd 扩展能比较好的统一接管)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
entry: path.resolve(__dirname, "./src/index.js"),
output: {
path: path.resolve(__dirname, "dist"),
publicPath: "/",
// 设置打包格式为 system
libraryTarget: "system",
},
// 排除依赖,通过 cdn 加载
externals: {
react: "react",
"react-dom": "react-dom",
},
module: {/** loader 配置 */}
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
scriptLoading: "systemjs-module",
}),
],
};

index.js 文件编译为 System.register 函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
System.register(
["react", "react-dom"],
function (__WEBPACK_DYNAMIC_EXPORT__, __system_context__) {
var __WEBPACK_EXTERNAL_MODULE_react__ = {};
var __WEBPACK_EXTERNAL_MODULE_react_dom__ = {};
Object.defineProperty(__WEBPACK_EXTERNAL_MODULE_react__, "__esModule", {
value: true,
});
Object.defineProperty(__WEBPACK_EXTERNAL_MODULE_react_dom__, "__esModule", {
value: true,
});
return {
setters: [
function (module) {
Object.keys(module).forEach(function (key) {
__WEBPACK_EXTERNAL_MODULE_react__[key] = module[key];
});
},
function (module) {
Object.keys(module).forEach(function (key) {
__WEBPACK_EXTERNAL_MODULE_react_dom__[key] = module[key];
});
},
],
execute: function () {
__WEBPACK_DYNAMIC_EXPORT__(
(() => {
"use strict";
var __webpack_modules__ = {
"./src/index.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
eval(/** index.js 编译结果 */);
},

"./node_modules/react-dom/client.js": (
__unused_webpack_module,
exports,
__webpack_require__
) => {
eval(/**react-dom/client.js 编译结果 */);
},

react: (module) => {
module.exports = __WEBPACK_EXTERNAL_MODULE_react__;
},

"react-dom": (module) => {
module.exports = __WEBPACK_EXTERNAL_MODULE_react_dom__;
},
};
/************************************************************************/
// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});

// Execute the module function
__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");

return __webpack_exports__;
})()
);
},
};
}
);

systemJs 加载流程

  • Q: systemjs-importmap 的作用是什么

    A: systemjs-importmap 作用是建立资源依赖的映射关系,由于浏览器不支持裸导入,因此通过一个自定义的 script 标签建立裸导入名称和资源路径的依赖关系

以单 React 子应用为例,加载主应用以及子应用的过程。

index.html 页面配置如下

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
<!DOCTYPE html>
<html lang="en">
<head>
<title>Root Config</title>
<meta name="importmap-type" content="systemjs-importmap" />
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"react": "https://cdn.jsdelivr.net/npm/react@16.14.0/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.production.min.js",
"@root/root-config": "//localhost:9000/root-config.js",
"@supos/supos-react-1": "//localhost:3000/supos-supos-react-1.js"
}
}
</script>

<script src="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script>
</head>

<body>
<main id="root"></main>
<div id="aa"></div>
<script>
System.import("@root/root-config");
</script>
</body>
</html>
  1. 主应用是一个编译后的 root-config.js 文件,先忽略 single-spa 相关的概念,简单理解为主应用通过 system.import 加载 @root/root-config 也就是通过 systemJS 映射之后的 root-config.js,在文件加载前 systemJS 已经分析了 systemjs-importmap script 标签中的依赖项,并生成了依赖图
1
2
3
4
5
6
7
8
9
10
11
12
13
{
depcache:{},
imports:{
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"react": "https://cdn.jsdelivr.net/npm/react@16.14.0/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.production.min.js",
"@fefw/root-config": "http://localhost:9000/fefw-root-config.js",
"@supos/supos-react-1": "http://localhost:3000/supos-supos-react-1.js",
"@supos/supos-react-2": "http://localhost:8081/supos-supos-react-2.js"
},
integrity:{},
scope:{}
}
  1. 解析并获取 @root/root-config 的资源路径 http:////localhost:9000/root-config.js, 只要请求的资源与依赖途中匹配,就会解析资源的请求路径,并记录当前环境变量快照后,尝试加载资源。

  2. 资源加载后

    如果资源是 systemJS 模块会自动执行 system.register, 再执行上下文中保存依赖数组,和声明函数。

    如果不是 systemJS 模块,例如公用的 react, react-dom, 会将资源直接包装成 systemJS 声明函数的执行结果,通常会使用 amd 扩展,如果不使用 amd 扩展,例如 react 等 umd 格式的资源会将导出对象直接挂载在 window 对象上,systemJS 会尝试与上一次的环境变量快照比对找到资源导出的对象。但是这种方式将导致污染全局对象,且不能多版本共存。请查看 amd 扩展源码实现

    紧接着执行声明函数,声明函数接受一个 export 函数,用于接受第三方资源中暴露的对象,同时返回一个包含 setters 数组和 execute 方法的对象,如果加载的资源没有依赖项,那就不会有 setters 数组(例如: 主应用无第三方依赖), 延迟执行声明函数可以有时机处理依赖项。

  3. 当资源的声明函数检查无额外依赖,则执行 execute 方法,内部执行 webpack 等构建工具打包后的代码,将返回的对象传递给 export 函数

  4. 此时主应用资源已经执行,会尝试匹配路由,并通过 system.import 加载子应用,再次进入到第 2 步的流程,当子应用加载完成并执行后,获取到了子应用的依赖 ['react','react-dom'], 以及子应用的声明函数,由于此时有依赖项所以需要等待依赖项加载完成,依赖项的加载也会进入到第 2 步的流程,通过 Promise.all 保证所有的依赖加载完成后再,执行声明函数。而第三步的 setter 方法会在子应用加载后调用,依赖中暴露的对象注入到声明函数中。

  5. react 资源加载后,由于并不是一个 systemJS 模块,会通过 amd 扩展,包装成 systemjs 注册的对象,而且由于 react 没有依赖,会在稍后直接执行 execute 方法, 由于 react-dom 不是 systemjs 模块, 声明函数的执行也会被包装成 systemjs 注册的对象, 因为 react-dom 依赖 react, 所以会在缓存中找到已经加载的 react 包装对象, 需要注意这时的 execute 都还没有执行。在所有资源都准备好之后,topLevelLoad 会统一执行 execute 方法,将资源导出对象暴露给依赖对象,接着执行父级资源的 setter 方法,将暴露出的依赖对象注入到,父级资源中。从而完成整个依赖关系的加载。

总结一下 systemJS 核心方法, import 可以递归的加载资源以及其依赖项, setter 方法将依赖资源暴漏的对象注入到父资源中。

systemjs 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// 1. 序列化import map
// importMap 只用作描述资源,建立映射关系,不会主动加载资源

function System() {}
var global = window;
// 记录加载的资源
System["@"] = {};

// 记录import资源隐射
var importMap = { imports: {} };

// baseURL 为当前的资源加载路径
var baseUrl = location.href.split("#")[0].split("?")[0];

var importMapPromise = Promise.resolve();
function processScripts() {
document.querySelectorAll("script").forEach((script) => {
// 处理 importmap
if (script.type === "systemjs-importmap") {
if (script.src) {
/** 如果是远程资源,则发起请求 */
} else {
// 解析 importmap
importMapPromise = importMapPromise.then(() =>
extendImportMap(importMap, script.innerHTML, baseUrl)
);
}
}
// 处理systemjs 模块
else if (script.type === "systemjs-module") {
System.import(script.src);
}
});
}

// 尽早执行, 并在 dom 全部加载后重新检测
processScripts();

window.addEventListener("DOMContentLoaded", processScripts);

function extendImportMap(importMap, content, baseUrl) {
const packages = JSON.parse(content);
for (let k in packages) {
importMap[k] = packages[k];
}
}

// 用于加载 system-module 以及依赖项

var firstGlobalProp = (secondGlobalProp = undefined);

function shouldSkipProperty(p) {
return !global.hasOwnProperty(p) || (!isNaN(p) && p < global.length);
}
function noteGlobalProps() {
firstGlobalProp = secondGlobalProp = undefined;
for (var p in global) {
if (shouldSkipProperty(p)) continue;
if (!firstGlobalProp) firstGlobalProp = p;
else if (!secondGlobalProp) secondGlobalProp = p;
lastGlobalProp = p;
}
return lastGlobalProp;
}

System.import = function (id) {
// 加载前记录下当前环境中的属性,依赖加载后和上次环境中的属性做对比,可以知道在环境中挂载了那些新的属性
noteGlobalProps();

return Promise.resolve().then(() => {
var load = getOrCreateLoad(id);
return load.completion || topLevelLoad(load);
});
};

function getOrCreateLoad(id) {
var load = System["@"][id];
if (load) return load;

var ns = Object.create(null);
Object.defineProperty(ns, Symbol.toStringTag, { value: "Module" });

var instantiatePromise = Promise.resolve()
.then(() => {
return System.instantiate(id);
})
.then((registration) => {
//依赖项
var deps = registration[0];

var _export = (name) => {
for (var p in name) {
ns[p] = name[p];
}
load.setter.forEach((setter) => setter(ns));
};

// 参数 __WEBPACK_DYNAMIC_EXPORT__ 函数,接受 webpack 模块化对象
var declare = registration[1](_export);

load.execute = declare.execute;
return [deps, declare.setters];
});

var linkPromise = instantiatePromise.then((res) => {
return Promise.all(
res[0].map((dep, index) => {
var setter = res[1][index];
var depLoad = getOrCreateLoad(importMap.imports[dep]);

if (setter) {
depLoad.setter.push(setter);
}

return depLoad;
})
).then((deps) => {
load.deps = deps;
});
});

return (load = System["@"][id] =
{
id,
importMapPromise,
linkPromise,
ns,
setter: [],
deps: [],
execute: () => {},
completion: void 0,
});
}

System.instantiate = function (id) {
var script = document.createElement("script");
script.async = true;
script.src = id;

// 异步等待 system 模块加载,并执行,执行结束后可以获取到模块注册的依赖信息
return new Promise((resolve, reject) => {
script.addEventListener("load", function () {
document.head.removeChild(script);
var register = System.getRegister();
resolve(register);
});
document.head.appendChild(script);
});
};

// 保存注册依赖以及回调函数

var lastRegister;
System.register = function (deps, declare) {
lastRegister = [deps, declare];
};

function getGlobalProp() {
var foundLastProp, result;
var n = 0;

// 依赖js引擎的行为
for (var p in global) {
if (shouldSkipProperty(p)) continue;

if ((n == 0 && p !== firstGlobalProp) || (n == 1 && secondGlobalProp !== p))
return p;
if (foundLastProp) {
// 下一个属性就是最新被赋值的属性
lastGlobalProp = p;
result = p;
} else {
// 匹配上一次最后一个属性
foundLastProp = p === lastGlobalProp;
}
n++;
}
return result;
}

System.getRegister = function () {
var _lastRegister = lastRegister;
lastRegister = void 0;

if (_lastRegister) return _lastRegister;

// 检查js文件执行之后在环境中添加的属性

var prop = getGlobalProp();

var globalExport = global[prop];

return [
[],
function (_exports) {
return {
execute: function () {
_exports(globalExport);
// 兼容esModule
_exports({ default: globalExport, __useDefault: true });
},
};
},
];
};

function topLevelLoad(load) {
return Promise.resolve(load.linkPromise)
.then(() => {
return Promise.all(load.deps.map((dep) => dep.linkPromise));
})
.then(() => {
// 等待所有依赖加载完成执行
postOrderExec(load);
});
}

function postOrderExec(load) {
var execute = load.execute;

// 首先执行依赖

load.deps.forEach((dep) => {
postOrderExec(dep);
});

execute();
}

amd 扩展源码实现

systemJS amd 扩展用于解决资源加载时对 window 对象的污染,还可以解决资源加载时的依赖问题, 例如 react-dom 依赖 react

reactreact-dom 为例展示资源加载过程, 资源已经被打包为 umd 格式,所以可以兼容 amd 规范。

  1. 首先在全局创建 define 方法,作为 amd 规范定义模块的方法。react 加载后自动执行 define 方法,在执行上下文中保存资源的依赖和执行函数。依赖默认有 exports
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getDepsAndExec(arg1, arg2) {
// define([], function () {})
if (arg1 instanceof Array) {
return [arg1, arg2];
}
}
let amdDefineDeps, amdDefineExec;
global.define = function (name, deps, execute) {
var depsAndExec;

depsAndExec = getDepsAndExec(name, deps);
amdDefineDeps = depsAndExec[0];
amdDefineExec = depsAndExec[1];
};
  1. 当资源加载成功后,会执行 systemJS.getRegister, amd 扩展会重写此方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var getRegister = systemPrototype.getRegister;
var lastRegisterDeclare;
var systemRegister = systemPrototype.register;
systemPrototype.register = function (name, deps, declare) {
lastRegisterDeclare = typeof name === "string" ? declare : deps;
systemRegister.apply(this, arguments);
};

systemPrototype.getRegister = function () {
var register = getRegister.call(this); //[[], function () { return {} }];
// 如果可以获取到 register 注册的依赖,表示此资源是标准的 systemJS 模块,直接返回
if (register && register[1] === lastRegisterDeclare) return register;

// 由于 amd 接管了模块的加载过程,所以在 window 上不会挂载 react 对象,getRegister 会返回空的声明函数结果,作为返回值
// amd 模块默认有 ["exports"] 依赖
// 此时 react 资源的 依赖对象和执行函数 都已经获取到 amdDefineDeps amdDefineExec

createAMDRegister(amdDefineDeps, amdDefineExec);
};
  1. 将 react 的加载包装成 systemJS register 数据结构, 后面继续执行 systemJS 第 4 步的加载逻辑
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
function createAMDRegister(amdDefineDeps, amdDefineExec) {
var exports = {};
var module = { exports: exports };
var depModules = [];
var setters = [];
var splice = 0;
for (var i = 0; i < amdDefineDeps.length; i++) {
var id = amdDefineDeps[i];
var index = setters.length;
if (id === "require") {
depModules[i] = unsupportedRequire;
splice++;
} else if (id === "module") {
depModules[i] = module;
splice++;
} else if (id === "exports") {
depModules[i] = exports;
splice++;
} else {
createSetter(i);
}
if (splice) amdDefineDeps[index] = id;
}
if (splice) amdDefineDeps.length -= splice;
var amdExec = amdDefineExec;
return [
amdDefineDeps,
function (_export) {
_export({ default: exports, __useDefault: true });
return {
setters: setters,
execute: function () {
var amdResult = amdExec.apply(exports, depModules);
if (amdResult !== undefined) module.exports = amdResult;
_export(module.exports);
_export("default", module.exports);
},
};
},
];

// needed to avoid iteration scope issues
function createSetter(idx) {
setters.push(function (ns) {
depModules[idx] = ns.__useDefault ? ns.default : ns;
});
}
}

微前端 ① 什么是微前端

什么是微前端

可以多团队,多框架共同构建 web 应用程序的技术。

微前端的价值

  • 技术栈无关 主框架不限制接入应用的技术栈,子应用具备完全自主权
  • 独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 独立运行时 每个子应用之间状态隔离,运行时状态不共享

微前端场景解决方案

  • 单实例:即同一时刻,只有一个子应用被展示,子应用具备一个完整的应用生命周期。通常基于 url 的变化来做子应用的切换。

  • 多实例:同一时刻可展示多个子应用。通常使用 Web Components 方案来做子应用封装,子应用更像是一个业务组件而不是应用。

最终目的是将不同框架,不同项目的中的页面或组件整合在指定的路由下同时展示,并能实现数据的隔离和消息的传递等。

架构实践

微前端正是将不同的框架和不同的项目整合在一个路由中,因此微前端框架的定位仅仅是 导航路由 + 资源加载框架

Future State,一个基于前端路由的应用,整个应用可以看作是一个状态树,每一个分支都可以 lazy load, 在程序启动时,描述 lazy load 状态数分支的对象,就可以叫做 Future State。

当主应用导航到 https://a.com/subApp1 的时候子应用加载, 点击子应用的某个链接可能被导航到 https://a.com/subApp1/project,一个微前端框架应该能在路由匹配的时候加载子应用,路由不匹配的时候卸载子应用, 这就是 single-spa 做的工作。

应该用什么集成方式,主应用应该用何种资源形式加载子应用?, 一种方式是 JS Entry, 将子应用的资源打包为 js 资源,例如 single-spa 将要求将子应用打包为 systemJs 格式当作子应用入口, qiankun 要求将子应用打包为 umd 格式(考虑到应用需要独立部署)。另一种是 HTML Entry 直接使用子应用打包出的 html 文件当作入口,本质是解析 html 中引用的资源,因此也可以优化为直接使用资源描述的对象,以减少一次请求。

模块导入

singleSpa 采用 systemJs 的模块化方案,使用 singleSpa 提供的工具,默认只会编译出 js 文件,独立部署需要其他工具支持,通过 systemJs amd 扩展,可以避免将资源加载后添加的对象挂载到 windows 上。

qiankun 采用 umd 的模块化方案。需要使用 windows 快照的方式,获取到哪些对象被注入到 windows 中。

样式隔离方案

css-module,BEM 通过对 class 添加唯一标识保证唯一性。

css-in-js 代表库 emotion ,本质还是将样式转换为唯一的 class 类名,通过 style 标签插入。

shadowDom 浏览器严格隔离

singleSpa 并没有提供样式隔离方案,需要子应用单独处理。

qiankun 提供 experimentalStyleIsolation 配置, 本质上是给最外层元素样式添加唯一标识,相当于 css-module 实现方案,缺点是子应用中的元素如果挂载到了根节点外面会导致样式不生效。如果使用 strictStyleIsolation 配置,会使用 shadowDom 方案,缺点是主应用访问不到子应用的元素。另外,还实现了 Dynamic Stylesheet,原理是应用切出/卸载后,同时卸载掉其样式表,浏览器会对样式表的插入、移除做整个 CSSOM 的重构,因此保证只有一个应用的样式表是生效的,但是这种方式对于多实例的微前端场景无法处理。

js 隔离

singleSpa 并没有提供沙箱的实现, qiankun 实现了基于 Proxy 的多实例沙箱。

一个简单沙箱就是对 windows 快照, 当子应用卸载时,将 windows 对象回滚到之前的状态。

基于 Proxy 的单实例沙箱,创建 Proxy 对象,将对象的操作反映到 windows 的属性改变, 但是当多个子应用实例的时候,对同一属性的修改会导致 windows 属性混乱。

基于 Proxy 的多实例沙箱,不在对 windows 对象操作,直接

框架比较

  • singleSpa 实现了最基本的路由功能,子应用加载功能。

  • qiankuan 基于 singleSpa 的封装,实现了 css 隔离,以及 js 沙箱,保证应用状态隔离, 还提供了更多的特性:

    子应用并行,多个微前端同时存在
    子应用嵌套,微前端嵌套其他的微前端
    父子应用通讯
    预加载,空闲时加载子应用的资源
    公共依赖加载
    按需加载,切换到相应页面的时候才去加载资源

Debian 作为时间服务器

配置权限

添加 sudo 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
su # 切换到root用户

apt install sudo

# 添加用户到sudo组
vi /etc/sudoers
username ALL=(ALL) ALL

# 添加环境路径,防止找不到安装的软件
vi ~/.bashrc
export PATH=$PATH:/usr/sbin::/sbin
. ~/.bashrc

安装 ntp

1
apt install ntp

配置 ntp

1
2
3
4
5
6
7
8
9
vi /etc/ntpsec/ntp.conf

server 127.127.1.0 prefer # 本地时间服务器

# 注释
# pool 0.debian.pool.ntp.org iburst
# pool 1.debian.pool.ntp.org iburst
# pool 2.debian.pool.ntp.org iburst
# pool 3.debian.pool.ntp.org iburst

启动服务

1
2
sudo systemctl start ntpsec
sudo systemctl enable ntpsec

同步

在需要同步的服务器上安装

1
2
3
4
5
sudo apt install ntpdate

# 同步时间

sudo ntpdate <时间服务器ip>

crontab 定时同步

1
2
3
4
5
6
7
sudo apt install cron

# 编辑定时任务
crontab -e

# 添加定时任务,每天执行一次
0 0 * * * sudo /usr/sbin/ntpdate <时间服务器ip>

React v16 源码分析 ⑫ Hooks 原理

针对 hooks 有三种策略,或者说三种类型的 dispatch

HooksDispatcherOnMount 将函数组件初始化的信息挂载到 Fiber 上面

1
2
3
4
5
6
7
8
9
10
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useEffect: mountEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
// ...其他 Hooks
};

HooksDispatcherOnUpdate 函数执行更新的时候,会执行这个对象对应的方法,此时 fiber 上已经存储了函数组件的信息,这些 Hooks 会去维护或更新这些信息。

1
2
3
4
5
6
7
8
9
10
11
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useMemo: updateMemo,
useReducer: updateReducer, /
useRef: updateRef,
useState: updateState,
// ...其他 Hooks
};

ContextOnlyDispatcher 防止开发者在函数组件外部调用 Hooks

1
2
3
4
5
6
7
8
9
10
11
export const ContextOnlyDispatcher: Dispatcher = {
readContext, // 允许读取 Context
useCallback: throwInvalidHookError, // 抛出非法调用错误
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
// ...其他 Hooks 同理
};

render 执行的时候会根据不同的上下文环境,给 hooks 赋值不同的方法。

1
2
3
4
5
6
7
const hook = {
memoizedState: null, //当前 Hook 的缓存状态
baseState: null, // 基础状态(用于更新对比或重置)
baseQueue: null, // 基础更新队列(待处理的优先级较低更新)
queue: null, // 当前更新队列(高优先级更新)
next: null, // 指向下一个 Hook 节点的指针
};

当进入一个函数组件时, 会被 renderWithHooks

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
export function renderWithHooks(
current, // 当前 Fiber 节点(null 表示首次挂载)
workInProgress, // 正在处理的 Fiber 节点(本次渲染目标)
Component, // 用户编写的函数组件
props, // 组件接收的 props
secondary, // 次要参数(通常为 ref)
nextRenderLanes // 本次渲染的优先级车道(Lane 模型)
) {
// 设置全局渲染相关变量
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;

// 重置 Hook 链表和副作用队列
workInProgress.memoizedState = null; // 清空 Hooks 链表
workInProgress.updateQueue = null; // 清空 Effect List

// 动态切换 Hooks 分发器
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // 挂载阶段分发器
: HooksDispatcherOnUpdate; // 更新阶段分发器

// 执行用户函数组件,触发 Hooks 调用
let children = Component(props, secondary);

// 渲染后清理工作
finishRenderingHooks(current, workInProgress);
return children;
}

function finishRenderingHooks(current, workInProgress) {
// 强制切换为错误分发器,防止外部调用 Hooks
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// ...其他清理逻辑
}

执行 mountState 相关代码,会清空 WorkInProgress 的 memorizedState 以及 updateQueue, 接下来时派那段组件究竟时初始化还是更新,为 ReactCurrentDispatcher.current 赋予不同的上下文

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
function mountState(initialState) {
// 1. 创建 hook 对象
const hook = mountWorkInProgressHook(); // 初始化 hook 对象,关联当前正在渲染的组件

// 2. 判断 initialState 是否是一个函数,如果是则调用它并获得初始状态
if (typeof initialState === "function") {
initialState = initialState(); // 如果 initialState 是函数,调用该函数获取状态值
}

// 2. 初始化 hook 的属性
// 2.1 设置 hook.memoizedState / hook.baseState
hook.memoizedState = hook.baseState = initialState;

// 定义 queue(队列),用于管理状态更新
const queue = {
pending: null, // 队列中待处理的更新
lanes: NoLanes, // 当前更新所属的 lane,通常表示优先级
dispatch: null, // 状态更新函数
lastRenderedReducer: basicStateReducer, // 上一次渲染的状态更新函数
lastRenderedState: initialState, // 上一次渲染的状态
};

// 2.2 设置 hook.queue
hook.queue = queue; // 将队列分配给 hook 的 queue 属性

// 2.3 设置 hook.dispatch
// 将 dispatchSetState 绑定到当前组件的上下文中,并传入队列
const dispatch = (queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue
));

// 3. 返回当前状态和 dispatch 函数
return [hook.memoizedState, dispatch]; // 返回当前状态和状态更新函数
}

function mountWorkInProgressHook() {
const hook = {
memoizedState: null, // Hook 自身维护的状态
baseState: null,
baseQueue: null,
queue: null, // Hook 自身维护的更新队列
next: null, // 指向下一个 Hook
};

// 如果当前组件的 Hook 链表为空,说明这是第一个 Hook
if (workInProgressHook === null) {
// 这是链表的第一个节点(头结点)
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 如果当前组件的 Hook 链表不为空,将新创建的 Hook 添加到链表的末尾(作为尾结点)
workInProgressHook = workInProgressHook.next = hook;
}

return workInProgressHook;
}

组件执行之后就会生成一个 hooks 链表,在更新过程中会移动指针依次指向每一个节点.

这也是为什么不可以将 useHooks 放在条件中,因为他需要按照链表的顺序依次被引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
function updateWorkInProgressHook() {
let nextCurrentHook;

// 获取当前 Fiber 的 alternate(用于双缓冲)
const current = currentlyRenderingFiber.alternate;

if (current !== null) {
// 如果当前有 alternate(即已经存在旧的 Fiber)
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}

// 获取下一个 Hook
nextCurrentHook = currentHook.next;

// 更新 workInProgressHook 的指向
let nextWorkInProgressHook;
if (workInProgressHook === null) {
// 当是第一个 Hook,直接从当前 Fiber 上获取第一个 hook
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 获取链表的下一个 hook
nextWorkInProgressHook = workInProgressHook.next;
}

// nextWorkInProgressHook 指向当前正在工作的 hook
if (nextWorkInProgressHook !== null) {
// 如果已经有工作中的 hook,重用它
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;

// 当前 hook 指向下一个 hook
currentHook = nextCurrentHook;
} else {
// 克隆当前 hook
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// 这是初始渲染,组件挂起、恢复时将渲染一个附加的 hook
const newHook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
nextCurrentHook = newHook; // 设置为当前 hook
} else {
// 如果是更新渲染,应该有一个有效的当前 hook
throw new Error("Rendered more hooks than during the previous render.");
}
}
}

const newHook = {
memoizedState: currentHook.memoizedState, // 当前 Hook 的状态值
baseState: currentHook.baseState, // 当前 Hook 的基础状态
baseQueue: currentHook.baseQueue, // 当前 Hook 的基础更新队列
queue: currentHook.queue, // 当前 Hook 的更新队列
next: null, // 指向下一个 Hook
};

// 如果 workInProgressHook 为 null,说明这是第一个 Hook
if (workInProgressHook === null) {
// 设置当前渲染组件的 memoizedState 为新的 Hook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// 否则,将新的 Hook 添加到链表的末尾
workInProgressHook = workInProgressHook.next = newHook;
}

return workInProgressHook;
}

useEffect / useLayoutEffect

  • useEffect:回调函数会在 commit 阶段完成后异步执行,所以它不会阻塞视觉渲染

  • useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作

  • useInsertionEffect:回调函数会在 commit 阶段的 Mutation 子阶段同步执行,与 useLayoutEffect 的区别在于执行时无法访问对 DOM 的引用。这个 Hook 是专门为 CSS-in-JS 库插入全局的 style 元素设计的。

在内部共同使用一套数据结构, next 与当前的函数作用域内的其他 effect 函数,形成环状链表。

1
2
3
4
5
6
7
8
9
const effect = {
// 用于区分 effect 类型 Passive | Layout | Insertion
tag, // effect 回调函数
create, // effect 创建函数
destroy, // 销毁函数
deps, // 依赖项
// 与当前 FC 的其他 effect 形成环状链表
next: null,
};
  • 声明阶段

    mount子阶段 执行 mountEffectImpl, 生成 hook 对象拿到依赖,将当前的 effect 加入到环状链表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
    // 生成 hook 对象
    const hook = mountWorkInProgressHook();
    // 保存依赖的数组
    const nextDeps = deps === undefined ? null : deps;

    // 修改当前 fiber 的 flag
    currentlyRenderingFiber.flags |= fiberFlags;

    // 将 pushEffect 返回的环形链表保存到 hook 对象的 memoizedState 中
    hook.memoizedState = pushEffect(
    HookHasEffect,
    hookFlags,
    create,
    undefined,
    nextDeps
    );
    }

    update子阶段执行 updateEffectImpl, 获取 hook 新旧值,进行依赖比较,打上响应的 tag,在 commit 阶段统一处理

    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
    function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
    const hook = updateWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    let destroy = undefined;

    if (currentHook !== null) {
    const prevEffect = currentHook.memorizedState;
    destroy = prevEffect.destroy;
    }

    if (nextDeps !== null) {
    const prevDeps = prevEffect.deps;

    // 浅比较依赖是否发生变化
    if (areHookInputsEqual(nextDeps, prevDeps)) {
    hook.memorizedState = pushEffect(hookFlags, create, destroy, nextDeps);
    return;
    }
    }

    if (deps !== prevEffect.deps) {
    fiberFlags |= hookFlags;
    }

    currentlyRenderingFiber.flags |= fiberFlags;

    // pushEffect 的作用是将当前 effect 添加到 FiberNode 的 updateQueue 中,
    // 然后返回当前 effect 保存在 Hook 节点的 memorizedState 属性中
    hook.memorizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps
    );
    }
  • 调度阶段, useEffect 独有的,因为 useEffect 函数会在 commit 之后异步执行,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
    ) {
    if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    pendingPassiveEffectsRemainingLanes = remainingLanes;
    // scheduleCallback 来自于 Scheduler,用于以某一优先级调度回调函数
    scheduleCallback(NormalSchedulerPriority, () => {
    // 执行 effect 回调函数的具体方法
    flushPassiveEffects();
    return null;
    });
    }
    }

    为了保证下一次 commit 执行前,上一次的 commit 调用的 effect 已经全部执行,因此会在 commit 入口处也会执行 flushPassiveEffects

    1
    2
    3
    4
    5
    function commitRootImpl(root, renderPriorityLevel) {
    do {
    flushPassiveEffects();
    } while (rootWithPendingPassiveEffects !== null);
    }
  • 执行阶段

    commitHookEffectListUnmount 遍历 Effect 链表依次执行 effect.destroy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function commitHookEffectListUnmont(
    flags: HookFlags,
    finishedWork: Fiber,
    nearestMountedAncestor: Fiber | null
    ) {
    const updateQueue: FunctionComponentUpdateQueue | null =
    finishedWork.updateQueue ? finishedWork.updateQueue : null;

    const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
    if ((effect.tag & flags) === flags) {
    // Unmount
    // 从 effect 对象上拿到 destroy 函数
    const destroy = effect.destroy;
    effect.destroy = undefined;
    // ...
    }
    effect = effect.next;
    } while (effect !== firstEffect);
    }
    }

    commitHookEffectListMount 遍历 Effect 链表依次执行 effect.create,在声明阶段的时候已经打上了不同的 tag,这时会根据 tag 来执行

    1
    2
    3
    4
    5
    6
    7
    8
    // 类型为 useInsertionEffect 并且存在 HasEffect tag 的 effect 会执行回调
    commitHookEffectListMount(Insertion | HasEffect, fiber);

    // 类型为 useEffect 并且存在 HasEffect tag 的 effect 会执行回调
    commitHookEffectListMount(Passive | HasEffect, fiber);

    // 类型为 useLayoutEffect 并且存在 HasEffect tag 的 effect 会执行回调
    commitHookEffectListMount(Layout | HasEffect, fiber);

useCallback / useMemo

与其他的 hooks 类似,mount 阶段创建 hooks 对象,更行阶段对比依赖,计算结果

1
2
3
4
5
6
7
8
9
10
11
12
13
function mountCallback(callback, deps) {
// 首先还是创建一个 hook 对象
const hook = mountWorkInProgressHook();

// 依赖项
const nextDeps = deps === undefined ? null : deps;

// 把要缓存的函数和依赖数组存到 hook 对象上
hook.memoizedState = [callback, nextDeps];

// 向外部返回缓存函数
return callback;
}
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
function updateCallback(callback, deps) {
// 获取之前的 hook 对象
const hook = updateWorkInProgressHook();

// 新的依赖项
const nextDeps = deps === undefined ? null : deps;

// 之前的值,也就是 [callback, nextDeps]
const prevState = hook.memorizedState;

if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1]; // 获取到之前的依赖项

// 对比依赖项是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 相同回 callback
return prevState[0];
}
}
}

// 否则重新缓存
hook.memorizedState = [callback, nextDeps];
return callback;
}

useRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mountRef(initialValue) {
// 创建 hook 对象
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };

// hook对象的 memorizedState 值 { current: initialValue }
hook.memorizedState = ref;
return ref;
}

function updateRef(initialValue) {
// 获取当前的 hook 对象
const hook = updateWorkInProgressHook();
return hook.memorizedState;
}

ref 创建之后会作为组件属性传递,在 react 的 render 阶段会标记 ref。

1
2
3
4
5
6
7
8
9
10
function markRef(current, workInProgress) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// 标记 Reg tag
workInProgress.flags |= Ref;
}
}

commit 的 mutation 子阶段删除旧的 ref

1
2
3
4
5
6
7
8
9
10
11
function commitLayoutEffectOnFiber(
finishedRoot,
current,
finishedWork,
committedLanes
) {
// 省略代码
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}

commit 的 layout 子阶段会重新赋值新的 ref

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
function commitAttachRef(finishedWork) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
// HostComponent 需要获取对应的 DOM 元素
instanceToUse = getPublicInstance(instance);
break;
default:
// ClassComponent 使用 FiberNode.stateNode 保存实例
instanceToUse = instance;
}

if (typeof ref === "function") {
// 函数类型,执行函数并将实例传入
let retVal;
retVal = ref(instanceToUse);
} else {
// { current: T } 类型则更新 current 指向
ref.current = instanceToUse;
}
}
}

通过 forwardRef 和 useImperativeHandle 操作元素, 尽量避手动绑定 ref,避免 ref 被修改。

Jenkins agent/pipeline

Agent

官方建议使用无论何时都使用 Agent 执行任务,而不是使用内置的节点

停用内置 Agent
  • 在 Manage Jenkins -> Nodes -> Build-In Node 的配置中,配置 Number of executors 为 0。

  • label 填写 don’t use Built-In node

  • Usage 选择 Only build jobs with label expressions matching this node。

  • 点击保存,这样就不会使用内置节点执行任务了。

创建新的 Agent

Agent 节点可以是物理机,也可以是虚拟机。必须装有和 Jenkins controller 相同的 Java 环境, 不需要安装 jenkins, 只用于执行任务。

  • 点击新建节点,选择 Permanent agent

  • Number of executors 填写数量不能大于物理核心数,或是虚拟核心数。

  • Remote root directory 会从 Jenkins controller 中同步 jenkins 相关文件,确保放在登录用户有权限执行的目录下,例如 /home/user/jenkins, user 是 Agent 节点的登录用户名。

  • Labels 可以描述当前 Agent 的用途内置的环境

  • Usage 选择 Use this node as much as possible

  • Launch method 选择 Launch agents via SSH,填写 Host,并创建登录凭证, 凭证的类型一定要选择 username with password

    在使用凭证登录的时候,需要校验相关权限,会去查找 Jenkins controller 上的 ~/.ssh 目录下的 known_hosts 文件,如果没有则会需要手动创建 .ssh 文件夹(确保权限正确,不是 root 用户),在使用以下命令同步认证信息 ssh-keyscan -H your_agent_host >> /home/your_user_name/.ssh/known_hosts

    Host Key Verification Strategy 选择 Known hosts file Verification Strategy 进行验证

  • 执行 docker 命令时报错 permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.48/containers/node-18:alpine/json": dial unix /var/run/docker.sock: connect: permission denied

    Jenkins 会以 jenkins 用户身份运行, 把该用户加入本机 docker 组(例如:docker),以便它可以读写 /var/run/docker.sock

    1
    2
    3
    4
    sudo usermod -aG docker jenkins

    # 如果仍然没有权限,尝试重启系统
    docker run hello-world
  • 点击保存,Jenkins 会自动检查 Agent 链接

Pipeline

workspace

通过在线的方式创建一个简单的 pipeline

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
pipeline {
agent any

stages {
stage('without docker') {
steps {
sh '''
touch without-docker.txt
'''
}
}

stage('with docker') {

agent {
docker {
image 'node:18-alpine'
}
}
steps {
sh '''
npm -v
touch with-docker.txt
'''
}
}
}
}

执行结束后在任务的 workspaces 目录下可以看到生成的多个文件夹,每个文件夹对应一个执行阶段

1
2
/home/jenkins/jenkins-agent/workspace/simple-pipeline on Agent1
/home/jenkins/jenkins-agent/workspace/simple-pipeline@2 on Agent1

如果想要合并这些文件夹,共用上一阶段的产物,添加如下的配置

1
2
3
4
5
6
agent {
docker {
image 'node:18-alpine'
reuseNode true
}
}
Gitlab Plugin Pipeline
  • 测试 Gitlab 对 Jenkins 的认证

    Jenkins 中点击右上角的头像,选择 security, 创建一个 API Token.

    点击任意一个任务,查看浏览器地址栏 https://jenkins.iftrue.me/job/simple-freesyle/,simple-freesyle 即为项目的名称(特殊字符需要转义,所以按地址栏中显示的准), 记下这个项目名称

    在 Gitlab 中进入到一个项目中, setting -> webhooks -> add webhook, 填写 Jenkins 的地址,规则为 https://[Jenkins用户ID]:[Jenkins_Api_Token]@jenkins.iftrue.me/project/[项目名称]

    点击测试通过,在内网环境中可能需要关闭 webhook 的 ssl 验证,才不会报错。(内网环境使用 nginx 作为统一入口,会提示 unable to get local issuer certificate

  • 配置 Jenkins 对 Gitlab 的认证

    此认证配置仅用于访问 GitLab API,以便向 GitLab 发送构建状态。它不用于克隆 git 仓库。用于克隆的凭证(通常是 SSH 凭证)应该在 git 插件中单独配置。

    创建 Gitlab access tokens, setting -> Access Tokens, 选择 api 权限,复制生成的 token

    在 Jenkins 中创建一个凭证,选择 GitLab API Token, 填写生成的 Gitlab Access Token

    jenkins -> System Manage , 找到 GitLab 部分,填写 GitLab host URL 和创建的 GitLab API Token 凭证。(内网环境可能有证书报错的问题,在 高级选项中勾选 Ignore SSL Certificate Errors)

    保存后测试链接是否成功。

  • 任务触发配置

    在一个 pipeline 项目中配置触发器,在 Build Triggers 配置中

    选择 Build when a change is pushed to GitLab,复制 GitLab webhook URL 地址,将项目部分的地址替换到 Gitlab 的 webhook 中,Jenkins 用户名和 Api token 仍然需要保留,点击测试通过。

    继续选择 Push Events,表示在 Gitlab 推送代码时触发 Jenkins 的构建。

    pipeline 中选择 pipeline script from SCM, 选择 GitLab 作为 SCM,填写仓库地址。

    保证在 Jenkins controller 中有 Git 环境,否则校验命令会执行失败。

    如果提示 Host key verification failed., Jenkins 凭证中创建一个 SSH Username with private key 类型的凭证,填写 Gitlab 服务器的用户名,Jenkins controller 中的私钥 (如果私钥有密码也需要填写密码)。

    在 Gitlab SSH Key 管理中添加 Jenkins controller 的公钥,这样可以完成 git 的认证。

    如果任务在 Jenkins agent 中执行,Jenkins 默认使用 known_hosts 文件进行认证,gitlab 的认证信息必须添加到 Jenkins agent 的 known_hosts 文件中。

    1
    ssh-keyscan -H gitlab_host >> /home/jenkins/.ssh/known_hosts

    也可以直接在 System -> Security -> Git Host Key Verification Configuration 中修改为 Accept first connection 会在首次链接后自动保存这个主机指纹。

    这样就可以在拉取代码后,执行项目中的 jenkinsfile 文件

拉取项目

没有编译的过程,当任务被触发时,agent 节点会直接连接上目标服务器,拉取项目并更新并重新执行项目

安装 Publish Over SSH 插件, 进入系统管理 -> Publish over SSH

填写 Jenkins controller 的 SSH Key 密码,添加一个服务器配置,填写服务器的 IP 地址和用户名, 点击测试通过

配置 jenkinsfile

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
pipeline {
agent any
stages {
stage('Deploy') {
steps {
sshPublisher(publishers:
[sshPublisherDesc(
// Publish over SSH 插件配置的服务器名称
configName: 'crawler server',
transfers: [
sshTransfer(
// 远程目录,从 /home/your_username 目录下创建
remoteDirectory: 'crawler',
// 不能使用数组,多类文件可以使用多个 sshTransfer
sourceFiles:'*/**'
),
sshTransfer(
// 由于ssh链接不是使用的交互式命令行所以.bashrc文件不会被执行
// 需要手动执行 source ~/.nvm/nvm.sh 以便于找到npm命令
execCommand: 'cd crawler && source ~/.nvm/nvm.sh && npm ci && npm run build && npm run start'
)
],
// 开启查看详细的报错信息
verbose: true
),
]
)
}
}
}
}

计算机基础

位/字节

特性 位(bit) 字节(byte)
定义 最小信息单位 基本存储和处理单位
大小 1 位 = 1 个二进制值 1 字节 = 8 位(现代标准)
状态数量 2^1 = 2 2^8 = 256
用途 表示简单状态或逻辑值 存储字符、数据或文件
典型应用 网络传输速率、标志位 内存大小、文件大小

单位转换

单位 换算为 Byte 换算为 bit
1 Byte 1 Byte 8 bit
1 KB 1024 Bytes (二进制) 8192 bit (二进制)
1 MB 1024 × 1024 Bytes (二进制) 8,388,608 bit (二进制)
1 GB 1024 × 1024 × 1024 Bytes (二进制) 8,589,934,592 bit (二进制)
1 TB 1024^4 Bytes (二进制) 8,796,093,022,208 bit (二进制)

继电器/MOS 管

  • 继电器

    继电器通过机械开关的闭合和断开,完成逻辑门(如与门、或门、非门)的功能。

    通过组合继电器,可以实现复杂的逻辑电路和算术运算,如加法器和条件判断。

    继电器的输入和输出电路物理隔离,适合处理不同电压和电流的控制任务。

  • MOS 管

    MOS 管作为场效应晶体管(FET)的一个重要分支,是构建逻辑门(如与、或、非等)的基础元件。

    数十亿个 MOS 管可以集成在单个芯片上,构成现代计算机的中央处理器(CPU)、存储器和其他数字电路。

    静态随机存取存储器(SRAM)和动态随机存取存储器(DRAM)都基于 MOS 管设计。DRAM 中,每个存储单元使用一个 MOS 管和一个电容,极大地提高了存储密度。

    静态随机存取存储器(SRAM)和动态随机存取存储器(DRAM)都基于 MOS 管设计。DRAM 中,每个存储单元使用一个 MOS 管和一个电容,极大地提高了存储密度。

    MOS 管非常小,可以集成到硅芯片中,从而实现计算机的小型化。集成电路(IC)和超大规模集成电路(VLSI)都是基于 MOS 技术。

计算机内存原理

MOS 管原理

半加器/全加器

半加器:处理一位二进制运算的逻辑电路,本位值相当于异或运算(00,11 相同的时候为 0,01,10 不同的时候为 1),进位值相当于与运算(相同时发生进位,值为 1)

全加器:相比于半加器的两个输入外,多一个上一次进位的输入,因此用两个半加器可以实现一个全加器,最后通过一个或门,确认进位的输出。

将多个全加器连接起来实现波纹进位加法器,可以实现多位的加法运算。但是由于用到的门过多造成延时过长,因此还有超前进位加法器,分层超前进位加法器。

Javascript 常见问题

柯里化

‌函数柯里化(Currying)是一种将接受多个参数的函数转换为接受单个参数的函数的技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const curry = <T extends (...args: any[]) => any>(fn: T): CurryFunction<T> => {
let args: any[] = [];
return function curryFunction(this: any, arg: any) {
args.push(arg);
if (args.length >= fn.length) return fn.apply(this, args);
return curryFunction;
} as CurryFunction<T>;
};

type CurryFunction<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => infer R
? P extends [infer F, ...infer Rest]
? (arg: F) => CurryFunction<(...args: Rest) => R>
: R
: never;

const a = curry((a: string, b: boolean, c: number): void => {
console.log(a, b, c);
});

反柯里化

1

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信