[譯] 經過 Webpack 實現 AngularJS 的延遲加載

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

隨着你的單頁應用擴大,其下載時間也愈來愈長。這對提升用戶體驗不會有好處(提示:但用戶體驗正是咱們開發單頁應用的緣由)。更多的代碼意味着更大的文件,直到代碼壓縮已經不能知足你的需求,你惟一能爲你的用戶作的就是不要再讓他一次性下載整個應用。這時,延遲加載就派上用場了。不一樣於一次性下載全部文件,而是讓用戶只下載他如今須要的文件。node

因此。如何讓你的應用程序實現延遲加載?它基本上是分紅兩件事情。把你的模塊拆分紅小塊,並實施一些機制,容許按需加載這些塊。聽起來彷佛有不少工做量,不是嗎?若是你使用 Webpack 的話,就不會這樣。它支持開箱即用的代碼分割特性。在這篇文章中我假定你熟悉 Webpack,但若是你不會的話,這裏有一篇介紹 。爲了長話短說,咱們也將使用 AngularUI RouterocLazyLoadwebpack

代碼能夠在 GitHub 上。你能夠隨時 fork 它。git

Webpack 的配置

沒什麼特別的,真的。實際上從你能夠直接從文檔中複製而後粘貼,惟一的區別是採用了 ng-annotate ,以讓咱們的代碼保持簡潔,以及採用 babel 來使用一些 ECMAScript 2015 的魔法特性。若是你對 ES6 感興趣,能夠看看這篇之前的帖子 。雖然這些東西都是很是棒的,可是它們都不是實現延遲加載所必需的東西。angularjs

// 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 內,這是在每個頁面上都須要強制下載的。正如你所看到的,咱們不會加載任何複雜的東西,除了全局的依賴。不一樣於加載控制器,咱們只加載路由配置。github

// 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 的依賴)。web

加載整個模塊

當用戶輸入 /home 路徑,瀏覽器就會下載 home 模塊。它包括兩個控制器,針對 homehome.about 這兩個state。咱們經過 state 的配置對象中的 resolve 屬性就能夠實現延遲加載。得益於 Webpack 的 require.ensure 方法,咱們能夠把 home 模塊建立成第一個代碼塊。它就叫作 1.bundle.js 。若是沒有 $ocLazyLoad.load,咱們會發現獲得一個錯誤 Argument 'HomeController' is not a function, got undefined,由於在 Angular 的設計中,啓動應用以後再加載文件的方式是不可行的。 可是 $ocLazyLoad.load 使得咱們能夠在啓動階段註冊一個模塊,而後在它加載完以後再去使用它。bootstrap

// 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
  ]);

僅加載控制器

咱們所作的是向前邁出的第一步,那麼咱們接着進行下一步。這一次,將沒有大的模塊,只有精簡的控制器。babel

// 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.controllermessages.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]);
  });

});
相關文章
相關標籤/搜索