Загрузка html и контроллера с сервера и создание динамических состояний UI - маршрутизатор

Я ищу решение для динамического загрузки содержимого приложения с сервера.

Мой сценарий:

Предположим, что у нас есть 2 пользователя (A и B), мое приложение состоит из разных модулей, например, скажем, shoppingList и калькулятор, теперь моя цель будет заключаться в том, что пользователь регистрируется в моем приложении из базы данных, я получаю права пользователя и в зависимости от того, какие права у него есть, я бы загрузил html для представлений и файлов контроллера для логической части с сервера, делая это, я бы создал состояния, необходимые для html и ctrl. Таким образом, в основном мое приложение очень мало соответствует логину, и все остальное вытаскивается с сервера в зависимости от Userrights.

Что я использую:

  • Кордова
  • AngularJs
  • Ionic Framework

Зачем мне это нужно, чтобы быть динамичным:

1) Возможность иметь приложение, содержащее логику входа в систему, поэтому при исправлении ошибок или добавлении модулей мне нужно только добавить файлы на сервер, чтобы предоставить Пользователю право на это, и он там, не нуждаясь в обновлении приложение.

2) Пользователь имеет только те функции, которые ему нужны, ему не нужно иметь все, только когда он имеет право на 1 модуль.

3) Приложение становится очень большим в данный момент, то есть каждый модуль имеет 5-10 состояний с собственными html и контроллерами. в настоящее время запланировано 50 различных модулей, поэтому вы можете выполнить математику.

Я посмотрел на это, чтобы получить вдохновение:

AngularJS, ocLazyLoad и загрузка динамических состояний

То, что я пробовал до сих пор:

Я создал 1 Html файл, который содержит весь модуль, поэтому у меня есть только 1 запрос http:

Предположим, что это мой ответ с сервера после входа пользователя в систему

Часть HTML:

var rights= [A,B,C,D]

angular.forEach(rights, function (value, key) {
     $http.get('http://myServer.com/templates/' + value + '.html').then(function (response) {
        //HTML file for the whole module
        splits = response.data.split('#');
        //Array off HTMl strings
        for (var l = 1; l <= splits.length; l++) {  
          //Putting all Html strings into templateCache                              
          $templateCache.put('templates/' + value +'.html', splits[l - 1]);

          }
        }
     });

Контроллер:

var rights= [A,B,C,D]

angular.forEach(rights, function (value, key) {
     $http.get('http://myServer.com/controller/' + value + '.js').then(function (response) {
        // 1 file for the whole module with all controllers
        splits = response.data.split('#');
        //Array off controller strings
        for (var l = 1; l <= splits.length; l++) {  
          //Putting all Controller strings into templateCache                              
          $templateCache.put('controllers/' + value +'.js', splits[l - 1]);

          }
        }
     });

После загрузки контроллеров я пытаюсь их зарегистрировать:

$controllerProvider.register('SomeName', $templateCache.get('controllers/someController));

Что не работает, поскольку это только строка...

Определение поставщиков:

.config(function ($stateProvider, $urlRouterProvider, $ionicConfigProvider, $controllerProvider) {

  // turns of the page transition globally
    $ionicConfigProvider.views.transition('none');
    $stateProviderRef = $stateProvider;
    $urlRouterProviderRef = $urlRouterProvider;
    $controllerProviderRef = $controllerProvider;


    $stateProvider

    //the login state is static for every user
  .state('login', {
      url: "/login",
      templateUrl: "templates/login.html",
      controller: "LoginCtrl"
  });

   //all the other states are missing and should be created depending on rights

$urlRouterProvider.otherwise('/login');


});

Часть Ui-Router:

//Lets assume here the Rights Array contains more information like name, url...
    angular.forEach(rights, function (value, key) {
       //Checks if the state was already added
         var getExistingState = $state.get(value.name)

         if (getExistingState !== null) {
              return;
         }

          var state = {
             'lang': value.lang,
             'params': value.param,
             'url': value.url,
             'templateProvider': function ($timeout, $templateCache, Ls) {
               return $timeout(function () {
               return $templateCache.get("templates" + value.url + ".html")
                                    }, 100);
                                },
             'ControllerProvider': function ($timeout, $templateCache, Ls) {
                return $timeout(function () {
                return $templateCache.get("controllers" + value.url + ".js")
                                        }, 100);
                                    }

                            $stateProviderRef.state(value.name, state);
                        });

                        $urlRouter.sync();
                        $urlRouter.listen();

Ситуация до сих пор:

Мне удалось загрузить html файлы и сохранить их в templateCache, даже загрузить их, но только если состояния были предопределены. Что я заметил здесь, это то, что иногда позволяет сказать, когда я удаляю элемент из списка и возвращаюсь к View the item was there again, возможно, это имеет какое-то отношение к кешу, я не уверен...

Мне удалось загрузить файлы контроллера и сохранить контроллеры в templateCache, но я действительно не знаю, как использовать $ControllerPrioviderRef.register с моими сохраненными строками...

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

PS: Я также посмотрел на require.js и OCLazyLoad, а также на этот пример пример динамического контроллера

Update:

Итак, мне удалось загрузить Html, создать State с помощью Controller, все работает нормально, за исключением того, что контроллер не работает вообще, ошибок нет, но кажется, ничего из логики контроллера не выполняется. В настоящее время единственным решением зарегистрировать контроллер из предыдущего загруженного файла было использование eval(),, которое является скорее хаком, а затем правильным решением.

Здесь код:

.factory('ModularService', ['$http', ....., function ( $http, ...., ) {
    return {
        LoadModularContent: function () {
            //var $state = $rootScope.$state;

            var json = [
            {
                module: 'Calc',
                name: 'ca10',
                lang: [],
                params: 9,
                url: '/ca10',
                templateUrl: "templates/ca/ca10.html",
                controller: ["Ca10"]

            },
            {
                module: 'SL',
                name: 'sl10',
                lang: [],
                params: 9,
                url: '/sl10',
                templateUrl: "templates/sl/sl10.html",
                controller: ['Sl10', 'Sl20', 'Sl25', 'Sl30', 'Sl40', 'Sl50', 'Sl60', 'Sl70']

            }
            ];

            //Load the html 
            angular.forEach(json, function (value, key) {
            $http.get('http://myserver.com/' + value.module + '.html')
            .then(function (response) {
               var splits = response.data.split('#');
               for (var l = 1; l <= value.controller.length; l++) {
                 $templateCache.put('templates/' + value.controller[l - 1] + '.html', splits[l - 1]);
                    if (l == value.controller.length) {
                       $http.get('http://myserver.com//'+value.module+'.js')
                       .then(function (response2) {
                          var ctrls = response2.data.split('##');
                          var fullctrl;
                          for (var m = 1; m <= value.controller.length; m++){

                            var ctrlName = value.controller[m - 1] + 'Ctrl';                                                                             

                            $controllerProviderRef
                            .register(ctrlName, ['$scope',...., function ($scope, ...,) {    
                                   eval(ctrls[m - 1]);
                            }])
                            if (m == value.controller.length) {

                              for (var o = 1; o <= value.controller.length; o++) {
                               var html = $templateCache
                              .get("templates/" + value.controller[o - 1] + ".html");

                                  var getExistingState = $state.get(value.controller[o - 1].toLowerCase());

                                 if (getExistingState !== null) {
                                                            return;
                                                        }

                                var state = {
                                 'lang': value.lang,
                                 'params': value.param,
                                 'url': '/' + value.controller[o - 1].toLowerCase(),
                                 'template': html,
                                 'controller': value.controller[o - 1] + 'Ctrl'
                                 };


                                  $stateProviderRef.state(value.controller[o - 1].toLowerCase(), state);
                                 }
                               }
                             }
                          });
                        }
                     }                            
                 });                      
            });
            // Configures $urlRouter listener *after* your custom listener

            $urlRouter.sync();
            $urlRouter.listen();

        }
    }
}])

Любая помощь оценена

Ответы

Ответ 1

Итак, давайте начнем с начала.

Вся логика приложения должна содержаться на сервере и обслуживаться через API-вызовы через REST, SOAP или аналогичные. Поступая таким образом, вы уменьшаете количество логики, встроенной в пользовательский интерфейс, что снижает нагрузку на клиента. Это в основном делает ваше клиентское приложение агентом рендеринга, содержащим только модели и представления для данных и логики, обслуживаемых API-интерфейсом.

Как указано в его комментарии, это не проблема для современного (или полусовременного) устройства.

Если вы настаиваете на том, чтобы сразу не загружать все макеты, вы можете разделить их на частичные, которые вы загружаете после входа в систему на основе пользовательских привилегий. Поступая таким образом, вы уменьшаете объем данных в памяти, хотя улучшение в лучшем случае будет сомнительным.

Ответ 2

Могу ли я предложить вам внести некоторые изменения в способ загрузки состояний?
Напишите script, который возвращает вам json с состояниями, к которым пользователь может получить доступ.
Ex.
ресурсы/маршрутизации config.yourLangage? пользователя = идентификатор пользователя-12345
это вернет json файл, который зависит от входа пользователя. Структура может быть примерно такой:

    [
      {
        "name": "home",
        "url": "/home",
        "templateUrl": "views/home.html",
        "controller": "HomeController",
        "dependencies": ["scripts/home/controllers.js", "scripts/home/services.js", "scripts/home/directives.js"]
      },
      {
        "name": "user",
        "url": "/user",
        "templateUrl": "views/user.html",
        "controller": "UserController",
        "dependencies": ["scripts/user/controllers.js", "scripts/user/services.js", "scripts/home/directives.js"]
      }
    ]

Затем напишите службу, которая будет читать состояния, к которым пользователь имеет доступ:

app.factory('routingConfig', ['$resource',
  function ($resource) {
    return $resource('resources/routing-config.yourLangage', {}, {
      query: {method: 'GET',
              params: {},
              isArray: true,
              transformResponse: function (data) {
                  // before that we give the states data to the app, let load all the dependencies
                  var states = [];
                  angular.forEach(angular.fromJson(data), function(value, key) {
                    value.resolve = {
                        deps: ['$q', '$rootScope', function($q, $rootScope){
                          // this will be resolved only when the user will go to the relative state defined in the var value
                          var deferred = $q.defer();

                          /*
                            now we need to load the dependencies. I use the script.js javascript loader to load the dependencies for each page.
                            It is very small and easy to be used
                            http://www.dustindiaz.com/scriptjs
                          */
                          $script(value.dependencies, function(){ //here we will load what is defined in the dependencies field. ex: "dependencies": ["scripts/user/controllers.js", "scripts/user/services.js", "scripts/home/directives.js"]
                            // all dependencies have now been loaded by so resolve the promise
                            $rootScope.$apply(function(){
                              deferred.resolve();
                            });
                          });

                          return deferred.promise;
                        }]
                      };
                    states.push(value);
                  });
                  return states;
                }
            }
    });
  }]);

Затем настройте приложение:

app.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', '$filterProvider', '$provide', '$compileProvider',
  function ($stateProvider, $urlRouterProvider, $locationProvider, $filterProvider, $provide, $compileProvider) {

    // this will be the default state where to go as far as the states aren't loaded
    var loading = {
        name: 'loading',
        url: '/loading',
        templateUrl: '/views/loading.html',
        controller: 'LoadingController'
    };

    // if the user ask for a page that he cannot access
    var _404 = {
        name: '_404',
        url: '/404',
        templateUrl: 'views/404.html',
        controller: '404Controller'
    };

    $stateProvider
      .state(loading)
      .state(_404);


    // save a reference to all of the providers to register everything lazily
    $stateProviderRef = $stateProvider;
    $urlRouterProviderRef = $urlRouterProvider;
    $controllerProviderRef = $controllerProvider;
    $filterProviderRef = $filterProvider;
    $provideRef = $provide;
    $compileProviderRef = $compileProvider;


    //redirect the not found urls
    $urlRouterProvider.otherwise('/404');

  }]);

Теперь позвольте использовать эту службу в app.run:

app.run(function ($location, $rootScope, $state, $q, routingConfig) {

  // We need to attach a promise to the rootScope. This will tell us when all of the states are loaded.
  var myDeferredObj = $q.defer();
  $rootScope.promiseRoutingConfigEnd = myDeferredObj.promise;

  // Query the config file
  var remoteStates = routingConfig.query(function() {
    angular.forEach(remoteStates, function(value, key) {
      // the state becomes the value
      $stateProviderRef.state(value);
    });
      // resolve the promise.
      myDeferredObj.resolve();
  });

  //redirect to the loading page until all of the states are completely loaded and store the original path requested
  $rootScope.myPath = $location.path();
  $location.path('/loading'); //and then (in the loading controller) we will redirect to the right state

  //check for routing errors
  $rootScope.$on('$stateChangeError', 
    function(event, toState, toParams, fromState, fromParams, error){
      console.log.bind(console);
  });

  $rootScope.$on('$stateNotFound', 
    function(event, unfoundState, fromState, fromParams){ 
        console.error(unfoundState.to); // "lazy.state"
        console.error(unfoundState.toParams); // {a:1, b:2}
        console.error(unfoundState.options); // {inherit:false} + default options
  });

});

В конце концов, LoadController:

app.controller('LoadingController', ['$scope', '$location', '$rootScope',
  function($scope, $location, $rootScope) {

    //when all of the states are loaded, redirect to the requested state
    $rootScope.promiseRoutingConfigEnd.then(function(){
      //if the user requested the page /loading then redirect him to the home page
      if($rootScope.myPath === '/loading'){
        $rootScope.myPath = '/home';
      }
      $location.path($rootScope.myPath);
    });

}]);

Таким образом, все супер гибко и лениво загружено.

Я уже написал 3 разных пользовательских портала, и я могу легко масштабироваться для всего портала пользователя, который я хочу.

Ответ 3

Я разработал приложение с учетом этих вещей. Вот моя архитектура.

Структура папки:

WebApp
|---CommonModule
    |---common-module.js //Angular Module Defination
    |---Controllers     //Generally Nothing, but if you have a plan to
                        //extend from one CommonController logic to several 
                        //module then it is usefull

    |---Services        //Common Service Call Like BaseService for all $http 
                        //call, So no Module Specific Service will not use 
                        //$http directly. Then you can do several common 
                        //things in this BaseService. 
                        //Like Error Handling, 
                        //CSRF token Implementation, 
                        //Encryption/Decryption of AJAX req/res etc.

    |---Directives      //Common Directives which you are going to use 
                        //in different Modules
    |---Filters         //Common Filters

    |---Templates       //Templates for those common directives

    |---index.jsp       //Nothing, Basically Redirect to 
                        //Login or Default Module

    |---scripts.jsp     //JQuery, AngularJS and Other Framworks scripts tag.
                        //Along with those, common controlers, services, 
                        //directives and filtes. 

    |---templates.jsp   //Include all common templates.

    |---ng-include.jsp  //will be used in templates.jsp to create angular 
                        //template script tag.
|---ModuleA
    |---moduleA-module.js //Angular Module Definition, 
                          //Use Common Module as Sub Module
    |---Controllers
    |---Services
    |---Directives
    |---Filters
    |---Templates
    |---index.jsp 
    |---scripts.jsp 
    |---templates.jsp
|---ModuleB
    |--- Same as above ...

Примечание: папка "Капитал" обозначает папку. Помимо ModuleA, для вашего случая будет LoginModule, или я могу использовать CommonModule для него.

Меху будет следующим.

<a href="/ModuleA/">Module A</a> <!--Note: index.jsp are indexed file 
                                 //for a directive -->
<a href="/ModuleB/">Module B</a>

Каждая из этих страниц JSP фактически является независимым приложением angular. Используя следующий код.

ModuleA/index.jsp

<!-- Check User Permission Here also for Security 
     If permission does not have show Module Unavailable Kind of JSP.
     Also do not send any JS files for this module.
     If permission is there then use this following JSP
-->
<!DOCTYPE HTML>
<html  lang="en" data-ng-app="ModuleA">
    <head> 
        <title>Dynamic Rule Engine</title>
        <%@ include file="scripts.jsp" %> 
        <%@ include file="templates.jsp" %> <!-- Can be cached it in
                                                 different way --> 
    </head>
    <body>
        <%@ include file="../common.jsp" %>
        <div id="ngView" data-ng-view></div>
        <%@ include file="../footer.jsp" %>
    </body>
</html>

ModuleA/scripts.jsp

<%@ include file="../CommonModule/scripts.jsp" %> <!-- Include Common Things
                                              Like Jquery Angular etc  -->
<scripts src="Controlers/ModlueAController1.js"></script>
.....

ModuleA/templates.jsp

<%@ include file="../CommonModule/templates.jsp" %> 
<!-- Include Common Templates for common directives -->
<jsp:include page="../CommonModule/ng-include.jsp"><jsp:param name="src" value="ModuleA/Templates/template1.jsp" /></jsp:include>
.....

CommonModule/нг-include.jsp

<script type="text/ng-template" id="${param.src}">
    <jsp:include page="${param.src}" />
</script>

Но основная проблема такого подхода - когда пользователь будет изменять модуль, страница будет обновлена.

EDIT: Существует файл ModuleA.module.js, который фактически содержит замедление модуля следующим образом.

angular.module('ModuleA.controllers', []);
angular.module('ModuleA.services', []);
angular.module('ModuleA.directives', []);
angular.module('ModuleA.filters', []);
angular.module('ModuleA', 
       ['Common', 
        'ModuleA.controllers' , 
        'ModuleA.services' , 
        'ModuleA.directives' , 
        'ModuleA.filters'])
    .config(['$routeProvider', function($routeProvider) {
        //$routeProvider state setup
    }])
    .run (function () {

    });

Ответ 4

Я думаю, что я делаю то, о чем вы спрашиваете. Я достигаю этого, используя будущие состояния UI-Router, ocLazyLoad и ui-routers. По существу, наша настройка позволяет нам иметь 50+ модулей, все в одной и той же базе кода, но когда пользователь открывает приложение. его запуск начинается только путем загрузки базовых файлов, необходимых для приложения. Затем, когда пользователь перемещается между состояниями, приложение загружает файлы, необходимые для этой части, по мере необходимости. (извинения за фрагментированный код, мне пришлось вырвать его из базы кода, но попытался предоставить только материал, который действительно имеет отношение к решению).

Во-первых, структура папки

  • Основное приложение

config.js

  • Модуль 1 (/module1)

module.js

controllers.js

  • Модуль 2 (/module2)

module.js

controllers.js

и т.д.

Config.js:

Первое, что мы делаем, это создать базовое состояние, это абстрактное состояние, поэтому пользователь никогда не сможет его просто нанести.

$stateProvider.state('index', {
    abstract: true,
    url: "/index",
    views: {
        '': {
            templateUrl: "views/content.html" // a base template to have sections replaced via ui-view elements
        }
    },
    ...
});

Затем мы настраиваем модули в ocLazyLoad. Это позволяет нам просто указывать ocLazyLoad для загрузки модуля и загружает все необходимые файлы (хотя в этом случае это только один файл, но он позволяет каждому модулю иметь разные пути).

$ocLazyLoadProvider.config({
    loadedModules: ['futureStates'],
    modules: [
        {
            name: 'module1',
            files: ['module1/module.js']
        },
        {
            name: 'module2',
            files: ['module2/module.js']
        }
    ]
});

Затем мы создаем функцию, позволяющую ui-router загружать модули по запросу (через состояния будущего).

function ocLazyLoadStateFactory($q, $ocLazyLoad, futureState) {
    var deferred = $q.defer();
    // this loads the module set in the future state
    $ocLazyLoad.load(futureState.module).then(function () {
        deferred.resolve();
    }, function (error) {
        deferred.reject(error);
    });
    return deferred.promise;
}
$futureStateProvider.stateFactory('ocLazyLoad', ['$q', '$ocLazyLoad', 'futureState', ocLazyLoadStateFactory]);

Затем мы настраиваем фактические состояния будущего. Это состояния, которые могут быть загружены в будущем, но мы не хотим их настраивать прямо сейчас.

$futureStateProvider.futureState({
    'stateName': 'index.module1', // the state name
    'urlPrefix': '/index/module1', // the url to the state
    'module': 'module1', // the name of the module, configured in ocLazyLoad above
    'type': 'ocLazyLoad' // the future state factory to use.
});
$futureStateProvider.futureState({
    'stateName': 'index.module2',
    'urlPrefix': '/index/module2',
    'module': 'module2',
    'type': 'ocLazyLoad'
});

Если вы хотите, чтобы список будущих состояний был предоставлен асинхронно:

$futureStateProvider.addResolve(['$http', function ($http) {
    return $http({method: 'GET', url: '/url'}).then(function (states) {
        $futureStateProvider.futureState({
            'stateName': 'index.module2',
            'urlPrefix': '/index/module2',
            'module': 'module2',
            'type': 'ocLazyLoad'
        });
    });
}]);

Затем мы конфигурируем модули следующим образом:
module1/module.js

$stateProvider.state('index.module1', {
    url: "/module1",
    abstract: true,
    resolve: {
        loadFiles: ['$ocLazyLoad', function($ocLazyLoad){
            return  return $ocLazyLoad.load(['list of all your required files']);
        }]
    }
})
$stateProvider.state('index.module1.sub1', {
    url: "/sub1",
    views: {
       // override your ui-views in here. this one overrides the view named 'main-content' from the 'index' state
       '[email protected]': {
            templateUrl: "module1/views/sub1.html"
        }
    }
})