XXHolic

XXHolic 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/XXHolic 编辑
编辑

个人动态

XXHolic 发布了文章 · 2020-08-28

Canvas 图像灰度处理

引子

在玩游戏的时候,碰到一个交互效果:背景一张看起来黑白的图,然后用擦除的交互,让图像变的有颜色。也想试试做这个效果,首先想到的是那个黑白的图是怎么形成的,于是就查资料,找到了用 Canvas 转换的方法。

思路

看起来是黑白的图像,其实是灰度图像,进一步说明见图像。这种图像的特点是像素的颜色分量取值都是一样的,而 Canvas 的方法 getImageData 可以获取到画布上的像素值数据,改变数据后,使用方法 putImageData 将数据绘制到画布上。这样就可以达到灰度图像的效果。

实现

这是示例页面,移动端访问如下:

65-gray

主要实现如下:

  /**
   * 图像灰度处理
   * @param {object} context canvas 上下文
   * @param {number} sx 提取图像数据矩形区域的左上角 x 坐标。
   * @param {number} sy 提取图像数据矩形区域的左上角 y 坐标。
   * @param {number} sw 提取图像数据矩形区域的宽度。这要注意一下,canvas 标签上 width 属性值,不是渲染后实际宽度值,否则在高清手机屏幕下且做了高清处理,只能获取到部分图像宽度。
   * @param {number} sh 提取图像数据矩形区域的高度。这要注意一下,canvas 标签上 height 属性值,不是渲染后实际高度值,否则在高清手机屏幕下且做了高清处理,只能获取到部分图像高度。
   */
  function toGray(context,sx, sy, sw, sh) {
    var imageData = context.getImageData(sx, sy, sw, sh);
    var colorDataArr = imageData.data;
    var colorDataArrLen = colorDataArr.length;
    for(var i = 0; i < colorDataArrLen; i+=4) {
      // 计算方式之一
      var gray=(colorDataArr[i]+colorDataArr[i+1]+colorDataArr[i+2])/3;
      colorDataArr[i] = gray;
      colorDataArr[i+1] = gray;
      colorDataArr[i+2] = gray;
    }
    context.putImageData(imageData,0,0);
  }

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-08-28

canvas 文本坐标(0,0)显示问题

引子

在测试 canvas 文字显示的时候,发现坐标设为(0,0),文字显示会有问题。

文本坐标(0,0)显示问题

刚开始本以为使用 canvas 的方法不对,尝试改变坐标后,发现又可以显示。这是问题示例,扫描访问二维码如下。

27-qrcode-problem

查询资料,发现了类似的问题,原因是 canvas 中的文本坐标位置,是按照属性 textBaseline 设置的基线作为参考,默认值是 alphabetic。效果如下图。

27-img-textbaseline

当位置坐标为(0,0)时,文本基线以上的就不在 canvas 显示区域内了,详细文档见 MDN textBaseline。将 textBaseline 设置为 top 就可以正常显示,这是正常示例,扫描访问二维码如下。

27-qrcode-normal

在测试的过程中,发现英文可以正常显示,但中文,字体大小不同,顶部显示可能有稍微的截断。目前想到的解决方法有:

  • 调整到适当的字体大小。
  • 将文本显示的位置稍微的下移。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-08-03

canvas 图片跨域处理

引子

近期的工作中处理图片合并时,碰到图片来源跨域的情况,在此记录。

图片跨域处理

在用 canvas 合成图片时,放在画布里面的图片,有些图片源是另外一个域名,由于同源策略,首先需要在服务配置中添加对应的 Access-Control-Allow-Origin,允许对应域名的请求。在这次处理过程中,还发现如果有用 CDN 进行加速,那么对应的 CDN 的配置也要添加这个请求头。

此外在合成图片的时候,要给对应的图片添加 crossOrigin 属性,否则会被认为污染了画布,无法继续合成。详细可见文档说明。

var img = new Image();
img.crossOrigin = "Anonymous";
img.src = '***';
img.onload = function() {}

最终合成图片的处理,要在图片加载完成的事件处理程序中才行,不然对应图片不会出现在合成的图片中。

参考资料

查看原文

赞 1 收藏 1 评论 2

XXHolic 发布了文章 · 2020-07-20

canvas 文字换行

引子

近期的工作中,遇到的功能需求,需要控制文字显示行数,超过就省略号显示。

文字换行

一般文字行数控制用 css 就可以实现,但在 canvas 中不行。在网站查询资料,就可以发现需要程序控制文字换行,主要使用到的方法是 measureText(),这个方法会返回一个度量文本的相关信息的对象,例如文本的宽度。

这里会有一个边界问题:如果文字在 canvas 边界出现换行,那么就可能出现文字显示不全的问题。

主要处理方法如下:

// 文本换行处理,并返回实际文字所占据的高度
function textEllipsis (context, text, x, y, maxWidth, lineHeight, row) {
  if (typeof text != 'string' || typeof x != 'number' || typeof y != 'number') {
    return;
  }
  var canvas = context.canvas;

  if (typeof maxWidth == 'undefined') {
    maxWidth = canvas && canvas.width || 300;
  }

  if (typeof lineHeight == 'undefined') {
    // 有些情况取值结果是字符串,比如 normal。所以要判断一下
    var getLineHeight = window.getComputedStyle(canvas).lineHeight;
    var reg=/^[0-9]+.?[0-9]*$/;
    lineHeight = reg.test(getLineHeight)? getLineHeight:20;
  }

  // 字符分隔为数组
  var arrText = text.split('');
  // 文字最终占据的高度,放置在文字下面的内容排版,可能会根据这个来确定位置
  var textHeight = 0;
  // 每行显示的文字
  var showText = '';
  // 控制行数
  var limitRow = row;
  var rowCount = 0;

  for (var n = 0; n < arrText.length; n++) {
    var singleText = arrText[n];
    var connectShowText = showText + singleText;
    // 没有传控制的行数,那就一直换行
    var isLimitRow = limitRow ? rowCount === (limitRow - 1) : false;
    var measureText = isLimitRow ? (connectShowText+'……') : connectShowText;
    var metrics = context.measureText(measureText);
    var textWidth = metrics.width;

    if (textWidth > maxWidth && n > 0 && rowCount !== limitRow) {
      var canvasShowText = isLimitRow ? measureText:showText;
      context.fillText(canvasShowText, x, y);
      showText = singleText;
      y += lineHeight;
      textHeight += lineHeight;
      rowCount++;
      if (isLimitRow) {
        break;
      }
    } else {
      showText = connectShowText;
    }
  }
  if (rowCount !== limitRow) {
    context.fillText(showText, x, y);
  }

  var textHeightValue = rowCount < limitRow ? (textHeight + lineHeight): textHeight;
  return textHeightValue;
}

这是示例,扫描访问二维码如下。

19-canvas-canvas-text

参考资料

查看原文

赞 1 收藏 1 评论 0

XXHolic 发布了文章 · 2020-07-13

canvas 图片圆角问题

引子

近期的工作中,是继 canvas 设置边框问题 之后碰到的第 4 个问题。

图片圆角问题

如果只是想要显示圆角的效果,设置 border-radius 就可以了,但如果要让 canvas 合成的图片显示为圆角,这种 css 方式不行。这是示例,扫描访问二维码如下。

19-canvas-border-radius

在网上查询资料,发现同样的问题,解决的方式是用 canvas 的裁剪功能。

解决方法

先画布上画一个有圆角的矩形,然后使用裁剪的方式 clip()

// 生成有圆角的矩形
function drawRoundedRect(cxt, x, y, width, height, radius) {
  cxt.beginPath();
  cxt.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 3 / 2);
  cxt.lineTo(width - radius + x, y);
  cxt.arc(width - radius + x, radius + y, radius, Math.PI * 3 / 2, Math.PI * 2);
  cxt.lineTo(width + x, height + y - radius);
  cxt.arc(width - radius + x, height - radius + y, radius, 0, Math.PI * 1 / 2);
  cxt.lineTo(radius + x, height + y);
  cxt.arc(radius + x, height - radius + y, radius, Math.PI * 1 / 2, Math.PI);
  cxt.closePath();
}

这是示例,扫描访问二维码如下。

19-canvas-radius-clip

参考资料

查看原文

赞 1 收藏 0 评论 0

XXHolic 发布了文章 · 2020-07-06

canvas 设置边框问题

引子

近期的工作中,是继 canvas 显示模糊问题 之后碰到的第 3 个问题。

设置边框问题

这个是示例,扫描访问二维码如下。

19-canvas-border

在手机上可以看到,设置边框后,图片就模糊了。如果 border 不占用 canvas 的高宽度,就没有那个问题,在画布上画个边框也可以。

原因应该跟 canvas 显示模糊问题中差不多,但疑问的是这种情况并没有少像素,而是展示的空间少了,画布上的像素多了,为什么也会模糊?难道是像素挤到一起重叠了?查了下资料,是有像素重叠的情况。真正原因是什么就不太确定了。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-07-03

canvas 显示模糊问题

引子

近期的工作中,是继 canvas 宽高问题 之后碰到的第二个问题。

显示模糊问题

在 PC 浏览器上显示时,没有发现明显的模糊,还可以接受。但在手机上就会有明显的模糊。这是示例,扫描访问二维码如下。

18-qrcode-canvas-image

示例中,用 css 控制 canvas 的宽高,里面的图片展示效果不一致。查询资料,在 stackoverflow 中发现同样的问题,通过实际测试发现:

  • canvas 元素自身的属性 widthheight,决定了多少像素可以显示在画布上,如果不设置,width 默认值是 300,height 默认值是 150。
  • css 的属性 widthheight,是指在屏幕上元素显示的大小,如果没有对 canvas 进行 css 设置,则会采用 canvas 的默认大小。
  • 如果设置了 css 属性 widthheight,当在画布里面使用 drawImage 设置图片宽高时,显示的宽高值会根据一定比例进行转换。例如在上面例子中,设置的图片是宽 300 高 90, canvas 默认宽高渲染像素 300 和 150,(css 高度/canvas 自身属性高度) drawImage 设置的高度 = (90/150) 90 = 54。

规范里面还真没看出来这些。

原因

在 stackoverflow 上也找到相关的问题,在回答中有相关介绍的文章HTML5 Rocks。原因是 canvas 绘制时独立于设备像素比(devicePixelRatio)。受到 devicePixelRatio 影响,在高清显示屏上,一个逻辑像素对应多个实际的设备物理像素。例如在 devicePixelRatio 为 2 的设备上,css 设置的 100px,意味着设备上要填充 200px 物理像素,那么当 canvas 绘制 100px 的区域时,实际是想在设备填充 100px 物理像素,但由于 devicePixelRatio 的作用,设备要求显示 200px 的物理像素,浏览器就智能的填充了像素之间的空格,以便以适当的大小显示元素。

在 Safari6 中支持一个属性 backingStorePixelRatio,该属性决定了浏览器在渲染 canvas 会用几个像素来绘制画布的信息。有些类似于 devicePixelRatio。Safari6 的值为 2,所以在 Safari6 中 canvas 不会模糊,但后来去掉了,现在浏览器不支持这个属性,具体见 Issue 277205

解决方法

解决的思路就是通过检测设备像素比,绘制对应倍数比例的 canvas 元素。方法如下:

function createHDCanvas (w=300,h=150) {
  var ratio = window.devicePixelRatio || 1;
  var canvas = document.createElement('canvas');
  canvas.width = w * ratio; // 实际渲染像素
  canvas.height = h * ratio; // 实际渲染像素
  canvas.style.width = `${w}px`; // 控制显示大小
  canvas.style.height = `${h}px`; // 控制显示大小
  canvas.getContext('2d').setTransform(ratio, 0, 0, ratio, 0, 0);
  return canvas;
}

这是对比示例,扫描访问二维码如下。

18-qrcode-canvas-image-hd

参考资料

查看原文

赞 1 收藏 0 评论 0

XXHolic 发布了文章 · 2020-06-24

canvas 宽高问题

引子

在最近的工作中碰到了合成图片的需求,首先想到的便是 canvas,到网上查找了一些资料,大部分也是使用 canvas。因为好久没有实际接触过这方面的东西了,感觉到一些兴奋。预估会收获不少。

宽高问题

因为是在手机上,需要进行不同尺寸的适配,在项目中使用的是 rem 单位,想着既然拥有全局属性 widthheight,那么理论上应该支持。进行了下面的尝试。

html 标签属性设置

这是示例,扫描二维码访问如下。

17-canvas-unit

发现这个想法不对,后来看规范中的描述,发现描述的已经很清楚了:

The canvas element has two attributes to control the size of the element's bitmap: width and height. These attributes, when specified, must have values that are valid non-negative integers. The rules for parsing non-negative integers must be used to obtain their numeric values. If an attribute is missing, or if parsing its value returns an error, then the default value must be used instead. The width attribute defaults to 300, and the height attribute defaults to 150.

canvas 元素有2个控制元素位图的属性:width 和 height。这些属性,当指定的时候,必须要是非负整数值。一定要使用解析非负整数的规则,来获取它们的数字值。如果一个属性值没有,或者解析的时候返回了一个错误,那么一定要使用默认的值。width 属性的默认值是 300,height 属性的默认值是 150。

仔细看看然后结合实践就会发现:html 标签上 width 和 height 的属性值带单位不会有作用

css 属性设置

除了直接设置元素属性,还可以通过 css 来控制 canvas 的宽高。这是示例,扫描二维码访问如下。

17-canvas-css

实践后发现是可以的。此外,从示例中还可以发现现象:

  1. 在 canvas 标签上没有设置 width 和 height 属性,用 css 只设置了 width 和 height 其中一种属性,那么另外一种属性的值,会根据默认宽高比例:300:150 = 2:1 进行转换得到。
  2. 在 canvas 标签上有设置 width 和 height 属性,用 css 只设置了 width 和 height 其中一种属性,那么另外一种属性的值,会根据 canvas 标签上 width 和 height 值的比例进行转换得到。

这种方式可以让整体的显示,达到自适应的效果。

动态计算

根据宽高比例用 js 动态计算后进行赋值,也可以到达一种显示自适应的效果。

以上的方式只是整体的显示,在实际应用中会有其它的问题,在 canvas 显示模糊问题中有更加详的细解释。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-06-17

JavaScript Hoisting

引子

关于 JavaScript 提升(Hoisting),一般在实际使用的过程中,只要遵循了“先声明,后使用”的约定,很少会碰到问题。但浏览器引擎中肯定考虑各种情况,经历了这些问题,吃一堑长一智,还是总结一下。

提升

先看下面打印的是什么。

console.info(a);
a = 1;
var a;
console.info(a);

学过 JavaScript 基础就会知道 var 声明会先提升,第一个会打印 undefined ,按顺序执行后, a 被赋值,第二个打印是 1 。基于这样的解释,个人产生的疑惑会有:

  • 为什么会提升?
  • 这个“提升”是指声明代码都提升到最上面了吗?

带着这些疑问,去查找资料,从中了解到下面这些内容。

JavaScript 代码在执行前都要进行编译,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内。编译通常会经历三个步骤:

  • 分词/词法分析(Tokenizing/Lexing) :这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。
  • 解析/语法分析(Parsing) :这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为抽象语法树(Abstract Syntax Tree,AST)。
  • 代码生成 : 将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。

JavaScript 引擎相对上面步骤要复杂得多,例如,在语法分析代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

编译的词法分析阶段基本能够知道全部(注意 eval 和 with)标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。也就是说,变量和函数在内的所有声明会在代码执行前先被处理。因此实际上声明的代码的位置是不会变动的,而是在编译阶段被放入内存中。

提升优先级

通过上面的了解,我们知道变量和函数声明都会提升,变量名和函数名是一样的时候会如何?

// 先变量声明,后函数声明
console.info(b);
var b = 1;
function b() {
  console.info(2);
}

// 先函数声明,后变量声明
console.info(c);
function c() {
  console.info(3);
}
var c = 4;

通过上面的例子可以发现:函数声明比变量声明优先提升

函数声明和函数表达式

函数声明语法如下:

function name([param[, param[, ... param]]]) { statements }

函数声明的 name 必须要有。

函数表达式和函数声明非常相似,它们有相同的语法。

var myFunction = function name([param[, param[, ... param]]]) { statements }

函数表达式的 name 非必需,写上 name 可以在调用堆栈时使用,当省去 name 时,就成了匿名函数。

函数表达式不会提升,所以不能在定义之前调用。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-06-10

关于性能工具

引子

Performance Metrics 中讲了性能指标,还介绍了关于测量的内容,除了使用浏览器提供的 API 进行测量,还可以借助一些工具进行测量。这次就来看看关于工具的介绍。以下是个人理解的部分翻译。

翻译时 How To Think About Speed Tools 原文 Last updated 2019-02-12 。

正文

谷歌已经发布了很多关于性能数据和性能工具的指导。本文的目的是为开发人员和营销人员整合这一指导,帮助他们了解如何考虑性能,并了解谷歌提供的所有性能工具。

关于性能的常见误区

误区 1

用户体验可以通过单一指标捕获

好的用户体验不是靠单一的点就能捕获,它是由你的用户使用过程中,一系列关键的事件组成。明白不同的衡量标准并跟踪它们,对于用户体验来说十分重要。

误区 2

用户体验可以通过单个“代表性用户”捕获

由于用户设备、网络连接和其它因素的差异,真实世界的性能差别很大。校准你的实验室和开发环境以测试类似各种不同的条件。使用现场数据告知不同的测试参数,例如设备类型(即移动设备与台式机)、网络连接(即3G或4G)和其他关键变量。

误区 3

我的网站加载速度对我来说很快,所以对我的用户来说加载速度应该也很快

开发人员测试加载性能的设备和网络通常比用户实际体验的速度快得多。使用现场数据了解用户使用的设备和网络,并在测试性能时适当地模拟这些条件。

理解实验室和现场数据

实验室数据

实验室数据是在一个受控环境中收集的性能数据,该环境拥有预定义的设备和网络设置。这提供了可重现的结果和调试功能,以帮助识别、隔离和修复性能问题。

优势

  • 有助于调试性能问题
  • 端到端并深入了解用户体验
  • 可重现的测试和调试环境

限制

  • 可能无法捕捉现实世界的障碍
  • 无法与实际页面 KPI 关联

注意:LighthouseWebPageTest 这样的工具收集的就是这种类型的数据。

现场数据

现场数据是你的用户在户外体验页面加载时,收集到的性能数据。

优势

  • 捕获真实世界的用户体验
  • 实现与业务关键性能指标的关联

限制

  • 度量指标受限
  • 调试能力有限

注意:Chrome User Experience Report 中公共数据设置 和 PageSpeed Insights 性能工具中速度得分报告就是这种类型的数据。

有哪些不同的性能工具?

  • Lighthouse : 为你的网站提供个性化建议,包含性能、可访问性、PWA、SEO、和其它最佳实践。
  • WebPageTest : 允许你在受控的实验室环境中比较一个或多个页面的性能,深入了解性能统计信息并在实际设备上测试性能。你可以在 WebPageTest 中运行 Lighthouse 。
  • TestMySite : 允许将你的移动站点速度与超过 10 个国家/地区的同行进行比较。移动站点速度是基于 Chrome 用户体验报告中的真实数据。允许你根据谷歌分析的基准数据,评估提高移动网站速度的潜在收入机会。允许你跨设备诊断网页性能,并提供一个从 WebPageTest 和 PageSpeed Insights 中得出用于提升体验的修复列表。
  • PageSpeed Insights : 显示站点关于速度的数据,以及提供常见优化提升建议。
  • Chrome Developer Tools : 允许你分析页面的运行时,以及确定和调试性能瓶颈。

备注: 在原文中还有提到工具 Speed ScorecardImpact Calculator ,点击跳转到的都是 TestMySite 网址。去试了一下,检测的结果都包含了对工具 Speed Scorecard 和 Impact Calculator 的描述,猜测是后来功能统一合并到 TestMySite 工具上,因此将原文中相关的描述都合并到了 TestMySite 的描述中。

因此你是…

开发者试图理解你网站当前的性能,如真实世界 Chrome 用户的体验,并寻找针对行业顶级趋势的审查建议和指导方针。

PageSpeed Insights 让你知道在真实世界中 Chrome 用户体验你站点的性能,并推荐优化的机会。

开发人员试图根据现代 web 性能最佳实践来理解和审查网站。

Lighthouse 包含详细性能评估。它为你提供页面中缺少的性能优化列表,以及通过实施每个优化所节省的时间,这可以帮你了解应该做什么。

开发人员在寻找有关如何调试/深入了解站点性能的技术指导。

Chrome Developer Tools(CDT)包含一个性能面板,允许你通过使用自定义配置分析站点来深入研究站点的性能问题,从而跟踪性能瓶颈。你可以在网站的生产或开发版本上使用 CDT。

WebPageTest 包含一套高级的度量和跟踪查看器。它能够深入了解你的网站在真实的移动硬件和网络条件下的性能。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-06-03

JavaScript 新旧替换五:函数嵌套

引子

看了 ReduxapplyMiddleware 方法的实现,里面函数嵌套的写法都用了新语法,就想关注一下函数嵌套一类新旧的不同。

上一篇 JavaScript 新旧替换四:继承

ES5 方式

普通嵌套

  function find(value) {
    return {
      in: function(arr) {
        return {
          combine: function(obj) {
            var result = arr.indexOf(value);
            obj.index = result;
            return obj;
          }
        };
      }
    };
  }

  var data = find(6).in([1,2,3,4,5,6]).combine({});
  console.info(data); // {index: 5}

管道机制

管道机制(pipeline)是指前一个函数的输出是后一个函数的输入。

  const plus = a => a + 1;
  const minus = a => a - 2;
  const multi = a => a * 3;
  const div = a => a / 4;

  function pipeline() {
    for (var len = arguments.length, funcs = [], key = 0; key < len; key++) {
      funcs[key] = arguments[key];
    }

    return function(val) {
      var result = funcs.reduce(function(a, b) {
        return b(a);
      }, val);
      return result;
    };
  }

  var cal = pipeline(plus,minus,multi,div);
  var result = cal(5);
  console.info(result); // 3

ES2015+ 方式

普通嵌套

  const find = (value) => (
    {
      in: (arr) => (
        {
          combine: (obj) => {
            const result = arr.indexOf(value);
            obj.index = result;
            return obj;
          }
        }
      )
    }
  );

  const data = find2(6).in([1,2,3,4,5,6]).combine({});
  console.info(data); // {index: 5}

管道机制

  const plus = a => a + 1;
  const minus = a => a - 2;
  const multi = a => a * 3;
  const div = a => a / 4;

  const pipeline = (...funcs) => val => funcs.reduce(function(a,b) {
    return b(a);
  }, val);

  const cal = pipeline(plus,minus,multi,div);
  const result = cal(5);
  console.info(result); // 3

参考资料

查看原文

赞 1 收藏 0 评论 0

XXHolic 发布了文章 · 2020-05-22

JavaScript 新旧替换四:继承

引子

在一些书籍中花费了不少的篇幅进行讲述,新的语法中也出现了相关的关键字,实现的方式中也涉及到 JavaScript 中很重要的知识点。

注意:JavaScript 中并没有类似 Java 中的类和继承,以下用“类”和“继承”是为了方便描述。

上一篇 JavaScript 新旧替换三:参数转换

ES5 方式

实现继承功能的方式有多种,JavaScript 中常用的继承模式是组合继承,这里以此为例。

  function Fruit(name) {
    this.name = name;
  }

  Fruit.prototype.showName = function() {
    console.info("Fruit Name:", this.name);
  };

  function Apple(name, color) {
    Fruit.call(this, name);

    this.color = color;
  }

  Apple.prototype = new Fruit();
  // 矫正语义指向,并不是必需
  Apple.prototype.constructor = Apple;

  Apple.prototype.showColor = function() {
    console.info("Apple Color:", this.color);
  };

  var apple = new Apple("apple", "green");
  console.info("apple:", apple);
  apple.showName();
  apple.showColor();

在组合继承中,主要的思路是:

  • 创建子类的时候,通过 Fruit.call(this, name) 绑定子类的 this,达到继承父类属性效果。
  • 将父类的实例赋给子类的 prototype 属性,子类的实例会沿着原型链查找,达到了继承父类方法的效果。

ES2015+ 方式

用新的语法实现上面的继承:

  class Fruit {
    constructor(name) {
      this.name = name;
    }

    showName() {
      console.info("Fruit Name:", this.name);
    }
  }

  class Apple extends Fruit {
    constructor(name, color) {
      super(name);
      this.color = color;
    }

    showColor() {
      console.info("Apple Color:", this.color);
    }
  }

  let apple = new Apple("apple", "green");
  console.info("apple:", apple);
  apple.showName();
  apple.showColor();

在书写形式上有很大的变化,但实际上也是通过原型链实现,通过 Babel 转译为 ES5 看下是怎样的实现思路。

首先说明一下 Babel 中转译有两种模式:normal 和 loose。

  • loose 模式下生成更简单、兼容性更好的代码。
  • normal 模式下生成符合标准语义的代码。

选择 normal 模式的转译更加合适,先来看下 Fruit 类转译后的实现:

"use strict";
/**
 * Symbol.hasInstance 属性,指向一个内部方法。
 * 当其它对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法。
 * 比如,foo instanceof Foo 在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。
 */
function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}

// 防止直接当方法调用
function _classCallCheck(instance, Constructor) {
  // 判断 instance 是否为 Constructor 的实例
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

/**
 *
 * Object.defineProperty 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
 *
 */
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  // 静态方法直接放在构造函数上
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

var Fruit =
  /*#__PURE__*/
  (function() {
    function Fruit(name) {
      _classCallCheck(this, Fruit);

      this.name = name;
    }

    _createClass(Fruit, [
      {
        key: "showName",
        value: function showName() {
          console.info("Fruit Name:", this.name);
        }
      }
    ]);

    return Fruit;
  })();

在上面转译的代码中,处理的主要思路有:

  • _classCallCheck 方法判断调用的方式,防止 Fruit() 这样直接调用。
  • _createClass 方法在 prototype 上添加公用方法,在 Fruit 上添加静态方法。

这种方式跟组合使用构造函数模式和原型模式创建对象很相似,不过表达的语义不太一样。

再来看下继承转译后的代码:

"use strict";

function _typeof(obj) {
  if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
    _typeof = function _typeof(obj) {
      return typeof obj;
    };
  } else {
    _typeof = function _typeof(obj) {
      return obj &&
        typeof Symbol === "function" &&
        obj.constructor === Symbol &&
        obj !== Symbol.prototype
        ? "symbol"
        : typeof obj;
    };
  }
  return _typeof(obj);
}

function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

// 获取对象原型
function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

/**
 *
 * Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
 * @param {*} superClass
 */
function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  });
  // 没有这一步的话,就拿不到父类的属性
  if (superClass) _setPrototypeOf(subClass, superClass);
}

// 设置对象原型
function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

var Fruit =
  /*#__PURE__*/
  (function() {
    function Fruit(name) {
      _classCallCheck(this, Fruit);

      this.name = name;
    }

    _createClass(Fruit, [
      {
        key: "showName",
        value: function showName() {
          console.info("Fruit Name:", this.name);
        }
      }
    ]);

    return Fruit;
  })();

var Apple =
  /*#__PURE__*/
  (function(_Fruit) {
    _inherits(Apple, _Fruit);

    function Apple(name, color) {
      var _this;

      _classCallCheck(this, Apple);

      // _getPrototypeOf(Apple).call(this, name) 调用的实际是父类的函数,注意没有使用 new ,返回的是默认的 undefined
      _this = _possibleConstructorReturn(
        this,
        _getPrototypeOf(Apple).call(this, name)
      );
      _this.color = color;
      return _this;
    }

    _createClass(Apple, [
      {
        key: "showColor",
        value: function showColor() {
          console.info("Apple Color:", this.color);
        }
      }
    ]);

    return Apple;
  })(Fruit);

  let apple = new Apple("apple", "green");
  console.info("apple:", apple);
  apple.showName();
  apple.showColor();

在上面转译的代码中,处理的主要思路是:

  1. _inherits 方法基于父类的 prototype 创建了一个新的对象,赋给了子类的 prototype。还将子类的 __proto__ 指向了父类,为的是继承父类的属性。
  2. 直接在子类的 prototype 上定义子类自己的方法。
  3. 在执行子类的构造函数时,修改了 this 的值。

ES2015+ 方式语法点

class

ES2015 引入了类的概念,通过 class 关键字可以定义类。类有下面一些特点:

  1. 必须要使用 new 调用,否则会报错。
  2. 类没有提升,这种规定的原因与继承有关,必须保证子类在父类之后定义。
  3. 类中默认使用的是严格模式。
  4. 不想被继承的方法加上 static 关键字。
  5. 类中 this 指向类的实例。

constructor

constructor 是构造方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。

class A {}
let obj = new A();
console.info(obj.constructor === A.prototype.constructor);

extends

class 继承通过 extends 实现。继承时,类中构造函数必须要执行 super 方法,否则创建实例的时候会报错。

class A {}
class B extends A {
  constructor() {

  }
}

let obj = new B();
// VM55958:3 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

子类中如果没有显式的写出构造方法,会默认的添加。

class A {}
class B extends A {}
// 等同于
class B extends A {
  constructor(...args) {
    super(...args)
  }
}

super

super 关键字可以当做函数或对象使用。使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

函数使用

  1. 代表父类的构造函数。
  2. 只能在子类的构造函数中使用,其它地方会报错。

对象使用

  1. 在普通方法中,指向父类的原型对象。
  2. 在静态方法中,指向父类。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-05-17

JavaScript 新旧替换三:参数转换

引子

在 ES2015 之前,有把函数的 arguments 转变为某种可以当作数组来使用的方法,现在可以摆脱这些方法了。

这是继 JavaScript 新旧替换二:赋值和取值的第三篇。

ES5 方式

主要是使用了 apply() 方法,该方法用途是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。该接受两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是 Array 的实例,也可以是 arguments 对象。

function foo() {
  var args = Array.prototype.slice.apply(arguments);
  console.info(args); // [1,2,3,4,5]
  console.info(args instanceof Array); // true
}
foo(1,2,3,4,5);

ES2015+ 方式

在 ES2015 中引入了一个新的运算符 ...,称为 spreadrest(展开或收集)运算符。不同情况下,会有不同的特性。

function foo(...args) {
  console.info(args); // [1,2,3,4,5]
  console.info(...args); // 1 2 3 4 5
}
foo(1,2,3,4,5);

这里运算符 ... 使用的特性有:

  • foo(...args) 中使用了收集的特性,把一系列值收集到一起成为一个数组。
  • console.info(...args) 中使用了展开的特性,这里把数组 args 展开为一组函数调用的参数。

在 ES2018 中这个运算符引入到了对象。它会取出对象的所有可遍历属性,拷贝到当前对象中。

使用于对象常见的几种情况如下:

一般对象

let obj = {a:1,b:2};
let objCopy = {...obj};
objCopy.a = 2;
console.info(obj); // {a:1,b:2}
console.info(objCopy); // {a:2,b:2}

let complexObj = {a:{b:1},c:2};
let complexObjCopy = {...complexObj};
complexObjCopy.a.b = 2;
console.info(complexObj); // {a:{b:2},b:2}
console.info(complexObjCopy); // {a:{b:2},b:2}

可见只是进行了浅拷贝,等同于使用 Object.assign() 方法。

数组

let objArray = {...['a','b']};
console.info(objArray); // {0: "a", 1: "b"}

let complexObjArray = {...['a',['b'],'c']};
console.info(complexObjArray); // {0: "a", 1: ["b"],2:"c"}

字符串

let objStr = {...'hi'};
console.info(objStr); // {0: "h", 1: "i"}

其它

{...1} // {}

{...true} // {}

{...undefined} // {}

{...null} // {}

{...NaN} // {}

应用

运算符 ... 较常见应用有:

复制对象或数组

let obj = {a:{b:1},c:2};
let objCopy = {...obj};
objCopy.a.b = 2;
console.info(obj); // {a:{b:2},b:2}
console.info(objCopy); // {a:{b:2},b:2}

let arr = ['1',['2']];
let arrCopy = [...arr];
arrCopy[1][0] = '3';
console.info(arr); // ['1',['3']]
console.info(arrCopy); // ['1',['3']]

需要注意都是浅拷贝。

合并对象或数组

let objA = {a:1}, objB = {b:2};
let obj = {...objA,...objB};
console.info(obj); // {a:1,b:2}

let arrA = ['1'], arrB = ['2'];
let arr = [...arrA,...arrB];
console.info(arr); // ['1',2']

与解构结合

与对象解构结合

与对象解构结合需要满足的条件有:

  • 等号右边是一个对象。
  • 解构必需是最后一个参数,否则会报错。
let {a,...b} = undefined; // Uncaught TypeError: Cannot destructure property `a` of 'undefined' or 'null'.

let {...q,k} = {m:1,n:2,k:3}; // Uncaught SyntaxError: Rest element must be last element

let {x,...z} = {x:1,y:2,z:3};
z // {y:2,z:3}

使用了扩展运算符后,把目标对象上所有可遍历但尚未被读取的属性,分配到指定对象上。

与数组解构结合

与数组解构结合需要满足的条件有:

  • 等号右边是一个数组。
  • 解构必需是最后一位,否则会报错。
let [...x] = 123; // 123 is not iterable

let [...q,k] = [1,2,3]; // Uncaught SyntaxError: Rest element must be last element

let [x,...z] = [1,2,3];
z // [2,3]

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-05-09

JavaScript 新旧替换二:赋值和取值

引子

在取值或赋值的时候,经常会声明一个临时中间变量,新的语法可以省去这步。下面主要是关于 Object 和 Array 的赋值和取值新的方式。

这是继 JavaScript 新旧替换一:变量声明的第二篇。

ES5 方式

Object

Object 类型一般可以这样赋值:

var boy = { name: "Jack",age: 18 };
// 或者这样
var girl = {};
girl.name = "Rose";
girl.age = 18;

Object 类型一般可以这样取值:

var boy = { name: "Jack",age: 18 };
var name = boy.name;
var age = boy['age'];

Array

Array 类型一般可以这样赋值:

var colors = new Array("red", "blue", "green");
// 或者这样
var fruit = ['apple','banana','peach'];
// 或者这样
var flower = [];
flower[0] = 'rose';
flower[1] = 'carnation';
flower[2] = 'tulip';

Array 类型一般可以这样取值:

var colors = new Array("red", "blue", "green");
var color1 = colors[0];

ES2015+ 方式

在 ES2015 中引入了一个新的语法特性,名为解构(destructuring)。它可以从对象或数组中提取值,然后对变量进行赋值。简化了取值和赋值的代码。看下面的例子。

ES5 方式:

function getPersonInfo () {
  return { name: 'Jack',age: 18 };
}
var message = getPersonInfo();
var personName = message.name,personAge = message.age;
console.info(personName,personAge); // Jack 18

使用解构:

function getPersonInfo () {
  return { name: 'Jack',age: 18 };
}
let { name:name,age:age } = getPersonInfo();
console.info(name,age); // Jack 18

Object

在上面的例子中,如果使用的变量名跟对象中的属性名一样,可以写的更加简洁:

let { name,age } = getPersonInfo();
console.info(name,age); // Jack 18

对比简化之前的形式,是略去了 name: 还是略去了 :name?实际上是略去 name: 部分。如果想要赋给非同名变量,可以这样做:

let { name:personName,age:personAge } = getPersonInfo();
console.info(personName,personAge); // Jack 18

关于这种形式,需要关注这个细节。至于原因,先来思考一下一般对象字面值:

var x = 10,y = 20;
var o = { a:x,b:y };
console.info(o.a,o.b); // 10 20

对于 { a:x,b:y },我们知道 a 是对象属性,x 是要赋给 a 的值。这种语法模式跟赋值符 = 的模式一样:target = source,我们可以很直观的理解这一点。在使用解构的时候:

let { name:personName,age:personAge } = getPersonInfo();

name:personNamename 表示属性的源值,personName 是要赋值的目标变量。可以发现,对象字面值是 target <-- source,而对象解构赋值是 source --> target。注意到这个反转,对解构语法的理解很有帮助。

还可以像下面这样对比的观察:

let m = 10,n = 20;
let o = { a:m,b:n };
let     { a:M,b:N } = o;

前面说解构是略去了 a: 这部分,把上面代码中两行的 a:b: 都去掉,看起来就好像是把 m 赋值给了 M,把 n 赋值给了 N

Array

function getColors() {
  return ["red", "blue", "green"];
}
let [color1,color2,color3] = getColors();
console.info(color1,color2,color3); // red blue green

数组解构跟对象解构不太一样,数组的元素是按次序排列,变量的取值由位置决定,而对象的属性是没有次序,变量必须跟属性同名,才能取到对应值。

数组解构的这种写法属于模式匹配:只要等号两边的模式相同,左边的变量就会被赋予对应的值。

数组解构的情况有:

完全解构

等号左边的模式,跟等号右边的数组完全匹配。

let [x,y,z] = [1,2,3];
console.info(x,y,z); // 1 2 3

let [a,,c] = [1,2,3];
console.info(a,c); // 1 3

部分解构

等号左边的模式,跟等号右边的数组部分匹配。

let [x,y] = [1,2,3];
console.info(x,y); // 1 2

let [a, [b], c] = [1, [2, 3], 4];
console.info(a,b,c); // 1 2 4

解构失败

let [x] = [];
console.info(x); // undefined

let [a,b] = [1];
console.info(a,b); // 1 undefined

解构失败,变量的值会为 undefined

默认值

解构可以制定默认值。使用默认值的条件是:

  • 对象的属性值 === undefined。
  • 数组成员 === undefined。

对象解构默认值:

let {x:y=1} = {};
console.info(y); // 1

let {a:b=1} = {a:2};
console.info(b); // 2

数组解构默认值:

let [x=1] = [undfined];
console.info(x); // 1

let [a=1] = [null];
console.info(a); // null

不只是声明

在上面的例子中,在 let 声明中应用了解构赋值,但解构是一个通用的赋值操作,不只是声明。

let m,n;
[m] = [1];
( {n} = {n:2} );
console.info(m); // 1
console.info(n); // 2

上面的例子中,变量都已经声明,这样解构就只是用于赋值。

对于对象解构,如果省略了 var/let/const 声明符,就必须用 () 括起来。如果不这么做,解析的时候就会被当作一个块语句而不是一个对象。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-05-01

JavaScript 新旧替换一:变量声明

引子

在工作中,最初接触 ES5 的语法比较多,后来渐渐的接触了新的语法。由于一些原因,需要在不同的项目使用不同的语法。时间长了,发现在写代码的时候,偏向用更加熟悉的旧语法,但感觉这么下去不太妙。于是,就想着针对工作中常用的旧语法,跟可以替换的新语法进行对比,加深印象,然后记录总结一下,有意识的更新相关知识点。

ES5 方式

在 JavaScript 中,变量可以用来保存任何类型的数据。每个变量只是一个用于保存值的占位符而已。在 ES5 中,使用 var 声明变量,这种声明方式的特点有:

  1. 声明的变量不赋值,会初始化默认值为 undefined
var testVariable;
console.info('testVariable=',testVariable); // undefined
  1. 可以一条语句定义多个变量,变量之间用逗号分开。
var name = 'Tom',
    age = 15;
  1. 如果省略 var 声明,则会创建一个全局变量,这样会污染全局变量,这种方式不推荐。
  2. var 声明的变量会自动添加到最近的环境中,当查找变量的时候,搜索过程是从作用域链的前端开始,向上逐级查询。如果在局部环境中找到了变量,则停止搜索,使用找到的变量。
  3. 同一作用域内重复声明同一变量时,后声明会覆盖前声明。
var testVariable = '123';
var testVariable = '12';
console.info('testVariable=',testVariable); //12
  1. 声明的变量会发生“变量提升”,也就是说可以先使用后声明,这种方式不推荐。
console.info('testVariable=',testVariable); // undefined
var testVariable = 1;

使用 var 声明比较经典的现象是在 for 循环语句中。

function printNum() {
  var numArray = [];
  for(var i=0;i<5;i++) {
    numArray.push(function (){
      console.info(i);
    })
  }
  numArray[0]();
}
printNum(); // 5

for 循环中用 var 声明的变量 i 在函数 printNum 作用域中都有效,每次循环执行的语句 console.info(i) 中的 ifor 循环中声明的 i,指向的是同一个 i,执行完最后一次循环时,i 的值就是 5。

ES2015+ 方式

新增的声明变量方式有:let、const。

let 声明

用法跟 var 类似,这种方式不同的地方有:

  1. 声明的变量,在 let 所在的代码块内有效。
{
  let testLet = 1;
  var testVar = 2;
}
console.info('testVar=',testVar); // 2
console.info('testLet=',testLet); // Uncaught ReferenceError: testLet is not defined
  1. 不会“变量提升”,要先声明后使用,否则会报错。
console.info('testLet=',testLet); // Uncaught ReferenceError: testLet is not defined
let testLet = 1;

这种过早访问 let 声明的引用导致的 ReferenceError 严格说叫做“暂时死亡区”(Temporal Dead Zone,TDZ)错误。这种情况下,使用 typeof 就会有问题。

console.info(typeof testVar);
console.info(typeof testLet);
var testVar = '123';
var testLet = 123; // Uncaught ReferenceError: testLet is not defined
  1. 同一作用域内,不允许重复声明同一个变量。
let testLet = 1;
let testLet = '123'; // Uncaught SyntaxError: Identifier 'testLet' has already been declared

同样在 for 循环中使用时:

function printNum() {
  var numArray = [];
  for(let i=0;i<5;i++) {
    numArray.push(function (){
      console.info(i);
    })
  }
  numArray[0]();
}
printNum(); // 0

for 循环头部的 let i 为每次循环都重新声明了一个变量 i。头部的声明是一个作用域,循环体内是另外一个单独的作用域。

for(let i=0;i<2;i++) {
  let i = '123';
  console.info(i);
}
// 123
// 123

const 声明

这种形式的声明,是用于创建常量。常量不是对这个值本身的限制,而是对赋值的那个变量的限制。变量实际是指向一个内存地址,const 就对这个内存地址所保存的数据进行了限制。对于简单的数据(例如数值、字符串、布尔值),值就保存在那个内存地址中,就等同于常量。如果复合类型的数据(例如数组和对象),内存地址中保存的就是一个指向实际数据的指针,const 保证的是这个指针固定,这个指针实际指向的内容就不能控制了。

const testConst = [1,2];
testConst.push(3);
console.info(testConst); // [1,2,3]

这种方式的特点有:

  1. let 一样,所在的代码块内有效。
{
  const TEST = "0.618";
}
console.info(TEST); // Uncaught ReferenceError: TEST is not defined
  1. 不会“变量提升”,要先声明且初始化后才能使用,否则会报错。同样存在“暂时死亡区”。
const K = 1.13198824;
console.info(K);
const TEST;
console.info(TEST);
// SyntaxError: Missing initializer in const declaration
  1. 同 let 一样,同一作用域内,不允许重复声明同一个变量。

选择那种方式?

在目前的工作中,发现使用 const 的频率非常高,一方面可能是由于使用 react 不可变数据,另一方面是听说 JavaScript 引擎在某些情况下对 const 进行了更好的优化。理论上说,引擎如果了解这个变量的值或类型不会改变,那么它就可以取消某些可能的追踪。实际到底有没有,这没有找到相关的信息。

在 stackoverflow 中也有类似的提问:Const in javascript? When to use it and is it necessary。还有一些文章中的描述,似乎想要用 const 来规范代码行为:必须要初始化。这种规范代码行为的方式感觉很奇怪,写代码很重要的一个功能就是清晰的表达你的意图,不仅仅对自己,也是对未来的维护者或合作者。可能有人觉得到时候如果要改数据,把 const 修改为 let 就可以了。这样也许可以达到目的,那么后来接手的人只要想修改数据,就把 const 修改为 let,这样子做真的好吗?

从代码的可读性和可理解性上,个人认同的方式是:当你想表明这个变量不会改变时才使用 const,这样更加合理。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-04-25

前端异常类型及捕获方式

引子

最近想起这方面的事情,就去花时间查找了相关资料,以下是个人的总结。

异常类型

从使用浏览器,浏览一个网页,与网页进行交互的过程,从用户的角度想一想会出现那些异常。

首先是使用浏览器一般都是基于操作系统,系统自身可能会出现问题,比如内存不够。这类情况归为系统异常

正常打开浏览器后,访问网页的时候,可能没有网络,或提示出现服务错误等等,这类情况归为网络异常

能够正常访问网页后,用户进行交互时,可能出现一种情况下点击有效,另一种情况下点击无效。这类情况归为应用异常

在上面感性的认知基础上,下面进一步进行细化。

系统异常

系统异常情况比较少,相关的可能有:

  • 浏览器崩溃

网络异常

网络异常中,相关的可能有:

  • XMLHttpRequest 请求异常
  • Fetch 请求异常
  • 静态资源加载异常

应用异常

应用异常可以用 JavaScript 中的错误对象体现出来:

  • EvalError : 与 eval() 有关的错误。
  • RangeError : 表示这个值不在允许值集或范围内。
  • ReferenceError : 表示发现一个无效的引用。
  • SyntaxError : 表示发生了解析错误。
  • TypeError :当其它类型错误都不符合时,TypeError 用于指示一个不成功的操作。
  • URIError :表示用于处理 URI 的函数(encodeURI 或 decodeURl)使用方式与其定义的不兼容。

比较常见的异常可以参考 Top 10 JavaScript errors from 1000+ projects

异常捕获

浏览器都具有某种向用户报告异常的机制,对于用户都是隐藏此类信息。对于开发者,一般在控制台可以看到相关信息。

下面看下捕获异常对应的方法。

try-catch 捕获

try-catch 使用的形式如下:

try {
  // 可能导致异常的代码
} catch(error) {
  // 发生异常时的处理
}

测试页面见这里,有下面的一些特点:

  • catch 块中会接收一个包含异常信息的对象,在不同的浏览器中包含的信息可能不同,但共同有一个保存异常信息的 message 属性。
  • 不能捕获语法异常。
  • 不能捕获异步异常。
  • 该方式捕获的异常,不会出现在控制台上,也不会被 error 事件捕获。

语法异常在开发的阶段就很容易发现,例如:

try {
  var num = '333;
} catch(error) {
  console.info('try-catch:', error);
}

不能捕获异步异常示例如下:

try {
  setTimeout(() => {
    name.forEach(() => {});
  },1000)
} catch(error) {
  console.info('try-catch:', error);
}

try-catch 比较适合用在那些可以预见可能出错的地方。

error 事件捕获

通常将这个事件绑定在 window 上,但由于历史原因,使用 DOM 不同级别的绑定方式,会有些差别。测试页面见这里

DOM0 级方式

也就是使用 window.onerror 指定处理程序,其特点有:

  • 参数有 5 个,对应含义分别为 message — 错误信息(字符串)、source — 发生异常的脚本URL(字符串)、lineno — 发生异常的行号(数字)、colno — 发生异常的列号(数字)、error — Error 对象。
  • 函数体内用 return true 可以阻止异常信息出现在控制台。
  • 可以捕获到异步异常。
  • 不能捕获到语法异常。
  • 不能捕获 try-catch 中的异常。
  • 不能捕获 scriptimginputaudiosourcetrack 标签元素 src 属性的加载异常(HTML5 不支持的 frame 标签不讨论)。测试页面见这里
  • 不能捕获 link 标签的加载异常,测试页面间这里这里

DOM2 级方式

也就是使用 window.addEventListener 指定处理程序,其特点有:

  • 参数对应 1 个,是一个 ErrorEvent 对象,其中包含信息相对 DOM0 级更加丰富。
  • 函数体内用 preventDefault() 方法可以阻止异常信息出现在控制台,但如果是加载资源异常无法阻止。
  • 可以捕获 scriptimginputaudiosource 标签元素 src 属性的加载异常(track 尝试了一下不行)。由于加载请求不会冒泡,所以需要在事件的捕获阶段监听才行。但这种方式无法知道 HTTP 的相关状态。测试页面见这里
  • 可以捕获 link 标签的加载异常,测试页面间这里这里
  • 可以捕获异步异常。
  • 不能捕获到语法异常。
  • 不能捕获到 try-catch 中的异常。

onerror 事件比较适合捕获预料之外的异常。

Promise、Async/Await 异常捕获

Async 函数返回的总是一个 Promise ,如果 Async 函数里面有异常,Promisereject 。所以统一的放到 Promise 里面的 catch 处理会比较方便。其特点:

  • 没有写 catchPromise 无法被 onerrortry-catch 捕获,测试页面见这里
  • Promisereject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件 unhandledrejection
  • 写了 catch 后,不会触发 unhandledrejection 事件,所以建议全局增加这个事件监听。
  • 本地测试验证的时候,注意跨域的限制,见 issues1issues2

XMLHttpRequest 请求异常捕获

XMLHttpRequest 目前来说应用非常广泛,有对应的 error 事件可以进行监听。在实际中,有可能团队内已有约定的一套异常状态码,可根据实际情况进行对应的处理。这是简单示例

Fetch 请求异常捕获

Fetch API 提供了一个获取资源的接口。它对于使用过 XMLHttpRequest 的人来说会很类似,但这个新的接口提供了更加强大的功能。有下面的一些点需要注意:

  • fetch 方法返回的是一个 Promise ,因此可以使用 catch 进行异常捕获。
  • 当接收到一个代表错误的 HTTP 状态码时,fetch 返回的 Promise 不会 reject ,即使响应的 HTTP 状态码是 404 或 500。相反,它会 resolve ,但会将 resolve 返回值的 ok 属性设置为 false ,仅当网络故障或请求被阻止时,才会 reject 。这是请求为 404 的示例

iframe 异常捕获

error 事件可以捕获到一些标签的 src 加载异常,但 iframe 的情况有些不一样。在网上找了一些资料,尝试了下面的一些方法:

  • 使用 window.onerror 方式,无法捕获,这是测试页面
  • 使用 window.frames[0].onerror 方式,无法捕获,这是测试页面
  • 在标签上绑定 onerror 事件,没有触发,无法捕获,这是测试页面
  • 绑定 onload 事件,可以触发,但无法直接的捕获,例如一般的网站都设置了 404 页面,当 src 加载的一个网页找不到时,就会默认使用 404 网页,虽然触发了 onload 事件,但仍然不知道是不是异常。这个时候,可以通过间接的检测这个显示 404 页面的一些信息,来判断是否异常,比如当发现这个页面 title 里面有 404 或者 not found 之类的词汇,那就说明 iframe 加载异常。这个根据实际情况,可以设置其它检测的标识。这是测试页面

还有一种思路就是用一个异步请求,让服务器端判断一下是否能够正常的访问 src 的资源,如果请求返回正常,那就直接动态设置 src 的值,否则就是异常情况,直接上报即可。

跨域

跨域的出现是由于浏览器的同源策略,一旦发生,会在控制台出现很明显的提示。通过查找资料发现,更多的方案是提前解决这个问题,但也有针对特定的情况进行捕获的方法。下面是相关的资料:

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-04-17

自定义 create-react-app

引子

使用 create-react-app 的时候,如果想要更改一些配置,一种方式是使用 eject 指令,但这样可能就无法同步后续的更新。另外一种方式是使用 react-app-rewired 覆盖对应的配置,这种方式有些无法依然设置。更好的方式是直接基于原构建脚本进行自定义修改,又方便同步后续更新。

简介

create-react-app 里面包含了多个不同的库,使用了 Lerna 进行管理。构建的相关脚本主要在 react-scripts 中。通过 Fork 的方式发布自己的版本,也可以同步官方的版本。

操作

1 Fork

登录 GitHub 的账号,Fork create-react-app 。更加详细的说明见 Fork a repo

72-fork

2 修改对应库

Fork 后,克隆对应的库到本地。在修改之前,建议基于发布的分支,创建一个自己的修改分支。下面作为示例,在 /packages/react-scripts/scripts/init.js 中添加一些打印日志。

72-modify

3 发布包

由于是 Fork 过来的包,里面的 package.json 的一些描述信息需要修改。至少里面的 name 字段值需要更改一下,示例的名称改为 customize-react-scripts 。其它描述信息,根据实际情况判断是否需要进行修改。

npm login
npm publish

发布包详细说明见 这里

4 使用自定义包

发布成功后,到一个目录下,执行下面的命令:

npx create-react-app test-app --scripts-version customize-react-scripts

可看到下面的信息提示。

72-start

安装成功后,可以看到前面添加的提示信息。

72-success

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-04-09

Git Commit 规范参考

引子

在 github 上逛逛就可以发现,其提交的 commit 都有一定格式,工作中也有相应的规定,时间长了就能体会到其中的好处。这种约束是一种良好的实践。抽出一些时间,更详细的了解相关的资料,然后做了一些实践和总结。

规范 Commit 的好处

  1. 提供更明确的历史信息,方便判断提交目的和浏览
  2. 可以过滤某些不必要的提交,方便快速查找信息
  3. 自动化生成 Changelog
  4. 向同事、公众与其他利益关系人传达变化的性质
  5. 基于提交的类型,自动决定语义化的版本变更

以上的好处,个人认为要有一个大的前提,就是每一个提交,尽量保证其目的单一性,比如说几个 bug 看似类似,就一次性修改提交。这样做,让 commit 的信息变的复杂化,阅读不方便,也容易让人想到一些不必要的关联性。

Commit 的格式

找了几个 start 较多的库,看看提交的格式。

  1. react-commit

react-commit

  1. vuejs-commit

vuejs-commit

  1. angular-commit

angular-commit

网上推荐的写法是第 2 和 3 种,也就是 Angular 规范,并有配套的工具。有一个文档对 commit 的格式要求有个描述叫约定式提交。下面就根据 Angular 规范和对应文档,看看详细的说明。

每个 commit message 包含一个 headerbodyfooterheader 有一个特殊的格式包含有 typescopesubject

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

header、body、footer 之间都要空一行,header 是必填项,scope 是选填项。commit message 的每一行的文字不能超过 100 个字符。这样子在 github 和 git 工具上更便于阅读。

Type

type 用于说明 commit 的类别,必须为以下类型的一种:

  • feat: 新的功能
  • fix: 修复 bug
  • docs: 只是文档的更改
  • style: 不影响代码含义的更改 (例如空格、格式化、少了分号)
  • refactor: 既不是修复 bug 也不是添加新功能的代码更改
  • perf: 提高性能的代码更改
  • test: 添加或修正测试
  • chore: 对构建或者辅助工具的更改,例如生成文档

Scope

scope 用于说明 commit 影响的范围,当影响的范围有多个时候,可以使用 *

Subject

subject 用于对 commit 变化的简洁描述:

  • 使用祈使句,一般以动词原形开始,例如使用 change 而不是 changed 或者 changes
  • 第一个字母小写
  • 结尾不加句号(.)

Body

body 用于对 commit 详细描述。使用祈使句,一般以动词原形开始,例如使用 change 而不是 changed 或者 changes。

body 应该包含这次变化的动机以及与之前行为的对比。

Footer

footer 目前用于两种情况。

1 不兼容的变动

所有不兼容的变动都必须在 footer 区域进行说明,以 BREAKING CHANGE: 开头,后面的是对变动的描述,变动的理由和迁移注释。

BREAKING CHANGE: isolate scope bindings definition has changed and
    the inject option for the directive controller injection was removed.

    To migrate the code follow the example below:

    Before:

    scope: {
      myAttr: 'attribute',
      myBind: 'bind',
      myExpression: 'expression',
      myEval: 'evaluate',
      myAccessor: 'accessor'
    }

    After:

    scope: {
      myAttr: '@',
      myBind: '@',
      myExpression: '&',
      // myEval - usually not useful, but in cases where the expression is assignable, you can use '='
      myAccessor: '=' // in directive's template change myAccessor() to myAccessor
    }

 The removed `inject` wasn't generaly useful for directives so there should be no code using it.

2 关闭 issue

如果 commit 是针对某个 issue,可以在 footer 关闭这个 issue。

## 关闭单个
Closes #234
## 关闭多个
Closes #123, #245, #992

Revert

如果 commit 用于撤销之前的 commit,这个 commit 就应该以 revert: 开头,后面是撤销这个 commit 的 header。在 body 里面应该写 This reverts commit <hash>.,其中的 hash 是被撤销 commit 的 SHA 标识符。

revert: feat(pencil): add 'graphiteWidth' option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

示例

feat($browser): onUrlChange event (popstate/hashchange/polling)

Added new event to $browser:
- forward popstate event if available
- forward hashchange event if popstate not available
- do polling when neither popstate nor hashchange available

Breaks $browser.onHashChange, which was removed (use onUrlChange instead)
fix($compile): couple of unit tests for IE9

Older IEs serialize html uppercased, but IE9 does not
Would be better to expect case insensitive, unfortunately jasmine does
not allow to user regexps for throw expectations.

Closes #351
style($location): add couple of missing semi colons
docs(guide): updated fixed docs from Google Docs

Couple of typos fixed:
- indentation
- batchLogbatchLog -> batchLog
- start periodic checking
- missing brace

Commit 相关的工具

填写提示工具 commitizen

这个工具是用来给 commit 一个引导的作用,根据提示一步一步的完善 commit。

在 Windows 环境下安装,个人遇到的问题有:

1、安装 commitizen 后,进行初始化,找不到命令

7-failed1

尝试成功的解决方法是用 yarn 执行指令:

yarn commitizen init cz-conventional-changelog --save-dev --save-exact

2、按照说明里面直接执行 npx git-cz无效,安装了 npx 也无效

发现在 node_modules\bin\ 目录下,并没有 npx 相关可执行文件,需要安装 npx。

安装后还是无效,需要用 yarn 来执行指令

yarn npx git-cz

7-npx

可以发现执行指令后,显示了对应的可执行文件的路径。

如果配置了 scripts,那么提交的时候需要执行对应的指令,才会触发这个工具的作用。

还有操作的问题,在自己的戴尔笔记本用上下键切换无效,也尝试过各种组合键,也是无效,外接的键盘有效。

格式校验工具 commitlint

validate-commit-msg 已不被推荐使用。安装对应要使用的提交规范,

#it also works for Windows
yarn add @commitlint/{config-conventional,cli} --dev

# Configure commitlint to use angular config
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

示例用的是 config-conventional 规范,这个安装后,还需要使用 commitmsg hook,推荐使用 husky。安装husky

yarn add husky --dev

然后配置 package.json

{
  "scripts": {
    "commitmsg": "commitlint -E GIT_PARAMS"
  }
}

提交时候就会触发校验,效果如下图。

7-validate-commit

2018.12.09:
在最新的版本中,由于 GIT_PARAMS 参数的问题,进行了更新,配置变化如下:

{
  "scripts": {
-   "precommit": "npm test",
-   "commitmsg": "commitlint -E GIT_PARAMS"
  },
+ "husky": {
+   "hooks": {
+     "pre-commit": "npm test",
+     "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
+   }
+ }
}

或者在项目文件夹下执行下面的命令,会自动进行更正。

./node_modules/.bin/husky-upgrade

生成 Changelog 工具 Conventional Changelog

使用的工具是 Conventional Changelog,推荐使用 standard-version。commit 符合 Conventional Commits Specification 中描述的格式,就可以用程序自动生成 Changelog。

先进行安装。

yarn add standard-version --dev

然后配置package.json配置执行的脚本。

{
  "scripts": {
    "release": "standard-version"
  }
}

执行该脚本命令,产生的结果:生成文件 CHANGELOG、自动修改库的版本号、产生一个 commit,用工具查看如下图所示。

7-changelog

每一次执行,不会覆盖之前的 CHANGELOG,只会在 CHANGELOG 的顶部添加新的内容。

感受

网上有很多类似的介绍,自己动手去实践,得到的比看到的多的多。

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-04-02

JavaScript 团队规范参考

引子

这是 CSS 团队规范参考 之后一直想写的一部分,后来和一起共事的人结合实际的项目情况,讨论过比较有针对性的 JavaScript 规范,在此基础上,结合个人的一些想法,弄出一个版本,当作参考。

规范

规范是一个团队里面共同遵守的约定。随着时间的推移,有很大可能性会发生变化。在实施中,结合实际项目情况不断的检验、思考、总结、调整,这样就可以逐渐形成最适合自身团队的规范。

原则

  • 临时处理的代码需要注释,以便及时删掉。
  • 块级别作用域用 constlet,不要使用 var
  • 声明引用变量时,统一使用 const, 因为这样可以防止被修改。
  • 创建对象用字面量的方式,禁止使用 new 的方式,new 的方式无法预期结果。
  • 声明对象属性名只能用驼峰形式。
  • 对象中使用简写属性,简写属性和非简写属性分开写。
  • 合并或浅复制对象时,使用 ... 替代 Object.assign()
  • 使用命名函数表达式取代函数声明,原因见 issue
  • 不要修改参数的数据结构和原始值。
  • 字符串用单引号。
  • 使用全等比较。
  • 要写分号。

命名

  • 目录和文件名称使用驼峰形式。
  • 命名语义化,层级相关语义。
  • 目录层级不要超过 3 层。

代码风格

缩进

使用 2 个空格。

分号

以下几种情况后需加分号:

  • 变量声明
  • 表达式
  • return
  • throw
  • break
  • continue
  • do-while

空格

以下几种情况不需要空格:

  • 对象的属性名后
  • 前缀一元运算符后
  • 后缀一元运算符前
  • 函数调用括号前
  • 无论是函数声明还是函数表达式,( 前不要空格
  • 数组的 [ 后和 ]
  • 对象的 { 后和 }
  • 运算符 ( 后和 )

以下几种情况需要空格:

  • 二元运算符前后
  • 三元运算符 ?: 前后
  • 代码块 {
  • 下列关键字前:else, while, catch, finally
  • 下列关键字后:if, else, for, while, do, switch, case, try, catch, finally, with, return, typeof
  • 单行注释 // 后(若单行注释和代码同行,则 // 前也需要),多行注释 *
  • 对象的属性值前
  • for 循环,分号后留有一个空格,前置条件如果有多个,逗号后留一个空格
  • 无论是函数声明还是函数表达式,{ 前一定要有空格
  • 函数的参数之间
// not good
var a = {
  b :1
};

// good
var a = {
  b: 1
};

// not good
++ x;
y ++;
z = x?1:2;

// good
++x;
y++;
z = x ? 1 : 2;

// not good
var a = [ 1, 2 ];

// good
var a = [1, 2];

// not good
var a = ( 1+2 )*3;

// good
var a = (1 + 2) * 3;

// not good
var doSomething = function (a,b,c){
  // do something
};

// good
var doSomething = function(a, b, c) {
  // do something
};

// not good
doSomething (item);

// good
doSomething(item);

// not good
for(i=0;i<6;i++){
  x++;
}

// good
for (i = 0; i < 6; i++) {
  x++;
}

空行

以下几种情况需要空行:

  • 变量声明后(当变量声明在代码块的最后一行时,则无需空行)
  • 注释前(当注释在代码块的第一行时,则无需空行)
  • 代码块后(在函数调用、数组、对象中则无需空行)
  • 文件最后保留一个空行
// 变量声明后
var x = 1;

// 当变量声明在代码块的最后一行时,则无需空行
if (x >= 1) {
  var y = x + 1;
}

var a = 2;

// 注释之前要有一个空行
a++;

function b() {
  // 当注释在代码块的第一行时,则无需空行
  return a;
}

// 代码块后
for (var i = 0; i < 2; i++) {
  if (true) {
    return false;
  }

  continue;
}

// not good
var obj = {
  foo: function() {
    return 1;
  },

  bar: function() {
    return 2;
  }
};

// not good
var foo = [
  2,
  function() {
    a++;
  },
  3
];

// good
var foo = {
  a: 2,
  b: function() {
    a++;
  },
  c: 3
};

换行

换行的地方,行末必须有 , 或者运算符。

以下几种情况不需要换行:

  • 下列关键字后:else, catch, finally
  • 代码块 {

以下几种情况需要换行:

  • 代码块 { 后和 }
  • 变量赋值后
// not good
var a = {
  b: 1
  , c: 2
};

// good
var a = {
  b: 1,
  c: 2
};

// not good
x = y
  ? 1 : 2;

// good
x = y ? 1 : 2;
x = y ?
    1 : 2;

//  'else', 'catch', 'finally' 后不需要换行
if (condition) {
  ...
} else {
  ...
}

try {
  ...
} catch (e) {
  ...
} finally {
  ...
}

// not good
function test()
{
  ...
}

// good
function test() {
  ...
}

// not good
var a, foo = 7, b,
  c, bar = 8;

// good
var a,
  foo = 7,
  b, c, bar = 8;

注释

  • 单行注释单独一行
  • 多行注释最少三行
// one row
var name = 'Tom';

/*
 * multi row
 */
var x = 1;

括号

下列关键字后必须有大括号(即使代码块的内容只有一行):

  • if
  • else
  • for
  • while
  • do
  • switch
  • try
  • catch
  • finally

变量

  • 标准变量采用驼峰式命名
  • ID 在变量名中全大写
  • URL 在变量名中全大写
  • Android 在变量名中大写第一个字母
  • IOS 全部大写
  • 常量全大写,用下划线连接

对比 ESLint

ESLint 的规则默认都是不启用的状态,官方提供了一个 recommended 的规则,详细见 ESLint Rules,从中可以发现:

  • ESLint 对代码语法、逻辑、潜在问题、样式进行了更全面的考虑。
  • ESLint 中包含的一些规则在实际过程中较容易进行辨别。
  • recommended 规则大部分都普遍适用,基于这个规则可以更方便的进行选择定制。

规范 ESLint 化

基于 recommended 规则,根据 ESLint 已有的配置规则,以及上面总结的规范进行自选配置。

{
  "rules": {
    "eqeqeq": "always", // 要求使用 === 和 !==
    "no-caller": "error", // 禁用 caller 或 callee
    "no-new": "error", // 禁止使用 new 以避免产生副作用
    "no-new-func": "error", // 禁止对 Function 对象使用 new 操作符
    "no-new-wrappers": "error", // 禁止对 Function 对象使用 new 操作符
    "no-array-constructor": "error", // 禁止使用 Array 构造函数
    "no-iterator": "error", // 禁用 __iterator__ 属性
    "no-proto": "error", // 禁用 __proto__ 属性
    "no-use-before-define": "error", // 禁止定义前使用
    "quotes": ["error", "single"], // 强制使用一致的反勾号、双引号或单引号
    "indent": ["error", 2], // 强制使用一致的缩进

    "no-multi-spaces": "error", // 禁止出现多个空格
    "array-bracket-spacing": ["error", "always"], // 禁止或强制在括号内使用空格
    "block-spacing": "error", // 禁止或强制在代码块中开括号前和闭括号后有空格
    "comma-spacing": ["error", { "before": false, "after": true }], // 强制在逗号周围使用空格
    "func-call-spacing": ["error", "never"], // 要求或禁止在函数标识符和其调用之间有空格
    "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], // 强制在对象字面量的键和值之间使用一致的空格
    "space-before-blocks": "error", // 要求或禁止语句块之前的空格
    "space-infix-ops": "error", // 要求中操作符周围有空格
    "arrow-spacing": "error", // 要求箭头函数的箭头之前或之后有空格
    "space-before-function-paren": ["error", "never"], // 禁止或强制圆括号内的空格
    "space-in-parens": ["error", "never"], // 禁止或强制圆括号内的空格

    "curly": "all", // 强制所有控制语句使用一致的括号风格
    "brace-style": "error", // 大括号风格要求
    "multiline-comment-style": ["error", "starred-block"], // 强制对多行注释使用特定风格
    "object-curly-newline": ["error", { "multiline": true }], // 强制在花括号内使用一致的换行符
    "eol-last": ["error", "always"], // 要求或禁止文件末尾保留一行空行
    "spaced-comment": ["error", "always"], // 要求或禁止在注释前有空白
    "comma-style": ["error", "last"], // 逗号风格
  }
}

参考资料

查看原文

赞 0 收藏 0 评论 0

XXHolic 发布了文章 · 2020-03-28

CSS 团队规范参考

引子

想起刚工作不久的时候,自己想要明确一套规范,而不只是看到什么就是什么,但实际经验欠缺,就只是简单的了解了一下。现在又有这样的念头了,也有对应的素材,尝试着总结一下,看下能弄出什么样来。

CSS 方法论

谈到 CSS 就离不开 HTML,现在有不少关于阐释 HTML 和 CSS 之间关系的理论。了解这些,对于组织 CSS 很有帮助。下面通过网上的一些资料,介绍一些认可度比较高的方法论。

OOCSS 方法

全称是 Object-Oriented CSS,面向对象的 CSS。在 2008 年的时候就被提出,在 OOCSS wiki 介绍中,该方法有两个主要的原则:

1 分离结构和外观

意思是将视觉特性定义为可复用的单元(例如背景和边框样式)。换句话说,就是把布局和设计的样式都独立出来。见下面的例子:

<div class="warn">warning</div>

样式 warn 表示警告的字体颜色,在系统内,只要是用到警告的地方,统一使用这个样式。

2 分离容器和内容

意思是指把内容从容器中分离开来。不论你放到那里,看起来都是一样的。见下面的例子:

<div class="dialog blue">
  <div class=“dialog-title”><span class="text">Title</span></div>
  <div class="dialog-content show"><span class="text">content</span></div>
</div>

这个例子中不同的模块显示不同的字体和颜色,一般可能会这样做:

.dialog-title .text {color: #333;}
.dialog-content .text {color: #666;}

但在 OOCSS 方法中是通过添加一个新的 class 来描述需要的样式:

<div class="dialog blue">
  <div class=“dialog-title”><span class="text title-text">Title</span></div>
  <div class="dialog-content show"><span class="text content-text">content</span></div>
</div>
.title-text {color: #333;}
.content-text {color: #666;}

当想提供一套组件让开发人员组合起来创建用户界面时,这种方法非常有用。Bootstrap 就是一个例子,它是一个自带各种皮肤的小组件系统。

SMACSS 方法

全称是 Scalable and Modular Architecture for CSS,模块化架构的可扩展 CSS。SMACSS 把样式系统划分为五个具体类别:

  • 基础:标签默认展示的样式,基本上就是元素选择器,可以包含属性选择器、伪类选择器、子选择器或兄弟选择器。在这个里面不应该出现 !important
  • 布局:把页面分成一些区域。
  • 模块:设计中的模块化、可复用的单元,例如列表、弹窗等。
  • 状态:描述在特定的状态或情况下,模块或布局的显示方式。
  • 主题:一个可选的视觉外观层,可以让你更换不同主题。

见下面的例子:

<div class="dialog dialog-blue">
  <div class=“dialog-title”><span class="text">Title</span></div>
  <div class="dialog-content is-show"><span class="text">content</span></div>
</div>

观察模块样式 dialogdialog-bluedialog-titledialog-content 和状态 is-show ,可以发现对于如何创建功能的小模块,OOCSS 和 SMACSS 有许多相似之处。它们都把样式作用域限定到根节点的 CSS 类名上,然后通过皮肤(OOCSS)或者子模块(SMACSS)进行修改。除了SMACSS 把代码划分类别之外,两者之间最显著的差异是使用皮肤而不是子模块,以及带 is 前缀的状态类名。

BEM 方法

全称是 Block Element Modifier,块元素修饰符。BEM 是一种基于组件的 Web 开发方法。其背后的想法是将用户界面划分为独立的块。其中包含的内容不仅仅是 CSS,其中 CSS 的内容结合 About HTML semantics and front-end architecture,得出一套命名规则:

  • 块名:所属组件的名称,一般形式为 .block
  • 元素:元素在块里面的名称,一般形式为 .block__element
  • 修饰符:任何与块或元素相关联的修饰符,一般形式为 .block--modifier

看下面例子:

<div class="dialog dialog-skin-blue">
  <div class=“dialog__title”><span class="dialog__text">Title</span></div>
  <div class="dialog__content dialog__content--show"><span class="dialog__text">content</span></div>
</div>

这种方式不建议使用元素选择器,名称过长时,用 - 连接。BEM 使用非常简洁的约定来创建 CSS 类名,而这些字符串可能会相当长。

这种方法在 OOCSS 或 SMACSS 里使用的好处是,每一个 CSS 类名都详细地描述了它实现了什么。代码中没有 show 或者 is-show 这样只在特定背景下才能理解的 CSS 类名。如果单独看 show 或者 is-show 这两个名字,我们并不知道它们的含义是什么。

虽然 BEM 法看起来很累赘、很冗余 ,但是当看到一个 dialog__content--show 的 CSS 类名,就知道它是表示:这个元素的名称是 content,位置在 dialog 组件里,状态为显示。

关于方法的选择

没有什么方法论是完美的,你可能会发现,不同的项目适合不同的方法。也许还有新的方法,我们还没有发现,不要因为一套规范很流行或者别的团队在使用就选择它。理解方案背后这么做的原因,不要害怕尝试,混合已有的方案,甚至自己或者团队一起创造出独一无二的方案。

规范

原则

  • 删除僵尸代码。
  • 不能有行内 CSS。
  • 不能有空的规则。
  • 选择器不能超过 3 层。
  • 一个标签上的类名不能超过 4 个,给 js 使用的类名不算。
  • 若无 ID 可用,给 js 使用的类样式必须带 js 前缀,且不能有具体样式。
  • 删除冗余 ID,避免使用 ID 选择器,如果有,转换为类选择器。
  • 给出必要的注释说明。
  • 抽离基础样式和功能性样式,并提供注释说明,对团队告知对应位置。
  • 避免使用 !important

由于 CSS 选择符是从右到左进行匹配,所以:

  • 避免使用标签,例如 .content ul li
  • 避免使用通配符。
  • 避免使用子选择符,例如 .content > ul >li

命名

  • 根据选择的方法,与团队成员共同商定命名形式,可能涉及到公用样式、组件样式、对应页面样式。
  • 类名使用小写。
  • ID 采用驼峰式命名。

代码风格

缩进

使用 2 个空格。

空格

需要空格情况:

  • { 前。
  • 冒号 : 后面。
  • !important 前。
  • 注释的开始和结尾。

不需要空格情况:

  • 冒号 : 前面。
  • !important! 后。
  • 多个规则的分隔符 前。
  • 属性值中 ( 后和 ) 前。
/* good */
.nav,
.footer {
  font-size: 18px;
  color: #666 !important;
  background-color: rgba(0,0,0,.5);
}

/*not good*/
.nav ,
.footer{
  font-size :18px;
  color: #666! important;
  background-color: rgba( 0, 0, 0, .5 );
}

空行

需要空行的情况:

  • 文件最后保留一个空行。
  • } 后留空行.
/* good */
.nav {
  font-size: 18px;
}

.footer {
  color: #666;
}

/* not good */
.nav {
  font-size: 18px;
}
.footer {
  color: #666;
}

换行

需要换行的情况:

  • { 后和 } 前。
  • 每个属性及对应值独占一行。
  • 多个规则的分隔符 , 后。
/* good */
.nav,
.footer {
  font-size: 18px;
  color: #666;
}

.body {
  font-size: 16px;
}

/* not good */
.nav,.footer {
  font-size: 18px;color: #666;
}

.body {font-size: 16px;}

分号

每个属性名末尾要加分号。

/* good */
.nav {
  font-size: 18px;
}

/* not good */
.nav {
  font-size: 18px
}

引号

统一使用双引号。

/* good */
.nav:before {
  content: "";
  font-size: 18px;
}

/* not good */
.nav:before {
  content: '';
  font-size: 18px;
}

颜色

使用小写字幕,能简写就使用简写。

/* good */
.nav {
  color: #ab1243;
  background-color: #236;
}

/* not good */
.nav {
  color: #AB1243;
  background-color: #223366;
}

属性值为 0 的情况

  • 不要带单位。
  • 在定义无边框样式时,使用 0 代替 none。
  • 去除小数点前面的 0。

属性简写

需要简写的属性有:

  • margin。
  • padding。
/* good */
.nav {
  margin: 10px 0 0 6px;
  padding: 2px 0 0 3px;
}

/* not good */
.nav {
  margin-top: 10px;
  margin-left: 6px;
  padding-top: 2px;
  padding-left: 3px;
}

属性声明顺序

建议顺序为:

  1. 布局定位属性。
[
  "display",
  "visibility",
  "float",
  "clear",
  "overflow",
  "clip",
  "zoom",
  "table-layout",
  "border-spacing",
  "border-collapse",
  "list-style",
  "flex",
  "flex-direction",
  "justify-content",
  "align-items",
  "position",
  "top",
  "right",
  "bottom",
  "right",
  "z-index",
]
  1. 自身属性。
[
  "margin",
  "box-sizing",
  "border",
  "border-radius",
  "padding",
  "width",
  "min-width",
  "max-widht",
  "height",
  "min-height",
  "max-height",
]
  1. 文本属性。
[
  "font-size",
  "line-height",
  "text-align",
  "vertical-align",
  "white-space",
  "text-decoration",
  "text-emphasis",
  "text-indent",
  "text-overflow",
  "word-wrap",
  "word-break",
  "color",
  "text-shadow",
]
  1. 其它属性。
[
  "background",
  "background-color",
  "background-image",
  "background-repeat",
  "background-attachment",
  "background-position",
  "background-clip",
  "background-origin",
  "background-size",
  "outline",
  "opacity",
  "filter",
  "box-shadow",
  "transition"
  "transform",
  "animation",
  "cursor",
  "pointer-events",
]

媒体查询

尽量将媒体查询的规则靠近与他们相关的规则,不要将他们一起放到一个独立的样式文件中,或者丢在文档的最底部,这样做只会让大家以后更容易忘记他们。

/* good */
.nav {
  font-size: 14px;
}

@media (min-width: 480px) {
  .nav {
    font-size: 16px;
  }
}

对比 stylelint

recommended 配置

对比 stylelint-config-recommended 配置中的规则,发现:

  1. 可能的错误中,大部分一般是比较少碰到,有的碰到也是比较好发现,因为在页面展示上很容易体现出来。
  2. 去重方面可以减少不必要的代码。
  3. 检查有效的源码和注释,可以减少冗余。

standard 配置

stylelint-config-standard 配置中的规则,发现:

  1. 空行、空格、换行在格式上情况更多。
  2. 区分了单行和多行的情况。
  3. 函数、选择器、媒体查询需要单独处理。

规范 stylelint 化

基于 stylelint-config-recommended 配置,根据 stylelint 已有的配置规则,以及上面总结的规范进行自选配置。

"use strict"

module.exports = {
  "extends": "stylelint-config-recommended",
  "rules": {
    // 缩进
    "indentation": 2,
    // 空格
    "block-opening-brace-space-before": "always",
    "declaration-colon-space-after": "always",
    "declaration-bang-space-before": "always",
    "comment-whitespace-inside": "always",
    "declaration-colon-space-before": "never",
    "declaration-bang-space-after": "never",
    "value-list-comma-newline-before": "never",
    // 空行
    "max-empty-lines": 1,
    // 换行
    "block-closing-brace-newline-after": "always",
    "block-opening-brace-newline-after": "always",
    "declaration-block-semicolon-newline-after": "always",
    "selector-list-comma-newline-after": "always",
    // 引号
    "string-quotes": "double",
    // 颜色
    "color-hex-length": "short",
    "color-hex-case": "lower",
  },
}

参考资料

查看原文

赞 0 收藏 0 评论 0