每日一题 —— 混杂整数序列按规则进行重新排序

背景:

假设我们取一个数字 x 并执行以下任一操作:

  • a:将 x 除以 3 (如果可以被 3 除)
  • b:将 x 乘以 2

每次操作后,记下结果。如果从 9 开始,可以得到一个序列

有一个混杂的整数序列,现在任务是对它们重新排序,以使其符合上述序列并输出结果

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
输入: "[4,8,6,3,12,9]"
输出: [9,3,6,12,4,8]

解释: [9,3,6,12,4,8] => 9/3=3 -> 3*2=6 -> 6*2=12 -> 12/3=4 -> 4*2=8

输入: "[3000,9000]"
输出: [9000,3000]

输入: "[4,2]"
输出: [2,4]

输入: "[4,6,2]"
输出: [6,2,4]

人话翻译: 对数组重新排序,使得数组每一项可以满足这样一个规则:arr[i] = arr[i + 1] * 3 或者 arr[i] = arr[i + 1] / 2

解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function changeArr(arr) {
const map = new Map();
let find = false;
let ret;
arr.forEach((n) => {
map.set(n, (map.get(n) || 0) + 1); // 定义数组中一个数剩余的次数
});
arr.forEach((n) => {
if (find) return;
dfs(n, 2, [n]);
});

function dfs(prev, index, res) {
if (find) return;
if (index === arr.length + 1) {
// 找完了,退出搜索
find = true;
ret = res;
}
if (map.has(prev * 2) && map.get(prev * 2) > 0) {
// 数组中有上个值 *2 的数据存在
map.set(prev * 2, map.get(prev * 2) - 1);
dfs(prev * 2, index + 1, [...res, prev * 2]); // 将这个值加到结果中,并
map.set(prev * 2, map.get(prev * 2) + 1); // 没有找到,把次数加回来
}
if (!(prev % 3) && map.get(prev / 3) > 0) {
// 当前值能被3整数并且被3整数的值存在
map.set(prev / 3, map.get(prev / 3) - 1);
dfs(prev / 3, index + 1, [...res, prev / 3]);
map.set(prev / 3, map.get(prev / 3) + 1); // 没有找到,把次数加回来
}
}
return ret;
}

来自: 2年前端,如何跟抖音面试官battle

Webpack是个什么鬼——了解编译结果

简述

webpack 是一款现代化的前端打包工具,那么webpack是怎么将模块化代码能够在浏览器运行的?让我们来看一下

MVP

从一个最小webpack实例开始:

src/index.js

1
console.log("Hello Webpack");

我们直接使用命令行进行打包, 结果如下:

webpack –mode development

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
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({

/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ (() => {

eval("console.log('Hello Webpack');\n\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");

/***/ })

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

webpack –mode development –devtool hidden-source-map

1
2
3
4
5
6
7
8
9
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
console.log('Hello Webpack');

/******/ })()
;

webpack –mode production

1
console.log("Hello Webpack");

可以看到, 对于简单代码来说, 是否使用webpack打包区别不大。稍微注意一下,在默认的development环境中引入了两个变量__webpack_exports____webpack_modules__。顾名思义,是分别管理导出内容与模块列表的两个代码

__webpack_modules__ 是一个key为代码(模块)路径,值为模块执行结果的一个对象。

我们来试试稍微复杂一点的例子:

使用import

src/index.js

1
2
3
import {add} from './utils'

console.log(add(1, 2));

src/utils.js

1
2
3
export function add(a, b) {
return a + b;
}

我们直接使用命令行进行打包, 结果如下:

webpack –mode development

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
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({

/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ \"./src/utils.js\");\n\n\nconsole.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2));\n\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?");

/***/ }),

/***/ "./src/utils.js":
/*!**********************!*\
!*** ./src/utils.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"add\": () => (/* binding */ add)\n/* harmony export */ });\nfunction add(a, b) {\n return a + b;\n}\n\n\n//# sourceURL=webpack://webpack-demo/./src/utils.js?");

/***/ })

/******/ });
/************************************************************************/
/******/ // 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;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __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");
/******/
/******/ })()
;

webpack –mode development –devtool hidden-source-map

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
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({

/***/ "./src/utils.js":
/*!**********************!*\
!*** ./src/utils.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "add": () => (/* binding */ add)
/* harmony export */ });
function add(a, b) {
return a + b;
}


/***/ })

/******/ });
/************************************************************************/
/******/ // 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;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./src/utils.js");


console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2));

})();

/******/ })()
;

(可以看到webpack --mode development --devtool hidden-source-map这个命令执行的结果和直接development是一样的,但是代码可读性更加高。之后的文章将以这个命令的输出为准)

webpack –mode production

1
(()=>{"use strict";console.log(3)})();

可以看到,webpack一旦发现了模块系统,那么就会增加很多中间代码(从注释 The module cache 到 变量 __webpack_exports__)

首先webpack每块代码都是以(() => {})() 这种形式的闭包来处理的,防止污染外部空间。

然后每一段都有一段注释来告知下面这块代码的逻辑是要做什么

我们来一一看一下:

module cache and require function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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;
}

定义了一个__webpack_module_cache__用于缓存模块

定义了一个__webpack_require__方法, 接受一个moduleId, 从下面可以看到moduleId是这个模块的路径(包括拓展名, 也即是__webpack_modules__管理的key值)

先判断缓存中是否存在这个模块,即是否加载,如果加载直接返回导出的数据,如果没有则在缓存中创建一个空对象{exports: {}}, 然后把module, module.exports, __webpack_require__作为参数去执行__webpack_modules__对应的方法

__webpack_modules__的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
var __webpack_modules__ = ({
"./src/utils.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"add": () => (add)
});
function add(a, b) {
return a + b;
}
})
});

可以看到,这里调用了一个__webpack_require__.r和一个__webpack_require__.d方法。目前我们不知道这两个方法是做什么用的。继续看下去。

webpack/runtime/define property getters

1
2
3
4
5
6
7
8
9
10
11
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();

ddefine的缩写。可以看到这个方法的作用就是定义导出的值。

其目的就是遍历definition对象将其一一填入exports。需要注意的是使用__webpack_require__.d的目的在于确保:

  • 只能有一个key存在,如果exports中已经存在过了这个导出值,则不会重复导入
  • 确保exports中的属性只有getter, 不能被外部设置

make namespace object

1
2
3
4
5
6
7
8
9
10
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();

这个方法完成了两个目的。

  • exports定义了Symbol.toStringTag的值为Module
  • exports定义了__esModule的值为true

目的在于完成导出模块的兼容性

我们试试换一种导出方式:
src/utils.js

1
2
3
4
exports.add = function(a, b) {
return a + b;
}

结果:

1
2
3
4
5
6
7
8
var __webpack_modules__ = ({
"./src/utils.js":
((__unused_webpack_module, exports) => {
exports.add = function(a, b) {
return a + b;
}
})
});

可以看到输出简洁了很多。但是结果是一样的。都是在exports中插入导出的方法, 只不过esmodule的方式更加谨慎一点

那么前面的__unused_webpack_module又是干嘛的呢?我们修改一下代码
src/utils.js

1
2
3
module.exports = function add(a, b) {
return a + b;
}

结果:

1
2
3
4
5
6
7
8
var __webpack_modules__ = ({
"./src/utils.js":
((module) => {
module.exports = function add(a, b) {
return a + b;
}
})
});

一个主要细节在于esmodule使用了__webpack_require__.d来确保其代码的只读性,而commonjs没有:

esmodule和commonjs的模块导出可访问性区别

CommonJS 模块输出的是一个值的拷贝, ES6 模块输出的是值的引用

举个例子

commonjs

1
2
3
4
5
6
var a = 1;
setTimeout(() => {
a = 2;
}, 0)

exports.a = a;

生成代码:

1
2
3
4
5
6
7
8
((module) => {
var a = 1;
setTimeout(() => {
a = 2;
}, 0)

module.exports.a = a;
})

esmodule:

1
2
3
4
5
6
var a = 1;
setTimeout(() => {
a = 2;
}, 0)
export { a }

输出代码:

1
2
3
4
5
6
7
8
9
10
11
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "a": () => (/* binding */ a)
/* harmony export */ });
var a = 1;
setTimeout(() => {
a = 2;
}, 0)
})

可以看到区别:

  • commonjs 输出的a: 1 -> 1
  • esmodule 输出的a: 1 -> 2

因为commonjs内部实现是赋值,程序导出以后原来的a和导出的a的关系就没有了

esmodule输出的一个对象,内部的getter会每次去拿最新的a的值


那么到此我们的中间代码就看完了,顺便还介绍了一下webpack的导出结果。完整的中间代码列表可以看这个文件

执行代码

在上面的示例中,我们得到以下代码:

1
2
3
4
5
6
7
var __webpack_exports__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils.js");

console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2));
})();

该代码作为项目的入口代码, 完成了以下逻辑

  • 通过 __webpack_require__.r 标记这个文件导出类型为esmodule
  • 执行 __webpack_require__ 并将导入的结果存放到临时变量 _utils__WEBPACK_IMPORTED_MODULE_0__
  • 执行 (0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2) 并导出结果。这里的(0, ...)是为了重置方法的this指向
    • Comma operator
    • 这个方法等价于
      1
      2
      const add = _utils__WEBPACK_IMPORTED_MODULE_0__.add
      add(1, 2)

让我们来微调一下代码:

1
2
3
const add = require('./utils')

console.log(add(1, 2));

输出:

1
2
3
4
5
var __webpack_exports__ = {};
(() => {
const add = __webpack_require__("./src/utils.js")
console.log(add(1, 2));
})();

可以看到, 其主要的区别就是__webpack_require__.r, 其他的区别不是很大。

动态代码

修改部分代码:

src/index.js

1
2
3
import('./utils').then(({add}) => {
console.log(add(1,2))
})

生成代码:

webpack –mode development –devtool hidden-source-map

dist/main.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
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({});
/************************************************************************/
/******/ // 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;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/ensure chunk */
/******/ (() => {
/******/ __webpack_require__.f = {};
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = (chunkId) => {
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ __webpack_require__.f[key](chunkId, promises);
/******/ return promises;
/******/ }, []));
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/get javascript chunk filename */
/******/ (() => {
/******/ // This function allow to reference async chunks
/******/ __webpack_require__.u = (chunkId) => {
/******/ // return url for filenames based on template
/******/ return "" + chunkId + ".js";
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/global */
/******/ (() => {
/******/ __webpack_require__.g = (function() {
/******/ if (typeof globalThis === 'object') return globalThis;
/******/ try {
/******/ return this || new Function('return this')();
/******/ } catch (e) {
/******/ if (typeof window === 'object') return window;
/******/ }
/******/ })();
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/load script */
/******/ (() => {
/******/ var inProgress = {};
/******/ var dataWebpackPrefix = "webpack-demo:";
/******/ // loadScript function to load a script via script tag
/******/ __webpack_require__.l = (url, done, key, chunkId) => {
/******/ if(inProgress[url]) { inProgress[url].push(done); return; }
/******/ var script, needAttach;
/******/ if(key !== undefined) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ for(var i = 0; i < scripts.length; i++) {
/******/ var s = scripts[i];
/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
/******/ }
/******/ }
/******/ if(!script) {
/******/ needAttach = true;
/******/ script = document.createElement('script');
/******/
/******/ script.charset = 'utf-8';
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key);
/******/ script.src = url;
/******/ }
/******/ inProgress[url] = [done];
/******/ var onScriptComplete = (prev, event) => {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var doneFns = inProgress[url];
/******/ delete inProgress[url];
/******/ script.parentNode && script.parentNode.removeChild(script);
/******/ doneFns && doneFns.forEach((fn) => (fn(event)));
/******/ if(prev) return prev(event);
/******/ }
/******/ ;
/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/ script.onerror = onScriptComplete.bind(null, script.onerror);
/******/ script.onload = onScriptComplete.bind(null, script.onload);
/******/ needAttach && document.head.appendChild(script);
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/publicPath */
/******/ (() => {
/******/ var scriptUrl;
/******/ if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
/******/ var document = __webpack_require__.g.document;
/******/ if (!scriptUrl && document) {
/******/ if (document.currentScript)
/******/ scriptUrl = document.currentScript.src
/******/ if (!scriptUrl) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ if(scripts.length) scriptUrl = scripts[scripts.length - 1].src
/******/ }
/******/ }
/******/ // When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
/******/ // or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
/******/ if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
/******/ scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
/******/ __webpack_require__.p = scriptUrl;
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
/******/
/******/ __webpack_require__.f.j = (chunkId, promises) => {
/******/ // JSONP chunk loading for javascript
/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ if(true) { // all chunks have JS
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
/******/ promises.push(installedChunkData[2] = promise);
/******/
/******/ // start chunk loading
/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId);
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ var loadingEnded = (event) => {
/******/ if(__webpack_require__.o(installedChunks, chunkId)) {
/******/ installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
/******/ if(installedChunkData) {
/******/ 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;
/******/ installedChunkData[1](error);
/******/ }
/******/ }
/******/ };
/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
/******/ } else installedChunks[chunkId] = 0;
/******/ }
/******/ }
/******/ };
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ // no on chunks loaded
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkIds[i]] = 0;
/******/ }
/******/
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
__webpack_require__.e(/*! import() */ "src_utils_js").then(__webpack_require__.bind(__webpack_require__, /*! ./utils */ "./src/utils.js")).then(({add}) => {
console.log(add(1,2))
})

/******/ })()
;

dist/src/utils_js.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([["src_utils_js"],{

/***/ "./src/utils.js":
/*!**********************!*\
!*** ./src/utils.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "add": () => (/* binding */ add)
/* harmony export */ });
function add(a, b) {
return a + b;
}


/***/ })

}]);

相同的代码我们跳过,我们首先来看一下入口文件的执行代码:

1
2
3
4
5
6
var __webpack_exports__ = {};
__webpack_require__.e("src_utils_js")
.then(__webpack_require__.bind(__webpack_require__, "./src/utils.js"))
.then(({add}) => {
console.log(add(1,2))
})

这个代码主要分成三部分:

  • 第一部分执行__webpack_require__.e
  • 第二部分生成一个__webpack_require__方法并绑定参数
  • 第三部分去执行实际逻辑。

我们来看下主要核心的中间代码__webpack_require__.e:

1
2
3
4
5
6
7
8
9
10
11
12
/* webpack/runtime/ensure chunk */
(() => {
__webpack_require__.f = {};
__webpack_require__.e = (chunkId) => {
return Promise.all(
Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, [])
);
};
})();

简单了解一下reduce

这段代码很奇怪,看上去来说实际可以视为作为一个forEach在使用。目的是试图去执行__webpack_require__.f这个对象中的所有方法,最后返回一个总的Promise

至于执行的方法,目前只有一个__webpack_require__.f.j,里面是一堆代码总之暂且放置不看,我们可以将其视为加载js文件即可(通过生成script的方式)。

我们可以将其视为加载好dist/src/utils_js.js并将该文件里声明的对象的map添加到__webpack_modules__即可。

此时使用__webpack_require__去走之前的逻辑就可以正常调用模块了。

这样就实现了代码分割。

一些动态加载的小细节

1
2
3
4
5
// main
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];

// src_utils_js.js
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([["src_utils_js"],{...})

通过这种命名空间方式解决了单页面多项目可能错误添加动态加载代码的问题。

1
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

重写数组的push方法,在push时做一些额外的操作
即执行chunkLoadingGlobal.push(arg1, arg2)时。执行webpackJsonpCallback(chunkLoadingGlobal.push, arg1, arg2)这种方式。老实说我没有想到这种写法的好处,但也算一种小技巧

总结

统一module方式

webpack 将两种形式导出方式进行了一定程度上的统一,即不论写法如何,都通过__webpack_require__对模块进行引入,而对于导出的模块来说,都统一成module的样式。

区别在于esmoduledefault导出和commonjs的module.exports导出略有区别

esmoduledefault导出在生成的代码中地位和一般的export导出是一样的

TRPG Engine 的项目工程化实践

First of all

一个人维护一个项目很久了, 差不多是时候总结一些经验做一些些输出了。因此来汇总一下这些年我对我的开源项目TRPG Engine做的工程化实践。

首先,我们要明确一点,即为什么要做工程化:

  • 提升开发效率
  • 降低开发成本
  • 提升产品质量
  • 降低企业成本

所有的代码, 所有的技术都依托于业务, 所有的手段都是为了最终目的而服务的。因此我们工程化最终目的就是提高产出。

Git workflow

参考文章:

Commitlint

使用 Commitlint 来保证项目成员或者外部贡献者的提交确保同样的格式。

TRPG Engine是使用 commitlint 来实现的提交内容校验

一般常用的一种提交方式是 angular 格式。

例:

1
2
3
fix: some message

fix(scope): some message

参考文档: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format

提交的类型说明:

1
2
3
4
5
6
7
8
feat:新功能(feature
fix:修补bug
docs:文档(documentation)
style: 格式(不影响代码运行的变动)
refactor:重构(即不是新增功能,也不是修改bug的代码变动)
perf:性能优化
test:增加测试
chore:构建过程或辅助工具的变动

https://github.com/angular/angular/blob/master/CONTRIBUTING.md#type

通过标准的 commit message 可以生成Change log

基于git commit message来自动收集并生成CHANGELOG

在升版本时可以通过standard-version来实现package.json version和changelog一起生成

Prettier and Eslint

确保项目有一个统一的格式是非常重要, 可以有效的防止贡献者因为不统一的格式带来的提交变更过大。

试想一下,一个人用的是4空格缩进,另一个人用的是2空格缩进。那么会来带的协作上的问题并导致code review无法正常进行

目前前端流行使用prettier + eslint的组合来确保项目的格式统一。

prettier是目前前端流行的Formatter工具, 用于保证项目能具有统一的格式,各大编辑器平台都具有插件支持,同时也支持许多语言

eslint 是一款确保你的代码质量的工具,特别是融合了tslint后更加成为了前端代码质量保证的唯一选择。其功能与prettier有部分交集,因此在同时使用两者时需要使用eslintprettier预设来确保不会出现冲突

另外推荐一个跨语言的格式工具, EditorConfig, 虽然功能没有prettier这么强大,但是他的优势是跨语言,更加具有通用性。

使用 lint-staged 来在工程化运行时中确保格式正确

Testing and benchmark

单元测试与基准测试是程序员要面对的两大关键测试方式。

单元测试应当用于几乎所有的代码, 来确保代码不出错(主要是可以防止其他协作者无意间改坏你的代码)

而基准测试用于对性能要求较高的场合,比如后端的高并发场景,以及前端的高CPU计算(By the way, 对于前端的高CPU场景我建议是使用web worker来处理,这样可以使用多线程而不会影响用户正常操作)

如何写单元测试

总所周知,一个纯函数是最好测的。那么单元测试的存在就相当于监督我们时刻注意将一些可以抽象出来的纯函数抽象出来,来方便写单元测试。能有效提高代码质量。

而对于一些副作用比较大的场景,我们需要想办法构建其上下文。比如TRPG Engine的后端代码,单元测试就是真实起了一个空白的数据库, redis, 和后端实例,通过数据库操作以及及时清理测试过的数据库代码来保证环境干净

对于比较难以测试的前端组件, TRPG Engine的做法是打快照,通过快照的变更告知开发者是否有一个 预期/非预期 的变更出现

单元测试的存在也应当集成到CI中,以确保每次执行都可用。

Bundler

在现代前端编程中, 打包编译是前端不得不重视的一环。

从less scss等css拓展语言, 到ts, coffee的js拓展。

从babel的es6,7,8,9支持, 到各种动态加载, 各种优化压缩。

面对日益复杂的现状,前端已经离不开打包工具的存在。

一般来说,我们常用的打包工具是webpackwebpack大而全,并提供足够的自定义化能力。是目前来说前端业务开发打包的不二之选。但成也萧何败萧何,webpack虽然十分强大, 但是配置非常非常复杂,甚至有webpack工程师这一说法,因此在一些特殊场景下, 我也十分推荐一些其他的打包工具。

CI/CD

Continuous Integration and Continuous Delivery
持续集成与持续交付

市面上有很多免费的CI系统, 比如 travis, appveyor, circleci, github action等等, 再比如gitlab自带的ci系统。

总的来说都大同小异, 我们使用CI系统无非是关注单元测试有没有跑通,如何可以的话顺便输出一份coverage覆盖率报告。如果再可以的话可以把代码编译了以后输出编译报告。来方便衡量每一次提交的代码质量。

一般来说CI/CD都放在一起来讲,因为只是最终的输出不一样罢了。

CD可以做一些每次提交都编译文件, 或者往特殊分支提交就部署页面的功能。(比如每次向docs提交代码都编译文档并部署到远程服务器上)

Analytics and Monitor

一些现成的分析服务:

  • Google Analytics
  • Datadog
  • Posthog
  • Sentry Tracking
  • Grafana
  • uptimerobot

这些工具是帮助你的项目在上线后能分析或监控的方式。通过这些工具可以收集用户的行为,检测服务可用性等。

监控可以帮助你的服务稳定可靠,发生宕机的情况能够第一时间发现,减少用户损失。没有监控的服务就是没有地图和罗盘的轮船 —— 什么时候沉默?天知道!

而用户行为收集是下一步迭代的重要依据,如果是用户比较少用的功能则可以考虑减慢开发进度。

对于监控,我推荐posthog,这是一款新兴的分析系统。推荐的理由很简单,因为他是开源的,我们可以自己部署,然后把他的数据进行二次加工与处理。

Performance

性能是提升用户体验的重要一环,即常规感知中的“卡不卡”。

我们有很多方式去提升性能,比如采集用户的首屏渲染时间,比如手动打开devtool去对具体某个操作进行堆栈分析,再比如用Lighthouse跑个分 —— google的工具都非常棒。

参考文档:

Logging

日志应当是我们分析问题最关键的一步,重视日志是一个有一定规模的项目最基本的一步。

大部分项目都会记录本地日志,但本地日志过于原始,很难产生一定价值。目前业内流行的方案是为日志单独准备一个elasticsearch服务, 所有日志中心化到一个数据库,再通过配套的 kibana 进行数据检索。

另外使用外部日志中心的好处在于项目的微服务化与集群化。因为项目离开单实例部署后日志分散在各地,更加难以查询问题。

对于 TRPG Engine 来说,目前使用的第三方日志服务是 Loggly, 因为ELK部署较耗资源,而其他大多数的日志服务都是收费的。Loggly具有一定的免费额度, 但是对中文编码不是很友好。

相关资源:

  • local file
  • Loggly
  • ELK
  • 阿里云日志腾讯云日志等服务商…

Error Report

除了日志, 我们可能需要一个单独的错误汇报中心。因为日志是一种被动式的、托底的问题查找方式。一个主动的错误汇报会让我们更早介入问题以防止更大的错误扩散。

TRPG Engine使用了Sentry作为错误汇报中心。开源,云服务具有一定免费额度,错误汇报可以带上堆栈信息和相关上下文,并且新的错误会发送邮件到相关人员。

开源对于企业的意义在于能够自己部署,企业也可以部署自己的sentry,就像是gitlab一样

  • Sentry

Develop for Distributed

有一点比较重要的就是在开始一个项目的时候就要考虑到之后的场景。在开发时就需要考虑分布式部署的场景。至少对于可能有分布式的场景进行一层抽象,就算现在不做,以后也要做。这点TRPG Engine走过很多弯路。

  • 比如日志,需要考虑使用外部日志的情况
  • 比如文件管理,需要考虑使用云文件
  • 比如配置,需要考虑使用外部的配置中心
  • 比如缓存,少用内存缓存而用外部缓存
  • 比如数据库 —— 当然这个大多数情况不用操心,除非用的是sqlite

因为现代的程序架构早就不是以前一台服务器打天下的时候了。有效组合各个服务可以帮助程序快速增长。

Coding with config

基于配置的代码会使你的程序更加灵活。特别是线上情况往往不适合发布,或者长期故障。通过配置我们可以将一部分代码关闭以保证整体代码可用性。

很多公司的功能开发分成两种管理方案,一种是做功能时切出一个功能分支,等到开发完毕后再合并到主分支。

还有一种方案是持续合并到主干分支,但是由配置来将新功能关闭。

说不清那种方案好,但是基于配置进行开发给与工程化代码更加灵活。

Read with Document and Comment

文档也是工程化代码的实践

一个静态文档网站可以帮助使用者快速理解整个项目

一行注释可以帮助代码阅读者理解你的用意,更重要的是可以防止其他的协作者在不了解你的用意的情况下改坏代码。

好的开源项目一定有足够文档,而一个好的企业项目代码中一定有很多注释。

对于企业业务项目来说,文档可能没有办法强制要求,但是需要明确一点的是注释是为自己写的,试想一下,一个复杂一点的方法,等一个月后,还能保证自己能理解当时自己的用意么?

Flexible Architecture

可变、灵活架构。

一个项目想要换底层架构是非常困难且痛苦的,想要解决这个问题,只有架构预先进行足够的设计,提前预想未来5年10年的业务变更。

比如插件化的架构就能保证业务代码的可拓展性。

MiniStar: 一个用于实现微内核(插件化)架构的前端框架

Dockerize

docker是现在开发的趋势,统一且单一的环境。

做过以前代码部署的工程师一定了解在不同环境下部署程序的痛苦,就算打包成一个war包也可能会有各种环境导致的奇怪问题。而docker就是解决这个问题的工具。

在实际中有很多使用场景:

  • 统一开发环境(统一开发环境)
  • 快速部署(无需搭建环境)
  • 集群部署(k8s, docker swarm)

记一个多平台End of Line的坑

问题

在执行自己编译的cli时出现:

1
env: node\r: No such file or directory

原因

在windows电脑发布代码, 其单行结束符为\r\n。然后mac执行时仅将\n视为换行符。因此程序试图去找node\r这个程序。当然是找不到的了。

如何处理该问题?

1
npx crlf --set=LF ./**/*.js

可以在发布脚本执行前执行一下以确保End of Line的正确。

相关库

参考资料

requirejs踩坑记录

灵异事件之薛定谔的白屏

场景复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<script>
setTimeout(function () {
var sha1Script = document.createElement('script');
sha1Script.src = 'https://xxx/sha1.min.js';
sha1Script.addEventListener('load', () => {
// any logic
})
document.body.appendChild(sha1Script);
})
</script>
</head>

<body>
<script type="text/javascript" src="require.min.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>

main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
require.config({
// ... here is config
});

requirejs.onError = function (err) {
console.error(err);
};

require([
// ...
], function () {
// main logic
});

控制台会出现报错Error: Mismatched anonymous define() module: function(){return y}, 但是有时候白屏有时候正常。

分析原因

可知白屏是因为标记为main logic的代码没有正常工作导致的。

分析下来可得如下结论

当出现错误时代码执行顺序为:

1
2
3
-> require.min.js
-> sha1.min.js
-> main.js

当出现正确时代码执行顺序为:

1
2
3
-> require.min.js
-> main.js
-> sha1.min.js

易得是代码执行时序的问题。那么问题来了, 为什么会出现这种情况?

源码分析

我们可以看一下requirejs的源码: https://github.com/requirejs/requirejs/blob/HEAD/require.js

当我们执行sha1.min.js时, sha1.min.js检测到当前有amd环境,调用define将自己注入到requirejs的运行时中, 因为没有调用require相关方法, 因此requirejs将其定义推入自身的globalDefQueue中。(相关代码: L2061)。

当我们执行main.js时, 调用require.config时, requirejs会尝试消费所有的globalDefQueue, 此时在queue中的参数为[null, [], function(){…}], 因为第一个参数(name)为null, 则会抛出异常Mismatched anonymous define() module...(相关代码: L1244)

requirejs 会通过调用自身的onError方法抛出异常, 如果没有手动覆盖onError的话会调用内置的defaultOnError方法(相关代码: L1870), 而defaultOnError的实现很简单:

1
2
3
function defaultOnError(err) {
throw err;
}

直接向顶层抛出异常, 导致整个script的运行时中断,后续的代码当然无法执行。

解决方案

可以看见原来的实现是有onError的覆写的, 只不过因为在require.config之后执行导致没有执行。

最佳的解决方案是尽可能早的覆写requirejs.onError方法。

吐槽

如果看源码的话。可以看到requirejs写明了允许匿名模块

Allow for anonymous modules

相关代码

但是实际使用中却会报错。而且默认的报错是直接向顶层抛出。

这种情况就是A做错了,但是却导致B无法正常执行。这种场景非常难debug

关于人生, 关于生老病死

今天凌晨4点多, 噩耗传来。小时候一直疼爱我的外公结束了自己的一生。自然死亡, 寿终正寝。

心里有说不出的难受,又一位长辈永远离开了我。昨天见了外公的最后一眼,面对躺在床上无法与人交流,生活不能自理的老人,纵有千言万语我却说不出一句话,而现在,我已经永远失去了这个机会。而逐渐的,令人恐惧而不得不面对的是,迟早会轮到我的父母。而我无疑是爱他们的。我不害怕死亡,但我害怕离别。

有时候我就会在想,宇宙广阔而浩瀚,人如浮游生物一般,终生被重力束缚在大地上。而短短这一生百年,大多数人也无法对人类史留下什么令人称道的痕迹。令人舒适的事在于所有的一切都依据之前的惯性前进,昨天、今天、明天,每天都按照一样的痕迹走下去,今天的自己比昨天的自己更加优秀一点,而明天的自己再比今天的自己更加优秀一点,太阳照常升起,照常落下。而在这惯性之中,难免生活会时不时来打破这种惯性:生老病死,自然天灾,人心莫测。有时候我会想,也许人生就是一场游戏,为了不让这个游戏太过无聊,设计者会在这场游戏中增加一些随机的事件,也许是好事、也许是坏事。但往往总是违背人们本身的意愿去发生。

有的时候,人生平静如水,让人觉得厌烦与无聊。而有的时候,意外却接踵而至,这时候,人又开始向往原来平静的生活了。人本身就是这样矛盾对立统一的综合体。

而拓展到整个宇宙的视角中,一个人内心不论是怎么样的波涛汹涌,在宇宙的尺度下都会显得无比渺小。对面这浩瀚空间,也许很多人用一生为之奋斗的东西,其实毫无意义。但也不能说是毫无意义,每个独立的个体都没有义务向他人负责,自我实现就是最大的负责。

然而, 生活还要继续, 人生还要前行。做好自己,行出不后悔,即可。

一些胡言乱语罢了。

HTTPS客户端证书校验

简介

互联网正在逐步走向越来越安全的趋势, Chrome 90 将默认使用https。而但凡对这方面有一定了解的都会知道https: 证书, 校验, 签发机构…等等等等。

而这些我们所熟知的东西, 即一般我们所说的通过ssl层进行传输与校验的, 一般指的是服务端证书。而我们今天要说说客户端安全证书。

Client Authenticate

客户端安全证书一般不常见, 只出现在对安全有一定需求的内部系统中。他的作用是规定哪些人可以访问: 客户端根据服务端配置的证书签发来下的子证书来对服务端的资源进行访问, 而服务端会对其进行校验 —— 校验不通过则不允许访问。

服务端证书恰恰相反: 他是客户端来校验服务端是否是一个正确的, 没有被篡改过的服务端。

Handshake

以下是一个客户端进行TLS握手授权的示例:

  • 协商阶段
    • 客户端发送一个 ClientHello 消息到服务端
    • 服务端返回一个 ServerHello 消息
    • 服务端发送他的 Certificate 消息
    • 服务端发送他的 ServerKeyExchange 消息以用于交换秘钥
    • 服务端发送一个 CertificateRequest 来请求客户端发送他的证书
    • 服务端发送一个 ServerHelloDone 消息表示服务端已经完成了协商消息的发送
    • 客户端返回一个 Certificate 消息, 其中包含了客户端的证书
    • 客户端发送 ClientKeyExchange 消息, 其中包含了公钥或者公钥加密的 PreMasterSecret
    • 客户端发送 CertificateVerify 消息, 这是使用客户机证书的私钥对先前握手消息的签名。可以使用客户端证书的公钥来验证此签名。这让服务器知道客户端可以访问证书的私钥,以确保客户端是合法的。
    • 协商完毕, 现在他们双方有一个用于对称加密的随机数秘钥了
  • 客户端发送一个ChangeCipherSpec 记录来告知服务器: 所有的信息都将进行身份验证
  • 服务端返回 ChangeCipherSpec
  • 握手完毕

参考文档

CORS 现代浏览器跨域请求简明手册

简介

本文主要是阐述与总结现代浏览器的跨域问题

同源策略

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

定义

如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同 (http:// 默认端口是80)
http://news.company.com/dir/other.html 失败 主机不同

开始一个跨域请求

你可以使用XMLHttpRequestFetch发起一个跨域请求

你可以在网站http://foo.com发起一个对http://bar.com的请求,如果对方网站许可,那么便能拿到对应的响应,否则则失败。

请求分两类

预检请求

预检请求是一个OPTIONS 请求,在跨域时预先发送到服务端以获取该服务器对跨域访问控制的一些配置,以决定接下来的请求是否会被发送。一般以Header头的形式返回, 相关配置一般以Access-Control-*作为开头

实际请求

简单请求

简单请求不会触发CORS 预检请求

若请求满足所有下述条件,则该请求可视为”简单请求”:

  • 使用下列方法之一:
    • GET
    • HEAD
    • POST
  • 除了被用户代理自动设置的首部字段(例如 Connection, User-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (需要注意额外的限制)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
    • Content-Type 的值仅限于下列三者之一:
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
  • 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
  • 请求中没有使用 ReadableStream 对象。

复杂请求

除了简单请求以外的所有请求都被称为复杂请求,复杂请求在进行跨域访问前会发送一个 CORS 预检请求

跨域请求响应Headers

跨域时携带Cookies

需要确保请求发起时信任对方

1
2
3
fetch('http://bar.com', {
credentials: 'include'
})

credentials 含义:

  • “omit”: Excludes credentials from this request, and causes any credentials sent back in the response to be ignored.
  • “same-origin”: Include credentials with requests made to same-origin URLs, and use any credentials sent back in responses from same-origin URLs.
  • “include”: Always includes credentials with this request, and always use any credentials sent back in the response.

1
2
3
4
5
const req = new XMLHttpRequest();
req.open('GET', url, true);
req.withCredentials = true;
req.onreadystatechange = handler;
req.send();

需要确保服务端响应头返回

1
Access-Control-Allow-Credentials: true

注意此时Access-Control-Allow-Origin不能为 “\“*

需要确保要发送的Cookie满足SameSite条件

注意Chrome 80 后将默认值从原来的None改为Lax, 相关影响可以看如下文章

参考文章