Инъекционный модуль динамически, только если требуется

Я использую Require.js в сочетании с Angular.js.

Некоторым контроллерам нужны огромные внешние зависимости, которые другим не нужны, например FirstController требует Angular UI Codemirror. Это дополнительно 135 кб, по крайней мере:

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

Я не хочу включать директиву и Codemirror lib каждый раз, когда моя страница загружается, чтобы сделать 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 (или любого другого модуля) в модуле приложения?

Мне все равно, если это хакерский способ выполнить это, если только он не требует изменения кода внешних зависимостей.

Если это полезно: Я запускаю Angular 1.2.0.

Ответ 1

Я пытаюсь смешать requirejs + Angular в течение некоторого времени. Я опубликовал небольшой проект в Github (angular-require-lazy) с моими усилиями до сих пор, поскольку область слишком велика для встроенного кода или скриптов. Проект демонстрирует следующие моменты:

  • Модули AngularJS ленивы загружены.
  • Директивы также могут быть ленивы загружены.
  • Существует механизм обнаружения "модуля" и метаданных (см. мой другой проект для домашних животных: require-lazy)
  • Приложение разбивается на пучки автоматически (т.е. работает с работами r.js)

Как это сделать:

  • Поставщики (например, $controllerProvider, $compileProvider) записываются из функции config (техника, которую я впервые увидел в angularjs-requirejs-lazy-controllers).
  • После bootstraping, angular заменяется нашей собственной оболочкой, которая может обрабатывать ленивые загружаемые модули.
  • Инжектор захватывается и предоставляется в качестве обещания.
  • Модули AMD могут быть преобразованы в модули Angular.

Эта реализация удовлетворяет вашим потребностям: она может lazy-load Angular модули (по крайней мере, ng-grid, которые я использую), определенно взламывает:) и не изменяет внешние библиотеки.

Комментарии/мнения более чем приветствуются.


(EDIT) Дифференциация этого решения от других заключается в том, что он не выполняет динамические вызовы require(), поэтому их можно построить с помощью r.js(и моего требовательного проекта). Помимо этого идеи более или менее сходятся в различных решениях.

Удачи всем!

Ответ 2

Внимание: используйте решение Nikos Paraskevopoulos, так как оно более надежно (я его использую), и имеет еще несколько примеров.


Хорошо, я, наконец, узнал, как это сделать с кратким ответом на этот ответ.

Как я уже сказал в своем вопросе, это стало очень хакерским способом. Он включает применение каждой функции в массиве _invokeQueue зависимого модуля в контексте модуля приложения.

Что-то вроде этого (обратите внимание в функции moduleExtender, пожалуйста):

define([ "angular" ], function( angular ) {
    // Returns a angular module, searching for its name, if it 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() { });

Ответ 3

Ключом к этому является то, что все модули, на которые зависит ваш модуль app, также должны быть ленивым модулем загрузки. Это связано с тем, что кэширование провайдера и экземпляра, которое angular использует для своей службы инжектора $, является закрытым, и они не предоставляют метод для регистрации новых модулей после завершения инициализации.

Таким образом, "хакерским" способом сделать это было бы отредактировать каждый из модулей, которые вы хотите ленивой загрузки, чтобы потребовать ленивый модуль загрузочного модуля (в примере, который вы связали, модуль находится в файле "appModules.js" '), затем отредактируйте каждый вызов контроллера, директивы, factory и т.д., чтобы использовать app.lazy.{same call} вместо этого.

После этого вы можете продолжать следовать примеру проекта, с которым вы связались, глядя на то, как маршруты приложений лениво загружаются (файл appRoutes.js показывает, как это сделать).

Не слишком уверен, что это помогает, но удачи.

Ответ 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 _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>