原文链接: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)排序了。