angularjs+requirejs實現按需加載的全面實踐

在進行有必定規模的項目時,一般但願實現如下目標:一、支持複雜的頁面邏輯(根據業務規則動態展示內容,例如:權限,數據狀態等);二、堅持先後端分離的基本原則(不分離的時候,能夠在後端用模版引擎直接生成好頁面);三、頁面加載時間短(業務邏輯複雜就須要引用第三方的庫,但極可能加載的庫和用戶本次操做不要緊);4,還要代碼好維護(加入新的邏輯時,影響的文件儘可能少)。css

想同時實現這些目標,就必須有一套按需加載的機制,頁面上展示的內容和全部須要依賴的文件,均可以根據業務邏輯須要按需加載。最近都是基於angularjs作開發,因此本文主要圍繞angularjs提供的各類機制,探索全面實現按需加載的套路。html

1、一步一步實現

基本思路:一、先開發一個框架頁面,它能夠完成一些基本的業務邏輯,而且支持擴展的機制;二、業務邏輯變複雜,須要把部分邏輯拆分到子頁面中,子頁面按需加載;三、子頁面中的展示內容也變了複雜,又須要進行拆分,按需加載;四、子頁面的內容複雜到依賴外部模塊,須要按需加載angular模塊。前端

一、框架頁

提到前端的按需加載,就會想到AMD( Asynchronous Module Definition),如今用requirejs的很是多,因此首先考慮引入requires。html5

index.htmlangularjs

<script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js"></script>

注意:採用手動啓動angular的方式,所以html中沒有ng-app。bootstrap

spa-loader.js後端

require.config({
    paths: {
        "domReady": '/static/js/domReady',
        "angular": "//cdn.bootcss.com/angular.js/1.4.8/angular.min",
        "angular-route": "//cdn.bootcss.com/angular.js/1.4.8/angular-route.min",
    },
    shim: {
        "angular": {
            exports: "angular"
        },
        "angular-route": {
            deps: ["angular"]
        },
    },
    deps: ['/test/lazyspa/spa.js'],
    urlArgs: "bust=" + (new Date()).getTime()
});

spa.jspromise

define(["require", "angular", "angular-route"], function(require, angular) {
    var app = angular.module('app', ['ngRoute']);
    require(['domReady!'], function(document) {
        angular.bootstrap(document, ["app"]); /*手工啓動angular*/
        window.loading.finish();
    });
});

二、按需加載子頁面

angular的routeProvider+ng-view已經提供完整的子頁面加載的方法,直接用。
注意必須設置html5Mode,不然url變化之後,routeProvider不截獲。app

index.html框架

<div>
    <a href="/test/lazyspa/page1">page1</a>
    <a href="/test/lazyspa/page2">page2</a>
    <a href="/test/lazyspa/">main</a>
</div>
<div ng-view></div>

spa.js

app.config(['$locationProvider', '$routeProvider', function($locationProvider, $routeProvider) {
    /* 必須設置生效,不然下面的設置不生效 */
    $locationProvider.html5Mode(true);
    /* 根據url的變化加載內容 */
    $routeProvider.when('/test/lazyspa/page1', {
        template: '<div>page1</div>',
    }).when('/test/lazyspa/page2', {
        template: '<div>page2</div>',
    }).otherwise({
        template: '<div>main</div>',
    });
}]);

三、按需加載子頁面中的內容

用routeProvider的前提是url要發生變化,可是有的時候只是子頁面中的局部要發生變化。若是這些變化主要是和綁定的數據相關,不影響頁面佈局,或者影響很小,那麼經過ng-if一類的標籤基本就解決了。可是有的時候要根據頁面狀態,徹底改變局部的內容,例如:用戶登陸前和登陸後局部要發生的變化等,這就意味着局部的佈局可能也挺複雜,須要做爲獨立的單元來對待。

利用ng-include能夠解決頁面局部內容加載的問題。可是,咱們能夠再考慮更復雜一些的狀況。這個頁面片斷對應的代碼是後端動態生成的,並且不只僅有html還有js,js中定義了代碼片斷對應的controller。這種狀況下,不只僅要考慮動態加載html的問題,還要考慮動態定義controller的問題。controller是經過angular的controllerProvider的register方法註冊,所以須要得到controllerProvider的實例。

spa.js

app.config(['$locationProvider', '$routeProvider', '$controllerProvider', function($locationProvider, $routeProvider, $controllerProvider) {
    app.providers = {
        $controllerProvider: $controllerProvider //注意這裏!!!
    };
    /* 必須設置生效,不然下面的設置不生效 */
    $locationProvider.html5Mode(true);
    /* 根據url的變化加載內容 */
    $routeProvider.when('/test/lazyspa/page1', {
        /*!!!頁面中引入動態內容!!!*/
        template: '<div>page1</div><div ng-include="\'page1.html\'"></div>',
        controller: 'ctrlPage1'
    }).when('/test/lazyspa/page2', {
        template: '<div>page2</div>',
    }).otherwise({
        template: '<div>main</div>',
    });
    app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) {
        /* 用這種方式,ng-include配合,根據業務邏輯動態獲取頁面內容 */
        /* !!!動態的定義controller!!! */
        app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) {
            $scope.openAlert = function() {
                alert('page1 alert');
            };
        }]);
        /* !!!動態定義頁面的內容!!! */
        $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>');
    }]);
}]);

四、動態加載模塊

採用上面子頁面片斷的加載方式存在一個侷限,就是各類邏輯(js)要加入到啓動模塊中,這樣仍是限制子頁面片斷的獨立封裝。特別是,若是子頁面片斷須要使用第三方模塊,且這個模塊在啓動模塊中沒有事先加載時,就沒有辦法了。因此,必需要可以實現模塊的動態加載。實現模塊的動態加載就是把angular啓動過程當中加載模塊的方式提取出來,再處理一些特殊狀況。

動態加載模塊深刻分析能夠參考這篇文章:
http://www.tuicool.com/articles/jmuymiE

可是,實際跑起來發現文章中的代碼有問題,就是「$injector」究竟是什麼?研究了angular的源代碼injector.js才大概搞明白是怎麼回事。

一個應用有兩個$injector,providerInjector和instanceInjector。invokeQueue和用providerInjector,runBlocks用instanceProvider。若是$injector用錯了,就會找到須要的服務。

  • routeProvider中動態加載模塊文件。

template: '<div ng-controller="ctrlModule1"><div>page2</div><div><button ng-click="openDialog()">open dialog</button></div></div>',
resolve: {
    load: ['$q', function($q) {
        var defer = $q.defer();
        /* 動態加載angular模塊 */
        require(['/test/lazyspa/module1.js'], function(loader) {
            loader.onload && loader.onload(function() {
                defer.resolve();
            });
        });
        return defer.promise;
    }]
}
  • 動態加載angular模塊

angular._lazyLoadModule = function(moduleName) {
    var m = angular.module(moduleName);
    console.log('register module:' + moduleName);
    /* 應用的injector,和config中的injector不是同一個,是instanceInject,返回的是經過provider.$get建立的實例 */
    var $injector = angular.element(document).injector();
    /* 遞歸加載依賴的模塊 */
    angular.forEach(m.requires, function(r) {
        angular._lazyLoadModule(r);
    });
    /* 用provider的injector運行模塊的controller,directive等等 */
    angular.forEach(m._invokeQueue, function(invokeArgs) {
        try {
            var provider = providers.$injector.get(invokeArgs[0]);
            provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
        } catch (e) {
            console.error('load module invokeQueue failed:' + e.message, invokeArgs);
        }
    });
    /* 用provider的injector運行模塊的config */
    angular.forEach(m._configBlocks, function(invokeArgs) {
        try {
            providers.$injector.invoke.apply(providers.$injector, invokeArgs[2]);
        } catch (e) {
            console.error('load module configBlocks failed:' + e.message, invokeArgs);
        }
    });
    /* 用應用的injector運行模塊的run */
    angular.forEach(m._runBlocks, function(fn) {
        $injector.invoke(fn);
    });
};
  • 定義模塊
    module1.js

define(["angular"], function(angular) {
    var onloads = [];
    var loadCss = function(url) {
        var link, head;
        link = document.createElement('link');
        link.href = url;
        link.rel = 'stylesheet';
        head = document.querySelector('head');
        head.appendChild(link);
    };
    loadCss('//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css');
    /* !!! 動態定義requirejs !!!*/
    require.config({
        paths: {
            'ui-bootstrap-tpls': '//cdn.bootcss.com/angular-ui-bootstrap/1.1.2/ui-bootstrap-tpls.min'
        },
        shim: {
            "ui-bootstrap-tpls": {
                deps: ['angular']
            }
        }
    });
    /*!!! 模塊中須要引用第三方的庫,加載模塊依賴的模塊 !!!*/
    require(['ui-bootstrap-tpls'], function() {
        var m1 = angular.module('module1', ['ui.bootstrap']);
        m1.config(['$controllerProvider', function($controllerProvider) {
            console.log('module1 - config begin');
        }]);
        m1.controller('ctrlModule1', ['$scope', '$uibModal', function($scope, $uibModal) {
            console.log('module1 - ctrl begin');
            /*!!! 打開angular ui的對話框 !!!*/
            var dlg = '<div class="modal-header">';
            dlg += '<h3 class="modal-title">I\'m a modal!</h3>';
            dlg += '</div>';
            dlg += '<div class="modal-body">content</div>';
            dlg += '<div class="modal-footer">';
            dlg += '<button class="btn btn-primary" type="button" ng-click="ok()">OK</button>';
            dlg += '<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>';
            dlg += '</div>';
            $scope.openDialog = function() {
                $uibModal.open({
                    template: dlg,
                    controller: ['$scope', '$uibModalInstance', function($scope, $mi) {
                        $scope.cancel = function() {
                            $mi.dismiss();
                        };
                        $scope.ok = function() {
                            $mi.close();
                        };
                    }],
                    backdrop: 'static'
                });
            };
        }]);
        /* !!!動態加載模塊!!! */
        angular._lazyLoadModule('module1');
        console.log('module1 loaded');
        angular.forEach(onloads, function(onload) {
            angular.isFunction(onload) && onload();
        });
    });
    return {
        onload: function(callback) {
            onloads.push(callback);
        }
    };
});

2、完整的代碼

  • index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta content="width=device-width,user-scalable=no,initial-scale=1.0" name="viewport">
        <base href='/'>
        <title>SPA</title>
    </head>
    <body>
        <div ng-controller='ctrlMain'>
            <div>
                <a href="/test/lazyspa/page1">page1</a>
                <a href="/test/lazyspa/page2">page2</a>
                <a href="/test/lazyspa/">main</a>
            </div>
            <div ng-view></div>
        </div>
        <div class="loading"><div class='loading-indicator'><i></i></div></div>
        <script src="static/js/require.js" defer async data-main="/test/lazyspa/spa-loader.js?_=3"></script>
    </body>
</html>
  • spa-loader.js

window.loading = {
    finish: function() {
        /* 保留個方法作一些加載完成後的處理,我實際的項目中會在這裏結束加載動畫 */
    },
    load: function() {
        require.config({
            paths: {
                "domReady": '/static/js/domReady',
                "angular": "//cdn.bootcss.com/angular.js/1.4.8/angular.min",
                "angular-route": "//cdn.bootcss.com/angular.js/1.4.8/angular-route.min",
            },
            shim: {
                "angular": {
                    exports: "angular"
                },
                "angular-route": {
                    deps: ["angular"]
                },
            },
            deps: ['/test/lazyspa/spa.js'],
            urlArgs: "bust=" + (new Date()).getTime()
        });
    }
};
window.loading.load();
  • spa.js

'use strict';
define(["require", "angular", "angular-route"], function(require, angular) {
    var app = angular.module('app', ['ngRoute']);
    /* 延遲加載模塊 */
    angular._lazyLoadModule = function(moduleName) {
        var m = angular.module(moduleName);
        console.log('register module:' + moduleName);
        /* 應用的injector,和config中的injector不是同一個,是instanceInject,返回的是經過provider.$get建立的實例 */
        var $injector = angular.element(document).injector();
        /* 遞歸加載依賴的模塊 */
        angular.forEach(m.requires, function(r) {
            angular._lazyLoadModule(r);
        });
        /* 用provider的injector運行模塊的controller,directive等等 */
        angular.forEach(m._invokeQueue, function(invokeArgs) {
            try {
                var provider = providers.$injector.get(invokeArgs[0]);
                provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
            } catch (e) {
                console.error('load module invokeQueue failed:' + e.message, invokeArgs);
            }
        });
        /* 用provider的injector運行模塊的config */
        angular.forEach(m._configBlocks, function(invokeArgs) {
            try {
                providers.$injector.invoke.apply(providers.$injector, invokeArgs[2]);
            } catch (e) {
                console.error('load module configBlocks failed:' + e.message, invokeArgs);
            }
        });
        /* 用應用的injector運行模塊的run */
        angular.forEach(m._runBlocks, function(fn) {
            $injector.invoke(fn);
        });
    };
    app.config(['$injector', '$locationProvider', '$routeProvider', '$controllerProvider', function($injector, $locationProvider, $routeProvider, $controllerProvider) {
        /**
         * config中的injector和應用的injector不是同一個,是providerInjector,得到的是provider,而不是經過provider建立的實例
         * 這個injector經過angular沒法得到,因此在執行config的時候把它保存下來
        */
        app.providers = {
            $injector: $injector,
            $controllerProvider: $controllerProvider
        };
        /* 必須設置生效,不然下面的設置不生效 */
        $locationProvider.html5Mode(true);
        /* 根據url的變化加載內容 */
        $routeProvider.when('/test/lazyspa/page1', {
            template: '<div>page1</div><div ng-include="\'page1.html\'"></div>',
            controller: 'ctrlPage1'
        }).when('/test/lazyspa/page2', {
            template: '<div ng-controller="ctrlModule1"><div>page2</div><div><button ng-click="openDialog()">open dialog</button></div></div>',
            resolve: {
                load: ['$q', function($q) {
                    var defer = $q.defer();
                    /* 動態加載angular模塊 */
                    require(['/test/lazyspa/module1.js'], function(loader) {
                        loader.onload && loader.onload(function() {
                            defer.resolve();
                        });
                    });
                    return defer.promise;
                }]
            }
        }).otherwise({
            template: '<div>main</div>',
        });
    }]);
    app.controller('ctrlMain', ['$scope', '$location', function($scope, $location) {
        console.log('main controller');
        /* 根據業務邏輯自動到缺省的視圖 */
        $location.url('/test/lazyspa/page1');
    }]);
    app.controller('ctrlPage1', ['$scope', '$templateCache', function($scope, $templateCache) {
        /* 用這種方式,ng-include配合,根據業務邏輯動態獲取頁面內容 */
        /* 動態的定義controller */
        app.providers.$controllerProvider.register('ctrlPage1Dyna', ['$scope', function($scope) {
            $scope.openAlert = function() {
                alert('page1 alert');
            };
        }]);
        /* 動態定義頁面內容 */
        $templateCache.put('page1.html', '<div ng-controller="ctrlPage1Dyna"><button ng-click="openAlert()">alert</button></div>');
    }]);
    require(['domReady!'], function(document) {
        angular.bootstrap(document, ["app"]);
    });
});

module1.js

'use strict';
define(["angular"], function(angular) {
    var onloads = [];
    var loadCss = function(url) {
        var link, head;
        link = document.createElement('link');
        link.href = url;
        link.rel = 'stylesheet';
        head = document.querySelector('head');
        head.appendChild(link);
    };
    loadCss('//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css');
    require.config({
        paths: {
            'ui-bootstrap-tpls': '//cdn.bootcss.com/angular-ui-bootstrap/1.1.2/ui-bootstrap-tpls.min'
        },
        shim: {
            "ui-bootstrap-tpls": {
                deps: ['angular']
            }
        }
    });
    require(['ui-bootstrap-tpls'], function() {
        var m1 = angular.module('module1', ['ui.bootstrap']);
        m1.config(['$controllerProvider', function($controllerProvider) {
            console.log('module1 - config begin');
        }]);
        m1.controller('ctrlModule1', ['$scope', '$uibModal', function($scope, $uibModal) {
            console.log('module1 - ctrl begin');
            var dlg = '<div class="modal-header">';
            dlg += '<h3 class="modal-title">I\'m a modal!</h3>';
            dlg += '</div>';
            dlg += '<div class="modal-body">content</div>';
            dlg += '<div class="modal-footer">';
            dlg += '<button class="btn btn-primary" type="button" ng-click="ok()">OK</button>';
            dlg += '<button class="btn btn-warning" type="button" ng-click="cancel()">Cancel</button>';
            dlg += '</div>';
            $scope.openDialog = function() {
                $uibModal.open({
                    template: dlg,
                    controller: ['$scope', '$uibModalInstance', function($scope, $mi) {
                        $scope.cancel = function() {
                            $mi.dismiss();
                        };
                        $scope.ok = function() {
                            $mi.close();
                        };
                    }],
                    backdrop: 'static'
                });
            };
        }]);
        angular._lazyLoadModule('module1');
        console.log('module1 loaded');
        angular.forEach(onloads, function(onload) {
            angular.isFunction(onload) && onload();
        });
    });
    return {
        onload: function(callback) {
            onloads.push(callback);
        }
    };
});

寫後感

年初定下的目標是堅持每週寫一篇本身在開發過程碰到的問題總結,本覺得是個簡單的事情,寫起來才發現寫文章的時間比寫代碼的花的時間還要長。由於寫代碼的時候只要功能實現了就好了,可是,寫文章的時候就必定要把代碼搞清楚纔敢寫,實際上就是逼着本身要認真研究源代碼,雖然壓力很大,但收穫更大。另外一方面,發現找到一個好題目挺難的,只是簡單的貼別人的代碼沒意思,但是本身想出來有價值,有意思的問題挺難的。所以你們要是以爲有啥有意思,有價值前端問題,分享一下吧,給個人年度寫做計劃幫幫忙

相關文章
相關標籤/搜索