【问题标题】:Inject module dynamically, only if required仅在需要时动态注入模块
【发布时间】:2024-01-06 13:40:02
【问题描述】:

我将 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 开心。
这就是为什么我现在只在路由被命中时才加载控制器,like what's done here

但是,当我需要类似的东西时

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 模块(或任何其他模块)?
我不在乎这是否是一种骇人听闻的方式来实现这一点,除非它涉及修改外部依赖的代码。

如果有用的话:我正在运行 Angular 1.2.0。

【问题讨论】:

    标签: angularjs requirejs


    【解决方案1】:

    我已经尝试混合 requirejs+Angular 有一段时间了。到目前为止,我努力在 Github (angular-require-lazy) 上发布了一个小项目,因为范围对于内联代码或小提琴来说太大了。该项目展示了以下几点:

    • AngularJS 模块是延迟加载的。
    • 指令也可以延迟加载。
    • 有一个“模块”发现和元数据机制(见我的另一个宠物项目:require-lazy
    • 应用程序会自动拆分成包(即使用 r.js 构建有效)

    它是怎么做的:

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

    此实现满足您的需求:它可以延迟加载 Angular 模块(至少是我正在使用的 ng-grid),绝对是 hackish :) 并且不修改外部库。

    非常欢迎发表评论/意见。


    (编辑)此解决方案与其他解决方案的区别在于它不进行动态require() 调用,因此可以使用 r.js(和我的 require-lazy 项目)构建。除此之外,这些想法或多或少在各种解决方案中趋同。

    祝大家好运!

    【讨论】:

    • 你有计划通过 E2E 使 Angular + Require.js 可测试吗?
    • 我一定会以某种方式使其可测试。老实说,我还没有尝试过 Angular E2E 测试。
    • 嗨,我做了更多更新,包括使用 Karma 进行测试。代码覆盖支持和详细文档即将推出!
    • 您能说一下,您的解决方案可以动态卸载模块吗?例如,如果我需要某个页面的某个模块,并且当该页面关闭时,我想清除加载的模块以释放内存。也许你可以给点建议……
    • @PashaTurok 不幸的是,我对此没有任何建议(这是一个非常有趣的话题,因为客户端应用程序变得越来越大)。
    【解决方案2】:

    关键是您的app 模块所依赖的任何模块也需要是延迟加载模块。这是因为 Angular 用于其 $injector 服务的提供程序和实例缓存是私有的,并且它们不会在初始化完成后公开注册新模块的方法。

    因此,执行此操作的“hacky”方法是编辑您希望延迟加载的每个模块以要求延迟加载模块对象(在您链接的示例中,模块位于文件 'appModules.js '),然后编辑每个控制器、指令、工厂等调用以改用app.lazy.{same call}

    之后,您可以继续关注您链接到的示例项目,查看应用路由是如何延迟加载的(“appRoutes.js”文件显示了如何执行此操作)。

    不太确定这是否有帮助,但祝你好运。

    【讨论】:

    • 是的,这在某种程度上有所帮助。谢谢。
    • 虽然我更喜欢我解决这个问题的方式(见答案),但你的也很好。谢谢。
    【解决方案3】:

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


    好的,我终于找到了如何通过这个answer 的简短帮助来实现这一点。

    正如我在问题中所说,这已经成为一种非常老套的方式。它涉及在应用模块的上下文中应用依赖模块的_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 = { ... }里面是什么吗?
    • 采用 Nikos 的解决方案。我不再使用这个了。
    • Nikos 的项目太复杂了,我看不懂 :(
    【解决方案4】:

    有一个指令可以做到这一点:

    https://github.com/AndyGrom/loadOnDemand

    示例:

    <div load-on-demand="'module_name'"></div>
    

    【讨论】:

      【解决方案5】:

      现有延迟加载技术的问题在于,它们会做我自己想做的事情。

      例如,使用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

      【讨论】:

        【解决方案6】:

        我正在向您发送示例代码。它对我来说很好。所以请检查:

        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>
        

        【讨论】:

          最近更新 更多