项目中使用 nodeJS 写 web 业务时,碰上了一些问题,花费了一些时间解决,记录一下以防再掉坑里。
我们使用 nodeJS 实现了一个 web 服务,对接了后端服务的网关,并且向前端返回静态资源。
JS 中的整数
首先是大整数的问题。对接用户中心时,我们的 node 服务需要给前端返回用户 id。然而,在使用此用户 id 去查询时,却发现查不到此用户的内容。在定位过程中,发现此用户的 id 甚至不在数据库中。最后发现是我们的 node 服务在返回真实 id 时,直接返回了整型,使 JS 丢失了精度。
1 | > 409119721175122920 |
首先复习一下 10 进制转换为 2 进制。
整数部分,除 2 取余,直至商数为 0,从下到上读余数,即是二进制的整数部分。小数部分,用其小数部分乘 2,取其整数部分的结果,再用计算后的小数部分依此重复计算,算到小数部分全为 0 为止,从上到下读所有计算后整数部分的数字,即是二进制的小数部分。
二进制科学计数法:59.25(10) = +111011.01(2) = +11.101101(2) x 2^4 = +1.1101101(2) x 2^5
JS 中的 Number 对象是采用双精度IEEE 754 64位浮点
类型来存储的,也是用二进制的科学计数法表示一个数的。对整数的表示等同于双精度浮点数(64-bit)对整数的表示。它的规则如下:
Sign(符号位) | Exponent(指数位偏移) | Fraction(有效数字) |
---|---|---|
1 | 11 | 52 |
+-1 | 规定指数偏移量为 2^(e - 1) - 1 位,64 位需要偏移 1023 | 1.f * 2 ^ p |
指数部分为什么引入偏移量?
方便运算。如果没有偏移量的存在,指数需引入符号位, 则需引入补码,比较计算更加复杂,为了简化操作,才使用无符号的阶码,引入偏移量的概念。
为什么指数需要11位?Why do higher-precision floating point formats have so many exponent bits? 。
表达式 | 约束 | 说明 |
---|---|---|
(−1)^s × 1.f × 2^(e−1023) | 0 < e < 2047, e 值域[1, 2046],指数值域 [−1022, +1023] | 规约化数值 |
(−1)^s × 0.f × 2^(-1022) | e = 0, f > 0。 指数部分的偏移量比规约形式少 1,对双精度浮点格式来说即 1022,所以非规约形式的指数 e 总是-1022。 | 非规约化数值,用来表示无限接近 0 的数 |
(−1)^s × 0 | e = 0, f = 0 | 正负 0,js 中它们相等 |
NaN | e = 2047, f > 0 | 指数位全为 1,有效数字大于 0,不是 number |
(−1)^s × ∞ (infinity) | e = 2047, f = 0 | 正负无穷 |
为什么 52 位有效数字能表示 53 位的精度呢?IEEE754 规定规约数的尾数第一位隐含为 1(二进制的科学计数法,第一位总是 1,规定省略),不存入比特位中。那么,1.xxxx(52 个 x)最多共有 53 位精确位,拿来做整数表示,这 53 个二进制 1 转换为 10 进制,值为 2^53 - 1,科学计数法表示为1.111…111 * 2^52 = 2^53 - 1,指数计算结果为52(e=1075)。
BigInt
:BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这是 Javascript 中可以用 Number 表示的最大安全整数。BigInt 可以表示任意大的整数。可以用在一个整数字面量后面加n
的方式定义一个 BigInt。
1 | // 最大安全整数 |
综上,前后端应当约定好,处理此类可能存在大整数的情况下,使用字符串来进行交互。这里我们使用了json-big-int来处理 json 中的大整数。
1 | import { parse } from "json-bigint"; |
怎么来判断大整数呢?
- 字符串位数
- ECMAScript 6 中 MAX_SAFE_INTEGER 和 MIN_SAFE_INTEGER
- bignumber中有更为严谨的判断方法
1 | Number.isSafeInteger = function (n) { |
为什么0.1 + 0.2 != 0.3?
首先,IEEE 754 舍入规则是:
Round to nearest, ties to even
向最接近的整数舍入;当到两边整数的距离相等时,向最接近的偶数舍入。(四舍六入五成双)
对于二进制而言,舍弃的位为1则进位,0则舍去。
0.1的二进制表示为 1.(1001*) * 2 ^ -4,存储为双精度浮点为
理论存储
0 01111111011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001 无限循环)
超过部分进一舍零,实际存储为
0 01111111011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
0.2的二进制表示为 1.(1001*) * 2 ^ -3,存储为双精度浮点为
理论存储
0 01111111100 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001 无限循环)
超过部分进一舍零,实际存储为
0 01111111100 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
相加时,指数需要统一为-3,0.1左移一位
0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 0
1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010
得到
10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111
科学计数表示为 1.(0011*) * 2
1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 1 (53位将被舍弃,向前进1位)
有效部分实际存储为
0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100
而0.3实际存储为
0 01111111101 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
签名按字典序
在数字签名鉴权时,通常会要求按一个字典序来排列参数。字典排序(lexicographical order)是一种对于随机变量形成序列的排序方法。其方法是,按照字母顺序,或者数字小大顺序,由小到大的形成序列(0-9A-Za-z)。而 Js 的 sort 默认采用字典序。
1 | [1, 2, 5, 10, "A", "", "ba", "ab", "aa"].sort(); // ["", 1, 10, 2, 5, "A", "aa", "ab", "ba"] |
- 多个字符串按字典序排列
- 一个字符串按字典序排列
- 全排列
nginx 开启 gzip
nginx 默认是关闭 gizp 的。
1 | server { |
JWT 退出登陆
当点了退出登陆后,怎么清除客户端侧的登陆信息呢:设置空的 session cookie。
1 | 'get /api/logout': (req, res) => { |
如果是多系统共用用户中心,在别的系统中退出,如何通知我们的系统清空用户状态呢?有一种方式是使用<img>
向我们的系统发送一个上述请求,清空服务端的 session,同时向浏览器种下空 session。