Git Product home page Git Product logo

blogs's Issues

JavaScript的数值存储探析与应用

JavaScript的数值存储探析

1. 浮点数的存储规则

JavaScript中的所有数字包括证书和小数只有一种类型:Numbr。它的实现遵循IEEE 754 标准,使用64位固定长度来表示,即标准的双精度浮点数double(单精度浮点数float则是32位)。

双精度的的精度比单精度的要高(因为存储所用的空间不同)。

IEEE754的标准,如图所示:

64 bit allocation

64位比特可分为三个部分:

  • 符号位S:第一位是正负数符号位(sign),0表正数,1表负数

  • 指数位E:中间的11位存储指数(exponent)

    指数e可以为正可以为负,其表示小数点的位置。如1.1011 * 2^4 ,e就是正数4,小数点在第一个1右侧再后移4位的位置;而 0.101 用指数表示就是1.01*2^{-1},e为-1。同时,IEEE754 标准要求,将此处的e+指数偏移量,得到的结果再化为二进制,就得到了我们的指数位E。如图所示:

    img

    指数偏移量公式(k为指数位个数):

    X = x^k-1

    如上图所示,双精度浮点数的指数位为11,即 X = 2^{11-1} =1023

    为什么要偏移1023?

    ”如果你知道为什么32位浮点数的指数偏移量是127,你就能知道为什么64位浮点数的指数偏移量是1023。

    在32位浮点数中,指数位有8位,它能表示的数字是从0到2的8次方,也就是256。但是指数有正有负,所以我们需要把256这个数字从中间劈开,一半表示正数,一半表示负数,所以就是-128到+128。哦,不对,忘记了中间还有个0,所以只能表示-128到127这256个数字。那么怎么记录负数呢?一种作法是把高位置1,这样我们只要看到高位是1的就知道是负数了,所谓高位置1就是说把0到255这么多个数字劈成两半,从0到127表示正数,从128到255表示负数。但是这种作法会带来一个问题:当你比较两个数的时候,比如130和30,谁更大呢?机器会觉得130更大,但实际上130是个负数,它应该比30小才对啊。所以为了解决这个麻烦,人们发明了另外一种方法:干脆把所有数字都给它加上128得了,这样-128加上128就变成了0,而127加上128变成了255,这样的话,再比较大小,就不存在负数比正数大的情况了。

    但是我要得到原来的数字怎么办呢?这好办,你只要再把指数减去128就得到了原来的数字,不是吗?比如说你读到0,那么减去128,就得到了负指数-128,读到255,减去128,就得到了127。

    那为什么指数偏移是127,不是128呢?因为人们为了特殊用处,不允许使用0和255这两个数字表示指数,少了2个数字,自然就只好采用127了。

    同理,64位浮点数,指数位有11位之多,2的11次方是2048,劈一半作偏移,可不就是1024吗?同理,去掉0和2048这两个数字,所以就用1023作偏移了。“

  • 尾数位M:最后的52位是尾数(mantissa),用来表示小数部分,位数不够用0补齐,超出部分进1舍0。

    尾数位决定精度。因此 JavaScript 中能精准表示的最大整数就是Math.pow(2, 53),十进制即9007199254740992。

因此,计算机存储二进制构成即为:符号位+指数位+尾数位

举个栗子:

29.5转换为二进制是11101.1

11101.1转换为科学计数法:1.11011*2^4

符号位 为0(正数)

指数位 为4,加上指数偏移量1023,即1027,转为二进制即 10000000011

尾数位 为11011,补满52位即: 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

所以29.5存储为计算机的二进制标准格式为

符号位+指数位+尾数位

0+10000000011+1101 1000 0000 0000 0000 0000 0000 0000 0000 0000

0000 0000 0000 ,即

0100 0000 0011 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

正好64位

好,现在整理一下步骤

计算机想存储一个数字

①首先将数字转换为二进制

②再把二进制转换为科学计数法表示

③分析科学计数表示法,得出 符号位【1】+(指数位+偏移量)【11】+尾数位【52】

④拼接成64位的二进制数

2. Number对象上的特殊值

MAX_SAFE_INTEGER

表示最大的安全整数。

由上一节可知,双精度浮点数的可准确表示的最大整数是
2^{53}-1

Number.MAX_SAFE_INTEGER === Math.pow(2,53) - 1  // true

相对的,单精度浮点数因尾数位只有23位,对应的最大安全整数为 2^{24}-1

MAX_VALUE

表示JS里内能表示的最大的值。

你或许以为就是64位全部拉满的情况:

0 11111111111 1111111111111111111111111111111111111111111111111111

但实际上,前文引用中提到过:“那为什么指数偏移是127,不是128呢?因为人们 为了特殊用处,不允许使用0和255这两个数字表示指数 ,少了2个数字,自然就只好采用127了。” 相对应的,64位存储时,11位的指数位,frac{2^{11}}{2},即1024也会用于特殊用途。

因此,最大值的64位应该是指数位对应十进制为拉满的情况下-1,64位即:

0 11111111110 1111111111111111111111111111111111111111111111111111

计算过程是:

0 11111111110 1111111111111111111111111111111111111111111111111111

转换成二进制的科学计数法表示如下:

1.1111111111111111111111111111111111111111111111111111 * 2^{2046 - 1023}

= 1.1111111111111111111111111111111111111111111111111111 * 2^{1023}

= 11111111111111111111111111111111111111111111111111111 * 2^{971}

= (2^{53} - 1) * 2^{971}

= 1.7976931348623157e+308

验证一下:

(Math.pow(2, 53) - 1) * Math.pow(2, 971) // 1.7976931348623157e+308
Number.MAX_VALUE === (Math.pow(2, 53) - 1) * Math.pow(2,971) // true

到此,我们就可以很容易地理解下面精度相关的问题了。

3. 特殊值的存储

前文提到,某些指数有特殊用途,即:

  1. 如果指数是0并且尾数的小数部分是0,这个数±0(和符号位相关)
  2. 如果指数 = 2^{e}-1并且尾数的小数部分是0,这个数是±[∞](无穷)同样和符号位相关)
  3. 如果指数 = 2^{e}-1并且尾数的小数部分非0,这个数表示为非数(NaN)。

以上规则,总结如下:

形式 指数 小数部分
0 0
非规约形式 0 大于0小于1
规约形式 1到 2^{e}-2 大于等于1小于2
无穷 2^{e}-1 0
NaN 2^{e}-1 非0

学以致用

我们先用前面学到的知识点来分析以下常见场景的误差产生的根本原因,最后来总结解决方案。

案例分析

1.1 精度丢失

// 加法 =====================
0.1 + 0.2 // 0.30000000000000004
0.7 + 0.1 // 0.7999999999999999
0.2 + 0.4 // 0.6000000000000001

// 减法 ====================
1.5 - 1.2 // 0.30000000000000004
0.3 - 0.2 // 0.09999999999999998
 
// 乘法 ====================
19.9 * 100 // 1989.9999999999998
0.8 * 3 // 2.4000000000000004
35.41 * 100 // 3540.9999999999995

// 除法 ====================
0.3 / 0.1 // 2.9999999999999996
0.69 / 10 // 0.06899999999999999

为什么0.1+0.2 === 0.30000000000000004?

0.1转换为64位下的存储格式:

0.1

>>> 0.0001100110011001100110011001100110011001100110011001101 >>> 1.100110011001100110011001100110011001100110011001101 * 2^{-4}

>>> 0011111110111001100110011001100110011001100110011001100110011010

同理,转换0.2

0.2

>>> 0.001100110011001100110011001100110011001100110011001101

>>> 1.100110011001100110011001100110011001100110011001101 * 2^{-3}

>>> 0011111111001001100110011001100110011001100110011001100110011010

可以看出来在转换为二进制时

0.1 >>> 0.0001 1001 1001 1001...(1001无限循环)
0.2 >>> 0.0011 0011 0011 0011...(0011无限循环)

“就像一些无理数不能有限表示,如 圆周率 3.1415926...,1.3333... 等,在转换为二进制的科学记数法的形式时只保留64位有效的数字,此时只能模仿十进制进行四舍五入了,但是二进制只有 0 和 1 两个,于是变为 0 舍 1 入。在这一步出现了错误,那么一步错步步错,那么在计算机存储小数时也就理所应当的出现了误差。这即是计算机中部分浮点数运算时出现误差,这就是丢失精度的根本原因

将0.1和0.2的二进制形式按实际展开,末尾补零相加,结果如下

0.00011001100110011001100110011001100110011001100110011010

+0.00110011001100110011001100110011001100110011001100110100

=0.01001100110011001100110011001100110011001100110011001110

0.1+0.2 >> 0.0100 1100 1100 1100...(1100无限循环)

则0.1 + 0.2的结果的二进制数科学记数法表示为为1.001100110011001100110011001100110011001100110011010 * 2^(-2), 省略尾数最后的0,即 1.00110011001100110011001100110011001100110011001101 * 2^(-2), 因此(0.1+0.2)实际存储时的形式是 0011111111010011001100110011001100110011001100110011001100110100
因计算机存储位数的限制而截断的二进制数字,再转换为十进制,就成了0.30000000000000004,刚好符合控制台里打印0.1+0.2的结果

所以,我们可以得出结论:十进制的浮点数在转换为二进制时,若出现了无限循环,会造成二进制的舍入操作,再转换为十进制时就会造成了计算误差。

1.2 大数危机

9999999999999999 == 10000000000000001===true ?

大整数的精度丢失和浮点数本质上是一样的,存储二进制时小数点的偏移量最大为52位,超出就会有舍入操作,因此JavaScript中能精准表示的最大整数是Math.pow(2, 53),十进制即9007199254740992,大于9007199254740992就可能会丢失精度。

使用parseInt()时也会有这种问题。

1.3 toFixed()对于小数最后一位为5时进位不正确问题

//firefox/chrome中toFixed 兼容性问题
1.35.toFixed(1) // 1.4 正确
1.335.toFixed(2) // 1.33  错误
1.3335.toFixed(3) // 1.333 错误
1.33335.toFixed(4) // 1.3334 正确
1.333335.toFixed(5)  // 1.33333 错误
1.3333335.toFixed(6) // 1.333333 错误

根本原因还是浮点数精度丢失问题:

如 1.005.toFixed(2) 返回的是 1.00 而不是 1.01

1.005.toPrecision(21) //1.00499999999999989342

2. 解决方案

“修复” 0.1+0.2 == 0.3

ES6在Number对象上新增了一个极小的常量——Number.EPSILON

Number.EPSILON
// 2.220446049250313e-16

引入这么一个小的数的值,目的在于为浮点数的计算设置一个误差范围,如果误差能够小于Number.EPSILON,我们就可以认为结果是可靠的。

测试是否相等

function equal(x, y){
  return Math.abs(x - y) < Number.EPSILON
}
equal(0.1+0.2,0.3) // true

2.2 修复数据展示

当你拿到可能有精度丢失的数据(如0.1+0.2),要展示时可以这样:

// Q: 为什么选12做默认精度?
// A: 经验选择
function strip(num, precision = 12) {
  return parseFloat(num.toPrecision(precision));
}
strip(0.1+0.2) // 0.3

但此方法仅用于最终结果的展示,在运算前这样处理是无意义的(计算中仍会丢失精度)。

修复 toFixed()

方案1

// 将小数末位的5改成6再调用toFixed()
function toFixed(number, precision) {
    var str = number + ''
    var len = str.length
    var last = str.substring(len - 1, len)
    if (last == '5') {
        last = '6'
        str = str.substring(0, len - 1) + last
        return (str - 0).toFixed(precision)
    } else {
        return number.toFixed(precision)
    }
}
console.log(toFixed(1.333335, 5))

方案2

// 先扩大再缩小
function toFixed(num, s) {
    var times = Math.pow(10, s)
    // 因为乘法同样存在精度问题,加上0.5保证不会扩大后尾数过多而parseInt后丢失精度
    var des = num * times + 0.5
    // 去除小数
    des = parseInt(des, 10) / times
    return des + ''
}
console.log(toFixed(1.333335, 5))

2.4 修复数据运算(+-*/)

修复常用算数运算符的方法原理都是扩大缩小法,但也有些细节要注意。

/**
* floatObj 包含加减乘除四个方法,能确保浮点数运算不丢失精度
*
* 精度丢失问题(或称舍入误差,其根本原因是二进制和实现位数限制有些数无法有限表示
* 以下是十进制小数对应的二进制表示
*      0.1 >> 0.0001 1001 1001 1001…(1001无限循环)
*      0.2 >> 0.0011 0011 0011 0011…(0011无限循环)
* 计算机里每种数据类型的存储是一个有限宽度,比如 JavaScript
  使用 64 位存储数字类型,因此超出的会舍去。舍去的部分就是精度丢失的部分。
*
* ** method **
*  add / subtract / multiply /divide
*
* ** explame **
*  0.1 + 0.2 == 0.30000000000000004 (多了 0.00000000000004)
*  0.2 + 0.4 == 0.6000000000000001  (多了 0.0000000000001)
*  19.9 * 100 == 1989.9999999999998 (少了 0.0000000000002)
*
* floatObj.add(0.1, 0.2) === 0.3
* floatObj.multiply(19.9, 100) === 1990
*
*/
var floatObj = (function () {
  /*
   * 判断obj是否为一个整数 整数取整后还是等于自己。利用这个特性来判断是否是整数
   */
  function isInteger(obj) {
    // 或者使用 Number.isInteger()
    return Math.floor(obj) === obj
  }
  /*
   * 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100
   * @param floatNum {number} 小数
   * @return {object}
   *   {times:100, num: 314}
   */
  function toInteger(floatNum) {
    // 初始化数字与精度 times精度倍数  num转化后的整数
    var ret = { times: 1, num: 0 }
    var isNegative = floatNum < 0 //是否是小数
    if (isInteger(floatNum)) {
      // 是否是整数
      ret.num = floatNum
      return ret //是整数直接返回
    }
    var strfi = floatNum + '' // 转换为字符串
    var dotPos = strfi.indexOf('.')
    var len = strfi.substr(dotPos + 1).length // 拿到小数点之后的位数
    var times = Math.pow(10, len) // 精度倍数
    /* 为什么加0.5?
          前面讲过乘法也会出现精度问题
          假设传入0.16344556此时倍数为100000000
          Math.abs(0.16344556) * 100000000=0.16344556*10000000=1634455.5999999999 
          少了0.0000000001
          加上0.5 0.16344556*10000000+0.5=1634456.0999999999 parseInt之后乘法的精度问题得以矫正
      */
    var intNum = parseInt(Math.abs(floatNum) * times + 0.5, 10)
    ret.times = times
    if (isNegative) {
      intNum = -intNum
    }
    ret.num = intNum
    return ret
  }

  /*
   * 核心方法,实现加减乘除运算,确保不丢失精度
   * 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
   * @param a {number} 运算数1
   * @param b {number} 运算数2
   */
  function operation(a, b, op) {
    var o1 = toInteger(a)
    var o2 = toInteger(b)
    var n1 = o1.num // 3.25+3.153
    var n2 = o2.num
    var t1 = o1.times
    var t2 = o2.times
    var max = t1 > t2 ? t1 : t2
    var result = null
    switch (op) {
      // 加减需要根据倍数关系来处理
      case 'add':
        if (t1 === t2) {
          // 两个小数倍数相同
          result = n1 + n2
        } else if (t1 > t2) {
          // o1 小数位 大于 o2
          result = n1 + n2 * (t1 / t2)
        } else {
          // o1小数位小于 o2
          result = n1 * (t2 / t1) + n2
        }
        return result / max
      case 'subtract':
        if (t1 === t2) {
          result = n1 - n2
        } else if (t1 > t2) {
          result = n1 - n2 * (t1 / t2)
        } else {
          result = n1 * (t2 / t1) - n2
        }
        return result / max
      case 'multiply':
        // 325*3153/(100*1000) 扩大100倍 ==>缩小100倍
        result = (n1 * n2) / (t1 * t2)
        return result
      case 'divide':
        // (325/3153)*(1000/100)  缩小100倍 ==>扩大100倍
        result = (n1 / n2) * (t2 / t1)
        return result
    }
  }

  // 加减乘除的四个接口
  function add(a, b) {
    return operation(a, b, 'add')
  }
  function subtract(a, b) {
    return operation(a, b, 'subtract')
  }
  function multiply(a, b) {
    return operation(a, b, 'multiply')
  }
  function divide(a, b) {
    return operation(a, b, 'divide')
  }
  return {
    add: add,
    subtract: subtract,
    multiply: multiply,
    divide: divide,
  }
})()


console.log(0.1 + 0.2) // 0.30000000000000004
console.log(floatObj.add(0.1, 0.2)) // 0.3

console.log(0.3 - 0.1) // 0.19999999999999998
console.log(floatObj.subtract(0.3, 0.1)) // 0.2

console.log(35.41 * 100) // 3540.9999999999995
console.log(floatObj.multiply(35.41, 100)) // 3541

console.log(0.3 / 0.1) // 2.9999999999999996
console.log(floatObj.divide()) // 3

当然,也可以用成熟的库来解决此类问题,如math.jsnumber-precision 等。

参考文章:

camsong/blog#9

https://zhuanlan.zhihu.com/p/100353781

https://segmentfault.com/q/1010000016401244/a-1020000016446375###

https://zh.wikipedia.org/wiki/IEEE_754#%E7%89%B9%E6%AE%8A%E5%80%BC

外网访问华硕路由器和群晖NAS

外网访问华硕路由器和群晖NAS

[TOC]

一、前言

年关将至,趁着放假前的一段平静,折腾了一下群晖和路由器的外网访问。过程中对网络多了一些理解,又幸得一两日空闲,遂作文以总结与指路。

要实现家庭设备外网访问有两种方式: 公网DDNS内网穿透 。本文只讨论相对体验更好的公网DDNS方案。

扩展阅读:利用zerotier的内网穿透实现外网访问群晖的方法

公网DDNS指利用DDNS动态域名解析技术,在外网通过域名访问家庭宽带的公网IP,由路由器提供端口转发,与内网设备直接通讯。

二、准备

使用公网DDNS直连方式需要满足 公网IP桥接模式端口映射 三个前提,缺一不可,然后利用DDNS动态域名解析技术使用域名来访问。

我的路由器是刷了梅林固件的华硕AC-86U,nas设备是群晖DS716+。

1. 申请公网IP

电信宽带的同学可以向客服申请公网IP,要问理由的话可以是安装家庭监控;移动宽带可以考虑换成电信宽带🐶(移动没有公网IP)。

公网IP:全球唯一,互联网上的所有人都可以通过你的IP访问到你,像是你的门牌地址

2. 桥接模式

通常情况下,我们家庭设备是这么连接的:光纤-光猫-路由器-其他设备。光猫默认是路由模式,它会从运营商处分配到一个IP(公网IP)。光猫再承担一个路由的功能,给连接他的路由器分配一个内网IP(这个过程叫NAT,网络地址转换)。

可打10000,说需要路由器拨号,电信客服可远程下发配置。动手能力强的也可搜索光猫的型号+破解,自行破解光猫管理员账号改为桥接模式。将光猫改为桥接模式后,它就承担一个中转的作用,由连接光猫的路由器去拨号上网拿到公网IP。有了这个一手的公网IP,我们就可以实现一些个性化的功能。

路由器拨号成功后,可以访问一些显示IP地址的网站(如:https://www.ip138.com/ ),并将获取到的IP和路由器管理页面的WAN IP做比对,如果全部一致,则基本确定拿到了公网IP。

内网IP:就像你住在一个私人庄园,公网IP就是门牌号,但具体庄园内路线怎么走的,外人是不知道的。内网IP像是公网IP的下级,别人只能找到你的上级,无法找到内网的你(除非设置端口转发,这个在后面说)

3. 获取宽带账号密码

光猫桥接,路由器拨号上网,所以需要的账号和密码,可以在电信APP内获取/修改,或拨打10000来询问客服获取。

4. 购买域名

本来,此步骤非必须,因为华硕和群晖本身都提供了DDNS服务。但是,因为家庭宽带通常被屏蔽了80和443端口,所以 此类免费DDNS的常用默认SSL证书Let's Encrypt会无法验证,导致无法在外网通过https访问, 而http是明文传输的,域名、账户、密码都会暴露在传输链路中,因此并不安全。所以个人建议像我一样自己注册域名,手动做SSL证书的验证,就可以解决这个问题。

我的域名是在阿里云注册的,最便宜的域名几块钱可以买一年。购买域名之后,你就可以用自己喜欢的域名来访问路由器和群晖啦,而不需要服务商的冗长的后缀,像asuscomm.com、synology.me等。

下文会用 qaq.com 来代表我们购买的域名

5. 申请SSL证书

前文有说,走https协议需要ssl证书,而证书是和域名关联的。我使用的是zerossl证书(https://zerossl.com/ ),它是免费的,具有90天的时效,个人网站使用足够了。

扩展阅读:不同类型SSL证书的区别

按照网站的验证指引,配合阿里云的域名控制台操作,就可以获取域名的SSL证书了~

验证主要是为了保证你是域名的控制人,所以步骤也很简单,填写域名后,按照zerossl分配给你的验证方式,配置在域名控制台就行了。

证书验证完成之后,可以把认证下载下来,解压之后可以获取三个文件,分别是certificate.crt、.private.key、ca_bundle.crt,后面会用到。

这一步,我们将SSL证书和域名关联了起来。

三、外网访问路由器管理页

1. 配置SSL证书

原本,如果用华硕路由器自带的DDNS客户端、并配合第三方DDNS,是可以省去很多麻烦事的,比如买域名、配置证书,但由于家庭宽带80端口被封导致Let's Encrypt证书无法验证,而华硕路由器为了安全考虑,外网访问时仅支持HTTPS协议。所以我们需要自己申请、验证、配置证书。

在华硕路由器管理页面:

  1. 点击高级设置-系统管理 -> 系统设置 -> “远程访问设置”的“从互联网设置RT-AC86U”选择“是”;
  2. 点击高级设置-外部网络 -> DDNS;
  3. 启用DDNS客户端出选“否”;
  4. HTTPS/SSL 证书处选择“导入您自己的证书”;
  5. 点击“上传”;
  6. “私人密钥”选择前文下载 .private.key 文件,“SSL证书”选择 certificate.crt 文件,点击确认。
  7. 点击“应用本页面设置”

再在页面查看证书的状态,等到状态变为启用,而且证书核发对象正是我们之前购买的域名qaq.com,到此,SSL证书的配置就完成了。

这一步,我们将SSL证书配置到了服务器(路由器)内。当浏览器访问宽带的IP时时,路由器会返回给客户端SSL证书信息,只要证书有效,浏览器就认可这个连接,加密通信得以被浏览器允许。

2. 配置阿里DDNS

这一步的目的是让固定的域名qaq.com和动态的家庭宽带IP及时关联并更新,让我们在访问域名时可以连接到路由器。

2.1 获取域名控制的AccessKey

2.1.1 创建用户

登录阿里云域名控制台,点击右上角头像-AccessKey管理,开始使用子账户AccessKey。创建用户 -> 填写用户名称,如登录名为router,显示名称为路由器,勾选编程访问(因为后面需要通过自动化的脚本来更新DDNS来匹配家庭宽带的公网IP),确定,验证手机号,用户创建完成。

2.1.2 添加权限

勾选用户,点击“添加权限”,只是自己用的话图省事可以选系统策略的AdministratorAccess,即管理所有阿里云资源的权限。

2.1.3 创建AccessKey

点击刚刚创建的用户-点击创建AccessKey,记下生成的AccessKey ID和AccessKey Secret。

2.2 配置阿里DDNS

路由器管理页面,软件中心-安装阿里DDNS插件,进入插件,选择开启Aliddns。服务配置栏,填入2.1步骤获取的id和secret,域名处注意,如果我们希望配置的是2级域名,如qaq.com,那域名处的两个输入框,第一个填@,第二个填qaq.com,其他的都按默认的就好了,点击“提交”。

提交之后,如果更新日志内显示IP更新成功,就可以访问qaq.com:8443来在外网登录路由器的管理页面了~

但还有个小问题,因为前面用的SSL证书是免费的,它的时效性是90天,90天后过期后如果还要我们再手动去验证证书势必非常麻烦,而梅林固件的软件中心有款插件Let's Encrypt也是为了解决这个问题而存在的。

这一步,我们将域名和宽度的IP地址关联了起来,并保持自动更新。

3. 配置Let's Encrypt插件自动续期SSL证书

梅林固件的路由器软件中心安装Let's Encrypt,进入插件:

打开开关,输入域名qaq.com,选择阿里DNS(万网),填入2.1.3步骤获取的id和secret,提交。

这一步,保持SSL证书的更新,防止浏览器不认可SSL证书的合法性而无法访问HTTPS连接的页面。

四、外网访问群晖管理页

1. 配置端口转发

路由器管理页,外部网络-端口转发-开启端口转发-添加配置文件。

服务名称:dsm admin

通信协议TCP

外部端口5001

本地IP地址选择你的nas在路由器里显示的名称

确定。

同样的,可以在这里把一些群晖常用的服务端口转发到公网,如afp的548、smb的137:139,445,webdav的5005,5006等,尽量不要把ssh的22端口转发到外网,即使要转发也要替换成大数字的端口如58822,防止被他人扫描端口攻击(会收到群晖的邮件提醒)。

2. 配置群晖服务的ssl证书

登录群晖的管理页-控制面板-安全性-证书-设置,将各种服务的证书配置都为固定域名qaq.com的证书。

至此,如果一切顺利,你已可以访问qaq.com:5001来登录群晖的dsm页了,并可以使用ds video、ds audio等一系列服务。

后记

有任何问题欢迎留言交流,有问必回。

因为对新手来说全过程比较冗长,当初为了搞定整个流程查了很多资料。记录难免疏漏,有变动会更新在博客里,理解**~!

async await 的错误处理方法

async await 的错误处理方法

1. try/catch

es6 的初学者必须知道的捕获错误的方法,因为它是相对来说最保险的,既可以捕获同步错误也可以捕获异步错误。

捕获异步错误:

run();

async function run() {
  try {
    await Promise.reject(new Error('Oops!'));
  } catch (error) {
    error.message; // "Oops!"
  }
}

捕获同步错误:

run();

async function run() {
  const v = null;
  try {
    await Promise.resolve('foo');
    v.thisWillThrow;
  } catch (error) {
    // "TypeError: Cannot read property 'thisWillThrow' of null"
    error.message;
  }
}

新手用 async/await 容易犯的错误之一,是忘记捕获错误,这是大忌,它会导致出现“S级”的异常:**控制台既没有报错,期待的效果也没有work,会导致很难定位问题。**以下的示例取自于一个个人比较常见的情景:

import api from './api.js' // 引入了一个请求库

run();

async function run() {
	const res = await axios.get('xxxx')  
  // 期待打印出请求结果,但因为没有做错误处理,
  // 导致当网络请求出错时,这一行既没有走到,
  // 控制台也没有抛出 Error,会很难定位问题
  console.log(res)
}

但这不意味着 try/catch 就万无一失了。try 代码块中被 return 的 rejected 的 promise 是无法被捕获到的:

run();

async function run() {
  try {
    // 注意: 这是一个 `return` ,不是 `await`
    return Promise.reject(new Error('Oops!'));
  } catch (error) {
    // 不会运行
  }
}

其实,将 return 改成 return await 可以解决这个问题的,但容易遗漏。

2. 以Golang 风格捕获错误

.then() 将 rejected 状态的 promise 转成成功状态的 promise 并返回错误,可以用 if (err) 来检查是否出错

run();

async function throwAnError() {
  throw new Error('Oops!');
}

async function noError() {
  return 42;
}

async function run() {
  // `.then(() => null, err => err)` 模式下,
  // 当错误发生时返回一个 error ,否则返回 null
  let err = await throwAnError().then(() => null, err => err);
  if (err != null) {
    err.message; // 'Oops'
  }

  err = await noError().then(() => null, err => err);
  err; // null
}

如果同时需要成功的值和错误信息,可以像写 Golang 一样写 Javascript

run();

async function throwAnError() {
  throw new Error('Oops!');
}

async function noError() {
  return 42;
}

async function run() {
  // `.then(v => [null, v], err => [err, null])` 的模式
  // 让你能用数组的形式同时获取 error 和 result
  let [err, res] = await throwAnError().
    then(v => [null, v], err => [err, null]);
  if (err != null) {
    err.message; // 'Oops'
  }

  [err, res] = await noError().
    then(v => [null, v], err => [err, null]);
  err; // null
  res; // 42
}

如果喜欢这种写法,还可以对它做一个封装:

// to.js
export default function to(promise) {  
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]);
}

// use
import to from './to.js';

run();

async function throwAnError() {
  throw new Error('Oops!');
}

async function noError() {
  return 42;
}

async function run() {
  // 用 to 来曝光promise
  let [err, res] = await to(throwAnError())
  if (err != null) {
    err.message; // 'Oops'
  }

  [err, res] = await to(noError())
  err; // null
  res; // 42
}

如果需要拓展 error 的信息、是否 errorFirst,甚至可以做更复杂的封装:

// to.js
function to (promise, errorProps = {}, errorFirst = true) {
	return promise.then((data) => errorFirst ? [null, data] : [data, null])
			  .catch(err => {
				  if(errorProps) Object.assign(err, errorProps)
				  errorFirst ? [err, null] : [null, err]
			  })
  }

3. catch()

try/catch 和 Golang风格的错误处理都有它们的用途,但是,要确保你处理了 run() 函数里所有错误的、最好的方法,是用 run().catch()(前提:run是异步函数)。换句话说,在调用函数时就处理错误,而不是单独处理每个错误。

run().
  catch(function handleError(err) {
    err.message; // Oops!
  }).
	// 处理 `handleError()`中的任何错误:如果出错则杀死进程
  catch(err => { process.nextTick(() => { throw err; }) });

async function run() {
  await Promise.reject(new Error('Oops!'));
}

记住,所有的 async 函数总是返回 promises。如果函数内有任何未捕获的错误发生了,这个 promise 也会 rejected。如果你的 async 函数返回了一个 rejected 的 promise,返回的 promise 也会 rejected 。

run().
  catch(function handleError(err) {
    err.message; // Oops!
  }).
	// 处理 `handleError()`中的任何错误:如果出错则杀死进程
  catch(err => { process.nextTick(() => { throw err; }) });

async function run() {
  // 注意:这是一个 `return` ,不是 `await`
  return Promise.reject(new Error('Oops!'));
}

总结

try/catch Golang风格 .catch()
优点 - 最保险的,可捕获同步、await的异步错误
- 流程控制准确
- 优雅
- 能捕获异步函数体内的同步和抛出的异步错误
缺点 - 无法捕获 try块中,被return 的异步错误(可用return await解决)
- 不能优雅地对错误分别处理
- 不适用于流程中不需要中断的错误
- 无法避免catch本身抛出异常且不好处理
- 大量重复 if(err !== null),可能遗漏

- 无法捕获外部函数体内的同步错误
- 只适用于有 .catch() 方法的异步函数
场景 非常适合简单流程、不希望出错后影响后续执行流程的情景 适合链路长、较复杂的异步流程;适合于单个环节出错不影响流程 适合处理期望外的错误,及时处理;适合给.catch() 做兜底

参考资料

http://thecodebarbarian.com/async-await-error-handling-in-javascript.html

https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/

huruji/blog#61

关于背景图长宽100%,图片却显示不全的思考

前言

前两天进行业务开发时,用到一个「券样式」的背景图(background-image),即下图的「返利¥1.30」:

image

容器的宽度会根据文字内容自适应,代码如下:

点击展开

css:

.coupon-like {
  padding: 8rpx 10rpx;
  background-size: 100% 100%;
}

html:(代码是微信小程序的写法)

<view
	class="coupon-like"
  style="background-image: url('{{commonImgUrl.goods2}}');"
  wx:if="{{item.amountExpectRebate}}"
>
  <view class="f22 c-theme">返利{{item.amountUiExpectRebate}</view>
</view>

模拟器上显示没问题,但到了真机上时,部分商品背景图片的边框缺失/变细了一块:

image

针对这个现象,抽空展开了一些查漏补缺的学习和简单研究。

在此之前先阐述几个概念,方便后面解释原因。

概念阐述

设备像素

Device Pixels (DP),设备本身的物理像素,出厂时就决定了。

设备独立像素

Device Independent Pixels (DIP/DP),设备独立像素,又称逻辑像素。独立于设备的、用于逻辑上衡量长度的单位,由底层系统的程序使用,会由相关系统转换为物理像素。

所以它只是一个虚拟像素单位,那么有什么用呢?

举个例子,iPhone 3GS 和 iPhone 4/4s 的尺寸都是 3.5 寸,但 iPhone 3GS 的分辨率是 320x480,iPhone 4/4s 的分辨率是 640x960,这也就是意味着同样长度的屏幕,iPhone 3GS 有 320 个物理像素,iPhone 4/4s 有 640 个物理像素。

如果我们按照真实的物理像素进行布局,比如说我们按照 320 物理像素进行布局,到了 640 物理像素的手机上就会有一半的空白,为了避免这种问题,就产生了虚拟像素单位。

我们统一 iPhone 3GS 和 iPhone 4/4s 都是 320 个虚拟像素,只是在 iPhone 3GS 上,最终 1 个虚拟像素换算成 1 个物理像素,在 iphone 4s 中,1 个虚拟像素最终换算成 2 个物理像素(某个方向上)。

DPR

设备像素比(devicePixelRatio)是默认缩放为100%时,设备物理像素和设备独立像素的比值:

DPR = 设备像素 / 设备独立像素(某一方向上)

浏览器端可从window.devicePixelRatio获取

早期没有DPR的概念。随技术发展,从iphone4开始,屏幕的PPI(像素密度/每英寸像素Pixels Per Inch)太高,人的视网膜无法分辨出屏幕上的像素点,这种屏幕被称为视网膜屏。它的分辨率提高了一倍,但屏幕尺寸却没有变化,这意味着同样大小的屏幕上,像素多了一倍,于是DPR = 2。

PPI

Pixels Per Inch,像素密度/每英寸像素。

计算方法,拿iPhone XR举例。

官方宣称:设备像素1792*828,326ppi,对角线6.1吋。

image

计算结果是323.6,有略微误差,个人猜测是屏幕对角线6.1吋是四舍五入得来的,如果算作6.05结果就是326.29,刚好就是官方宣称的326ppi

试验说明

html:

 <view
    class="pr"
    wx:for="{{1000}}"
    style="width:{{60 + index*0.1}}rpx;height:48rpx;margin-right:10rpx;"
  >
    <image src="{{commonImgUrl.goods2}}" class="bg-img" />
    <view class="pr" style="z-index:2;text-align:center;">{{(60+index*0.1)*0.552}}</view>
  </view>

0.552是我的测试设备iphone XR的 px/rpx值

真机上表现如下,循环产生的1000个图片,间歇性地连续出现右边缘丢失的情况,图中的数字即图片的渲染的设备独立像素DPI(截取了其中一段):

image

可以发现规律:连续出现异常显示的图片的DPI,均围绕某一特定像素值(向下取整都是36px)。

解释

  • 为什么边框会丢失?

    (免责声明:以下内容来自于资料搜索与自己的推测,不代表客观事实,有不同想法欢迎探讨)

    基于上文概念阐述DPR的存在,图片的物理像素与渲染所用的实际设备像素不一定一致,因此设备对图片在屏幕上的显示像素会有自己的算法。当100px * 100px尺寸的图片,在设备上覆盖面积不等于100px * 100px的物理像素时,就会用到设备自身的算法,来计算每个物理像素点应该显示什么颜色。

    所以背景图的边缘是被设备的显示算法给“算没了”。

    下图或许可以帮你更好地理解:

image

(当原图尺寸与设备像素不一致时,每个像素点上到底显示什么颜色,由设备的显示算法决定)

  • 有什么解决方法?

    避免对固定图片设置动态宽度,因为这本身也是一种不太合理的做法:图片会随着宽度的变化而变形失真。

FAQs

  • 为什么不用css写边框而用background-image

    为了跟左侧的「x元券」写法统一,也方便以后优化而不用改写法,而且这不是本文的重点(拍桌

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.