执行文件分析

同步模块加载流程

webpack4 和 webpack5 同步模块加载流程基本相同

  1. 默认执行入口文件
  2. 查找模块缓存
  3. 如果没有缓存创建缓存对象,否则直接返回
  4. 创建模块后执行模块
  5. 通过__webpack_require__.r为模块添加module标识
  6. 如果有模块依赖通过__webpack_require__重新执行步骤 (1)
  7. 把导出结果添加到__webpack_exports__ ,default 属性上
  8. 导出到下一个模块使用

webpack4 通过闭包以参数的形式导入,webpack5 则是直接写在作用于内部

webpack4 模块的返回值直接添加到exprots对象中,webpack5的返回值则是通过函数,类似注入的形式添加

webapck4 同步模块

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
(function(modules) { // webpackBootstrap
// 模块的缓存
var installedModules = {};

// 核心require方法
function __webpack_require__(moduleId) {
// 如果存在缓存直接返回
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建一个新的module并放入的缓存中
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};

// 执行模块函数
// 导出的对象会被添加到module.exports对象中
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// 标识为loading状态
module.l = true;

// 返回模块中导出的对象
return module.exports;
}


// expose the modules object (__webpack_modules__)
// 所有的模块对象
__webpack_require__.m = modules;

// expose the module cache
// 被缓存的模块对象
__webpack_require__.c = installedModules;


// 在导出对象上定义 __esModule 属性,es6模块化规范
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};

// 加载入口文件并导出
// __webpack_require_方法上添加一个静态属性s,标识入口文件模块索引
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({
"./src/app.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
console.log('app')
function App(){};
__webpack_exports__["default"] = (App)
}),

"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var _app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/app.js");
Object(_app__WEBPACK_IMPORTED_MODULE_0__["default"])();
console.log("index.js");
})
});

webpack5 同步模块

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
(() => {
"use strict";
// 类似react hooks的写法,直接在内部作用域中定义并执行
var __webpack_modules__ = ({
"./src/app.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);

// 在指定的对象上面添加属性
__webpack_require__.d(__webpack_exports__, {
// 定义一个函数不会使用到未初始化的变量
"default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
console.log('app');
function App(){}
const __WEBPACK_DEFAULT_EXPORT__ = (App);
}),
"./src/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
var _app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/app.js");
// ???????
(0,_app__WEBPACK_IMPORTED_MODULE_0__.default)();
console.log("index.js");
})
});
// 模块缓存
var __webpack_module_cache__ = {};

// __webpack_require__没有变化
function __webpack_require__(moduleId) {
// Check if module is in cache
if(__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}

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;
}

(() => {
// define getter functions for harmony(和谐) exports
//
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
// 如果exports没有想添加的属性时
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
// 在exports上添加注入的属性
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();

// hasOwnProperty简写
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();

// 添加Module标识
(() => {
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();


// 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");

})()
;

webpack4异步模块加载过程

  • 定义全局的变量 webpackJsonp用于接受异步的chunk,重写了webpackJsonp.push方法,在异步chunkpush的时候会调用 webpackJsonpCallback

  • __webpack_require__(__webpack_require__.s = "./src/index.js"); 执行入口文件

    • 在缓存中能查到直接返回
    • 创建一个初始化的模块对象,并加入到缓存中
    • 执行模块对应的函数 –> 进入下面环节
    • 执行结束后模块加载状态标记为以完成
  • 如果文件中依赖了异步的chunk,会通过 __webpack_require__.e(chunk名称)方法引入

    • __webpack_require__.e 创建promises[]用于保存异步模块创建的Promise
    • 如果缓存中有正在pending中的模块,则把 Promise 对象添加到数组中
    • 创建Promise对象 保存 [resolve, reject, Promise]
    • 创建script标签appendhead标签中, (pending中,执行入口文件后面的方法)
    • 异步模块加载完成后自动执行,调用 window[“webpackJsonp”] push 方法,执行webpackJsonpCallback函数
    • 把异步chunk创建prosmie对象时的resolve函数添加到resolves数组中
    • 在缓存中把异步chunk标记为完成(0)
    • 在模块集合中添加新加载的异步模块
    • 在全局的数组webpackJsonp添加异步加载的模块
    • 循环执行resolve方法,全部执行后执行Promise.allthen方法
  • __webpack_require__.bind(null, "./src/app.js")加载刚刚添加到模块集合中的异步模块

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
(function(modules) {
// 加载异步模块成功后的回调函数
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];

// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// 把模块请求promise的resolve函数添加到数组中
resolves.push(installedChunks[chunkId][0]);
}
// 标记模块已经加载完成
installedChunks[chunkId] = 0;
}

//在模块集合中添加异步请求的模块
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}

// 异步模块的信息添加到全局的jsonpArray中
if(parentJsonpFunction) parentJsonpFunction(data);

while(resolves.length) {
resolves.shift()();
}
};


// The module cache
var installedModules = {};

// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
// 标识模块加载的4个状态
var installedChunks = {
"main": 0
};

// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".js"
}

// require方法无变化
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}

// 这个文件只包含入口chunk
// 额外的chunks使用chunk loading函数
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
// 检查已经安装的包中有没有当前模块
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".

// a Promise means "currently loading".
// 4个状态中只有Promise为真
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// installedChunkData = [resolve, reject,Promise]
promises.push(installedChunkData[2] = promise);

// 开始加载异步模块
var script = document.createElement('script');
var onScriptComplete;

script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);

// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in IE.
// 避免IE内存泄漏
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};

// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;

// expose the module cache
__webpack_require__.c = installedModules;

// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};

// __webpack_public_path__
__webpack_require__.p = "";

// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };


// 定义全局变量
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// push方法会调用webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;


// 加载入口模块
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({
"./src/index.js":
(function(module, exports, __webpack_require__) {
__webpack_require__.e(0)
.then(__webpack_require__.bind(null, "./src/app.js"))
.then(module=>{(module.default)()});
console.log("index.js");
})

});

/0.js

1
2
3
4
5
6
7
8
9
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/app.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
console.log('app');
function App(){};
__webpack_exports__["default"] = (App);
})
}]);

简单Node部署

创建非 root 账户

有些云服务器会禁止 root 用户登录, 但是如果分发的服务器使用 root 登录,可以创建一个非 root 账号, 防止权限过高导致误操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# root 用户下执行,添加一个 user1 用户
adduser user1

Adding user `user1' ...
Adding new group `user1' (1002) ...
Adding new user `user1' (1002) with group `user1' ...
Creating home directory `/home/user1' ...
Copying files from `/etc/skel' ...
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for user1
Enter the new value, or press ENTER for the default
Full Name []: sunzhiqi
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n]

将指定的用户(user)添加到 sudo 组中,从而授予该用户使用 sudo 命令的权限

1
gpasswd -a user1 sudo

之前 root 用户登录的窗口不要关闭,如果新用户无法登录,可以使用 root 用户窗口重启 ssh 服务,再次尝试

1
service ssh restart

免密登录

如果使用 ssh 工具可以记住登录密码,如果使用命令行需要配置 免密登录

修改默认端口

修改完成后需要同步修改本地的 config 免密登录配置文件,同时配置服务器防火墙放行端口

1
2
3
4
vi /etc/ssh/sshd_config

#修改端口字段
# Port 22222

禁用 root 登录

1
2
3
4
5
6
7
8
9
vi /etc/ssh/sshd_config

# 表示禁止密码登录,但是可以通过SSH登录
# 通常需要设置为no
#PermitRootLogin prohibit-password

# 使用密码登录,通常设置为no
# PasswordAuthentication yes

iptables 配置

1
sudo touch /etc/network/if-up.d/iptables.up

写入规则

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
*filter

# -A INPUT:向 INPUT 链添加规则,用于处理进入系统的数据包。
# -m state:指定使用 state 模块,state 模块允许基于连接状态来过滤数据包。
# --state ESTABLISHED,RELATED:允许已建立连接(ESTABLISHED)和相关连接(RELATED)的数据包通过。简单来说,允许已经建立的连接的数据包通过,例如响应来自客户端的请求。
# -j ACCEPT:匹配此规则的数据包将被接受(允许通过)。
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT


# -A OUTPUT:向 OUTPUT 链添加规则,用于处理从系统发送的数据包。
# -j ACCEPT:所有匹配此规则的数据包将被接受,即允许所有出站流量。
-A OUTPUT -j ACCEPT


# -A INPUT:向 INPUT 链添加规则,处理进入系统的数据包。
# -p tcp:指定使用 TCP 协议。
# --dport 443:指定目标端口为 443,这通常用于 HTTPS 流量。
# -j ACCEPT:匹配此规则的 TCP 数据包将被接受,允许 HTTPS 流量进入系统。
-A INPUT -p tcp --dport 443 -j ACCEPT


# -A INPUT:向 INPUT 链添加规则,处理进入系统的数据包。
# -p tcp:指定使用 TCP 协议。
# --dport 80:指定目标端口为 80,通常用于 HTTP 流量。
# -j ACCEPT:允许所有 HTTP 流量(目标端口 80)进入系统。
-A INPUT -p tcp --dport 80 -j ACCEPT


# -A INPUT:向 INPUT 链添加规则,处理进入系统的数据包。
# -p tcp:指定使用 TCP 协议。
# -m state:使用 state 模块来匹配连接状态。
# --state NEW:只允许新的连接通过,即连接尚未建立的流量。
# --dport 39999:指定目标端口为 39999。
# -j ACCEPT:允许目标端口为 39999 的新连接通过。
-A INPUT -p tcp -m state --state NEW --dport 22222 -j ACCEPT



# -A INPUT:向 INPUT 链添加规则,处理进入系统的数据包。
# -p icmp:指定使用 ICMP 协议,通常用于网络诊断,如 ping 命令。
# -m icmp:使用 ICMP 模块进行匹配。
# --icmp-type 8:指定 ICMP 类型为 8,这表示“回显请求”类型,通常是 ping 请求。
# -j ACCEPT:允许 ping 请求(ICMP 类型 8)进入系统。
-A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT


# -A INPUT:将规则添加到 INPUT 链,表示处理进入系统的流量。
# -m limit 5/min:使用 limit 模块来限制日志记录的频率。这里表示每分钟最多记录 5 次 拒绝的连接请求。这样可以避免日志记录过多导致磁盘空间不足。
# -j LOG:指示 iptables 执行日志记录操作。
# --log-prefix "iptables denied: ":为每个被记录的日志条目添加前缀。日志条目将以 iptables denied: 开头,便于区分其他日志。
# --log-level 7:设置日志级别为 7。日志级别越高,记录的详细信息越多。级别 7 是最高级别,通常用于调试。
# 这条规则的作用是:记录所有被拒绝的连接请求,并且每分钟最多记录 5 次,并将日志保存到系统的日志文件中(通常是 /var/log/syslog 或 /var/log/messages)。
-A INPUT -m limit 5/min -j LOG --log-prefix "iptables denied: " --log-level 7

# 拒绝所有未被允许的入站流量,即如果流量没有匹配任何允许规则,则会被拒绝并发送一个拒绝消息。
-A INPUT -j REJECT


# 拒绝所有经过路由的流量。通常这种配置用于禁止路由器或网关机器转发流量。
-A FORWARD -j REJECT


# -A INPUT:将这条规则添加到 INPUT 链,表示处理进入系统的流量。
# -p tcp:指定协议为 TCP。
# --dport 80:指定目标端口为 80,即 HTTP 流量。
# -i eth0:表示该规则仅适用于 eth0 网络接口。也就是说,只有通过该接口的流量才会应用此规则。
# -m state:使用 state 模块来匹配数据包的连接状态。
# --state NEW:匹配所有 新的连接,即第一次尝试建立连接的流量。
# -m recent --set:使用 recent 模块,标记(--set)每个新连接的源 IP 地址。这会将源 IP 地址添加到一个最近连接的列表中。这个模块通常用于追踪和限制访问频率。
# 这条规则的作用是:对所有新的、来自 eth0 接口的 HTTP 请求进行标记,记录这些请求的源 IP 地址。
-A INPUT -p tcp --dport 80 -i eth0 -m state --state NEW -m recent --set



# -A INPUT:将规则添加到 INPUT 链,处理进入的流量。
# -p tcp:使用 TCP 协议。
# --dport 80:指定目标端口为 80,即 HTTP 流量。
# -i eth0:指定网络接口为 eth0。
# -m state:使用 state 模块来匹配数据包的连接状态。
# --state NEW:匹配所有新的连接。
# -m recent --update:使用 recent 模块来更新连接源 IP 的记录。
# --seconds 60:指定检查时间窗口为 60 秒。即在过去 60 秒内的请求将被视为重复的请求。
# --hitcount 150:如果源 IP 在 60 秒内的请求次数超过 150 次,则触发 DROP 动作。
# -j DROP:如果源 IP 地址的请求次数超过 150 次,就会被 丢弃,即拒绝这个源 IP 地址的流量。
# 这条规则的作用是:如果一个源 IP 地址在 60 秒内对端口 80 发起超过 150 次新连接,则丢弃该 IP 的请求。这通常用于 防止 DoS(拒绝服务)攻击 或者 流量过载,限制某个 IP 在短时间内过多的连接请求。
-A INPUT -p tcp --dport 80 -i eth0 -m state --state NEW -m recent --update --seconds 60 --hitcount 150 -j DROP


COMMIT

使用 iptables-restore 从 /etc/network/if-up.d/iptables.up 文件中加载并恢复防火墙规则

1
sudo iptables-restore < /etc/network/if-up.d/iptables.up

启动防火墙

1
2
3
sudo ufw enable

sudo ufw status

配置开机自动写入

1
2
3
4
5
6
7
8
# 创建文件
touch /etc/network/if-up.d/iptables

# 写入配置

#!/bin/sh
iptables-restore /etc/iptables.up

fail2ban

Fail2ban 是一个开源的入侵防护工具,主要用于防止基于暴力破解的攻击(如 SSH、HTTP、FTP 等服务的暴力登录尝试)。它通过监控日志文件,检测可疑行为(如多次失败的登录尝试),然后根据配置的规则采取措施,例如暂时或永久禁止攻击者的 IP 地址。

1
sudo apt install fail2ban

修改配置

1
2
3
4
vi /etc

# 换成自己的邮箱
destemail = root@localhost

启动服务

1
service fail2ban start

配置 node 环境

安装必要依赖

1
sudo apt install vim openssl build-essential libssl-dev wget curl git

安装 nvm,指定默认版本

1
nvm alias default v20.xx

设置系统文件参数

修改了 fs.inotify.max_user_watches 参数,增加每个用户可以监控的文件数(即文件监控器的数量)。

将此设置永久性地添加到 /etc/sysctl.conf 配置文件中,并应用该更改,使得新的文件监控数值生效。

1
2
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

强制同步 cnpm

如果使用了 cnpm 源,导致有些包不是最新的,使用以下命令强制同步

1
cnpm sync [package]

nginx

如果服务器预装了 apache2 需要先删除

1
2
3
4
5
# 删除 Apache2 服务的自动启动配置,它会删除 /etc/rc.d 目录下的相关启动链接。
update-rc.d -f apache2 remove

# 卸载 Apache2 软件包
sudo apt remove apache2

添加 nginx 配置文件

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
upstream node_learn {
server 127.0.0.1:3000; # 定义后端服务器
}

server {
listen 80;
server_name site.iftrue.me; # 配置服务器监听端口和域名

# 优化代理设置
location / {
proxy_set_header X-Real-IP $remote_addr; # 设置请求头,传递真实的客户端 IP 地址
proxy_set_header X-Forwarded-For $proxy_add_X_forwarded_for; # 设置请求头,传递 X-Forwarded-For 头信息
proxy_set_header Host $http_host; # 保留原始请求的 Host 头
proxy_set_header X-Nginx-Proxy true; # 添加 X-Nginx-Proxy 头,标识请求是经过 Nginx 代理的
proxy_pass http://node_learn; # 转发请求到后端应用
proxy_redirect off; # 关闭代理重定向
proxy_cache my_cache; # 启用缓存
proxy_cache_valid 200 1h; # 缓存 200 状态的响应 1 小时
proxy_cache_use_stale error timeout updating; # 使用过期的缓存响应来应对错误或超时
}

# 静态资源代理和缓存
location /assets/ {
# 将静态资源代理到 Node.js 应用或提供本地静态文件
proxy_pass http://node_learn/assets/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_X_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_cache my_cache;
proxy_cache_valid 200 1d; # 缓存静态资源 1 天
}

# 如果静态文件已经存储在磁盘中,可以使用 root 或 alias 来提供静态文件
location /static/ {
root /var/www/site.iftrue.me; # 假设静态资源位于这个目录
try_files $uri $uri/ =404; # 如果找不到文件返回 404
}

# 你也可以为其他静态资源(如 CSS、JS)设置缓存
location ~* \.(css|js|jpg|jpeg|png|gif|ico|woff|woff2|ttf|svg)$ {
root /var/www/site.iftrue.me;
try_files $uri $uri/ =404;
expires 30d; # 设置静态资源缓存 30 天
add_header Cache-Control "public, no-transform"; # 设置缓存控制头
}

# 其他错误页面、重定向等配置
error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
internal;
}
}

如果需要隐藏响应头中的 nginx 版本信息配置 nginx.conf

1
2
3
4
# /etc/nginx/nginx.conf

# 取消此行注释
server_tokens off;

mysql

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
# 安装服务
sudo apt install -y mysql-server

# 检查服务是否启动
systemctl status mysql

# 执行MySQL安全脚本mysql_secure_installation,提高MySQL的安全性
# 该脚本允许设置或更改root密码策略、移除匿名用户、禁止root远程登录、删除测试数据库等。
sudo mysql_secure_installation

# 首次登录没有密码
sudo mysql -u root -p

#给root用户设置密码
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;EXIT;

# 创建对应的数据库

CREATE DATABASE my_database
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
# 查看所有数据库
SHOW DATABASES;

# 删除数据库
DROP DATABASE database_name;
# 修改字符集和排序
ALTER DATABASE database_name
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

# 创建新用户
# 'localhost':表示该用户只能从本地机器连接到数据库。如果希望该用户可以从任何主机连接,可以使用 '%',如 'new_user'@'%'。
CREATE USER 'new_user'@'localhost' IDENTIFIED BY 'your_password';
# 查看所有用户
SELECT User, Host FROM mysql.user;


#ALL PRIVILEGES:赋予所有权限。
# my_database.*:指示该用户可以对 my_database 数据库中的所有表进行操作。
# 'localhost':表示该用户只能从本地连接。
GRANT ALL PRIVILEGES ON my_database.* TO 'new_user'@'localhost';
# 如果希望用户只具备某些操作的权限(例如只读权限),可以单独授予特定权限
GRANT SELECT ON my_database.* TO 'new_user'@'localhost';
GRANT SELECT, INSERT ON my_database.* TO 'new_user'@'localhost';


# 可以访问所有数据库
# GRANT ALL PRIVILEGES ON *.* TO 'new_user'@'localhost';


# 刷新权限
FLUSH PRIVILEGES;


# 查看端口
SHOW VARIABLES LIKE 'port';

cat /etc/mysql/mysql.conf.d/mysqld.cnf

pm2

1
2
3
4
5
6
7
8
9
10
npm install -g pm2

# 启动服务
pm2 start server.js

# 应用列表
pm2 list

# 应用详细信息
pm2 show [appName]

配置 pm2 ecosystem.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"apps": [
{
"name": "node-learn",
"script": "bin/www",
"env": {
"NODE_ENV": "development"
},
"env_production": {
"NODE_ENV": "production"
}
}
],
"deploy": {
"production": {
// 远程服务器上用于执行部署的用户。这个用户将有权限操作目标服务器进行部署工作。
"user": "ubuntu",
// 部署目标服务器的 IP 地址。这里设置了一个服务器的 IP 地址
// 表示要将代码部署到这个服务器。
"host": ["192.168.48.171"],
// 通过 SSH 连接远程服务器时使用的端口号
"port": "22",
// 这是你要部署的 Git 分支。origin/master 表示远程仓库的 master 分支
"ref": "origin/main",
// 这是 Git 仓库的 URL,表示从哪里拉取代码。
"repo": "git@git.oschina.net:wolf18387/backend-website.git",
// 这是部署目标路径,代码将被拉取到这个路径下。
// 所有应用文件将被放置在服务器的 /www/website/production 目录下。
"path": "/www/node-learn",
// 这个选项用于 SSH 连接时禁用 SSH 主机密钥检查。
// 这通常用于自动化部署,避免因首次连接服务器时需要确认主机密钥而中断部署。
"ssh_options": "StrictHostKeyChecking=no",
// 部署前的准备脚本
"pre-setup": "rm -rf /www/node-learn/*",
// 部署前的准备脚本,在拉取代码后执行
// 在服务器上执行的脚本
// 如果使用的是nvm安装的node需要加上 source ~/.nvm/nvm.sh
// 需要指定环境变量 export NODE_ENV=production deploy.env 只会在 deploy 阶段加载
"post-setup": "source ~/.nvm/nvm.sh && npm install && export NODE_ENV=production && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all",

"post-deploy": "source ~/.nvm/nvm.sh && npm install && pm2 startOrRestart ecosystem.json --env production",
"env": {
"NODE_ENV": "production"
}
}
}
}

执行部署

部署前确认各个服务器之间可以互相免密访问,本地=>生产服务器=>git 服务器, 将主机加入目标主机的 known hosts 中

确认服务器的目录是否有写入权限

windows 环境会执行失败,需要在 linux 虚拟机中执行

1
2
3
4
5
6
7
#  设置远程环境,创建应用目录,初始化 Git 仓库,执行 pre-setup 钩子
# current 当前服务运行的文件夹会软连接到source文件夹上
# source clone下来的源代码
# shared 配置文件等
# 只应该在初始化的时候执行一次
npx pm2 deploy production setup

部署成功后可以登录服务器检查部署情况

1
2
3
pm2 list

pm2 logs

高级用法

泛型定义

泛型(Generics)是只在定义函数,接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特征

泛型 T 作用于只限在函数的内部

必须要把val指定为any类型,可以传入任意类型,但是不够灵活
val不是所有类型都可以,所以需要使用泛型

1
2
3
4
5
6
7
8
9
10
function  createArray(length:number,val:any):Array<any> {
let result:Array<any> =[];
for(let i=0;i<length;i++){
result.push(val);
}
return result;
}

let result = createArray(3,"x");
console.log(result);

期望的是val的什么类型,最终返回的就是这个类型的数组

1
2
3
4
5
6
7
8
9
10
function  createArray<T>(length:number,val:T):Array<T> {
let result:Array<T> =[];
for(let i=0;i<length;i++){
result.push(val);
}
return result;
}
// 在是用的时候传入声明的类型
let result = createArray<string>(3,"x");
console.log(result);

接口泛型

因为T是未知类型,所以返回值中不能经行运算

1
2
3
4
5
6
7
8
9
interface M {
<T>(a:T,b:number):T
}

const fn:M = function <T>(a:T,b:number) {
return a;
}

fn("1",1);

多类型参数

1
2
3
4
5
const swap = function swap<A,B>(arr:[A,B]):[B,A] {
return [arr[1],arr[0]];
}

console.log(swap<string,number>(['a',2]))

默认泛型

1
2
3
4
5
const swap = function swap<A,B=number>(arr:[A,B]):[B,A] {
return [arr[1],arr[0]];
}

console.log(swap<string>(['a',2]))

泛型约束

在函数中使用泛型的时候,由于预先不知道具体使用的类型,所以不能访问相应类型的方法

1
2
3
4
function logger<T>(val:T):number {
// 报错: 不能确定val的类型,所以不能检查
return val.length;
}
1
2
3
4
5
6
7
interface Logger {
length:number
}

function logger<T extends Logger>(val:T):number {
return val.length;
}

泛型类型别名

interface定义一个真实的接口,是一个真正的类型

type 一般用来定义别名,并不是一个真正的类型

1
2
3
4
5
6
7
8
9
interface Cart<T> {
list:T[]
}

// 泛型类型别名
type Cart2<T> ={list:T[]}|T[];

const c1:Cart2<string> = ['1']
const c2:Cart2<string> = {list:['1']}

交叉类型

1
2
3
4
5
6
7
8
9
interface Bird{
fly():void
}

interface Boy{
name:string
}

type BirdBoy = Bird & Boy

typeof 获取类型

1
2
3
4
5
6
7
8
const boy  = {
name:'1'
}
type Boy = typeof boy;

const p:Boy ={
name:"123"
}

索引访问

1
2
3
4
5
6
7
8
interface Boy{
name:string,
info:{
id:number
}
}

const boy:Boy['info']['id'] = 123

索引类型查询操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const boy:Boy = {
name:"boy",
id:1,
info:{
aaa:123
}
}

type PropType = keyof Boy;

function getData(target:Boy,prop:PropType) {
return target[prop];
}

getData(boy,'info')

映射类型

部分属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Boy{
name:string,
id:number,
info:{
aaa:number
}
}

// 部分属性
type PartialBoy = {
[k in keyof Boy]?:Boy[k]
}

const boy:PartialBoy = {
name:'123'
}

原生提供的方法及实现

1
2
3
4
5
6
7
8
9
10
11
// Partial 实现
type Partial<T> ={
[key in keyof T]?:T[key]
}

// 原生方法
type PartialBoy = Partial<Boy>;

const boy:PartialBoy = {
name: "name"
}

所有属性必须有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Required 实现
type Required<T> ={
[key in keyof T]-?:T[key]
}

// 原生方法
type PartialBoy = Required<Boy>;

const boy:PartialBoy = {
name: "name",
id:123,
info:{
aaa:111
}
}

属性只读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Readonly 实现
type Readonly<T> ={
readonly [key in keyof T]:T[key]
}

// 原生方法
type PartialBoy = Readonly<Boy>;

const boy:PartialBoy = {
name: "name",
id:123,
info:{
aaa:111
}
}

// 报错
boy.name = 1;

pick 指定属性

1
2
3
4
5
6
7
8
9
10
11
12
13
// Pick 实现
// 只能摘取某一项返回
type Pick<T,K extends keyof T> ={
[key in K]:T[key]
}

// 原生方法
type PartialBoy = Pick<Boy,'id'>;

const boy:PartialBoy = {
id:123,
name:'123' // 报错
}

条件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Bird {
name1:string
}

interface Fish {
name2:string
}

interface Water {
name3:string
}

interface Sky {
name4:string
}

type Combin<T> = T extends Bird ? Sky : Water;

const ins:Combin<Fish | Bird> = {
name4: '123',
name3:'123'
}

TypeScript 相关概念

interface vs type

  • interface 可以使用 扩展(extends)来继承其他接口,并且支持 声明合并(Declaration Merging)
    type 不能像接口那样直接进行声明合并,但它可以通过 交叉类型(&)来合并类型。一般称作类型别名

  • interface 更多地用于定义对象的结构,也可以用来定义类的形状(即类实现的接口)。
    type 是更通用的类型别名,可以用于任何类型,包括基本类型、联合类型、交叉类型、元组等。

  • 鼠标放在 interface 上会显示接口名称,但是放在 type 上显示的是对应的类型,因此 type 并不定义新的类型。

const vs readonly

不在同一个维度,一个是类型,一个是变量,如果描述一个类型不可变使用 readonly, 如果是变量不可变使用 const。

数字类型索引

数字类型索引的类型必须是字符串索引的子类型。

1
2
3
4
5
6
7
8
class Animal {}
class Dog extends Animal {}
const a: {
[k: number]: Dog;
[k: string]: Animal;
} = {
1: new Dog(),
};

class interface

class 具有静态部分类型,和实例类型。 不要用构造器签名去给类实现接口使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface IA {
todo(): void;
}
interface IACons {
new (): IA;
}

class A implements IA {
todo() {}
}

function create(cons: IACons): IA {
return new cons();
}

受保护的构造方法

可以考虑用抽象类代替

1
2
3
class A {
protected constructor() {}
}

类型断言

1
2
3
4
const a: object | any[] = [];

(a as any[]).push(1);
(<any[]>a).push(1);

类型保护

  • 使用类型谓词

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    interface Fish {
    swim: () => void;
    name: () => void;
    }

    interface Bird {
    fly: () => void;
    name: () => void;
    }

    function isFish(obj: Fish | Bird): obj is Fish {
    // 返回一个boolean
    return (obj as Fish).swim !== undefined;
    }
    function fn(obj: Fish | Bird) {
    if (isFish(obj)) obj.swim();
    else obj.fly();
    }
  • typeof

    1
    2
    3
    4
    function fn(obj: string | number) {
    if (typeof obj === "string") obj.substring;
    else obj.toFixed;
    }
  • ! 操作符

    1
    2
    3
    4
    5
    6
    // 在嵌套函数中无法正确推断数据类型
    function fn(name: string | null | undefined) {
    return function () {
    name!.substring;
    };
    }

类型约束

  • unknown 是 TypeScript 中的一个特殊类型,它表示任何类型。与 any 不同,unknown 类型的值不能直接操作,必须先进行某种类型的检查。
1
type of = [1, 2, 3] extends unknown ? 1 : 2; // 1

元组转为普通对象

会移除,number 索引签名,所有数组方法, length 属性,所有可能的数字字符串索引

保留元组特有的具体数字属性映射

1
type ObjFromTuple = Omit<Tuple, keyof any[]>;

逆变 协变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal {}

class Cat extends Animal {
Cat() {}
}
class Dog extends Animal {
Dog() {}
}
class SmallDog extends Dog {
public name = 1;
}

// 错误 参数类型不安全,定义的类型应该是参数类型的父类型,参数可以多出不使用,但是不能少
const fn: (v: Dog) => Dog = (v: SmallDog) => new SmallDog();
// 错误
const fn1: (v: Dog) => Dog = (v: SmallDog) => new Animal();
const fn2: (v: Dog) => Dog = (v: Animal) => new SmallDog();
// 错误 返回值类型不安全,定义的类型应该是返回值类型的子类型,返回值可以返回当前有的类型,但是不能多返回
const fn3: (v: Dog) => Dog = (v: Animal) => new Animal();
1
2
3
4
5
6
7
8
9
10
11
type T1 = { name: string };
type T2 = { age: number };

// 参数协变,因此返回的是交叉类型
type UnionToIntersection<T> = T extends {
a: (x: infer U) => void;
b: (x: infer U) => void;
}
? U
: never;
type T3 = UnionToIntersection<{ a: (x: T1) => void; b: (x: T2) => void }>; // T1 & T2

Symbol实现

Symbol类型的行为

1.类型检测

Symbol 值通过 Symbol 函数生成,使用 typeof,结果为 “symbol”

2.不能使用new

Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。

3.instanceof 的结果为 false
1
2
var s = Symbol('foo');
console.log(s instanceof Symbol); // false
4.参数接受
1
2
var s1 = Symbol('foo');
console.log(s1); // Symbol(foo)

如果接受的是一个对象调用的则是对象的 toString 方法

5.不能运算

Symbol 值不能与其他类型的值进行运算,会报错。

1
2
3
var sym = Symbol('My symbol');

console.log("your symbol is " + sym); // TypeError: can't convert symbol to string
6.可以转换为字符串
1
2
3
4
5

var sym = Symbol('My symbol');

console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'
7.可以当作属性名

Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
[mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
console.log(a[mySymbol]); // "Hello!"
8.不能枚举

Symbol 作为属性名,该属性不会出现在 for…in、for…of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。

1
2
3
4
5
6
7
8
9
10
11
var obj = {};
var a = Symbol('a');
var b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

var objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols);
// [Symbol(a), Symbol(b)]
9.Symbol.for

如果我们希望使用同一个 Symbol 值,可以使用 Symbol.for。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

1
2
3
4
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');

console.log(s1 === s2); // true
10.Symbol.keyFor

Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

1
2
3
4
5
var s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)); // "foo"

var s2 = Symbol("foo");
console.log(Symbol.keyFor(s2) ); // undefined

polyfile分析

Symbol 返回一个独一无二的值,根据官方文档symbol的创建步骤

Symbol ( [ description ] )

When Symbol is called with optional argument description, the following steps are taken:

If NewTarget is not undefined, throw a TypeError exception.
If description is undefined, var descString be undefined.
Else, var descString be ToString(description).
ReturnIfAbrupt(descString).
Return a new unique Symbol value whose [[Description]] value is descString.

  • 如果使用 new ,就报错
  • 如果 description 是 undefined,让 descString 为 undefined
  • 否则 让 descString 为 ToString(description)
  • 如果报错,就返回
  • 返回一个新的唯一的 Symbol 值,它的内部属性 [[Description]] 值为 descString

还需要定义一个 [[Description]] 属性,如果直接返回一个基本类型的值,是无法做到这一点的,所以我们最终还是返回一个对象

第一版

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
var SymbolPolyfill = function Symbol(description) {
// 实现特性第 2 点:Symbol 函数前不能使用 new 命令
if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

// 实现特性第 4 点:如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。
var descString = description === undefined ? undefined : String(description)

// 实现第六点:可以转为字符串
var symbol = Object.create({
toString: function() {
return 'Symbol(' + this.__Description__ + ')';
},
// 不能与其他值运算
// 对于原生 Symbol,显式调用 valueOf 方法,会直接返回该 Symbol 值,
// 而我们又无法判断是显式还是隐式的调用,所以这个我们就只能实现一半,要不然实现隐式调用报错,要不然实现显式调用返回该值,
valueOf: function() {
throw new Error('Cannot convert a Symbol value')
}
})

Object.defineProperties(symbol, {
'__Description__': {
value: descString,
writable: false,
enumerable: false,
configurable: false
}
});

// 实现特性第 6 点,因为调用该方法,返回的是一个新对象,两个对象之间,只要引用不同,就不会相同
return symbol;
}

第三版

当我们模拟的所谓 Symbol 值其实是一个有着 toString 方法的 对象,当对象作为对象的属性名的时候,就会进行隐式类型转换,还是会调用我们添加的 toString 方法,对于 Symbol(‘foo’) 和 Symbol(‘foo’)两个 Symbol 值,虽然描述一样,但是因为是两个对象,所以并不相等,但是当作为对象的属性名的时候,都会隐式转换为 Symbol(foo) 字符串,这个时候就会造成同名的属性。

1
2
3
4
5
6
7
8
9
10
var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');

console.log(a === b); // false

var o = {};
o[a] = 'hello';
o[b] = 'hi';

console.log(o); // {Symbol(foo): 'hi'}

为了防止不会出现同名的属性,毕竟这是一个非常重要的特性,迫不得已,我们需要修改 toString 方法,让它返回一个唯一值,所以第 8 点就无法实现了,而且我们还需要再写一个用来生成 唯一值的方法,就命名为 generateName,我们将该唯一值添加到返回对象的 Name 属性中保存下来。

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
var generateName = (function(){
var postfix = 0;
return function(descString){
postfix++;
return '@@' + descString + '_' + postfix
}
})()

var SymbolPolyfill = function Symbol(description) {

if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

var descString = description === undefined ? undefined : String(description)

var symbol = Object.create({
toString: function() {
return this.__Name__;
}
})

Object.defineProperties(symbol, {
'__Description__': {
value: descString,
writable: false,
enumerable: false,
configurable: false
},
'__Name__': {
value: generateName(descString),
writable: false,
enumerable: false,
configurable: false
}
});

return symbol;
}

第四版

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
var generateName = (function(){
var postfix = 0;
return function(descString){
postfix++;
return '@@' + descString + '_' + postfix
}
})()

var SymbolPolyfill = function Symbol(description) {

if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

var descString = description === undefined ? undefined : String(description)

var symbol = Object.create({
toString: function() {
return this.__Name__;
},
valueOf: function() {
return this;
}
})

Object.defineProperties(symbol, {
'__Description__': {
value: descString,
writable: false,
enumerable: false,
configurable: false
},
'__Name__': {
value: generateName(descString),
writable: false,
enumerable: false,
configurable: false
}
});

return symbol;
}

// 遍历 forMap,查找该值对应的键值即可。
var forMap = {};

Object.defineProperties(SymbolPolyfill, {
'for': {
value: function(description) {
var descString = description === undefined ? undefined : String(description)
return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
},
writable: true,
enumerable: false,
configurable: true
},
'keyFor': {
value: function(symbol) {
for (var key in forMap) {
if (forMap[key] === symbol) return key;
}
},
writable: true,
enumerable: false,
configurable: true
}
});

redux实现原理

现有状态管理工具

redux mobx mobx-state-tree redoil vuex

redux flux 是一个工具,每一层分的清楚但是麻烦

store -> container
currentState -> _value
action-> f
currentReducer ->map
middleware- io functor 异步问题

store 是一个容器含有state 和reducer,reducer 是一个纯函数,他可以查看之前的状态,执行一个action并且返回一个新的状态

mobx 强调的是轻量,去掉了Reducer. 只有action+state @observer(函数组件)

mobx-state-lite => provider

原理图

Action

Action 是store数据的唯一来源, store.dispatch() 将 action 传到 store。

我们应该尽量减少在 action 中传递的数据。尽量传递数据标识或修改条件,如索引,过滤条件,而不是修改数据之后传入整个数据对象。

通常Action是一个对象

1
2
3
4
{
type: SET_VISIBILITY_FILTER,
filter: SHOW_COMPLETED
}

可以用函数简单的包装

1
2
3
4
const addTodo  = (text) => ({
type: ADD_TODO,
text
})

因为action最终回传递到dispatch中,可以通过一个函数来自动实现这一步,也就是actionCreater

1
const boundTode = (text) => dispatch(addTodo(text))

Reducer

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。

reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

1
(previousState, action) => newState

之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:

1.修改传入参数;
2.执行有副作用的操作,如 API 请求和路由跳转;
3.调用非纯函数,如 Date.now() 或 Math.random()。

只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

1
2
3
4
5
6
7
8
9
10
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}

注意:

  • 不能直接修改state
  • 在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state。

拆分Reducer

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
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
default:
return state
}
}

function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}

function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}

redux 提供了合并reducer的方法与上面的完全等价,会按照不同的key值,把reducer分组

1
2
3
4
5
6
import { combineReducers } from 'redux'

const todoApp = combineReducers({
visibilityFilter,
todos
})

Store

Store 就是把它们联系到一起的对象。

维持应用的 state;
提供 getState() 方法获取 state;
提供 dispatch(action) 方法更新 state;
通过 subscribe(listener) 注册监听器;
通过 subscribe(listener) 返回的函数注销监听器。

通过redux提供的createStore方法创建store

./redux.js

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

// 接受reducer 和 初始化状态返回store对象
function createStore(reducer,initState){
let currentreducer = reducer;
let store = initState;
let currentListen = [];


// 直接返回store的引用
function getState(){
return store;
}

// 把事添加到监听队列中
function subscribe(listen){
currentListen.push(listen);
}

//派发一个action,通过reducer处理,返回并保存新的state
function dispatch(action){
store = currentreducer(action,store);
// 发布通知
for(let i=0;i<currentListen.length;i++){
currentListen[i]();
}
}

// 返回的store对象
return {
getState,
dispatch,
subscribe
}
}
export {
createStore,
}

combineReducers

combineReducers用于合并reducer,需要注意的是reducer和combineReducers都是函数
所以combineReducers的实现是把,多个reducer循环执行并且按reducer拆分store

1
2
3
4
5
6
7
8
9
10
11
12
function combineReducers(reducers){
// 缓存keys,避免重复执行
const keys = Object.keys(reducers);
return function(action,store){
// 初始化的store可能为undefined
store = store || {};
for(let k of keys){
store[k] = reducers[k](action,store[k])
}
return store;
}
}

因为第一次执行的时候,每个reducer如果有默认的state应该先合并一次

需要在createrStore中自动执行一次dispatch({type:Symbol('')})

Middleware

有时希望能在状态改变前和改变后处理

1
2
3
4
5
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}

重写了dispatch函数,并保持了对老的dispatch的引用
新的dispatch是对老的dispatch的封装,可以在中间添加逻辑处理

1
2
3
4
5
6
7
8
9
function patchStoreToAddLogging(store) {
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}

由于不满足函数式编程的思想,这里通过直接返回一个函数来处理
并把老的dispatch方法通过next参数传入
传入的next需要在redux中实现

1
2
3
4
5
6
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}

applyMiddleware

返回一个新的store对象,并提供通过middleware包装过的dispatch方法

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
function applyMiddleware(...middlewares){
return function(createStore){
// 新的createStore方法传入 reducers 和 initstate
return function(...args){
const store = createStore(...args);
// 保留dispatch的引用
const next = store.dispatch;
// 提供给middleware的第一个参数,如果直接调用dispatch回抛出错误
const middlewareApi = {
getState:store.getState,
dispatch:()=> new Error()
}
//为每个middle提供getState方法
middlewares = middlewares.map(middleware=>middleware(middlewareApi));

//重写dispatch方法,每一个middleware执行,都会触发传入的dispatch的引用
//a(next) 返回middleware中最有一个函数,需要传入一个action
//(action) =>{
// console.log('error state',store.getState());
// next(action);
// console.log('action');
// }
// b函数传入的就是这个返回函数
// 在最终调用的时候action回一级一级的被传下去
const dispatch = middlewares.reduce((a,b)=>b(a(next)));
return {
...store,
dispatch
}
}
}
}

同时redux.js中需要修改

1
2
3
4
5
6
function createStore(reducer,initState,middleware){
if(middleware) {
// 重写create函数,相当于在外层添加一个拦截器
return middleware(createStore)(reducer,initState)
}
}

compose

把 reducer middlewares 的逻辑单独提取出来

1
2
3
4
5
6
7
8
9
10
11
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}
//注意最后返回的时候包裹了一个函数,传入外部参数,也就是上面的store.dispatch
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

最终完整的代码

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
const applyMiddleware = function(...middlewares){
return (createStore)=>(...args) =>{
const store = createStore(...args);
console.log(middlewares);

let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}

const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}

const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}

bindActionCreators

我们希望可以创建一个action对象,直接调用上面的方法,而忽略dispatch的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const actions1 =(payload)=>({
type: "A",
payload
});
const actions2 =(payload)=>({
type: "B",
payload
})
const actions3 =(payload)=>({
type: "C",
payload
})

const actions = bindActionCreators({
actions1,
actions2,
actions3,
},store.dispatch)

actions.actions1()
actions.actions2({name: 'wefwegwekfl'})
actions.actions3({age: '30'})

需要实现一个bindActionCreators方法

惟一会使用到 bindActionCreators 的场景是当你需要把 action creator 往下传到一个组件上

却不想让这个组件觉察到 Redux 的存在,而且不希望把 dispatch 或 Redux store 传给它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const bindActionCreator = function(actionCreator,dispatch){
return function(){
return dispatch(actionCreator.apply(this,arguments))
}
}

// 接受的是一个action对象集合
const bindActionCreators = function(actionCreators,dispatch){
const boundActionCreators = {}
for(let key in actionCreators){
if(typeof actionCreators[key]==='function'){
// 最终返回一个以每个action名字为key的对象
// 每个key对应的值为一个可以触发dispatch方法的函数
// 相当于为每个actio自动绑定了dispatch 方法
boundActionCreators[key] = bindActionCreator(actionCreators[key],dispatch)
}
}
return boundActionCreators;
}


export default bindActionCreators

Express实践 ③ 项目部署

统一的错误处理

http-errors 可以统一错误抛出,避免自定义错误对象。

1
2
3
var E = require("http-errors");

throw new E.BadRequest(["报错了"]);

可以使用统一的错误处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = function errorHandle(res, err) {
let status = 500;
let message = [];

if (err instanceof E.HttpError) {
status = err.status;
message = err.message;
}

res.status(status).json({
status: false,
errors: message,
});
};

腾讯 OSS 服务端上传

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
// 引入模块
var COS = require("cos-nodejs-sdk-v5");
var crypto = require("crypto");
var multer = require("multer");
// 创建实例
var cos = new COS({
SecretId: "xxx",
SecretKey: "xxx",
});

// 存储桶名称,由bucketname-appid 组成,appid必须填入,可以在COS控制台查看存储桶名称。 https://console.cloud.tencent.com/cos5/bucket
var Bucket = "xxx-1255610650";
// 存储桶Region可以在COS控制台指定存储桶的概览页查看 https://console.cloud.tencent.com/cos5/bucket/
// 关于地域的详情见 https://cloud.tencent.com/document/product/436/6224
var Region = "ap-nanjing";

const storage = multer.memoryStorage();
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 限制文件大小(10MB)
fileFilter: (req, file, cb) => {
// 可选:限制文件类型
const allowedTypes = ["image/jpeg", "image/png"];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("只允许上传 JPG/PNG 文件"));
}
},
});

const push = async (req, res) => {
if (!req.file) {
throw new E[400](["你没有选择文件"]);
}

const ext = req.file.originalname.split(".").pop();
const fileName = `${crypto.randomBytes(64).toString("hex")}.${ext}`;

return cos.putObject({
Bucket,
Region,
Key: fileName, // 对象存储路径
Body: req.file.buffer, // 文件内容(Buffer)
ContentType: req.file.mimetype, // 文件类型
});
};

module.exports = { middleware: upload.single("file"), push };

接口添加中间件

1
2
3
const { middleware: uploadMiddleware } = require("./utils/upload");

app.use("/upload", uploadMiddleware, uploadRouter);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.post("/", async function (req, res, next) {
try {
if (!req.file) {
throw new Error();
}
const result = await push(req);

// 同时保存到数据库中
// ...
res.json(result);
} catch (error) {
res.errorHandle(error);
}
});

腾讯 OSS 客户端上传

需要先获取临时令牌,服务端提供接口, allowPrefix allowActions 必填

服务端可以把桶的名称,地区统一返回

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
router.get("/sts", (req, res, next) => {
var config = {
secretId: "xxx", // 固定密钥
secretKey: "xxx", // 固定密钥
proxy: "",
durationSeconds: 1800,
// host: 'sts.tencentcloudapi.com', // 域名,非必须,默认为 sts.tencentcloudapi.com
endpoint: "sts.tencentcloudapi.com", // 域名,非必须,与host二选一,默认为 sts.tencentcloudapi.com

// 放行判断相关参数
bucket: "node-learn-1255610650",
region: "ap-nanjing",
allowPrefix: "*",
allowActions: [
// 简单上传
"name/cos:PutObject",
// 分块上传
"name/cos:InitiateMultipartUpload",
"name/cos:ListMultipartUploads",
"name/cos:ListParts",
"name/cos:UploadPart",
"name/cos:CompleteMultipartUpload",
],
};

var shortBucketName = config.bucket.substr(0, config.bucket.lastIndexOf("-"));
var appId = config.bucket.substr(1 + config.bucket.lastIndexOf("-"));
var policy = {
version: "2.0",
statement: [
{
action: config.allowActions,
effect: "allow",
principal: { qcs: ["*"] },
resource: [
"qcs::cos:" +
config.region +
":uid/" +
appId +
":prefix//" +
appId +
"/" +
shortBucketName +
"/" +
config.allowPrefix,
],
// condition生效条件,关于 condition 的详细设置规则和COS支持的condition类型可以参考https://cloud.tencent.com/document/product/436/71306
// 'condition': {
// // 比如限定ip访问
// 'ip_equal': {
// 'qcs:ip': '10.121.2.10/24'
// }
// }
},
],
};
STS.getCredential(
{
secretId: config.secretId,
secretKey: config.secretKey,
proxy: config.proxy,
durationSeconds: config.durationSeconds,
endpoint: config.endpoint,
policy: policy,
},
function (err, tempKeys) {
res.json({
...tempKeys,
bucket: "xxx-1255610650",
region: "ap-nanjing",
});
}
);
});

客户端调用

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
fetch("/upload/sts")
.then((res) => res.json())
.then((res) => {
const cos = new COS({
SecretId: res.credentials.tmpSecretId, // sts服务下发的临时 secretId
SecretKey: res.credentials.tmpSecretKey, // sts服务下发的临时 secretKey
SecurityToken: res.credentials.sessionToken, // sts服务下发的临时 SessionToken
StartTime: res.startTime, // 建议传入服务端时间,可避免客户端时间不准导致的签名错误
ExpiredTime: res.expiredTime, // 临时密钥过期时间
});
cos.uploadFile(
{
Bucket: res.bucket,
Region: res.region,
Key: "xxx.txt",
Body: document.getElementById("file").files[0], // 要上传的文件对象。
onProgress: function (progressData) {
console.log("上传进度:", progressData);
},
},
function (err, data) {
console.log("上传结束", err || data);
}
);
});

redis

安装 redis 并提供一个简单的工具文件

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
// redis-utils.js
const { createClient } = require("redis");

// 全局 Redis 客户端实例
let client = null;

/**
* 初始化 Redis 客户端(单例模式)
*/
const initializeRedis = async () => {
if (client?.isOpen) return;

try {
client = createClient({
url: "redis://localhost:6379", // 默认连接地址
// password: 'your_password', // 如果需要密码验证
});

// 监听连接错误事件
client.on("error", (err) => console.error("Redis connection error:", err));

await client.connect();
console.log("Redis client connected");
} catch (err) {
console.error("Failed to connect to Redis:", err);
throw err;
}
};

/**
* 存储数据(支持对象/数组自动序列化)
* @param {string} key - 存储键名
* @param {object|array|string|number|boolean} value - 存储值
* @param {number} [ttl] - 过期时间(秒),不传则永久保存
*/
const setKey = async (key, value, ttl) => {
if (!client?.isOpen) await initializeRedis();

try {
const serializedValue =
typeof value === "string" ? value : JSON.stringify(value);

if (ttl) {
await client.setEx(key, ttl, serializedValue);
} else {
await client.set(key, serializedValue);
}
} catch (err) {
console.error(`Failed to set key "${key}":`, err);
throw err;
}
};

/**
* 获取存储的数据(自动反序列化对象/数组)
* @param {string} key - 要获取的键名
* @returns {Promise<any>}
*/
const getKey = async (key) => {
if (!client?.isOpen) await initializeRedis();

try {
const value = await client.get(key);
if (!value) return null;

try {
return JSON.parse(value);
} catch {
return value;
}
} catch (err) {
console.error(`Failed to get key "${key}":`, err);
throw err;
}
};

/**
* 删除指定键
* @param {string} key - 要删除的键名
* @returns {Promise<boolean>}
*/
const deleteKey = async (key) => {
if (!client?.isOpen) await initializeRedis();
const result = await client.del(key);
return result > 0;
};

const scanKeys = async (pattern, count = 100) => {
if (!client?.isOpen) await initializeRedis();

const foundKeys = [];
let cursor = 0;

do {
const reply = await client.scan(cursor, { MATCH: pattern, COUNT: count });

cursor = reply.cursor;
foundKeys.push(...reply.keys);
} while (cursor !== 0);

return foundKeys;
};

/**
* 关闭 Redis 连接
*/
const closeRedis = async () => {
if (client?.isOpen) {
await client.quit();
console.log("Redis connection closed");
}
};

module.exports = {
initializeRedis,
setKey,
getKey,
deleteKey,
closeRedis,
};

安装 redis insight 客户端, 并通过 docker 启动 redis 服务

1
2
3
4
5
6
7
8
9
10
version: '3.8' # 指定 Docker Compose 文件格式版本

services:
redis:
# 服务名称
image: redis:7.4 # Redis 镜像版本(注意:Redis 官方镜像没有 7.4 版本,建议使用最新稳定版如 7.2
ports:
- "6379:6379" # 端口映射(宿主机:容器)
restart: unless-stopped # 容器自动重启策略

分页缓存策略,使用 : 拼接字段作为 key,冒号在 redis 中有特殊的意义,

1
2
3
4
5
const key = "article:math:1";
redis.setKey(key, value);

// 使用通配符查询所有key
const userKeys = await scanKeys("user:*");

发送邮件

搜索 Google App Passwords 创建新密码

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
const nodemailer = require("nodemailer");

const sendEmail = async () => {
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: "fengaiqi000@gmail.com",
pass: "创建的google账户免密",
},
});

const mailOptions = {
from: "fengaiqi000@gmail.com",
to: "sunzhiqi@live.com",
subject: "Test Email",
text: "Hello, this is a test email sent using Nodemailer and OAuth2!",
};

transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.log(error);
}
console.log("Email sent: " + info.response);
});
};

sendEmail().catch(console.error);

Rabbit MQ

常用于,发送电子邮件、发送短信、应用内通知、文件处理、数据分析与报告生成、订单处理、秒杀。

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
// send.js
var amqplib = require("amqplib");
const url = `amqp://localhost`;
const QUEUE_NAME = "task1";
async function sendMessage(message) {
let connection;
try {
connection = await amqplib.connect(url, {
username: "node",
password: "123456",
port: "5672",
vhost: "/",
});
console.log("✅ 成功连接到 RabbitMQ 服务器");

// 2. 创建通道
const channel = await connection.createChannel();
console.log("🔄 通道已创建");

// 3. 声明队列(如果不存在则创建)
await channel.assertQueue(QUEUE_NAME, {
durable: true, // 持久化队列(服务器重启后保留)
});

// 4. 发送消息
const sent = channel.sendToQueue(
QUEUE_NAME,
Buffer.from(message),
{ persistent: true } // 消息持久化
);

if (sent) {
console.log(`📤 消息已发送: ${message}`);
} else {
console.error("❌ 消息发送失败");
}
} catch (error) {
console.error("🔥 发生错误:", error);
} finally {
// 5. 延迟500ms后关闭连接
if (connection) {
await new Promise((resolve) => setTimeout(resolve, 500));
await connection.close();
console.log("🚪 连接已关闭");
}
process.exit(0);
}
}

// 执行发送
const message = process.argv[2] || "你好,RabbitMQ!";
sendMessage(message);
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
// receiver.js
var amqplib = require("amqplib");

const QUEUE_NAME = "task1";

// 构造连接URL(处理密码特殊字符)
const url = `amqp://localhost`;

async function startConsumer() {
let connection;
try {
// 1. 建立连接
connection = await amqplib.connect(url, {
username: "node",
password: "123456",
port: "5672",
vhost: "/",
});
console.log("✅ 成功连接到 RabbitMQ 服务器");

// 2. 创建通道
const channel = await connection.createChannel();
console.log("🔄 通道已创建");

// 3. 声明队列(与发送端配置一致)
await channel.assertQueue(QUEUE_NAME, {
durable: true, // 必须与发送端队列声明一致
});
console.log(`📭 正在监听队列:${QUEUE_NAME}`);

// 4. 设置消费参数
channel.prefetch(1); // 每次只处理一个消息
console.log("⏳ 等待消息中... (按 CTRL+C 退出)");

// 5. 启动消费者
channel.consume(
QUEUE_NAME,
async (msg) => {
if (msg) {
try {
const content = msg.content.toString();
console.log(`📥 收到消息: ${content}`);

// 模拟业务处理(例如耗时操作)
await new Promise((resolve) => setTimeout(resolve, 1000));

// 手动确认消息(确保 noAck: false)
channel.ack(msg);
console.log("✔️ 消息已确认");
} catch (error) {
console.error("⚠️ 消息处理失败:", error);
channel.nack(msg); // 否定确认并重新入队
}
}
},
{
noAck: false, // 关闭自动确认
}
);

// 保持进程运行
await new Promise(() => {});
} catch (error) {
console.error("🔥 发生错误:", error);
process.exit(1);
} finally {
if (connection) {
await connection.close();
console.log("🚪 连接已关闭");
}
}
}

// 启动消费者
startConsumer();

日志记录

创建一个日志表方便管理

1
npx sequelize model:generate --name Log --attributes level:string,message:string,meta:string,timestamp:date

修改对应的迁移文件

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
"use strict";

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable("Logs", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED,
},
level: {
allowNull: false,
type: Sequelize.STRING(16),
},
message: {
allowNull: false,
type: Sequelize.STRING(2048),
},
meta: {
allowNull: false,
type: Sequelize.STRING(2048),
},
timestamp: {
allowNull: false,
type: Sequelize.DATE,
},
});
},

async down(queryInterface) {
await queryInterface.dropTable("Logs");
},
};

修改一下模型

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
const { Model } = require("sequelize");

module.exports = (sequelize, DataTypes) => {
class Log extends Model {
static associate(models) {}
}

Log.init(
{
level: DataTypes.STRING(16),
message: DataTypes.STRING(2048),
meta: {
type: DataTypes.STRING,
get() {
// 转换报错信息
try {
return JSON.parse(this.getDataValue("meta"));
} catch (error) {
return this.getDataValue("meta");
}
},
},
timestamp: DataTypes.DATE,
},
{
sequelize, // 使用传入的 Sequelize 实例
modelName: "Log",
timestamps: false, // 禁用自动生成 createdAt 和 updatedAt 字段
tableName: "Logs", // 显式指定表名(可选)
}
);

return Log;
};

创建日志工具文件

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
// config/logger.js
const { createLogger, format, transports } = require("winston");
const MySQLTransport = require("winston-mysql").MySQLTransport;
const path = require("path");

// 根据环境变量加载数据库配置
const env = process.env.NODE_ENV || "development";
const dbConfig = require(path.join(__dirname, "../config/config.json"))[env];

// MySQL 传输配置
const mysqlTransportOptions = {
host: dbConfig.host,
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
table: "logs", // 存储日志的表名
fields: {
// 字段映射配置
level: "level",
meta: "meta",
message: "message",
timestamp: "timestamp",
service: "service",
},
};

// 创建日志记录器
const logger = createLogger({
level: "info", // 默认日志级别
format: format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.errors({ stack: true }), // 捕获错误堆栈
format.json() // JSON 格式输出
),
defaultMeta: { service: "clwy-api" }, // 全局元数据
transports: [
// MySQL 日志传输(生产环境使用)
new MySQLTransport(mysqlTransportOptions),
],
});

// 开发环境添加控制台输出
if (env !== "production") {
logger.add(
new transports.Console({
format: format.combine(
format.colorize(), // 终端颜色输出
format.printf((info) => {
return `${info.timestamp} [${info.level}] ${info.message} ${
info.stack || ""
}`;
})
),
})
);
}

module.exports = logger;

React Lane 模型

二进制操作常用于权限控制,在表示多种状态叠加(一对多)的场景更加方便。

1
2
3
4
// 负数用补码来表示

5 = 0b00000101
-5 = 反码 + 1 = 0b11111010 + 1 = 0b11111011

对 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
25
26
27
28
29
30
// 初始化一些 flags
const NoFlags = 0b0000000000000000; // 0,表示没有任何标志
const PerformedWork = 0b0000000000000001; // 1,表示执行过某项工作
const Placement = 0b0000000000000010; // 2,表示需要放置某项内容
const Update = 0b0000000000000100; // 4,表示需要更新

// 一开始将 flag 变量初始化为 NoFlags,表示没有任何操作
let flag = NoFlags;

// 这里就是在组合多个状态
flag = flag | PerformedWork | Update; // 按位或运算,将 PerformedWork 和 Update 结合进来

// 要判断是否有某个 flag,直接通过 & 来进行判断即可
// 判断是否有 PerformedWork 类型的更新
if (flag & PerformedWork) {
// 执行对应操作
console.log("执行 PerformedWork");
}

// 判断是否有 Update 类型的更新
if (flag & Update) {
// 执行对应操作
console.log("执行 Update");
}

// 判断是否有 Placement 类型的更新
if (flag & Placement) {
// 执行对应操作
console.log("执行 Placement");
}

上下文

react 中有许多上下文变量,使用位运算来标记,可以方便的表示进入或移出上下文

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
// 未处理于 React 上下文
export const NoContext = /* 0b000 */;

// 处理于 batchedUpdates 上下文
const BatchedContext = /* 0b001 */;

// 处理于 render 阶段
export const RenderContext = /* 0b010 */;

// 处理于 commit 阶段
export const CommitContext = /* 0b100 */;

// 是否处于 RenderContext 上下文中,结果为 true
if ((executionContext & RenderContext) !== NoContext) {
// 在这里执行与 RenderContext 相关的逻辑
}

// 是否处于 CommitContext 上下文中,结果为 false
if ((executionContext & CommitContext) !== NoContext) {
// 在这里执行与 CommitContext 相关的逻辑
}

// 如果要移除某个上下文
executionContext &= ~RenderContext; // 从当前上下文中移除 RenderContext

// 是否处于 RenderContext 上下文中,结果为 false
if ((executionContext & RenderContext) !== NoContext) {
// 在这里执行与 RenderContext 相关的逻辑
}


Lane 模型

schedule 是一个单独的包定义了 5 种优先级。

Lane 是 react 内部的更细粒度的优先级管理,react 所有的更新都只能通过事件或异步任务触发, 所以 React 定义了自己的优先级:

  • discreteEventPriority: 离散事件 input focus,blur,touchStart 等
  • continuousEventPriority: 连续事件 drag mousemove, scroll 等;
  • DefaultEventPriority: 默认优先级 通过计时器产生的任务
  • idleEventPriority: 对应空闲情况的优先级

每个优先级对应的值就是 Lane, 因此需要与 schedule 优先级相互转换。

1
2
3
4
5
6
7
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010;

//...
  • react 优先级转为 scheduler, 先将 lanes 转为 EventPriority

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    export function lanesToEventPriority(lanes: Lanes): EventPriority {
    const lane = getHighestPriorityLane(lanes);

    if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
    }

    if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
    }

    if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
    }
    return IdleEventPriority;
    }

    再将 EventPriority 转为 schedule 优先级

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    let schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
    case DiscreteEventPriority:
    schedulerPriorityLevel = ImmediateSchedulerPriority;
    break;

    case ContinuousEventPriority:
    schedulerPriorityLevel = UserBlockingSchedulerPriority;
    break;

    case DefaultEventPriority:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;

    case IdleEventPriority:
    schedulerPriorityLevel = IdleSchedulerPriority;
    break;

    default:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;
    }
  • scheduler 优先级转为 react 优先级

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const schedulerPriority = getCurrentSchedulerPriorityLevel(); // 获取当前调度器优先级

    switch (schedulerPriority) {
    case ImmediateSchedulerPriority:
    return DiscreteEventPriority;
    case UserBlockingSchedulerPriority:
    return ContinuousEventPriority;
    case NormalSchedulerPriority:
    case LowSchedulerPriority:
    return DefaultEventPriority;
    case IdleSchedulerPriority:
    return IdleEventPriority;
    default:
    return DefaultEventPriority;
    }
expiration Time 模型

如果同一时间出发了多个更新,应该先去更新哪一个。

早期 react 使用 expiration Time 模型,这一点和 scheduler 的设计是一致的。不同的优先级对应不同的 deadline, 每次 schedule 执行的时候,选出一个最高的优先级执行。

但是这种模式无法表示批的概念,当优先级大于 priorityBunch 就会划分到同一批,但是无法将提交的不同更新种的某种类型的任务算作同一批。因此基于上面的原因引入了 lane 模型。

lane 是如何灵活表达批的概念?

1
2
3
4
5
6
7
8
9
10
11
12
// 要使用的批次
let batch = 0;

// laneA 和 laneB 是不相邻的优先级
const laneA = 0b00000000000000001000000; // 代表某个优先级
const laneB = 0b00000000000000000000001; // 代表另一个优先级

// 将 laneA 纳入批中
batch |= laneA; // 将 laneA 的优先级合并到 batch 中

// 将 laneB 纳入批中
batch |= laneB; // 将 laneB 的优先级合并到 batch 中

updateLane

用于调度更新的优先级 通过 requestUpdateLane 创建

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
export function requestUpdateLane(fiber: Fiber): Lane {
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
// 初次加载时为SyncLane
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
? (SyncLane: Lane)
: (SyncBatchedLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// 这是一个渲染阶段的更新,这些都没有得到官方的支持
// old behavior is to give this the same "thread" (expiration time) as
// 原来的方案是赋予它与当前渲染相同的过期时间
// whatever is currently rendering. So if you call `setState` on a component
// 如果你在一个组件上调用setState,他们会在相同的渲染中稍后生效,
// that happens later in the same render, it will flush. Ideally, we want to
// 会发生闪屏
// remove the special case and treat them as if they came from an
// 理想情况下,我们希望删除特例,并将它们视为来插入的事件
// interleaved event. Regardless, this pattern is not officially supported.
// 无论如何,这种模式并没有得到官方的支持。
// This behavior is only a fallback. The flag only exists until we can roll
// 这种行为值是一个回退机制,标识只存存在于我们可以离开setState警告之前
// out the setState warning, since existing code might accidentally rely on
// 因为现有代码可能意外地依赖于当前行为。
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}

// The algorithm for assigning an update to a lane should be stable for all
// updates at the same priority within the same event. To do this, the inputs
// 对于同一事件中具有相同优先级的所有更新,为车道分配更新的算法应该是稳定的(幂等的)。
// to the algorithm must be the same. For example, we use the `renderLanes`
// 为此,算法的输入必须相同。
// to avoid choosing a lane that is already in the middle of rendering.
// 我们使用“renderLanes”来避免选择已经在渲染过程中的车道。
// However, the "included" lanes could be mutated in between updates in the
// 然而 included 车道可能在两次相同事件中的更新被改变
// same event, like if you perform an update inside `flushSync`. Or any other
// 就像在“flushSync”中执行更新一样
// code path that might call `prepareFreshStack`.
// 或者任何其他可能调用“prepareFreshStack”的代码。
// The trick we use is to cache the first of each of these inputs within an
// 我们使用的技巧是在事件中缓存这些输入中的第一个
// event. Then reset the cached values once we can be sure the event is over.
// 然后在确定事件结束后重置缓存的值。
// Our heuristic for that is whenever we enter a concurrent work loop.
// 启发式方法是,每当我们进入一个并发工作循环时
// We'll do the same for `currentEventPendingLanes` below.
if (currentEventWipLanes === NoLanes) {
currentEventWipLanes = workInProgressRootIncludedLanes;
}

const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
if (currentEventPendingLanes !== NoLanes) {
currentEventPendingLanes =
mostRecentlyUpdatedRoot !== null
? mostRecentlyUpdatedRoot.pendingLanes
: NoLanes;
}
return findTransitionLane(currentEventWipLanes, currentEventPendingLanes);
}

// TODO: Remove this dependency on the Scheduler priority.
// To do that, we're replacing it with an update lane priority.
const schedulerPriority = getCurrentPriorityLevel();

// The old behavior was using the priority level of the Scheduler.
// This couples React to the Scheduler internals, so we're replacing it
// with the currentUpdateLanePriority above. As an example of how this
// could be problematic, if we're not inside `Scheduler.runWithPriority`,
// then we'll get the priority of the current running Scheduler task,
// which is probably not what we want.
let lane;
if (
// TODO: Temporary. We're removing the concept of discrete updates.
(executionContext & DiscreteEventContext) !== NoContext &&
schedulerPriority === UserBlockingSchedulerPriority
) {
lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
} else {
const schedulerLanePriority =
schedulerPriorityToLanePriority(schedulerPriority);

if (decoupleUpdatePriorityFromScheduler) {
// In the new strategy, we will track the current update lane priority
// inside React and use that priority to select a lane for this update.
// For now, we're just logging when they're different so we can assess.
const currentUpdateLanePriority = getCurrentUpdateLanePriority();

if (
schedulerLanePriority !== currentUpdateLanePriority &&
currentUpdateLanePriority !== NoLanePriority
) {
if (__DEV__) {
console.error(
"Expected current scheduler lane priority %s to match current update lane priority %s",
schedulerLanePriority,
currentUpdateLanePriority
);
}
}
}

lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
}

return lane;
}

eventTime

eventTime

用于的调度更新的时间戳 通过 requestEventTime 创建

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
// 用二进制来表示所处的上下文状态
export const NoContext = /* */ 0b0000000;
const BatchedContext = /* */ 0b0000001;
const EventContext = /* */ 0b0000010;
const DiscreteEventContext = /* */ 0b0000100;
const LegacyUnbatchedContext = /* */ 0b0001000;
const RenderContext = /* */ 0b0010000;
const CommitContext = /* */ 0b0100000;
export const RetryAfterError = /* */ 0b1000000;

var currentEventTime = NoTimestamp = -1;;
// executionContext = NoContext
export function requestEventTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return now();
}
// 不在react中,可能在浏览器的事件中
if (currentEventTime !== NoTimestamp) {
// 对所有更新使用相同的开始时间,直到再次进入react中
return currentEventTime;
}
// 这是React运行后后的第一次更新。计算新的开始时间。
currentEventTime = now();
return currentEventTime;
}

获取时间戳的时候,如果Date对象初始化的时间过长,在使用的时候还需要把初始化的时间减去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let getCurrentTime;
const hasPerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';

if (hasPerformanceNow) {
const localPerformance = performance;
getCurrentTime = () => localPerformance.now();
} else {
const localDate = Date;
const initialTime = localDate.now();
getCurrentTime = () => localDate.now() - initialTime;
}

var Scheduler_now = Scheduler.unstable_now = getCurrentTime;
var initialTimeMs$1 = Scheduler_now$1();
// 如果初始化的事件戳非常小,直接使用 initialTimeMs,对于现代浏览器直接使用performance.now
// 在老的浏览器中回退使用Date.now, 返回Unix事件戳,这时需要减去模块初始化的时间来模拟 performance.now
// 并把时间控制在32位以内

var now = initialTimeMs$1 < 10000 ? Scheduler_now$1 : function () {
return Scheduler_now$1() - initialTimeMs$1;
};

createUpdate

Update

用于记录组件状态的改变,保存到UpdateQueue中,多个Update可以同时存在

/packages/react-reconciler/src/ReactUpdateQueue.new.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function createUpdate(eventTime: number, lane: Lane): Update<*> {
const update: Update<*> = {
eventTime,
lane,
// export const UpdateState = 0; 更新
// export const ReplaceState = 1; 替换更新
// export const ForceUpdate = 2; 强制更新
// export const CaptureUpdate = 3; 捕获哦错误后更新
tag: UpdateState,
payload: null, // 更新内容,比如`setState`接收的第一个参数
callback: null, // 对应的回调,`setState`,`render`都有
next: null, // 指向下一个更新
};
return update;
}

enqueueUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// fiber 节点中的updateQueue,默认是null
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// 只在未挂载时执行
return;
}

const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
// pending update链表,最新的更新在链表的顶端
// pending = 3->2->1->3....
const pending = sharedQueue.pending;
if (pending === null) {
// 只有一个update时候,循环引用
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信