分类 Web Front End 下的文章

最近处理一些用户录入数据时,发现有一些明明是纯数字的数据,却通过不了类型校验(无法解析成 Int/BigInt)类型,由于一直都是通过日志查看错误,觉得很疑惑,后来找到原始数据,发现因为这个『字符串』带了一个 zero width space,导致程序异常。

zero width space 在大多数文本输出的地方都是不可见的(顾名思义,没有宽度的空格),因此通过类似页面、日志排查都很难发现这个问题。

当把这个字符串手动复制粘贴到 Chrome 的控制台时,可以看到,这个 zero width space 表现成了一个·,这应该是 Chrome 为了方便调试设计的。

zero width space

而粘贴到 iTerm 终端,则可以直接看到 Unicode 值(但是当作为日志输出到终端时,是无法显示的)

zero width space in iTerm

这个字符一般来源都是用户从一些特定的平台复制而来,因此建议在类似的用户数据录入处进行一下相应处理,这里需要注意,一般语言的标准库的 trim 处理都是不会处理 zero width space 的,所以只能手动移除\u200B( 正则下)。

事实上,还有很多不可见字符,具体请参照这个回答:

https://stackoverflow.com/a/11305926/2530524

Control character in ASCII

引言

在一年多之前,我写了一篇文章《The way to Angular 2 | Part.1》介绍 Angular,最初打算持续完成一个系列,然后在我写完文章之后,Angular 和 TypeScript 都发生了相当大的变化,整个系列也搁浅了。 如今,我打算换一个角度,继续这个系列。

为什么要使用 Angular

angular.io_features

为什么要使用 TypeScript

www.typescriptlang.org_index.htm

- 阅读剩余部分 -

随机数与随机字符串

早前我都使用Math.random()来生成随机数,然后转成16进制字符串来生成随机字符串,后来看到了这个回答(stackoverflow)Math.random() 确实不是一个好的选择,因此我需要更加科学的方法。

window.crypto (Web Cryptography API)

Web Cryptography API 已经是 W3C Recommendation 级别,常见浏览器的近期版本也都已经实现了。 0ivS.png 使用的时候也可以不用担心兼容性问题。

crypto.getRandomValues

crypto.getRandomValues 是我们用来实现随机字符串的主要方法,这个方法从实现和随机性的角度来说,更加高效、可靠。 10.2.1. The getRandomValues method The getRandomValues method generates cryptographically random values. It must act as follows:

  1. If array is not of an integer type (i.e., Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array or UInt8ClampedArray), throw a TypeMismatchError and terminate the algorithm.
  2. If the byteLength of array is greater than 65536, throw a QuotaExceededError and terminate the algorithm.
  3. Overwrite all elements of array with cryptographically random values of the appropriate type.
  4. Return array.
Note Do not generate keys using the getRandomValues method. Use the generateKey method instead.

使用方法就是传入一个 Int 的数组,然后返回被加密之后的数组(覆盖原始数组)。

科学的随机字符串生成方法

知道了crypto.getRandomValues的使用方法之后,我们就可以来实现一个科学的随机字符串生成方法了。 首先毫无疑问我们需要一个 UintArray,这比我们手动生成随机数的数组要简单多了。

let len = 64;
const arr = new Uint8Array(len / 2);

假设我们要生成长度为 64 的随机字符串,len 就是 64,之所以在生成 Uint8Array 时候,要将长度除以2,是因为最终我们是通过16进制字符串的形式来输出的,并且在头部补0取末尾两位,所以只需要一半长度的 Uint8Array 就可以了。 用crypto.getRandomValues()加密这个数据

window.crypto.getRandomValues(arr);

把这个 Uint8Array 转换成普通数组,并且把每一个值转换成16进制字符串(首位补0),取末尾2位,最后拼接到一起,就是最终的随机字符串了。

// dec2hex :: Integer -> String
function dec2hex (dec) {
  return ('0' + dec.toString(16)).substr(-2);
}
    
// generateId :: Integer -> String
function generateId (len = 40) {
  const arr = new Uint8Array(len / 2);
  window.crypto.getRandomValues(arr);
  return Array.from(arr, dec2hex).join('');
}

使用这个方法来生成随机字符串,更加高效、科学、可靠。 gist: Generate random string/characters in JavaScript

问题

起因是这样的,一开始在项目中写了一个原始的 Popup 组件(通过 react-redux 的 connect 方法接入 redux),后来有了一个更高级的需求,我就想要直接 extends 这个原始的 Popup 组件,然后覆写一些方法。 然后遇到了一个问题,继承之后的子组件没有父组件的自定义方法。

解疑

最初一度怀疑是否是 babel 的 decorator 插件有问题(毕竟不是浏览器天然支持的特性),但是仔细看了 decorator 插件的源码,发现并不存在这种问题。 后来在这个插件的 issues 里看到了这么一个 issue,问题并不算是同一个,但是作者提到,react-redux 的 connect 方法可能实现有一些问题(当然这个地方并不是),我就想到去看看 connect 的实现。 然后就看到了底层方法 connectAdvanced 中有这么一句代码:

export default connectAdvanced() {
    // ...

    return hoistStatics(Connect, WrappedComponent);
}

hoistStatics 是一个第三方库 hoist-non-react-statics,它干的事情也比较简单:

Copies non-react specific statics from a child component to a parent component. Similar to Object.assign, but with React static keywords blacklisted from being overridden.

简单来说,就是复制所有非 React 自有的属性,因为使用了 Object.getOwnPropertyNames()的方法,所以我们定义在 Component 上的所有方法就不会被复制到这个新的对象上了。 而在 connectAdvanced 内部,render 时实际是这样的:

render() {
  const selector = this.selector
  selector.shouldComponentUpdate = false

  if (selector.error) {
      throw selector.error
  } else {
      return createElement(WrappedComponent, this.addExtraProps(selector.props))
  }
}

通过 createElement 方法给原始的 WrappedComponent 传入额外的 props,来实现 connect 的效果。 总之,最终 connect 方法返回这个对象,其实和原始的 Component 的结构并不一样,虽然它的 WrappedComponent 属性指向了原始的 Component,但是显然在我们的场景下,并不能使用。

那怎么办呢

通常的推荐解决方式就是『高阶组件』,这样就屏蔽了 connect 的逻辑。

为什么要用 hoist-non-react-statics 这种模式

关于这个问题,作者在 这一个 issue 的回答中 也说了很多,总结来说,就是为了避免使用者可以直接的接触到 connect 之后的组件的内部方法,防止因为各种改动导致组件的 "break",这也算是为了提升健壮性。

build delightful, modern, responsive web experiences. — Google Web Fundamentals

引言

前端的性能指标在通常情况下是一个很难量化的指标,面向的用户群体和环境复杂,在现有条件下,网页的绝对性能仍然取决于用户的网络连接。
因此本文基本参考 Google 在 PageSpeed Insights 中的标准,考量点都是与网络无关的因素:服务器配置、网页的 HTML 结构及其所用的外部资源。
实际上前端的性能优化,非常依赖于开发者的经验,技术和环境的快速发展,大多数开发者还没能建立起完善、可靠的性能优化体系。因此,我更喜欢参考大型企业的标准,来制定优化规则。

Performance

首先要提到这个单词,一般来说,它可以被翻译成高性能,不过,在前端领域,其实 Performance 还有另外一层含义,『高表现力』。
高表现力就意味着高可用性、高易用性和高度的用户友善设计。不过这些内容层次太高,本文只讨论高性能这一个环节。

大型企业的标准

从 Google 建立以来,它一直是这个世界上最顶尖的 Web 技术开发和使用者之一,我认为它所施行和推广的 Web 技术体系,是可以作为我们开发过程中的有效参考的。
首先要介绍的是两个 Google 下属的直接面向前端性能优化的项目,PageSpeed 和 Web Fundamentals。

PageSpeed

PageSpeed 是个历史悠久的项目,从建立以来就是针对高性能的网站开发。它的工作流程就是,抓取网页,按照一定的规则去分析网页,给出参考的性能评分和优化建议。
所以首先要看的就是它的规则。在性能方面,PageSpeed 的规则概括为以下几条:

  • Avoid landing page redirects
  • Enable compression
  • Improve server response time
  • Leverage browser caching
  • Minify resources
  • Optimize images
  • Optimize CSS Delivery
  • Prioritize visible content
  • Remove render-blocking JavaScript
  • Use asynchronous scripts

在这些规则之中,有一些是已经成为成熟的方案,而被广泛采用,成为整个技术实现的基本环节,这些规则不再需要阐述,因为大家都在这么做了,比如 Enable compression (gzip 压缩)。所以下面只介绍一些比较容易被忽略的问题。

1. Avoid landing page redirects

先介绍一下 Landing Page,根据 Wiki 的定义

a single web page that appears in response to clicking on a search engine optimized search result or an online advertisement.

简单来说就是用户从另一个页面(站点)跳转到目标站点的入口页面,通常情况下就是网站的首页。所以这条规则就是避免首页的重定向
理由也很简单,最好的情况下,每一次重定向就增加一次往返(HTTP 请求-响应),而糟糕的情况下就会增加很多次。每一次的往返中包含了 DNS 查询,TCP 握手,TLS 认证环节。
以 Snow/Beef (主站退单/手淘退单)为例。为了区分不同的订单退单情况,这两个项目都使用服务器端跳转来引导客户端页面转向到对应的路由(业务逻辑)。
那么从用户的角度来说,这里就多了一次往返逻辑,用户需要多等待一次请求/响应,然后再进入对应的页面。当然,这也是因为受限于客户端(APP)的 WebView 环境而选择一种解决方案。

2. Optimize images

这个问题主要发生在一些设计素材上,为了保证高度还原而选择高质量的图片,就忽略了图片的大小。
有时候也可以适当考虑,使用 CSS 来实现,以减小图片的使用率。

讨论

如同上文所说,这些规则基本上已经成为了业界广泛采用的标准,以现在的环境来看,这些大方向上的实践已经没有太多问题。因此,如果想要更进一步的提升页面性能,那就需要更加细致的挖掘。

Web Fundamentals

Web Fundamentals 是 Google 2014 年发起的项目。

The fundamental knowledge you need to build delightful, modern, responsive web experiences.

项目总结也很简单:提供令人赏心悦目的现代化响应式 Web 体验所必须的基础知识。其中有一个章节『Performance』着重介绍了网页性能这一块的知识,并且,由于项目较新,所以讨论到的内容以及技术也适应了新的环境,比较有参考价值。

概述

这个章节这样总结提高性能的技巧:提高性能表现的过程要从最小化,或者至少从优化用户下载数据开始。提高代码效率的前提是要理解浏览器是如何渲染这些文件的。最后,你需要一些方法来测试你的优化的效果。
最小化(优化)数据内容大小,提高代码效率,测试优化效果。这些就是一个完整的优化方法。

1. Optimizing Content Efficiency

这一节都是关于如何优化『delivery』的效率,作者总结了几个要点:

  • 限制不必要的下载
  • 优化文本类型资源的编码和传输大小
  • 优化图片
  • 优化 Web Font
  • HTTP 缓存

对比一下之前在 PageSpeed 一节提到的优化点,这里新增了『限制不必要的下载』、『优化 Web Font』。

  1. 限制不必要的下载现在在我们的项目体系中,开源项目占了越来越大的比重,模块化的概念越来越深入,在开发过程中,我们会引入相当多的第三方资源,所以仔细检查这些资源,并认真考量它们是否有必要被引入。同时,对于多页面应用,应该检查每个页面是否只下载了它所需要的资源。 当然,还需要优化资源的大小,以及优化它的技术实现。
  2. 优化 Web Font 这种技术其实受硬件发展的影响更多一点,网络的发展,使得页面可以更快地加载越来越多、越来越大的资源,所以更加丰富的 Web Font 也流行起来。带来的问题是,资源大小的增加和页面文字重新渲染的问题。

以金融运营平台为例,引入了 Font-awesome、Glyphicon 两种 Web Font(icon font),Font-awesome 用来处理各种图标,而 Glyphicon 是因为 ui-grid 这个库依赖于它。两个 Web Font 其实非常大,并且 Glyphicon 只有 ui-grid 使用了,并且用到的字体非常少。在后续优化中,针对 Glyphicon 的 CSS 设计,使用 Font-awesome 写了替代规则,从而移除了 Glyphicon。
文章中其实提供了相当多的 Web Font 优化技巧,也可以作为很好的参考。

2. Critical Rendering Path

浏览器在获取 HTML,CSS 和 JavaScript 文件之后,将他们渲染成页面上每一个对应的像素,所经过的步骤就是 critical rendering path,就是也就是概述中提到的理解浏览器如何渲染页面。当然,浏览器引擎对于大多数人来说还是太复杂、太深奥了一点,我们可以了解一下简略的内容。

  1. Constructing the Object Model第一步简单来说就是构建 DOM 和 CSSOM 树。所以很显然,为了浏览器可以尽快的渲染页面,我们必须尽快地把 HTML 和 CSS 交给浏览器。这就是为什么我们说要把 CSS 资源写在 head 标签中,而把 JavaScript 资源写在 body 标签的结尾处,为了让浏览器优先获取 HTML 和 CSS,不让 JavaScript 阻塞页面渲染。
  2. Render-tree Construction, Layout, and Paint 上一部已经生成了 DOM 和 CSSOM 树,这一个步骤首先就是要把它们合并成 Render 树。要注意的是,Render 树只会包含渲染页面所需要的节点。接着 Layout 引擎会计算每一个对象的位置和大小。最后把 Render 树渲染到屏幕上。
  3. Render Blocking CSS 首先要了解,HTML 和 CSS 都是阻塞性的资源,浏览器在处理完 CSSOM 之前,不会进行任何渲染动作,所以我们保证 CSS 资源的加载速度。

另外,Media type 和 Media query 是可以把 CSS 标记为非阻塞形式,这样就可以保证浏览器只处理需要的资源。不过即使是这样,浏览器还是需要先把所有 CSS 资源下载完成,所以,依然需要保证 CSS 资源的优化。

  1. Adding Interactivity with JavaScript 同样,JavaScript 也会阻塞 DOM 的构建,延长页面的渲染时间,所以确保只加载必须的 JavaScript 的资源,同时,不会影响到页面功能的 JavaScript 资源使用异步加载(或 async 特性),来提高页面渲染速度。

async 这种特性,使用的比较多的,就是各类页面分析、页面监控服务的 JavaScript 库,因为它们的逻辑和页面主线功能互不影响。

到这里为止,页面的渲染就完成了,在这之后,作者还分享了针对 Critical Rendering Path 的各种优化方法,可以进一步了解学习(都包括在文末参考文献中)。

3. Rendering Performance

这一节的主要内容就是如何提升你的代码效率。毫无疑问,你需要了解浏览器处理 HTML,CSS 和 JavaScript 的方式。作者提到:高性能的页面,不仅仅是加载迅速,同时还要能正确地运行;页面滚动必须无延迟跟随手指,动画和交互必须如丝般顺滑,这就是所说的『令人赏心悦目的 Web 体验』。
绝大部分设备 1 秒钟刷新屏幕 60 次,这就是我们说的页面动画必须达到 60 fps 的原因,然后简单计算一下的话,每一帧有 16 毫秒时间,除去浏览器工作的开销,基本上我们在一帧之内有 10 毫秒的时间来完成我们的各种工作,那么,如果说某一个任务超过了 10 毫秒,就会带来一个直观的感受,『页面卡了』。
浏览器的一次工作流程就是:JavaScript > Style > Layout > Paint > Composite 所以也很容易理解,为了提升页面效率,就需要尽可能地减少浏览器的工作步骤,而在上面的五个步骤中,Layout 和 Paint 是可以被避免的,这就是我们优化的关键

  1. Optimize JavaScript Execution简单来说,在不恰当的时间点运行的 JavaScript 或者运行周期比较长的 JavaScript 代码,就会导致性能问题,这些就是优化的关键点。
    当然,首先了解到,实际上在浏览器中执行的代码和你写的 JavaScript 代码并不一样,现代浏览器都采用了 JIT 编译器来优化你写的代码,以提升运行效率,不过这是比较复杂的问题了,我们还是从简单的地方入手。
    先了解一个概念 Visual Changes,就是那些不会直接影响页面样式的代码逻辑,比如查找数据和数据排序。

    • 避免使用 setTimeout 和 setInterval 来进行 Visual Change 操作,使用 requestAnimationFrame 来进行替代。
    • 把耗时较长的 JavaScript 移动到 Web Worker 中执行,和主线程分离。
    • 细化 JavaScript 任务,使得 DOM 的变化分布到不同的帧中。
  2. Reduce the Scope and Complexity of Style Calculations这一条很好理解,降低样式的复杂度。由于每一次 DOM 的变更,浏览器都需要重新计算元素样式,进行 Layout(或者 reflow),所以降低样式复杂度,是提升页面渲染效率的很重要的一环。

    • 降低选择器的复杂度;使用以 class 为中心的 CSS 设计模式,比如 BEM。
    • 减少样式计算会涉及到的元素数量。
  3. Avoid Large, Complex Layouts and Layout ThrashingLayout 就是浏览器计算元素在页面内的位置和大小的过程。和样式计算类似,Layout 的开销收到需要进行 Layout 计算的元素的数量,以及一次 Layout 的复杂程度的影响。

    • Layout 通常情况下会包括整个 document。
    • DOM 元素的数量会影响性能;应该尽可能避免触发 Layout。
    • 评估 Layout 模型的性能;新的 Flexbox 模型通常要比旧的 Flexbox 模型以及基于 Float 的模型要更快。
    • 避免强制同步 Layout(在 JavaScript 执行之前强制浏览器执行 Layout 动作,请参考上文浏览器 render 的过程),以及 Layout 抖动;先读取样式值,然后再进行写入操作。当任何几何属性发生变更的时候(比如 width, height, left, top),都会触发 Layout。

Layout 是开销非常大的操作,应该在可能的情况下避免它。对于 Flexbox,目前主流浏览器都已经支持了 Flex 布局,在环境允许的情况下,应当优先选择 Flexbox,并且使用新的规范。

  1. Paint Complexity and Reduce Paint Areas Paint 通常是整个渲染过程中运行时间最长的任务。

    • 除了 transform 和 opacity 之外的任何属性变更都会触发 paint。
    • paint 一般是渲染流程中开销最大的部分。
    • 通过图层提升和动画编排来减少需要 paint 的区域。
    • Chrome DevTools 可以获取 paint 的分析数据。
这里提到了图层提升这个概念,就类似于 Sketch、GIMP、Photoshop 中的图层,不同的独立图层可以单独计算并最终合并。

在 CSS 中,will-change 属性会创建一个新的图层,然后把值设为 transform 就会创建一个合成器层。这个方法支持 Chrome、Opera、Firefox。
那么对于那些不支持的浏览器,就只能使用 3D 变形来创建一个新的图层。利用transform: translateZ(0) 强制进行 3D 变形。需要注意,创建新的图层会增加内存和管理开销,所以需要合理安排图层。

  1. Stick to Compositor-Only Properties and Manage Layer Count 这一节深入探讨了合成器的概念,如果对于动画性能优化有兴趣,可以深入了解一下。
  2. Debounce Your Input Handlers 对各种输入操作的处理进行防抖。输出操作的处理是一个潜在的性能问题,他们可能会阻塞帧的计算,并且增加额外的、不必要的 Layout 任务。

    • 避免长时间的输入操作处理任务;它们会阻止页面滚动。
    • 不要在输出操作处理任务中进行样式变更。
    • 对处理任务进行防抖;存储事件参数,并且在下一个 requestAnimationFrame 的回调中进行样式变更。
这里举出实际的例子就是,如果监听了 `touchstart`, `touchmove`, `touchend` 之类的事件,并且调用了 `preventDefault()` 那么合成器线程就必须要等待你的事件回调结束,在这段时间内,滚动操作就会被阻止。事实上,即使你没有调用 `preventDefault()` 合成器也还是要等待,就会导致用户的滚动操作被锁住,出现丢帧之类的问题。

从规则到实践

以『前端』、『性能』、『优化』之类的关键词 Google 一下,可以得到数不清的结果,关于各类优化的奇巧淫技,不需要累述,我们更需要关注的是,如何有效、高效、科学地进行性能优化,就如同我们优化页面性能一样,去优化我们的开发过程。

参考文献

  1. PageSpeed Insights Rules
  2. Web Fundamentals – Performance