xiter

xiter 查看完整档案

深圳编辑南阳理工学院  |  计算机科学与技术 编辑金证  |  前端开发工程师 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

xiter 赞了文章 · 4月13日

Semver(语义化版本号)扫盲

最近Github 10周年在朋友圈里沸沸扬扬刷屏,小编在工作中却惊讶的发现不少同事对版本号中的beta和rc没有概念,使用npm install package@next时,也不清楚next代表的含义。于是,决定写一篇文章科普一下由 Github 起草的Semver(语义化版本)的相关知识。

实际案例

首先,我们来看看目前最流行的前端框架之一的React最近5个月的版本发布日志,截图来自npmjs.com:

从上图,我们不难得出几个结论:

  • 软件的版本通常由三位组成,形如:X.Y.Z
  • 版本是严格递增的,此处是:16.2.0 -> 16.3.0 -> 16.3.1
  • 在发布重要版本时,可以发布alpha, rc等先行版本
  • alpha和rc等修饰版本的关键字后面可以带上次数和meta信息

可以说,React 发布版本时做的相当到位,版本给人的感觉非常清晰,也很严谨。这得益于 Semver(语义化版本) 规范的功劳。那么,Semver是在什么场景下出现的呢?它的出现又解决了什么问题?这里要和大家科普下“依赖地狱”的概念。

依赖地狱

通俗而言,“依赖地狱”指开发者安装某个软件包时,发现这个软件包里又依赖不同特定版本的其它软件包。随着系统功能越来越复杂,依赖的软件包越来越多,依赖关系也越来越深,这个时候可能面临版本控制被锁死的风险。

因此,Github 起草了一个具有指导意义的,统一的版本号表示规则,称为 Semantic Versioning(语义化版本表示)。该规则规定了版本号如何表示,如何增加,如何进行比较,不同的版本号意味着什么。

官网:https://semver.org/ 中文版:https://semver.org/lang/zh-CN/

下面是遵从了Semver规范的React依赖图,截图来自npm.broofa.com:

可以看出,遵从了Semver规范的包依赖非常清晰,没有出现循环依赖、依赖冲突等常见问题。

版本格式

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  • 主版本号(major):当你做了不兼容的 API 修改,
  • 次版本号(minor):当你做了向下兼容的功能性新增,可以理解为Feature版本,
  • 修订号(patch):当你做了向下兼容的问题修正,可以理解为Bug fix版本。

先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

先行版本

当要发布大版本或者核心的Feature时,但是又不能保证这个版本的功能 100% 正常。这个时候就需要通过发布先行版本。比较常见的先行版本包括:内测版、灰度版本了和RC版本。Semver规范中使用alpha、beta、rc(以前叫做gama)来修饰即将要发布的版本。它们的含义是:

  • alpha: 内部版本
  • beta: 公测版本
  • rc: 即Release candiate,正式版本的候选版本

比如:1.0.0-alpha.0, 1.0.0-alpha.1, 1.0.0-beta.0, 1.0.0-rc.0, 1.0.p-rc.1 等版本。alpha, beta, rc后需要带上次数信息。

版本发布准则

列举出比较实用的一些规则:

  • 标准的版本号必须采用XYZ的格式,并且X、Y 和 Z 为非负的整数,禁止在数字前方补零,版本发布需要严格递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。
  • 某个软件版本发行后,任何修改都必须以新版本发行。
  • 1.0.0 的版本号用于界定公共 API。当你的软件发布到了正式环境,或者有稳定的API时,就可以发布1.0.0版本了。
  • 版本的优先层级指的是不同版本在排序时如何比较。判断优先层级时,必须把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较。

npm包依赖

当执行npm install package -S 来安装三方包时,npm 会首先安装包的最新版本,然后将包名及版本号写入到 package.json 文件中。

比如,通过npm 安装 react 时:

{
  "dependencies": {
    "react": "~16.2.0"
  }
}

项目对包的依赖可以使用下面的 3 种方法来表示(假设当前版本号是 16.2.0):

  • 兼容模块新发布的补丁版本:~16.2.0、16.2.x、16.2
  • 兼容模块新发布的小版本、补丁版本:^16.2.0、16.x、16
  • 兼容模块新发布的大版本、小版本、补丁版本:*、x

npm包发布

通常我们发布一个包到npm仓库时,我们的做法是先修改 package.json 为某个版本,然后执行 npm publish 命令。手动修改版本号的做法建立在你对Semver规范特别熟悉的基础之上,否则可能会造成版本混乱。npm 考虑到了这点,它提供了相关的命令来让我们更好的遵从Semver规范:

  • 升级补丁版本号:npm version patch
  • 升级小版本号:npm version minor
  • 升级大版本号:npm version major

当执行 npm publish 时,会首先将当前版本发布到 npm registry,然后更新 dist-tags.latest 的值为新版本。

当执行 npm publish --tag=next 时,会首先将当前版本发布到 npm registry,并且更新 dist-tags.next 的值为新版本。这里的 next 可以是任意有意义的命名(比如:v1.x、v2.x 等等)

OK,现在你应该知道 npm install package@next时next代表的含义了吧!

团队开源

腾讯IVWEB团队的工程化解决方案feflow已经开源

如果对您的团队或者项目有帮助,请给个Star支持一下哈~

查看原文

赞 25 收藏 11 评论 2

xiter 赞了文章 · 2020-11-17

匠心打造canvas签名组件

本文首发于CSDN网站,下面的版本又经过进一步的修订。
原文:匠心打造canvas签名组件

导读

6月又是项目吃紧的时候,一大波需求袭来,猝不及防。

度过了漫长而煎熬的6月,是时候总结一波。最近移动端的一款产品原计划是引入第三方的签名插件,该插件依赖复杂,若干个js使用document.write顺序加载,插件源码是ES5的,甚至说是ES3都不为过。为了能够顺利嵌入我们的VUE项目,我阅读了两天插件的源码(demo及文档不全,囧),然后花了一天多点的时间使用ES6引用它。鉴于单页应用中,任何非全局资源都不该提前加载的指导性原则,为了做到动态加载,我甚至还专门写了一个simple的vue组件iload.js去顺序加载这些资源并执行回调。一切看似很完美,结果发现demo引用的一个压缩的js中居然写死了插件相关DOM节点的id和style,此刻我的内心几乎是崩溃的。这样的一个插件我怕是无力引入了吧。

虽然嘴上这么说,身体还是很诚实的,费尽千辛万苦我还是把这个插件用在了项目中。随着项目推进,业务上经过多次沟通,我们砍掉了该签名插件的数字证书验证部分。也就是说,这么大的一个插件,只剩下用户签名的功能,我完全可以自己做啊。于是我悄悄移除了这个插件,为这几天的调研和码字过程划上了一个完美的句号(深藏功与名)。

签名是若干操作的集合,起于用户手写姓名,终于签名图片上传,中间还包含图片的处理,比如说减少锯齿、旋转、缩小、预览等。canvas几乎是最适合的解决方案。

手写

从交互上看,用户签名的过程,只有开始的手写部分是有交互的,后面是自动处理。为了完成手写,需要监听画布的两个事件:touchstart、touchmove(移动端touchend在touchmove之后不触发)。前者定义起始点,后者不停地描线。

const canvas = document.getElementById('canvas');
const touchstart = (e) => {
  /* TODO 定义起点 */
};
const touchmove = (e) => {
  /* TODO 连点成线,并且填充颜色 */
};
canvas.addEventListener('touchstart', touchstart);
canvas.addEventListener('touchmove', touchmove);

注: 以下默认canvas和context对象已有。

可以先戳这里体验把后面将要提到的签名组件 canvas-draw

描线

既然要连点成线,自然需要一个变量来存储这些点。

const point = {};

接下来就是画线的部分。canvas画线只需4行代码:

  1. 开始路径(beginPath)

  2. 定位起点(moveTo)

  3. 移动画笔(lineTo)

  4. 绘制路径(stroke)

考虑到start和move两个动作,那么一个描线的方法就呼之欲出了,如下:

const paint = (signal) => {
  switch (signal) {
    case 1: // 开始路径
      context.beginPath();
      context.moveTo(point.x, point.y);
    case 2: // 前面之所以没有break语句,是为了点击时就能描画出一个点
      context.lineTo(point.x, point.y);
      context.stroke();
      break;
  }
};

绑定事件

为了兼容PC端的类似需求,我们有必要区分下平台。移动端,使用手指操作,需要绑定的是touchstart和touchmove;PC端,使用鼠标操作,需要绑定的是mousedown和mousemove。如下一行代码可用于判断是否移动端:

const isMobile = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(navigator.userAgent);

描线的方法准备妥当后,剩下的就是在适当的时候,记录当前划过的点,并且调用paint方法进行绘制。这里可以抽象出一个事件生成器:

let pressed = false; // 标示是否发生鼠标按下或者手指按下事件
const create = signal => (e) => {
  if (signal === 1) {
    pressed = true;
  }
  if (signal === 1 || pressed) {
    e = isMobile ? e.touches[0] : e;
    point.x = e.clientX - left + 0.5; // 不加0.5,整数坐标处绘制直线,直线宽度将会多1px(不理解的不妨谷歌下)
    point.y = e.clientY - top + 0.5;
    paint(signal);
  }
};

以上代码中的left和top并非内置变量,它们分别表示着画布距屏幕左边和顶部的像素距离,主要用于将屏幕坐标点转换为画布坐标点。以下是一种获取方法:

const { left, top } = canvas.getBoundingClientRect();

很明显,上述的事件生成器是一个高阶函数,用于固化signal参数并返回一个新的Function。基于此,start和move回调便呈现了。

const start = create(1);
const move = create(2);

为了避免UI过度绘制,让move操作执行得更加流畅,requestAnimationFrame优化自然是少不了的。

const requestAnimationFrame = window.requestAnimationFrame;
const optimizedMove = requestAnimationFrame ? (e) => {
  requestAnimationFrame(() => {
    move(e);
  });
} : move;

剩下的也是绑定事件中关键的一步。PC端中,mousedown和mousemove没有先后顺序,不是每一次画布之上的鼠标移动都是有效的操作,因此我们使用pressed变量来保证mousemove事件回调只在mousedown事件之后执行。实际上,设置后的pressed变量总需要还原,还原的契机就是mouseup和mouseleave回调,由于mouseup事件并不总能触发(比如说鼠标移动到别的节点上才弹起,此时触发的是其他节点的mouseup事件),mouseleave便是鼠标移出画布时的兜底逻辑。而移动端的touch事件,其天然的连续性,保证了touchmove只会在touchstart之后触发,因此无须设置pressed变量,也不需要还原它。代码如下:

if (isMobile) {
  canvas.addEventListener('touchstart', start);
  canvas.addEventListener('touchmove', optimizedMove);
} else {
  canvas.addEventListener('mousedown', start);
  canvas.addEventListener('mousemove', optimizedMove);
  ['mouseup', 'mouseleave'].forEach((event) => {
    canvas.addEventListener(event, () => {
      pressed = false;
    });
  });
}

旋转

想要在移动端签名,往往面临着屏幕宽度不够的尴尬。竖屏下写不了几个汉字,甚至三个都够呛。如果app webview或浏览器不支持横屏展示,此时并不是意味着没有了办法,起码我们可以将整个网页旋转90°。

方案一:起初我的想法是将画布也一同旋转90°,后来发现难以处理旋转后的坐标系和屏幕坐标系的对应关系,因此我采取了旋转90°绘制页面,但是正常布局画布的方案,从而保证坐标系的一致性(这样就不用重新纠正canvas画布的坐标系了,关于纠正坐标系后续还有方案二,请耐心阅读)。

由于用户是横屏操作画布的,完成签名后,图片需要逆时针旋转90°才能保上传到服务器。因此还差一个旋转的方法。实际上,rotate方法可以旋转画布,drawImage方法可以在新的画布中绘制一张图片或老的画布,这种绘制的定制化程度很高。

rotate

rotate用于旋转当前的画布。

语法: rotate(angle),angle表示旋转的弧度,这里需要将角度转换为弧度计算,比如顺时针旋转90°,angle的值就等于-90 * Math.PI / 180。ratate旋转时默认以画布左上角为中心,如果需要以画布中心位置为中心,需要在rotate方法执行前将画布的坐标原点移至中心位置,旋转完成后,再移动回来。如下:

const { width, height } = canvas;
context.translate(width / 2, height / 2); // 坐标原点移至画布中心
context.rotate(90 * Math.PI / 180); // 顺时针旋转90°
context.translate(-width / 2, -height / 2); // 坐标原点还原到起始位置

实际上,这种变换处理,使用transform(Math.cos(90 * Math.PI / 180), 1, -1, Math.cos(90 * Math.PI / 180), 0, 0)同样可以顺时针旋转90°。

drawImage

drawImage用于绘制图片、画布或者视频,可自定义宽高、位置、甚至局部裁剪。它有三种形态的api:

  • drawImage(img,x,y),x,y为画布中的坐标,img可以是图片、画布或视频资源,表示在画布的指定坐标处绘制。

  • drawImage(img,x,y,width,height),width,height表示指定图片绘制后的宽高(可以任意缩放或调整宽高比例)。

  • context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height),sx,sy表示从指定的坐标位置裁剪原始图片,并且裁剪swidth的宽度和sheight的高度。

通常情况下,我们可能需要旋转一张图片90°、180°或者-90°。代码如下:

const rotate = (degree, image) => {
  degree = ~~degree;
  if (degree !== 0) {
    const maxDegree = 180;
    const minDegree = -90;
    if (degree > maxDegree) {
      degree = maxDegree;
    } else if (degree < minDegree) {
      degree = minDegree;
    }

    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    const height = image.height;
    const width = image.width;
    const angle = (degree * Math.PI) / 180;

    switch (degree) {
      // 逆时针旋转90°
      case -90:
        canvas.width = height;
        canvas.height = width;
        context.rotate(angle);
        context.drawImage(image, -width, 0);
        break;
      // 顺时针旋转90°
      case 90:
        canvas.width = height;
        canvas.height = width;
        context.rotate(angle);
        context.drawImage(image, 0, -height);
        break;
      // 顺时针旋转180°
      case 180:
        canvas.width = width;
        canvas.height = height;
        context.rotate(angle);
        context.drawImage(image, -width, -height);
        break;
    }
    image = canvas;
  }
  return image;
};

缩放

旋转后的画布,通常需要进一步格式化其宽高才能上传。此处还是利用drawImage去改变画布宽高,以达到缩小和放大的目的。如下:

const scale = (width, height) => {
  const w = canvas.width;
  const h = canvas.height;
  width = width || w;
  height = height || h;
  if (width !== w || height !== h) {
    const tmpCanvas = document.createElement('canvas');
    const tmpContext = tmpCanvas.getContext('2d');
    tmpCanvas.width = width;
    tmpCanvas.height = height;
    tmpContext.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
    canvas = tmpCanvas;
  }
  return canvas;
};

上传

我们做了这么多的操作和转换,最终的目的还是上传图片。

首先,获取画布中的图片:

const getPNGImage = () => {
  return canvas.toDataURL('image/png');
};

getPNGImage方法返回的是dataURL,需要转换为Blob对象才能上传。如下:

const dataURLtoBlob = (dataURL) => {
  const arr = dataURL.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bStr = atob(arr[1]);
  let n = bStr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bStr.charCodeAt(n);
  }
  return new Blob([u8arr], { type: mime });
};

完成了上面这些,才能一波ajax请求(xhr、fetch、axios都可)带走签名图片。

const upload = (blob, url, callback) => {
  const formData = new FormData();
  const xhr = new XMLHttpRequest();
  xhr.withCredentials = true;
  formData.append('image', blob, 'sign');

  xhr.open('POST', url, true);
  xhr.onload = () => {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      callback(xhr.responseText);
    }
  };
  xhr.onerror = (e) => {
    console.log(`upload img error: ${e}`);
  };
  xhr.send(formData);
};

设置

完成了上述功能,一个签名插件就已经成型了。除非你迫不及待想要发布,否则,这样的代码我是不建议拿出去的。一些必要的设置通常是不能忽略的。

通常画布中的直线是1px大小,这么细的线,是不能模拟笔触的,可如果你要放大至10px,便会发现,绘制的直线其实是矩形。这在签名过程中也是不合适的,我们期望的是圆滑的笔触,因此需要尽量模拟手写。实际上,lineCap就可指定直线首尾圆滑,lineJoin可以指定线条交汇时的边角圆滑。如下是一个simple的设置:

context.lineWidth = 10;         // 直线宽度
context.strokeStyle = 'black';     // 路径的颜色
context.lineCap = 'round';         // 直线首尾端圆滑
context.lineJoin = 'round';     // 当两条线条交汇时,创建圆形边角
context.shadowBlur = 1;         // 边缘模糊,防止直线边缘出现锯齿
context.shadowColor = 'black';  // 边缘颜色

优化

一切看似很完美,直到遇到了retina屏幕。retina屏是用4个物理像素绘制一个虚拟像素,屏幕宽度相同的画布,其每个像素点都会由4倍物理像素去绘制,画布中点与点之间的距离增加,会产生较为明显的锯齿,可通过放大画布然后压缩展示来解决这个问题。

let { width, height } = window.getComputedStyle(canvas, null);
width = width.replace('px', '');
height = height.replace('px', '');

// 根据设备像素比优化canvas绘图
const devicePixelRatio = window.devicePixelRatio;
if (devicePixelRatio) {
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  canvas.height = height * devicePixelRatio; // 画布宽高放大
  canvas.width = width * devicePixelRatio;
  context.scale(devicePixelRatio, devicePixelRatio); // 画布内容放大相同的倍数
} else {
  canvas.width = width;
  canvas.height = height;
}

重置坐标系

由于采取了方案一,签名的工作流变成了:『页面顺时针旋转90°绘制、画布正常竖屏绘制』—>『手写签名』—>『逆时针旋转画布90°』—> 『合理缩放画布至屏幕宽度』—> 『导出图片并上传』。由此可见方案一流程复杂,处理起来也比较麻烦。

换个角度想想,既然画布是可以旋转的,我刚好可以利用这种坐标系的反向旋转去抵消页面的正向旋转,这样页面上点的坐标就可以映射到画布本身的坐标上。于是有了方案二。

方案二:页面顺时针旋转90°,画布跟随着一起旋转(画布的坐标系也跟着旋转90°);然后再逆向旋转画布90°,重置画布的坐标系,使之与页面坐标系映射起来。

顺时针旋转90°的页面如下所示:

页面顺时针旋转90°

此时canvas画布也随着页面顺时针旋转90°,想要重置画布坐标系,可借由rotate逆向旋转90°,然后由translate平移坐标系。以下代码包含了顺逆时针旋转90°、180° 的处理(为了便于描述,假设画布充满屏幕):

context.rotate((degree * Math.PI) / 180);
switch (degree) {
  // 页面顺时针旋转90°后,画布左上角的原点位置落到了屏幕的右上角(此时宽高互换),围绕原点逆时针旋转90°后,画布与原位置垂直,居于屏幕右侧,需要向左平移画布当前高度相同的距离。
  case -90:
    context.translate(-height, 0);
    break;
  // 页面逆时针旋转90°后,画布左上角的原点位置落到了屏幕的左下角(此时宽高互换),围绕原点顺时针旋转90°后,画布与原位置垂直,居于屏幕下侧,需要向上平移画布当前宽度相同的距离。
  case 90:
    context.translate(0, -width);
    break;
  // 页面顺逆时针旋转180°回到了同一个位置(即页面倒立),画布左上角的原点位置落到了屏幕的右下角(此时宽高不变),围绕原点反方向旋转180°后,画布与原位置平行,居于屏幕右侧的下侧,需要向左平移画布宽度相同的距离,向右平移画布高度的距离。
  case -180:
  case 180:
    context.translate(-width, -height);
}

拥有了对画布坐标系重置的能力,我们能够将画布逆时针旋转90°、甚至180°,都是可行的。如下:

页面逆时针旋转90°

页面顺时针旋转180°

当然重置画布坐标系后,需要注意清屏时,清屏的范围也有可能发生变化,需要稍作如下处理。

const clear = () => {
  let width;
  let height;
  switch (this.degree) { // this.degree是画布坐标系旋转的度数
    case -90:
    case 90:
      width = this.height; // 画布旋转之前的高度
      height = this.width; // 画布选择之前的宽度
      break;
    default:
      width = this.width;
      height = this.height;
  }
  this.context.clearRect(0, 0, width, height);
};

方案一简单粗暴,布局上,canvas画布虽然不需要旋转,但需要单独绝对定位布局,给页面视觉展示带来不便,同时,上传图片之前需要对图片做旋转、缩放等处理,流程复杂。

方案二用纠正画布坐标系的方式,省去了布局和图片上的特殊处理,一步到位,因此方案二更佳。

以上,涉及的代码可以在这里找到:canvas-draw,这是一个借助vue cli 搭建起来的壳,主要是为了方便调试,核心代码见 canvas-draw/draw.js,喜欢的同学不妨轻点star。


本问就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论.

本文作者:louis

本文链接: http://louiszhai.github.io/20...

参考文章:

查看原文

赞 10 收藏 29 评论 6

xiter 收藏了文章 · 2020-11-17

匠心打造canvas签名组件

本文首发于CSDN网站,下面的版本又经过进一步的修订。
原文:匠心打造canvas签名组件

导读

6月又是项目吃紧的时候,一大波需求袭来,猝不及防。

度过了漫长而煎熬的6月,是时候总结一波。最近移动端的一款产品原计划是引入第三方的签名插件,该插件依赖复杂,若干个js使用document.write顺序加载,插件源码是ES5的,甚至说是ES3都不为过。为了能够顺利嵌入我们的VUE项目,我阅读了两天插件的源码(demo及文档不全,囧),然后花了一天多点的时间使用ES6引用它。鉴于单页应用中,任何非全局资源都不该提前加载的指导性原则,为了做到动态加载,我甚至还专门写了一个simple的vue组件iload.js去顺序加载这些资源并执行回调。一切看似很完美,结果发现demo引用的一个压缩的js中居然写死了插件相关DOM节点的id和style,此刻我的内心几乎是崩溃的。这样的一个插件我怕是无力引入了吧。

虽然嘴上这么说,身体还是很诚实的,费尽千辛万苦我还是把这个插件用在了项目中。随着项目推进,业务上经过多次沟通,我们砍掉了该签名插件的数字证书验证部分。也就是说,这么大的一个插件,只剩下用户签名的功能,我完全可以自己做啊。于是我悄悄移除了这个插件,为这几天的调研和码字过程划上了一个完美的句号(深藏功与名)。

签名是若干操作的集合,起于用户手写姓名,终于签名图片上传,中间还包含图片的处理,比如说减少锯齿、旋转、缩小、预览等。canvas几乎是最适合的解决方案。

手写

从交互上看,用户签名的过程,只有开始的手写部分是有交互的,后面是自动处理。为了完成手写,需要监听画布的两个事件:touchstart、touchmove(移动端touchend在touchmove之后不触发)。前者定义起始点,后者不停地描线。

const canvas = document.getElementById('canvas');
const touchstart = (e) => {
  /* TODO 定义起点 */
};
const touchmove = (e) => {
  /* TODO 连点成线,并且填充颜色 */
};
canvas.addEventListener('touchstart', touchstart);
canvas.addEventListener('touchmove', touchmove);

注: 以下默认canvas和context对象已有。

可以先戳这里体验把后面将要提到的签名组件 canvas-draw

描线

既然要连点成线,自然需要一个变量来存储这些点。

const point = {};

接下来就是画线的部分。canvas画线只需4行代码:

  1. 开始路径(beginPath)

  2. 定位起点(moveTo)

  3. 移动画笔(lineTo)

  4. 绘制路径(stroke)

考虑到start和move两个动作,那么一个描线的方法就呼之欲出了,如下:

const paint = (signal) => {
  switch (signal) {
    case 1: // 开始路径
      context.beginPath();
      context.moveTo(point.x, point.y);
    case 2: // 前面之所以没有break语句,是为了点击时就能描画出一个点
      context.lineTo(point.x, point.y);
      context.stroke();
      break;
  }
};

绑定事件

为了兼容PC端的类似需求,我们有必要区分下平台。移动端,使用手指操作,需要绑定的是touchstart和touchmove;PC端,使用鼠标操作,需要绑定的是mousedown和mousemove。如下一行代码可用于判断是否移动端:

const isMobile = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i.test(navigator.userAgent);

描线的方法准备妥当后,剩下的就是在适当的时候,记录当前划过的点,并且调用paint方法进行绘制。这里可以抽象出一个事件生成器:

let pressed = false; // 标示是否发生鼠标按下或者手指按下事件
const create = signal => (e) => {
  if (signal === 1) {
    pressed = true;
  }
  if (signal === 1 || pressed) {
    e = isMobile ? e.touches[0] : e;
    point.x = e.clientX - left + 0.5; // 不加0.5,整数坐标处绘制直线,直线宽度将会多1px(不理解的不妨谷歌下)
    point.y = e.clientY - top + 0.5;
    paint(signal);
  }
};

以上代码中的left和top并非内置变量,它们分别表示着画布距屏幕左边和顶部的像素距离,主要用于将屏幕坐标点转换为画布坐标点。以下是一种获取方法:

const { left, top } = canvas.getBoundingClientRect();

很明显,上述的事件生成器是一个高阶函数,用于固化signal参数并返回一个新的Function。基于此,start和move回调便呈现了。

const start = create(1);
const move = create(2);

为了避免UI过度绘制,让move操作执行得更加流畅,requestAnimationFrame优化自然是少不了的。

const requestAnimationFrame = window.requestAnimationFrame;
const optimizedMove = requestAnimationFrame ? (e) => {
  requestAnimationFrame(() => {
    move(e);
  });
} : move;

剩下的也是绑定事件中关键的一步。PC端中,mousedown和mousemove没有先后顺序,不是每一次画布之上的鼠标移动都是有效的操作,因此我们使用pressed变量来保证mousemove事件回调只在mousedown事件之后执行。实际上,设置后的pressed变量总需要还原,还原的契机就是mouseup和mouseleave回调,由于mouseup事件并不总能触发(比如说鼠标移动到别的节点上才弹起,此时触发的是其他节点的mouseup事件),mouseleave便是鼠标移出画布时的兜底逻辑。而移动端的touch事件,其天然的连续性,保证了touchmove只会在touchstart之后触发,因此无须设置pressed变量,也不需要还原它。代码如下:

if (isMobile) {
  canvas.addEventListener('touchstart', start);
  canvas.addEventListener('touchmove', optimizedMove);
} else {
  canvas.addEventListener('mousedown', start);
  canvas.addEventListener('mousemove', optimizedMove);
  ['mouseup', 'mouseleave'].forEach((event) => {
    canvas.addEventListener(event, () => {
      pressed = false;
    });
  });
}

旋转

想要在移动端签名,往往面临着屏幕宽度不够的尴尬。竖屏下写不了几个汉字,甚至三个都够呛。如果app webview或浏览器不支持横屏展示,此时并不是意味着没有了办法,起码我们可以将整个网页旋转90°。

方案一:起初我的想法是将画布也一同旋转90°,后来发现难以处理旋转后的坐标系和屏幕坐标系的对应关系,因此我采取了旋转90°绘制页面,但是正常布局画布的方案,从而保证坐标系的一致性(这样就不用重新纠正canvas画布的坐标系了,关于纠正坐标系后续还有方案二,请耐心阅读)。

由于用户是横屏操作画布的,完成签名后,图片需要逆时针旋转90°才能保上传到服务器。因此还差一个旋转的方法。实际上,rotate方法可以旋转画布,drawImage方法可以在新的画布中绘制一张图片或老的画布,这种绘制的定制化程度很高。

rotate

rotate用于旋转当前的画布。

语法: rotate(angle),angle表示旋转的弧度,这里需要将角度转换为弧度计算,比如顺时针旋转90°,angle的值就等于-90 * Math.PI / 180。ratate旋转时默认以画布左上角为中心,如果需要以画布中心位置为中心,需要在rotate方法执行前将画布的坐标原点移至中心位置,旋转完成后,再移动回来。如下:

const { width, height } = canvas;
context.translate(width / 2, height / 2); // 坐标原点移至画布中心
context.rotate(90 * Math.PI / 180); // 顺时针旋转90°
context.translate(-width / 2, -height / 2); // 坐标原点还原到起始位置

实际上,这种变换处理,使用transform(Math.cos(90 * Math.PI / 180), 1, -1, Math.cos(90 * Math.PI / 180), 0, 0)同样可以顺时针旋转90°。

drawImage

drawImage用于绘制图片、画布或者视频,可自定义宽高、位置、甚至局部裁剪。它有三种形态的api:

  • drawImage(img,x,y),x,y为画布中的坐标,img可以是图片、画布或视频资源,表示在画布的指定坐标处绘制。

  • drawImage(img,x,y,width,height),width,height表示指定图片绘制后的宽高(可以任意缩放或调整宽高比例)。

  • context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height),sx,sy表示从指定的坐标位置裁剪原始图片,并且裁剪swidth的宽度和sheight的高度。

通常情况下,我们可能需要旋转一张图片90°、180°或者-90°。代码如下:

const rotate = (degree, image) => {
  degree = ~~degree;
  if (degree !== 0) {
    const maxDegree = 180;
    const minDegree = -90;
    if (degree > maxDegree) {
      degree = maxDegree;
    } else if (degree < minDegree) {
      degree = minDegree;
    }

    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    const height = image.height;
    const width = image.width;
    const angle = (degree * Math.PI) / 180;

    switch (degree) {
      // 逆时针旋转90°
      case -90:
        canvas.width = height;
        canvas.height = width;
        context.rotate(angle);
        context.drawImage(image, -width, 0);
        break;
      // 顺时针旋转90°
      case 90:
        canvas.width = height;
        canvas.height = width;
        context.rotate(angle);
        context.drawImage(image, 0, -height);
        break;
      // 顺时针旋转180°
      case 180:
        canvas.width = width;
        canvas.height = height;
        context.rotate(angle);
        context.drawImage(image, -width, -height);
        break;
    }
    image = canvas;
  }
  return image;
};

缩放

旋转后的画布,通常需要进一步格式化其宽高才能上传。此处还是利用drawImage去改变画布宽高,以达到缩小和放大的目的。如下:

const scale = (width, height) => {
  const w = canvas.width;
  const h = canvas.height;
  width = width || w;
  height = height || h;
  if (width !== w || height !== h) {
    const tmpCanvas = document.createElement('canvas');
    const tmpContext = tmpCanvas.getContext('2d');
    tmpCanvas.width = width;
    tmpCanvas.height = height;
    tmpContext.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
    canvas = tmpCanvas;
  }
  return canvas;
};

上传

我们做了这么多的操作和转换,最终的目的还是上传图片。

首先,获取画布中的图片:

const getPNGImage = () => {
  return canvas.toDataURL('image/png');
};

getPNGImage方法返回的是dataURL,需要转换为Blob对象才能上传。如下:

const dataURLtoBlob = (dataURL) => {
  const arr = dataURL.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bStr = atob(arr[1]);
  let n = bStr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bStr.charCodeAt(n);
  }
  return new Blob([u8arr], { type: mime });
};

完成了上面这些,才能一波ajax请求(xhr、fetch、axios都可)带走签名图片。

const upload = (blob, url, callback) => {
  const formData = new FormData();
  const xhr = new XMLHttpRequest();
  xhr.withCredentials = true;
  formData.append('image', blob, 'sign');

  xhr.open('POST', url, true);
  xhr.onload = () => {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      callback(xhr.responseText);
    }
  };
  xhr.onerror = (e) => {
    console.log(`upload img error: ${e}`);
  };
  xhr.send(formData);
};

设置

完成了上述功能,一个签名插件就已经成型了。除非你迫不及待想要发布,否则,这样的代码我是不建议拿出去的。一些必要的设置通常是不能忽略的。

通常画布中的直线是1px大小,这么细的线,是不能模拟笔触的,可如果你要放大至10px,便会发现,绘制的直线其实是矩形。这在签名过程中也是不合适的,我们期望的是圆滑的笔触,因此需要尽量模拟手写。实际上,lineCap就可指定直线首尾圆滑,lineJoin可以指定线条交汇时的边角圆滑。如下是一个simple的设置:

context.lineWidth = 10;         // 直线宽度
context.strokeStyle = 'black';     // 路径的颜色
context.lineCap = 'round';         // 直线首尾端圆滑
context.lineJoin = 'round';     // 当两条线条交汇时,创建圆形边角
context.shadowBlur = 1;         // 边缘模糊,防止直线边缘出现锯齿
context.shadowColor = 'black';  // 边缘颜色

优化

一切看似很完美,直到遇到了retina屏幕。retina屏是用4个物理像素绘制一个虚拟像素,屏幕宽度相同的画布,其每个像素点都会由4倍物理像素去绘制,画布中点与点之间的距离增加,会产生较为明显的锯齿,可通过放大画布然后压缩展示来解决这个问题。

let { width, height } = window.getComputedStyle(canvas, null);
width = width.replace('px', '');
height = height.replace('px', '');

// 根据设备像素比优化canvas绘图
const devicePixelRatio = window.devicePixelRatio;
if (devicePixelRatio) {
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  canvas.height = height * devicePixelRatio; // 画布宽高放大
  canvas.width = width * devicePixelRatio;
  context.scale(devicePixelRatio, devicePixelRatio); // 画布内容放大相同的倍数
} else {
  canvas.width = width;
  canvas.height = height;
}

重置坐标系

由于采取了方案一,签名的工作流变成了:『页面顺时针旋转90°绘制、画布正常竖屏绘制』—>『手写签名』—>『逆时针旋转画布90°』—> 『合理缩放画布至屏幕宽度』—> 『导出图片并上传』。由此可见方案一流程复杂,处理起来也比较麻烦。

换个角度想想,既然画布是可以旋转的,我刚好可以利用这种坐标系的反向旋转去抵消页面的正向旋转,这样页面上点的坐标就可以映射到画布本身的坐标上。于是有了方案二。

方案二:页面顺时针旋转90°,画布跟随着一起旋转(画布的坐标系也跟着旋转90°);然后再逆向旋转画布90°,重置画布的坐标系,使之与页面坐标系映射起来。

顺时针旋转90°的页面如下所示:

页面顺时针旋转90°

此时canvas画布也随着页面顺时针旋转90°,想要重置画布坐标系,可借由rotate逆向旋转90°,然后由translate平移坐标系。以下代码包含了顺逆时针旋转90°、180° 的处理(为了便于描述,假设画布充满屏幕):

context.rotate((degree * Math.PI) / 180);
switch (degree) {
  // 页面顺时针旋转90°后,画布左上角的原点位置落到了屏幕的右上角(此时宽高互换),围绕原点逆时针旋转90°后,画布与原位置垂直,居于屏幕右侧,需要向左平移画布当前高度相同的距离。
  case -90:
    context.translate(-height, 0);
    break;
  // 页面逆时针旋转90°后,画布左上角的原点位置落到了屏幕的左下角(此时宽高互换),围绕原点顺时针旋转90°后,画布与原位置垂直,居于屏幕下侧,需要向上平移画布当前宽度相同的距离。
  case 90:
    context.translate(0, -width);
    break;
  // 页面顺逆时针旋转180°回到了同一个位置(即页面倒立),画布左上角的原点位置落到了屏幕的右下角(此时宽高不变),围绕原点反方向旋转180°后,画布与原位置平行,居于屏幕右侧的下侧,需要向左平移画布宽度相同的距离,向右平移画布高度的距离。
  case -180:
  case 180:
    context.translate(-width, -height);
}

拥有了对画布坐标系重置的能力,我们能够将画布逆时针旋转90°、甚至180°,都是可行的。如下:

页面逆时针旋转90°

页面顺时针旋转180°

当然重置画布坐标系后,需要注意清屏时,清屏的范围也有可能发生变化,需要稍作如下处理。

const clear = () => {
  let width;
  let height;
  switch (this.degree) { // this.degree是画布坐标系旋转的度数
    case -90:
    case 90:
      width = this.height; // 画布旋转之前的高度
      height = this.width; // 画布选择之前的宽度
      break;
    default:
      width = this.width;
      height = this.height;
  }
  this.context.clearRect(0, 0, width, height);
};

方案一简单粗暴,布局上,canvas画布虽然不需要旋转,但需要单独绝对定位布局,给页面视觉展示带来不便,同时,上传图片之前需要对图片做旋转、缩放等处理,流程复杂。

方案二用纠正画布坐标系的方式,省去了布局和图片上的特殊处理,一步到位,因此方案二更佳。

以上,涉及的代码可以在这里找到:canvas-draw,这是一个借助vue cli 搭建起来的壳,主要是为了方便调试,核心代码见 canvas-draw/draw.js,喜欢的同学不妨轻点star。


本问就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论.

本文作者:louis

本文链接: http://louiszhai.github.io/20...

参考文章:

查看原文

xiter 赞了文章 · 2020-10-09

有赞美业前端: 持续标准化 Code Review

关键字:代码质量 团队建设 流程优化

一、背景

1. 技术栈

美业技术团队前端对外的业务项目的主要编程技术栈是:

image

2. 团队架构

在构建项目的前期,前端对业务项目按端来划分人员,各端各司其职,各自沉淀。

中期随着产品的基本成型,前端团队人员按照业务领域划分成了多个子业务组前端,各组负责4端中对应模块的业务。

于是,我们美业团队20个前端几乎每个人都要维护4个不同的编码上下文的项目,好处是技术多样性丰富,但是瓶颈也同样存在,一个人需要拥有多端的开发能力,在编码规范和代码风格检查尽可能统一的情况下,因为上述技术体系的差异,我们还是不得不需要熟悉四端的技术架构、开发流程、数据流处理、资产市场、最佳实践。

这是很有挑战的,业务小组之间成立了一个小型的前端技术委员会,来应对这种变化:

  • 总结原先的项目技术规范,统一宣讲、培训、文档化
  • 打造统一标准化的研发流程
  • 并且持续汲取新的实践并迭代

image

3. 代码质量问题

随即,我们在代码质量上迎来了一些问题:

  • 项目Bug较多,同样的坑不同的人会踩
  • 迭代后的代码难维护,包括代码可读性差、复用度低等
  • 模块的整体设计也欠缺,扩展能力难以支撑业务发展。

对代码质量的把控方面,现状流程是:我们半年要对几端的项目代码进行一次整体的code review。

但是和垃圾回收一样,整体的标记清除占用人员的时间较长,会影响届时涉及人员的业务开发进度。

于是我们想探索一种适合我们团队和业务发展,小步快跑模式的code Review,尽可能早的从一开始就参与进来,更高频率,增强审查和设计把控,减少后面返工和带来Bug所影响的整体效率。

二、定义需求

有了这样的背景和改进诉求,我发现我们得重新定义一下我们做这件事情的目的和价值。

经过思考和讨论,我们做 Code Review 的核心目的只有两个:

1. 从源头把控代码质量和效率

  • 统一代码评判标准和认知
  • 发现边界问题
  • 提出改进建议

2. 共享和迭代集体代码智慧

  • 交流计思路和编码实践
  • 沉淀最佳实践
  • 迭代统一规范

同时要做上述理想中的 Code Review,我们可能不得不面临这些实践过程中会遇到的问题:

image

基于这些诉求和待解决问题,我们需要对整体的标准和每一次 Code Review 的关键控制点进行细化和量化,于是有了我们第一版 Code Review 的 SOP(标准作业流程)。

三、V1.0 标准化

1. 建立规范

1.1 宣讲

宣讲各端统一代码规范和最佳实践、编码原则。Code Review的基础是有基本的代码规范和原则,同时要同步给大家。

1.2 review 小组

成立专门的 code review 小组,小组成员是各端经验丰富的人员,这样才能比较好的保障初期 Review 有比较好的效果,可以让 Code 人员有更大所获,先富带后富,经过多次 Code Review 对齐标准之后,更多 Code 优秀的同学也可以加入进来,讨论对规范和原则的实践。

1.3 设定可迭代的代码质量评价维度和标准:

每项1~5分,基准分为3分,得分在此基础上根据评分点浮动,总评分为各项得分的平均分。

① 基本面:对团队既定规范的遵循和代码开发的改动量是我们做评分的第一个基础。

  • 难度大、工作量大,可酌情加分
  • 是否符合基本规范

② 架构设计:是否有整体设计,设计是否合理,设计是否遵循了设计,这是第二个维度

  • 如果有设计文档,是否按照设计文档思路来写代码,是可酌情加分
  • review的人是否发现了更好的解决方案
  • 代码是否提供了很好的解决思路

③ 代码:代码细节上是否尽可能保持简单和易读,同时鼓励细节优化是我们的第三个维度

  • 是否明显重复代码
  • 是否合理抽取枚举值,禁止使用“魔法值”
  • 是否合理使用已有的组件和方法
  • 对已有的、不合理的代码进行重构和优化
  • 职责(组件、方法)、概念是否清晰

④ 健壮性:错误处理、业务逻辑的边界和基本的安全性是我们的第四个维度

  • 边界和异常是否考虑完备
  • 在review阶段是否发现明显bug
  • 是否考虑安全性(xss)

⑤ 效率:是否贡献了整体,为物料库和工具库添砖加瓦,与整体沉淀形成闭环,是我们第五个维度的初衷

  • 是否抽取共用常量到beauty-const库,加分
  • 是否抽取沉淀基础组件和通用业务组件到组件库,加分

1.4 申请格式

Code 人在企业微信群发起 Review 申请,统一参考格式,内容包括:

mr地址、产品文档、UI稿、技术设计、效率平台、接口文档

原则是能为 Review 方尽可能提供足够的信息来判断 Code 的好坏,去更好发现深层次问题。
当然文档也说不到全部的上下文,所以我们需要分配 Review 人员时候要考虑技术栈和业务相关熟悉度,必要时候 Code 人员要向 Review 人员口述需求、代码思路和重点。

1.5 review 要求

  • code review 必须在提测前进行,确保上线前能够完成 Review。
  • review 人根据 code review 评分标准 打出各项评分,计算出本次 code review 总评分
  • 有需要可在备注中说明原因,代码相关的备注可以直接在 gitlab 进行
  • code 需要解决反馈的问题
  • 要求提测邮件中体现 code review 情况(评分、遗留问题)
  • mr 统一由 feature 分支合到 release 分支
  • review 记录,定期同步分享

1.6 review 重点

  • 建议check代码改动范围,重点关注核心代码改动的影响
  • review可以针对重点代码进行
  • 每项checklist,如果有不符合checklist内容的,请在后面【评分解释】中具体指出
  • 「讨论沉淀」内容可包括但不限于:技术设计情况、review过程中发现的亮点与不足、值得探讨的东西、发现的bug
  • 周会定期同步 review 情况,分享优秀代码

2. 单次流程

image

3. 产出示例

image

  • 通过统一提交文档和口述,Code 方养成了技术设计和理清重点和评估影响范围的习惯。
  • 通过讨论和反馈 Code Review 双方都从讨论中获取到了编码智慧的碰撞和交流,整体的代码水平总和得到了提升。
  • 通过 Review,代码的总体设计和细节得到了更好的保障。
  • 通过沉淀最佳实践和改进建议,团队规范和 Code Review 形成了内循环。

四、V2.0 平台化

1.0版本在持续的细节迭代,做到了比较满意的标准化作业,但是有几个比较大的缺陷:

1.操作欠缺自动化

  • 流程的很多环节明显可以自动化,节省重复的工作量
  • 对流程的把控依赖人,容易执行不到位

2.信息欠缺数字化

  • 对 code review 的评分统计需要人工,工作量大
  • code review 的总览和数据分析可以支撑更好的判断团队问题和决策提升整体代码质量的策略

3.流程欠缺可视化

  • 所有流程应该是可以大盘总览,单个详情全面的
  • 每个code review事务的状态是可见的

所以我们有了把 Code Review 整套流程做成已有的内部前端工具平台中一个模块的想法,以期达到可视化、自动化、数字化的目的。

投入产出比是我们需要考虑的,我们很笃定。因为虽然这件事情没有直接的业务价值,但是有非常好的质量把控和能力量化的价值,并且有标杆式的团队建设价值,人员成长了,更好地为业务服务。

1. 需求分析

image

在完成上述基本需求之后,我们同时在收集反馈新增了

  1. code 人可指定 review 人员
  2. 支持项目多端配置
  3. 首页 review 得分榜排名展示
  4. 最佳实践统一导出
  5. 打通关联项目平台串联项目

2. 技术设计

image
image

结合数据库表设计之后,我们就分工开整了。

3. 产品效果图

image
image
image

  • Code Review 平台化之后,打通了相关平台,自动化了人工的重复操作,聚焦在 Code 和 思考上面,无需关心流程状态。
  • 团队对 Code 情况有了更好的全局性把控,能够进一步根据情况和数据分析对代码质量提升做决策。

五、可持续保障机制

前人种树,后人除了乘凉之外得继续浇灌。流程和机制是死的,我们得用一些更加有温度的一些策略让它持续可以迭代和发展继承下去。

  1. 半年度颁奖:半年我们会把半年大家的评分成绩统计出来,做一次激励,树立标杆,鼓励大家继续写出更好的代码,也继续的配合 Code Review。
  2. 专人 Owner:作为一个技术项目来持续维护和收集反馈意见迭代,服务小伙伴,为团队建设助力。
  3. 纳入考核:作为复杂的大型 SaaS 项目的开发者,代码能力是我们考核小伙伴专业能力的重要维度。
附带一些半年颁奖的图:

image
image
image
image
image

本文篇幅有限,实践过程中很多的细节问题和处理没有阐述,比如 code、review 双方的协作处理等。欢迎进一步讨论。

微信:zz94530

目前有赞深圳研发团队大量招聘高级前端,欢迎咨询和投简历~

查看原文

赞 47 收藏 20 评论 15

xiter 关注了专栏 · 2020-10-09

有赞美业前端团队

关注 2795

xiter 赞了文章 · 2019-11-29

图解:JavaScript中Number的一些表示上/下限

自己整理、设计的,转载请注明原帖。先从这个demo看起:http://alvarto.github.io/Visu...

数轴

请输入图片描述

说明

关于Number表示的内存模型

参考国际标准IEEE 754,我画了一张图帮助理解:

请输入图片描述

注,这里的字符是从左到右排的,和wiki之类的资料顺序相反。wiki资料考虑的是比较的顺序(符号-指数位-有效数字),而我这里考虑到的是阅读顺序(从0到63位,从左到右)。

中间的指数位是如何同时表示正负指数值的呢,和“符号位+有效数字位”的常规表示方法不同,指数是使用偏移法来做的:

IEEE 754:指数偏移值

指数偏移值(exponent bias),是指浮点数表示法中的指数域的编码值为指数的实际值加上某个固定的值,IEEE 754标准规定该固定值为2^(e-1)-1,其中的e为存储指数的比特的长度。
以单精度浮点数为例,它的指数域是8个比特,固定偏移值是28-1 - 1 = 128−1 = 127.单精度浮点数的指数部分实际取值是从128到-127。例如指数实际值为1710,在单精度浮点数中的指数域编码值为14410,即14410 = 1710 + 12710.
采用指数的实际值加上固定的偏移值的办法表示浮点数的指数,好处是可以用长度为e个比特的无符号整数来表示所有的指数取值,这使得两个浮点数的指数大小的比较更为容易。

因此,在JavaScript里面的指数位,是从1-2^(11-1),也就是从-1023开始,表示了(-1023,1024)这个区间。

实际指数值存储的指数值
-10221
01023
10232046

Number保留了指数值0和2047用于表示一些特殊的值。总的表示表格如下:

XY表示的值
=0=0±0
≠0=2047NaN
=0=2047±Infinity
≠0=0反规格化值(Denormalized):f(0.x , 1 , z)
∈(0,2047)规格化值(Normalized):f(1.x , y , z)
<h3>f(i,j,k) = (-1)k · 2-1023+j · i</h3>

精确表示到个位的最大整数

前52位能表示的最大值是下面这个(下面是52位+1位默认的1):

parseInt("11111111111111111111111111111111111111111111111111111",2)
-> 9007199254740991 //即2^53-1

而下一个值是:

parseInt("100000000000000000000000000000000000000000000000000000",2)
-> 9007199254740992 //即2^53

根据内存模型,画一张图就可以知道:

请输入图片描述

从第2^53位开始,第一个进制被舍弃,这个时候,2^53+1==2^53,每两个值都会有一个值出现这种不精确的情形。再过N个值,会出现每4个值里面都有3个值不精确;再过M个值,会出现每2^K个值里有2^K-1个值不精确;以此类推……(小题目:这个N值是多少?)

最大可表示的正数

请输入图片描述

验证:

Number.MAX_VALUE.toString(2)
-> "1111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

var a = Number.MAX_VALUE.toString(2).split("") , b = [ a.filter(function(i){return i==0}).length , a.filter(function(i){return i==1}).length ] ; b
-> [971, 53]

Number.MAX_VALUE === (Math.pow(2,53)-1)*Math.pow(2,971)
-> true

QED

最小可表示的正数

还记得前面的表格吗:

XY表示的值
≠0=0反规格化值(Denormalized):f(0.x , 1 , z)
∈(0,2047)规格化值(Normalized):f(1.x , y , z)
<h3>f(i,j,k) = (-1)k · 2-1023+j · i</h3>

非规格化值是这样表示的:

请输入图片描述

最小正数的内存模型

请输入图片描述

验证:

Number.MIN_VALUE.toString(2)
-> "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"

var a = Number.MIN_VALUE.toString(2).split(""); a.filter(function(i){return i==0}).length - 1
-> 1073

Number.MIN_VALUE === Math.pow(2,-1074)
-> true

参考资料

除了IEEE 754的维基页面,还有这篇文章,解释的非常清晰:"How numbers are encoded in JavaScript"

最后再推一次:输入表达式,返回对应Number值的内存模型的DEMO
http://alvarto.github.io/Visu...

查看原文

赞 30 收藏 56 评论 16

认证与成就

  • 获得 19 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-07-28
个人主页被 677 人浏览