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 的设置,不然构建也会失败。

问题:

angular 1.4.x 版本开始,<div ng-repeat="(key, value) in myObj"> ... </div> 的方法移除了按照首字母排序的规则,改为使用浏览器默认的方法,也就是for key in myObj

因此当我们使用 angular-filter 的 groupBy 方法将数组分组时,分组后的数组不会再按照 key 排序,某些情况下就会造成一些麻烦。

解决方案:

根据 这一条说明,利用 toArray 和 orderBy 可以解决这一个问题。

toArray 方法可以将对象转换成固定的数组。这个方法接受一个 bool 类型的参数,用来指定是否将对象的 key 作为转换后数组中的元素的一个属性输出,如果设为 true,那么数组元素将会多一个属性 $key 代表原来的对象的 key

所以我们可以这样完成 groupBy 方法的排序:

<ul ng-repeat="group in myObj | groupBy: 'groupKey' | toArray: true | orderBy: '$key'">
  <li ng-bind="$key"></li>
  <li ng-repeat="item in group"></li>
</ul>

这样,groupBy 的结果就会按照对应的参数的首字母(这里是groupKey)排序了。

默认情况下,$resource 服务会对它进行的 http 请求的 response 默认进行一些处理,它会尝试 map 返回值。
如果你的服务器端返回的值符合标准 json 格式,那么不会遇到问题,但是如果你的服务器返回了类似 12 或者 abc 这样子的数据时,你就会发现当你在回调中试图获取 response 时,发生了一些意想不到的事情。
你实际获得的结果可能是这样子的:

{
 "0": "1",
 "1": "2",
 $promise: obj,
 ...
}
{
 "0": "a",
 "1": "b",
 "2": "c",
 $promise: obj,
 ...
}

你得到的值已经是被错误的 map 过了。这当然不是我们想要的,因此,我们需要手动来处理一下。

所以,我们在 $resource 的对应 action 中指定一下 transformResponse 参数。

顾名思义,transformResponse 就是用来指定对 response 的处理方法的,它接受一个方法或者一个由多个方法组成的数组作为值。

function(data, headersGetter)|Array.<function(data, headersGetter)>

因此我们给 $resource 的某个 action 下添加如下一个参数。

transformResponse: function (response) {
 return {data: response};
}

最终的结构是这样子的:

$resource('/api/test', {}, {
  save: {
    method: 'POST',
    transformResponse: function(response) {
      return {
        data: response
      };
    }
  }
});

这样我们就可以在对应的 action (这里是 save 方法) 的回调中取得我们想要的 response 了。
假设你在回调函数中使用 response 作为回调变量名,你可以通过 response.data来获取真实的 response,它不会再被 angular 自动处理了。

一般来说,当我们在页面内部一个可滚动元素上使用滚动时,如果滚动距离超过它的顶部或者底部,就会触发页面滚动,但是在一些情况下,我们不希望引起页面滚动,因为这会影响用户操作。

下面是一种阻止页面滚动的方法。

var scroll = document.getElementsByClassName('scroll')[0];
function handleScroll (e) {
  if (scroll.scrollHeight > scroll.offsetHeight) { // 仅在元素处于可滚动状态下才进行阻止
    if (e.deltaY < 0 && scroll.scrollTop === 0) {
      e.preventDefault();
    }
    if (e.deltaY > 0 && (scroll.scrollTop + scroll.offsetHeight) >= scroll.scrollHeight) {
      e.preventDefault();
    }
  }
}
scroll.addEventListener('mousewheel', handleScroll);

DEMO: JSBIN