隨著你的單頁應用擴大,其下載時間也越來越長。這對提高使用者體驗不會有好處(提示:但使用者體驗正是我們開發單頁應用的原因)。更多的代碼意味著更大的檔案,直到代碼壓縮已經不能滿足你的需求,你唯一能為你的使用者做的就是不要再讓他一次性下載整個應用。這時,消極式載入就派上用場了。不同於一次性下載所有檔案,而是讓使用者只下載他現在需要的檔案。
所以。如何讓你的應用程式實現消極式載入?它基本上是分成兩件事情。把你的模組拆分成小塊,並實施一些機制,允許按需載入這些塊。聽起來似乎有很多工作量,不是嗎?如果你使用 Webpack 的話,就不會這樣。它支援開箱即用的代碼分割特性。在這篇文章中我假定你熟悉 Webpack,但如果你不會的話,這裡有一篇介紹 。為了長話短說,我們也將使用 AngularUI Router 和 ocLazyLoad 。
代碼可以在 GitHub 上。你可以隨時 fork 它。
Webpack 的配置
沒什麼特別的,真的。實際上從你可以直接從文檔中複製然後粘貼,唯一的區別是採用了 ng-annotate ,以讓我們的代碼保持簡潔,以及採用 babel 來使用一些 ECMAScript 2015 的魔法特性。如果你對 ES6 感興趣,可以看看 這篇以前的文章 。雖然這些東西都是非常棒的,但是它們都不是實現消極式載入所必需的東西。
// webpack.config.jsvar 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 modulelet 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 modulelet 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 modulelet 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 viewrequire.ensure([], () => resolve(require('./views/messages.new.html')));});},controller: 'MessagesNewController as vm',resolve: {loadMessagesNewController: ($q, $ocLazyLoad) => {return $q((resolve) => {require.ensure([], () => {// load only controller modulelet 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 dependencyrequire('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', ]);controller = $controller('MessagesAllController', { msgStore: msgStoreMock });}));it('saves msgStore.all() in msgs', () => {expect(msgStoreMock.all).toHaveBeenCalled();expect(controller.msgs).toEqual(['foo', ]);});});
以上內容是小編給大家分享的Webpack 實現 AngularJS 的消極式載入,希望對大家有所協助!