Внедрить модуль динамически, только при необходимости

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

6 ответов

Решение

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

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

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

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

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

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


(РЕДАКТИРОВАТЬ) Отличие этого решения от других в том, что оно не делает динамическое require() звонки, таким образом, могут быть собраны с помощью r.js (и моего проекта require-lazy). Кроме того, идеи более или менее сходятся в различных решениях.

Всем удачи!

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


Хорошо, я наконец-то узнал, как этого добиться, с краткой помощью с этим ответом.

Как я уже сказал в своем вопросе, это стало очень хакерским способом. Это включает применение каждой функции в _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() { });

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

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

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

Не уверен, что это поможет, но удачи.

Существует директива, которая сделает это:

https://github.com/AndyGrom/loadOnDemand

пример:

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

Я отправляю вам пример кода. Это работает нормально для меня. Поэтому, пожалуйста, проверьте это:

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>

Проблема с существующими методами ленивой загрузки состоит в том, что они делают то, что я хочу сделать сам.

Например, используя 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

Другие вопросы по тегам