AngularJS:延迟加载控制器和内容

27
在这个简化的场景中,我有两个文件:index.htm和lazy.htm。
index.htm:
var myApp = angular.module('myApp', []);
myApp.controller('embed',function($scope){
    $scope.embed = 'Embedded Controller';
});                  
<div ng-controller="embed">{{embed}}</div>    
<div ng-include="'lazy.htm'"></div>

懒惰.htm

myApp.controller('lazy',function($scope){
    $scope.lazy = 'Lazy Controller';
});
<div ng-controller="lazy">
    {{lazy}}
</div>

结果出现错误:"参数'lazy'不是函数,得到了未定义"

使用函数替代

lazy.htm

function lazy($scope) {
    $scope.lazy = 'Lazy Controller';
}
<div ng-controller="lazy">
    {{lazy}}
</div>

这个方法适用于版本1.3 beta 14及以下。在beta 15中,全局控制器函数被移除了:https://github.com/angular/angular.js/issues/8296

那么现在,获取lazy.htm的angular化内容有更好的方法吗?

更新:

在这篇文章中(http://ify.io/lazy-loading-in-angularjs),我发现了另一种可能的解决方案。$controllerProvider允许我们在angular启动后注册新的控制器。非常有效。已通过v1.3.0-beta.18测试

index.htm:

var myApp = angular.module('myApp', [])
.controller('embed',function($scope){
    $scope.embed = 'Embedded Controller';
})
.config(function($controllerProvider) {
    myApp.cp = $controllerProvider;
});

<div ng-controller="embed">{{embed}}</div>    
<div ng-include="'lazy.htm'"></div>

懒惰的.htm

myApp.cp.register('lazy',function($scope){
    $scope.lazy = 'Lazy Controller';
});
<div ng-controller="lazy">
    {{lazy}}
</div>

更新2:

另外两个可行的选择是:

lazy.htm

_app = $('[ng-app]').scope();    
_app.lazy = function($scope) {
    $scope.lazy = 'Lazy Controller';
};

或者

var $rootScope = $('[ng-app]').injector().get('$rootScope');        
$rootScope.lazy = function($scope) {
    $scope.lazy = 'Lazy Controller';
}; 

但我认为这最后两个例子不应在生产中使用。


1
我也发现requirejs与其他库难以使用,反之亦然。这就是为什么我创建了一个库,它更易于使用,并且已经通过angular测试。底部有一个演示应用程序:gngeorgiev.github.io/Modulerr.js。您还可以将所有脚本合并为一个,而无需依赖于Modulerr.js。 - Georgi-it
这个问题给了我人生中最美好的时刻之一。通过简单地使用$controllerProvider,现在我可以拥有自包含的客户端代码片段。也就是说,HTML和JS在一个文件中。 - Saeed Neamati
你可以使用requireJs和ocLazyload动态加载文件并注入模块。请参考文章http://www.codeproject.com/Articles/1039826/Angularjs-Lazy-loading-with-Requirejs-and-OcLazylo。 - Jeeva J
在这里尝试ocLazyload示例应用程序:http://www.freakyjolly.com/how-to-lazy-load-modules-controllers-angularjs/ - Code Spy
9个回答

17

你也可以使用jquery来解决 $routeProvider 的问题。

app.js

/* Module Creation */
var app = angular.module ('app', ['ngRoute']);

app.config(['$routeProvider', '$controllerProvider', function($routeProvider, $controllerProvider){

/*Creating a more synthesized form of service of $ controllerProvider.register*/
app.registerCtrl = $controllerProvider.register;

function loadScript(path) {
  var result = $.Deferred(),
  script = document.createElement("script");
  script.async = "async";
  script.type = "text/javascript";
  script.src = path;
  script.onload = script.onreadystatechange = function (_, isAbort) {
      if (!script.readyState || /loaded|complete/.test(script.readyState)) {
         if (isAbort)
             result.reject();
         else
            result.resolve();
    }
  };
  script.onerror = function () { result.reject(); };
  document.querySelector("head").appendChild(script);
  return result.promise();
}

function loader(arrayName){

    return {
      load: function($q){
                var deferred = $q.defer(),
                map = arrayName.map(function(name) {
                    return loadScript('js/controllers/'+name+".js");
                });

                $q.all(map).then(function(r){
                    deferred.resolve();
                });

                return deferred.promise;
        }
    };
}

$routeProvider  
    .when('/', {
        templateUrl: 'views/foo.html',
        resolve: loader(['foo'])
    })
    .when('/bar',{
        templateUrl: 'views/bar.html',
        controller: 'BarCtrl',
        resolve: loader(['bar'])
    })
    .otherwise({
        redirectTo: document.location.pathname
    });
}]);

/views/foo.html

<section ng-controller='FooCtrl'>
    {{text}}
</section>

js/controllers/foo.js

/*Here we use the synthesized version of $controllerProvider.register 
to register the controller in view*/
app.registerCtrl('FooCtrl',function($scope){
    $scope.text = 'Test';
});

/views/bar.html

<section>
    {{text2}}
</section>

js/controllers/bar.js

app.registerCtrl('BarCtrl',function($scope){
    $scope.text2 = 'Test';
});

1
这并不总是有效 - 有时getScript在视图呈现后执行。/* 这里是魔法,jQuery getScript将在呈现视图之前获取我想要的目录中的脚本 */ - Amit
嗨,谢谢观看,我做了一个新版本,请现在尝试。 - André Betiolo
1
它也会在任何现代构建系统上失败。 - Victor Zakharov
这个运行得很好...但是有没有办法避免重复脚本的出现,因为它会每次都添加。不检查是否已经添加过。 - Anil Kumar
你可以缓存该文件,或者建议将该文件保持非常小,例如几kb。如果偶然有第二次下载,也不应影响性能。 - André Betiolo

4
你可以使用纯AngularJS实现懒加载。
创建“LazyService”:
var ng = angular.module('app');

ng.factory('lazyService', [ '$http', function($http) {
    var jsPath = 'js/${ name }.js';
    var promisesCache = {};

    return {
        loadScript: function(name) {
            var path = jsPath.replace('${ name }', name);
            var promise = promisesCache[name];
            if (!promise) {
                promise = $http.get(path);
                promisesCache[name] = promise;

                return promise.then(function(result) {
                    eval(result.data);
                    console.info('Loaded: ' + path);
                });
            }

            return promise;
        }
    }
}]);

接下来,定义您的配置:

var ng = angular.module('app', [ 'ngRoute' ]);

ng.config([ '$routeProvider', '$controllerProvider', '$provide', function($routeProvider, $controllerProvider, $provide) {
    // Lazy loading
    ng.lazy = {
        controller: $controllerProvider.register,
        //directive: $compileProvider.directive,
        //filter: $filterProvider.register,
        factory: $provide.factory,
        service: $provide.service
    }

    $routeProvider
    .when('/', {
        templateUrl: 'view/home.html'
    })
    .when('/vendor', {
        templateUrl: 'view/vendor.html',
        resolve: {
            svc: [ 'lazyService', function(lazyService) {
                return lazyService.loadScript('services/vendor');
            }],
            ctrl: [ 'lazyService', function(lazyService) {
                return lazyService.loadScript('controllers/vendor');
            }]
        }
    });
. . .

在“js/services/vendor.js”中创建服务如下:
var ng = angular.module('app');
ng.lazy.service('vendorService', [ function() {
. . .

在“js/controllers/vendor.js”中,创建以下控制器:
var ng = angular.module('app');
ng.lazy.controller('vendorController', [ function() {
. . .

when 中的 "resolve" 属性定义了在路由加载之前应该解决哪些 Promise。


我一直在使用你的解决方案,但现在我想在路由改变时取消请求,我使用资源进行请求,你能帮我吗?谢谢。 - Mozhdeh.Hamidi
这种方法有点糙,如果我们正在使用babel和typescript,很遗憾我们不能将它放在angular.module('app')中,因为似乎angularJS 1.8.x重建了类并且忽略了.lazy,最后我使用了window.lazy,这样lazy对象就可以在任何地方使用了。嗯,我还在调试中,努力让它正常工作... - undefined

3

////JConfig文件--------

window.angularApp.config(function ($routeProvider,$controllerProvider,$compileProvider,$provide, azMessages) {

$routeProvider.when('/login', {
             resolve: {
                 load: ['$q', '$rootScope', function ($q, $rootScope) {
                     var deferred = $q.defer();
                     require([

                         //load required Js file here

                ], function () {
                    $rootScope.$apply(function () {
                        deferred.resolve();
                    });
                });
                     return deferred.promise;
                 } ]
             }
         });


  $routeProvider.otherwise({ redirectTo: '/login' });

    window.angularApp.components = {
        controller: $controllerProvider.register,
        service: $provide.service,
        directive: $compileProvider.directive
    }

//控制器声明

angularApp.components.controller('DiscussionController',[function(){

}]);

require 只接受一个参数。 - Victor Zakharov

2

起初,我使用了André Betiolo的答案。然而,它并不总是有效,因为ajax加载是非阻塞的,有时视图会在脚本加载之前请求控制器。

作为解决方案,我强制函数在所有脚本成功加载之前不返回。这有点hackish,但确保加载在完成解析之前成功。它也允许加载多个控制器。

app.js

var app = angular.module ('app', ['ngRoute']);

app.config(['$routeProvider', '$controllerProvider', function($routeProvider, $controllerProvider){

    /*Creating a more synthesized form of service of $ controllerProvider.register*/
    app.registerCtrl = $controllerProvider.register;

    //jquery to dynamically include controllers as needed
    function controllers(controllers){
        var numLoaded = 0;
        for (i = 0; i < controllers.length; i++) {
            $.ajaxSetup({async:false});
            $.getScript('js/controllers/' + controllers[i] + '.js').success(function(){
                numLoaded++;
                if (numLoaded == controllers.length) {
                    return true; //only return after all scripts are loaded, this is blocking, and will fail if all scripts aren't loaded.
                }
            });
        }
    }

    $routeProvider
        .when('/', {
            templateUrl: 'views/foo.html',
            resolve: {
                load: function () {
                    controllers(['foo'])
                }
            }
        })
        .when('/bar',{
            templateUrl: 'views/bar.html',
            controller: 'BarCtrl',
            resolve: {
                load: function () {
                    controllers(['bar','foo']) //you can load multiple controller files
                }
            }
        })
        .otherwise({
            redirectTo: document.location.pathname
        });
}]);

/views/foo.html

<section ng-controller='FooCtrl'>
    {{text}}
</section>

/views/bar.html

<section ng-controller='BarCtrl'>
    {{text2}}
</section>
<section ng-controller='FooCtrl'>
    {{text}}
</section>

/controllers/bar.js

app.registerCtrl('BarCtrl',function($scope){
    $scope.text2 = 'Test';
});

我做了一个新版本,请现在尝试。 - André Betiolo

1
尝试使用 Angular JS 的 ARI 插件。它可以帮助您按需延迟加载控制器脚本。

1

1
最好的方法是使用指令并将控制器和模板绑定在一起,这样就可以在适当的时间进行绑定。目前,在lazy.htm中绑定没有发生在正确的时间,除非您像第二个示例中所示那样声明全局函数。

1
理想情况下,Angular会强制你将HTML和JS分开,因为在更新的版本中可能更经常地进行此操作。
您可能需要使用requireJS。http://solutionoptimist.com/2013/09/30/requirejs-angularjs-dependency-injection/ 为了玩个小把戏,您可以尝试一下。
ng-controller-controller="'lazy'"

或者
在HTML中

ng-controller-controller="myObject.controller"

某处注入

$scope.myObject.controller = $controller('lazy', {$scope: $scope})

0

我正在向您发送示例代码。对我来说它运行良好。所以请检查一下:

var myapp = angular.module('myapp', ['ngRoute']);

/* Module Creation */
var app = angular.module('app', ['ngRoute']);

app.config(['$routeProvider', '$controllerProvider', function ($routeProvider, $controllerProvider) {

app.register = {
    controller: $controllerProvider.register,
    //directive: $compileProvider.directive,
    //filter: $filterProvider.register,
    //factory: $provide.factory,
    //service: $provide.service
};


//    so I keep a reference from when I ran my module config
function registerController(moduleName, controllerName) {
    // Here I cannot get the controller function directly so I
    // need to loop through the module's _invokeQueue to get it
    var queue = angular.module(moduleName)._invokeQueue;
    for (var i = 0; i < queue.length; i++) {
        var call = queue[i];
        if (call[0] == "$controllerProvider" &&
           call[1] == "register" &&
           call[2][0] == controllerName) {
            app.register.controller(controllerName, call[2][1]);
        }
    }
}


var tt = {
    loadScript:
function (path) {
    var result = $.Deferred(),
    script = document.createElement("script");
    script.async = "async";
    script.type = "text/javascript";
    script.src = path;
    script.onload = script.onreadystatechange = function (_, isAbort) {
        if (!script.readyState || /loaded|complete/.test(script.readyState)) {
            if (isAbort)
                result.reject();
            else {
                result.resolve();
            }
        }
    };
    script.onerror = function () { result.reject(); };
    document.querySelector(".shubham").appendChild(script);
    return result.promise();
}
}

function stripScripts(s) {
    var div = document.querySelector(".shubham");
    div.innerHTML = s;
    var scripts = div.getElementsByTagName('script');
    var i = scripts.length;
    while (i--) {
        scripts[i].parentNode.removeChild(scripts[i]);
    }
    return div.innerHTML;
}


function loader(arrayName) {
    return {
        load: function ($q) {
            stripScripts(''); // This Function Remove javascript from Local
            var deferred = $q.defer(),
            map = arrayName.map(function (obj) {
                return tt.loadScript(obj.path)
                .then(function () {
                    registerController(obj.module, obj.controller);
                })
            });

            $q.all(map).then(function (r) {
                deferred.resolve();
            });
            return deferred.promise;
        }
    };
};



$routeProvider
    .when('/first', {
        templateUrl: '/Views/foo.html',
        resolve: loader([{ controller: 'FirstController', path: '/MyScripts/FirstController.js', module: 'app' },
            { controller: 'SecondController', path: '/MyScripts/SecondController.js', module: 'app' }])
    })

    .when('/second', {
        templateUrl: '/Views/bar.html',
        resolve: loader([{ controller: 'SecondController', path: '/MyScripts/SecondController.js', module: 'app' },
        { controller: 'A', path: '/MyScripts/anotherModuleController.js', module: 'myapp' }])
    })
    .otherwise({
        redirectTo: document.location.pathname
        });
}])

在 HTML 页面中:

<body ng-app="app">

<div class="container example">
    <!--ng-controller="testController"-->

    <h3>Hello</h3>

    <table>
        <tr>
            <td><a href="#/first">First Page </a></td>
            <td><a href="#/second">Second Page</a></td>
        </tr>
    </table>




        <div id="ng-view" class="wrapper_inside" ng-view>
        </div>
    <div class="shubham">
    </div>
</div>

谢谢


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接