分类 Web Front End 下的文章

最近在由于某个开发的移动平台的工作环境变成了 Firefox,在一次重新安装了 Ubuntu 系统之后,mp3 无法正常播放,提示错误大致如下

 Warning: Cannot play media. No decoders for requested formats: audio/mpeg

不管是通过是 <audio src></audio> 还是 new Audio() 的方式,都报出这个错误,最后总结出来三条解决方法:

  1. 安装 ubuntu-restricted-extras sudo apt-get install ubuntu-restricted-extras
  2. 安装 gnash sudo apt-get install gnash (一个 gnu 做的 flash 播放器,其中包含了浏览器插件)
    这条比较容易忽略,虽然我也不明白为什么 Firefox 调用解码器还和 flash 有关。
  3. 安装最新版的 Firefox,实测 59 版本就算满足了上面两条,还是无法正常播放,而 63 版本正常。

最近处理一些用户录入数据时,发现有一些明明是纯数字的数据,却通过不了类型校验(无法解析成 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 的回调中进行样式变更。

这里举出实际的例子就是,如果监听了 touchstarttouchmovetouchend 之类的事件,并且调用了 preventDefault() 那么合成器线程就必须要等待你的事件回调结束,在这段时间内,滚动操作就会被阻止。事实上,即使你没有调用 preventDefault() 合成器也还是要等待,就会导致用户的滚动操作被锁住,出现丢帧之类的问题。

从规则到实践

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

参考文献

  1. PageSpeed Insights Rules
  2. Web Fundamentals – Performance

前端项目

启动流程

  1. 页面载入之后,引入的脚本是 boot.js,由 es2015 的 shim,systemjs,systemjs 的配置文件和 boot.js 组成。
  2. 执行 boot。设定 cache 时间,import app/app.js,执行 init 函数。
  3. init 过程,生成 GrafanaAPP。
    1. 从 app/core/config 引入配置参数,实际配置参数是 app/core/settings.js
    2. 定义依赖,ngModuleDependencies(第三方模块),核心模块 app/core/core(controller, directive…),功能模块(’app/features/all’)
    3. Systemjs 加载所有功能模块之后,向模块里注入providers。
  4. angular app 的初始设置由后端直接渲染到页面中,页面文件 public/views/index.html,参数是 window.grafanaBootData
  5. app 启动时会自动加载所有在 app/featrures/all.js 中声明过的模块,所以如果有新的功能模块,确保它最终在该文件中声明了。

开发说明

  1. app/core/utils/kbn.js 是各类格式化函数。
  2. 如何定义模块
    1. typescript 编写的模块按照 ES2015 的模块化语法编写。
    2. JavaScript 编写的模块按照 AMD 格式定义。依赖会自动注入。root 目录是 public。
  3. 引入新的第三方模块。
    1. 可以直接放入 public 下的 vendor 目录,然后在 app/system.conf.js 中加入定义,之后可以直接通过模块名加载。
  4. 如果是 angular 的模块,在 app/app.ts 中的 ngModuleDependencies 数组中加入模块名来加载它。

数据

  1. dashboard controller 初始化完成之后,会通过 dashboardSrv 创建新的 dashboard 数据对象,然后在 dashboard_ctrl 的 setupDashboardInternal 方法中初始化各个注册的模块。
  2. 如果你添加了新的功能模块,需要依赖 dashboard 数据,可以在 dashboard_ctrl 中注入 service 方法,然后在 setupDashboardInternal 进行 init 动作。
  3. 所有需要持久化的数据都挂载在 dashboard 数据对象下,如果有新的需要持久化的数据,可以挂载在 dashboard 数据对象下,注意避免 key 的冲突。每次用户保存 dashboard 的时候,dashboard 数据对象就会提交到后端保存。

原文链接:http://michalzalecki.com/lazy-load-angularjs-with-webpack/

随着你的单页应用扩大,其下载时间也越来越长。这对提高用户体验不会有好处(提示:但用户体验正是我们开发单页应用的原因)。更多的代码意味着更大的文件,直到代码压缩已经不能满足你的需求,你唯一能为你的用户做的就是不要再让他一次性下载整个应用。这时,延迟加载就派上用场了。不同于一次性下载所有文件,而是让用户只下载他现在需要的文件。

所以。如何让你的应用程序实现延迟加载?它基本上是分成两件事情。把你的模块拆分成小块,并实施一些机制,允许按需加载这些块。听起来似乎有很多工作量,不是吗?如果你使用 Webpack 的话,就不会这样。它支持开箱即用的代码分割特性。在这篇文章中我假定你熟悉 Webpack,但如果你不会的话,这里有一篇介绍 。为了长话短说,我们也将使用 AngularUI Router 和 ocLazyLoad 。

代码可以在 GitHub 上。你可以随时 fork 它。

Webpack 的配置

没什么特别的,真的。实际上从你可以直接从文档中复制然后粘贴,唯一的区别是采用了 ng-annotate ,以让我们的代码保持简洁,以及采用 babel 来使用一些 ECMAScript 2015 的魔法特性。如果你对 ES6 感兴趣,可以看看这篇以前的帖子 。虽然这些东西都是非常棒的,但是它们都不是实现延迟加载所必需的东西。

// webpack.config.js
var config = {
  entry: {
    app: ['./src/core/bootstrap.js'],
  },
  output: {
    path:     __dirname + '/build/',
    filename: 'bundle.js',
  },
  resolve: {
    root: __dirname + '/src/',
  },
  module: {
    noParse: [],
    loaders: [
      { test: /\.js$/, exclude: /node_modules/,
        loader: 'ng-annotate!babel' },
      { test: /\.html$/, loader: 'raw' },
    ]
  }
};

module.exports = config;

应用

应用模块是主文件,它必须被包括在 bundle.js 内,这是在每一个页面上都需要强制下载的。正如你所看到的,我们不会加载任何复杂的东西,除了全局的依赖。不同于加载控制器,我们只加载路由配置。

// app.js
'use strict';

export default require('angular')
  .module('lazyApp', [
    require('angular-ui-router'),
    require('oclazyload'),
    require('./pages/home/home.routing').name,
    require('./pages/messages/messages.routing').name,
  ]);

路由配置

所有的延迟加载都在路由配置中实现。正如我所说,我们正在使用 AngularUI Router ,因为我们需要实现嵌套视图。我们有几个使用案例。我们可以加载整个模块(包括子状态控制器)或每个 state 加载一个控制器(不去考虑对父级 state 的依赖)。

加载整个模块

当用户输入 /home 路径,浏览器就会下载 home 模块。它包括两个控制器,针对 home 和 home.about 这两个state。我们通过 state 的配置对象中的 resolve 属性就可以实现延迟加载。得益于 Webpack 的 require.ensure 方法,我们可以把 home 模块创建成第一个代码块。它就叫做 1.bundle.js 。如果没有 $ocLazyLoad.load,我们会发现得到一个错误 Argument 'HomeController' is not a function, got undefined,因为在 Angular 的设计中,启动应用之后再加载文件的方式是不可行的。 但是 $ocLazyLoad.load 使得我们可以在启动阶段注册一个模块,然后在它加载完之后再去使用它。

// home.routing.js
'use strict';

function homeRouting($urlRouterProvider, $stateProvider) {
  $urlRouterProvider.otherwise('/home');

  $stateProvider
    .state('home', {
      url: '/home',
      template: require('./views/home.html'),
      controller: 'HomeController as vm',
      resolve: {
        loadHomeController: ($q, $ocLazyLoad) => {
          return $q((resolve) => {
            require.ensure([], () => {
              // load whole module
              let module = require('./home');
              $ocLazyLoad.load({name: 'home'});
              resolve(module.controller);
            });
          });
        }
      }
    }).state('home.about', {
      url: '/about',
      template: require('./views/home.about.html'),
      controller: 'HomeAboutController as vm',
    });
}

export default angular
  .module('home.routing', [])
  .config(homeRouting);

控制器被当作是模块的依赖。

// home.js
'use strict';

export default angular
  .module('home', [
    require('./controllers/home.controller').name,
    require('./controllers/home.about.controller').name
  ]);

仅加载控制器

我们所做的是向前迈出的第一步,那么我们接着进行下一步。这一次,将没有大的模块,只有精简的控制器。

// messages.routing.js
'use strict';

function messagesRouting($stateProvider) {
  $stateProvider
    .state('messages', {
      url: '/messages',
      template: require('./views/messages.html'),
      controller: 'MessagesController as vm',
      resolve: {
        loadMessagesController: ($q, $ocLazyLoad) => {
          return $q((resolve) => {
            require.ensure([], () => {
              // load only controller module
              let module = require('./controllers/messages.controller');
              $ocLazyLoad.load({name: module.name});
              resolve(module.controller);
            })
          });
        }
      }
    }).state('messages.all', {
      url: '/all',
      template: require('./views/messages.all.html'),
      controller: 'MessagesAllController as vm',
      resolve: {
        loadMessagesAllController: ($q, $ocLazyLoad) => {
          return $q((resolve) => {
            require.ensure([], () => {
              // load only controller module
              let module = require('./controllers/messages.all.controller');
              $ocLazyLoad.load({name: module.name});
              resolve(module.controller);
            })
          });
        }
      }
    })
    ...

我相信在这里没有什么特别的,规则可以保持不变。

加载视图(Views)

现在,让我们暂时放开控制器而去关注一下视图。正如你可能已经注意到的,我们把视图嵌入到了路由配置里面。如果我们没有把里面所有的路由配置放进 bundle.js,这就不会是一个问题,但现在我们需要这么做。这个案例不是要延迟加载路由配置而是视图,那么当我们使用 Webpack 来实现的时候,这会非常简单。

// messages.routing.js
  ...
  .state('messages.new', {
        url: '/new',
        templateProvider: ($q) => {
          return $q((resolve) => {
            // lazy load the view
            require.ensure([], () => resolve(require('./views/messages.new.html')));
          });
        },
        controller: 'MessagesNewController as vm',
        resolve: {
          loadMessagesNewController: ($q, $ocLazyLoad) => {
            return $q((resolve) => {
              require.ensure([], () => {
                // load only controller module
                let module = require('./controllers/messages.new.controller');
                $ocLazyLoad.load({name: module.name});
                resolve(module.controller);
              })
            });
          }
        }
      });
  }

  export default angular
    .module('messages.routing', [])
    .config(messagesRouting);

当心重复的依赖

让我们来看看 messages.all.controller 和 messages.new.controller 的内容。

// messages.all.controller.js
'use strict';

class MessagesAllController {
  constructor(msgStore) {
    this.msgs = msgStore.all();
  }
}

export default angular
  .module('messages.all.controller', [
    require('commons/msg-store').name,
  ])
  .controller('MessagesAllController', MessagesAllController);
// messages.all.controller.js
'use strict';

class MessagesNewController {
  constructor(msgStore) {
    this.text = '';
    this._msgStore = msgStore;
  }
  create() {
    this._msgStore.add(this.text);
    this.text = '';
  }
}

export default angular
  .module('messages.new.controller', [
    require('commons/msg-store').name,
  ])
  .controller('MessagesNewController', MessagesNewController);

我们的问题的根源是 require('commons/msg-store').name 。它需要 msgStore 这一个服务,来实现控制器之间的消息共享。此服务在两个包中都存在。在 messages.all.controller 中有一个,在 messages.new.controller 中又有一个。现在,它已经没有任何优化的空间。如何解决呢?只需要把 msgStore 添加为应用模块的依赖。虽然这还不够完美,在大多数情况下,这已经足够了。

// app.js
'use strict';

export default require('angular')
  .module('lazyApp', [
    require('angular-ui-router'),
    require('oclazyload'),
    // msgStore as global dependency
    require('commons/msg-store').name,
    require('./pages/home/home.routing').name,
    require('./pages/messages/messages.routing').name,
  ]);

单元测试的技巧

把 msgStore 改成是全局依赖并不意味着你应该从控制器中删除它。如果你这样做了,在你编写测试的时候,如果没有模拟这一个依赖,那么它就无法正常工作了。因为在单元测试中,你只会加载这一个控制器而非整个应用模块。

// messages.all.controller.spec.js
'use strict';

describe('MessagesAllController', () => {

  var controller,
      msgStoreMock;

  beforeEach(angular.mock.module(require('./messages.all.controller').name));
  beforeEach(inject(($controller) => {
    msgStoreMock = require('commons/msg-store/msg-store.service.mock');
    spyOn(msgStoreMock, 'all').and.returnValue(['foo', 8]);
    controller = $controller('MessagesAllController', { msgStore: msgStoreMock });
  }));

  it('saves msgStore.all() in msgs', () => {
    expect(msgStoreMock.all).toHaveBeenCalled();
    expect(controller.msgs).toEqual(['foo', 8]);
  });

});

Angular 2 已经进入 beta 通道有一段时间了,也有不少人开始在各种情景中使用 Angular 2。我自己也在过去两个月里进行了一些基于 Angular 2 的项目尝试。

The way from Angular 1.x to 2

这应该是大多数人很关心的一个问题,从 1.x 到 2,Angular 究竟改变了什么?
一言难尽,这依旧是 Angular,依旧是最基础的那套思想,然后已经从最基础的设计架构层面进行了变更。

新的设计架构,真正的模块化
ANGULAR2 ARCHITECTURE OVERVIEW

  1. Component,控制页面上的每一个 View。
  2. Template,改进以后模板语法,熟悉 Angular 1.x 的人应该可以比较快地上手。
  3. Metadata,因为采用 Class 作为基础而引入的新特性,它向 Angular 描述了如何去处理一个 Class。
  4. Data Binding,Angular 2 将数据绑定的形式(或者说语法)设计成了 4 种,相比原来的,更加统一。
  5. Directive,Directive 依旧和以前一样执行它的任务,根据自身的指令将 DOM 转化成相应的形态。
  6. Serivces,服务一直都是一个宽广的概念,任何应用需要的值、方法和功能都可以是一个服务。Angular 2 没有对 Service 进行任何特殊的设定(没有 Service 相关的基础 Class),只要按照应用的需求进行设计就可以了。
  7. Dependency Injection,依赖注入现在语法变得更加简单,而在 TypeScript 版本中,Angular 还可以让 Component 根据构造参数去查询 Service(自动提示和类型校验会变得友善和精确)。
  8. 其他
    • 应用启动方法
    • 基于 Zone 的变化检测
    • 针对 Component 的路由
    • 事件体系
    • 表单模块
    • 新的生命周期设计
    • filter 被重命名为 Pipe(管道),更加贴切了

TypeScript

毫无疑问,在迈向 Angular 2 的路上,会遇到的第一道坎就是 TypeScript。其实官方是有 JavaScript 和 Dart 版本的,但是作为官方推荐的编写语言,TypeScript 带来的好处也是显而易见的:

  1. 强类型。动态语言总是让人觉得不放心,在逻辑和结构复杂的大型应用中,难免会出现因为使用偏差而导致的错误,TypeScript 提供的强类型校验,可以有效地避免这个问题。
  2. ES2015 的超集。从最古老的 JavaScript 语法,一直到最新的 Async/Await 以及 Decorator,得益于 TypeScript 的编译器,我们现在就可以从这些新特性中受益。
  3. 模块化。不管是 AMD、CMD 还是 ES2015 的模块化设计, TypeScript 的编译器都可以帮你自动处理(将它自己的模块化语法转换成你需要的标准)。
  4. 编译器检查。编译过程中的错误检查可以在很大程度上提高我们的 debug 效率,也可以辅助我们解决一些问题。
  5. 开发环境。静态类型检查,自动补全,代码提示等等。
  6. 健壮性。以上这些都是为了提高你的应用的健壮性和可扩展性。

我基本上已经表明了自己的立场,我是非常推荐使用 TypeScript 来进行 Angular 2 的应用开发的,理由也已经在上面描述过。
我认为 TypeScript 不会成为向 Angular 2 升级的阻碍。毫不夸张地说,将现有的 JavaScript 文件后缀名改成.ts就可以正常使用,因此 TypeScript 对于 plain JavaScript 的兼容性是非常好的。
你在 TypeScript 中写的东西依旧是 JavaScript,并非一种全新的语言。就像 TypeScript 官方所说:始于 JavaScript,终于 JavaScript。

介绍

在 React-Native 中,实现了一个类似于 webpack 的包系统,命令行工具提供了一个 bundle 的功能,用来打包整个项目的文件。

Bundle

熟悉 webpack 的人应该知道,webpack 最终会将所有引入的文件(JS、CSS、模板等)全部打包到一个 bundle.js 中,最终项目只需要引入这一个文件就可以。

而 React-Native 的打包机制与 webpack 非常类似,最终打包出一个index.platform.bundle(platform 根据平台区分)供最终 APP 打包使用。

一些踩过的坑

1. 包系统带来的问题

因为 React-Native 的包管理机制和 webpack 类似,因此它会对所有与模块相关的关键字进行检测并做对应的操作。检测机制非常简单:

exports.IMPORT_RE = /(\bimport\s+(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g;
exports.EXPORT_RE = /(\bexport\s+(?:[^'"]+\s+from\s+)??)(['"])([^'"]+)(\2)/g;
exports.REQUIRE_RE = /(\brequire\s*?\(\s*?)(['"])([^'"]+)(\2\s*?\))/g;
exports.SYSTEM_IMPORT_RE = /(\bSystem\.import\s*?\(\s*?)(['"])([^'"]+)(\2\s*?\))/g;

因此所有涉及到关键字的地方,都会被尝试自动引入资源文件并且打包。

这就有可能会导致一个问题,当一个模块不存在的时候(其实这可能是我们外部提供的模块),就会出现打包失败的情况。

这时候,我们可以使用 React-Native 提供的方法来处理这一问题。它提供了一个注释关键字@providesModule用来标记我们已经手动引入到系统中的模块。

比如之前我在做一个 fetch 的全局拦截器适配的时候,就遇到了这个问题 (PR 链接)

这个库本身为了对环境进行最大程度的兼容,会在环境不支持 fetch 的时候,尝试引入whatwg-fetch模块,作者使用了 try-catch 来容错,然而在 React-Native 里,打包程序会因为找不到whatwg-fetch模块而直接报错。

这时候,就可以使用@providesModule来处理。

/*
* @providesModule whatwg-fetch
*/

在这个模块中加入这一段注释,React-Native 的 bundle 程序就不会在尝试加载这个额外的模块,也就不会再报错了。

2. Android 平台 APK 生成

React-Native 的 Android 平台支持还不久,还有很多没有完善的地方。生成 APK 还不够自动化,需要手动执行操作。

根据官方的流程说明,在大多数情况下,都可以完成 APK 的构建。也有一些小的坑会遇到。

  1. 使用官方所推荐的构建命令./gradlew assembleRelease,需要保证你已经设定好了签名的 key,不然生成的 APK 是无法正常安装的。
  2. 由于./gradlew assembleRelease这个脚本执行过程中也会调用 React-Native 的 bundle 命令,因此,如果你此前已经开启了一个 React-Native packager 的服务器,就会在构建过程中报错,会告诉你 React-Native 的调用出错了,你可以检查一下是否已经有一个 packager 的服务器已经在运行了,把它停掉重新打包即可。
  3. 官方说明中提到可以通过开启 Proguard 来减小生成的 APK 文件的体积。但是,请务必确保你已经配置好了 Proguard 的设置,不然构建也会失败。