动态注入模块,仅在需要时注入。

44

我正在将Require.js与Angular.js结合使用。

一些控制器需要大量的外部依赖项,而其他控制器则不需要,例如FirstController需要Angular UI Codemirror。至少会增加额外的135 kb:

require([
  "angular",
  "angular.ui.codemirror" // requires codemirror itself
], function(angular) {
  angular.module("app", [ ..., "ui.codemirror" ]).controller("FirstController", [ ... ]);
});

我不想每次加载页面时都要包含指令和Codemirror库,只是为了让Angular开心。
这就是为什么我现在只在路由被命中时加载控制器,就像这里所做的那样

然而,当我需要类似

define([
  "app",
  "angular.ui.codemirror"
], function(app) {
  // ui-codemirror directive MUST be available to the view of this controller as of now
  app.lazy.controller("FirstController", [
    "$scope",
    function($scope) {
      // ...
    }
  ]);
});
我该如何告诉Angular在应用程序模块中也注入ui.codemirror模块(或任何其他模块)?
我不介意通过一种Hackish的方法来实现这一点,除非它涉及修改外部依赖项的代码。
如果有用的话:我正在运行Angular 1.2.0。
6个回答

33

我一直在尝试混合使用requirejs+Angular技术。我在Github上发布了一个小项目(angular-require-lazy)来展示我的努力,因为这个范围对于内联代码或fiddles来说太大了。该项目演示了以下几点:

  • AngularJS模块可以进行惰性加载。
  • 指令也可以进行惰性加载。
  • 有一个“模块”发现和元数据机制(请参见我的其他小型项目:require-lazy
  • 应用程序会自动分成捆绑包(即使用r.js构建)。

实现方式如下:

  • 提供者(例如$controllerProvider$compileProvider)从config函数中捕获(这是我在angularjs-requirejs-lazy-controllers中第一次看到的技术)。
  • 在引导之后,angular被我们自己的包装器替换,可以处理惰性加载的模块。
  • 注入器被捕获并作为承诺提供。
  • AMD模块可以转换为Angular模块。

这个实现满足了你的需求:它可以惰性加载Angular模块(至少是我使用的ng-grid),肯定有点hackish :),并且不会修改外部库。

意见/评论非常欢迎。


(编辑) 与其他解决方案的区别在于它不执行动态require()调用,因此可以使用r.js进行构建(以及我的require-lazy项目)。除此之外,各种想法基本上是一致的。

祝大家好运!


1
你有计划通过 E2E 使 Angular + Require.js 可测试吗? - gustavohenke
1
我一定会想办法让它可测试。说实话,我还没有尝试过 Angular E2E 测试。 - Nikos Paraskevopoulos
2
嗨,我进行了一些更新,包括使用Karma进行测试。代码覆盖率支持和详细文档即将到来! - Nikos Paraskevopoulos
1
你能否说一下,你的解决方案是否可以动态卸载模块?例如,如果我需要某个页面的某个模块,并且当该页面关闭时,我想清除已加载的模块以释放内存。也许你可以给些建议... - user2022068
1
很抱歉,我对此没有任何建议(这是一个非常有趣的话题,随着客户端应用程序变得越来越大,它变得越来越重要)。 - Nikos Paraskevopoulos
显示剩余2条评论

7

注意:请使用Nikos Paraskevopoulos的解决方案,因为它更可靠(我正在使用它),并且有更多的示例。


好的,我终于找到了如何通过这个答案来实现。正如我在问题中所说的那样,这是一种非常hacky的方式。它涉及将依赖模块的_invokeQueue数组中的每个函数应用于应用程序模块的上下文中。

大致就像这样(请注意moduleExtender函数):

define([ "angular" ], function( angular ) {
    // Returns a angular module, searching for its name, if it's a string
    function get( name ) {
        if ( typeof name === "string" ) {
            return angular.module( name );
        }

        return name;
    };

    var moduleExtender = function( sourceModule ) {
        var modules = Array.prototype.slice.call( arguments );

        // Take sourceModule out of the array
        modules.shift();

        // Parse the source module
        sourceModule = get( sourceModule );
        if ( !sourceModule._amdDecorated ) {
            throw new Error( "Can't extend a module which hasn't been decorated." );
        }

        // Merge all modules into the source module
        modules.forEach(function( module ) {
            module = get( module );
            module._invokeQueue.reverse().forEach(function( call ) {
                // call is in format [ provider, function, args ]
                var provider = sourceModule._lazyProviders[ call[ 0 ] ];

                // Same as for example $controllerProvider.register("Ctrl", function() { ... })
                provider && provider[ call[ 1 ] ].apply( provider, call[ 2 ] );
            });
        });
    };

    var moduleDecorator = function( module ) {
        module = get( module );
        module.extend = moduleExtender.bind( null, module );

        // Add config to decorate with lazy providers
        module.config([
            "$compileProvider",
            "$controllerProvider",
            "$filterProvider",
            "$provide",
            function( $compileProvider, $controllerProvider, $filterProvider, $provide ) {
                module._lazyProviders = {
                    $compileProvider: $compileProvider,
                    $controllerProvider: $controllerProvider,
                    $filterProvider: $filterProvider,
                    $provide: $provide
                };

                module.lazy = {
                    // ...controller, directive, etc, all functions to define something in angular are here, just like the project mentioned in the question
                };
                module._amdDecorated = true;
            }
        ]);
    };

    // Tadaaa, all done!
    return {
        decorate: moduleDecorator
    };
});

完成这个步骤后,例如,我只需要执行以下操作:
app.extend( "ui.codemirror" ); // ui.codemirror module will now be available in my application
app.controller( "FirstController", [ ..., function() { });

你能举个例子说明 module.lazy = { ... } 里面是什么吗? - Freewind
2
采用Nikos的解决方案。我不再使用这个。 - gustavohenke
Nikos的项目太复杂了,我无法理解 :( - Freewind

4
关键在于,您的app模块所依赖的任何模块也需要是懒加载模块。这是因为Angular用于其$injector服务的提供程序和实例缓存是私有的,并且它们不公开在初始化完成后注册新模块的方法。
因此,“hacky”的方法是编辑要想要延迟加载的每个模块,以要求懒加载模块对象(在您链接的示例中,该模块位于文件“appModules.js”中),然后编辑每个控制器、指令、工厂等调用,使用app.lazy.{相同的调用}代替。
之后,您可以继续按照您链接的示例项目进行,查看如何懒加载应用程序路由(“appRoutes.js”文件显示如何执行此操作)。
不太确定这是否有帮助,但祝您好运。

是的,这在某种程度上有所帮助。谢谢。 - gustavohenke
虽然我更喜欢我解决这个问题的方式(请参见答案),但你的方法也非常好。谢谢。 - gustavohenke

2

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>


0

现有的懒加载技术存在的问题是它们做了我想自己做的事情。

例如,使用requirejs,我只想调用:

require(['tinymce', function() {
   // here I would like to just have tinymce module loaded and working
});

然而它并不是以那种方式工作的。为什么?据我所知,AngularJS只是将模块标记为“将来要加载的”,如果我稍等一会儿,它就会起作用——我将能够使用它。因此,在上面的函数中,我想调用一些类似于loadPendingModules()的函数。

在我的项目中,我创建了一个简单的提供者('lazyLoad'),它正是做这件事情,没有其他更多的东西,因此现在,如果我需要完全加载某个模块,我可以执行以下操作:

myApp.controller('myController', ['$scope', 'lazyLoad', function($scope, lazyLoad) {

    // ........

    $scope.onMyButtonClicked = function() {

        require(['tinymce', function() {
            lazyLoad.loadModules();

            // and here I can work with the modules as they are completely loaded
        }]);
    };

    // ........

});

这里是源文件的链接(MPL许可证): https://github.com/lessmarkup/less-markup/blob/master/LessMarkup/UserInterface/Scripts/Providers/lazyload.js


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