Организация кода в больших AngularJS и JavaScript приложениях

    От переводчика: Думаю, что статьи по архитектуре приложения и организации кода наиболее важны на начальном этапе, т. к., в отличие от всего остального, основу приложения поменять очень трудно. [Оригинал статьи]

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

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

    Стопки на полу


    Давайте взглянем на ангуляровский шаблонный проект, официальую отправную точку для создания своего приложения. Каталог «app» имеет следующую структуру:

    • css/
    • img/
    • js/
      • app.js
      • controllers.js
      • directives.js
      • filters.js
      • services.js
    • lib/
    • partials/

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

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

    Ящик для носков


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

    Давайте представим, что делаем простой сайт электронной коммерции с формой логина, каталогом товаров и интерфейсом корзины. Также мы определили новые сущности для моделей (бизнес-логики и состояния) и сервисов (прокси для HTTP/JSON терминалов), а не свалил их в одну кучу «сервисы». Наш яваскрипт-каталог будет выглядеть следующим образом:

    • controllers/
      • LoginController.js
      • RegistrationController.js
      • ProductDetailController.js
      • SearchResultsController.js
    • directives.js
    • filters.js
    • models/
      • CartModel.js
      • ProductModel.js
      • SearchResultsModel.js
      • UserModel.js
    • services/
      • CartService.js
      • UserService.js
      • ProductService.js

    Отлично! Объекты теперь разложены так, что легко просматривать дерево файлов или перемещаться по нему, используя горячие клавиши; в списке изменений теперь легко указать, какие были внесены изменения, и т.д. Это одно из основных улучшения, но до сих пор остаются некоторые ограничения.

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

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

    Модульность


    Будем надеяться, что банальные метафоры не были слишком утомительными, и вот резюме:

    • Нового разработчика в команде попросили исправить ошибку в одном из многочисленных экранов приложения.
    • Разработчик, разгребая структуру каталога, видит что все контроллеры, модели и сервисы аккуратно организованы. К сожалению, это не говорит ему ничего о том, как и какие объекты связаны друг с другом.
    • Если в какой-то момент разработчик хочет повторно использовать часть кода, он должен выгребать файлы из разных папок и неизбежно забудет взять код из другой папки где-то в другом месте.

    Верьте или нет, редко возникает необходимость повторно использовать все контроллеры из приложения электронной коммерции в новом приложении отчетности, которое вы делаете. Однако может быть необходимо повторно использовать часть аутентификационной логики. Было бы неплохо, если бы всё это лежало в одном месте? Давайте реорганизуем приложение на основе функциональных областей:

    • cart/
      • CartModel.js
      • CartService.js
    • common/
      • directives.js
      • filters.js
    • product/
      • search/
        • SearchResultsController.js
        • SearchResultsModel.js
      • ProductDetailController.js
      • ProductModel.js
      • ProductService.js
    • user/
      • LoginController.js
      • RegistrationController.js
      • UserModel.js
      • UserService.js

    Теперь любой сторонний разработчик может открыть папки верхнего уровня и сразу понять, что делает приложение. Объекты в одной папке относятся друг к другу и некоторые из них зависят от других. Понять, как работают процесс авторизации и регистрации так же просто, как просмотреть файлы в этой папке. Примитивная процедура повторного использования с помощью копирования/вставки может быть проведена, по крайней мере, путем копирования папки в другой проект.

    В Ангуляре мы можем взять этот шаг за основу и создать модуль с соответствующим кодом:

    var userModule = angular.module('userModule',[]);
     
    userModule.factory('userService', ['$http', function($http) {
      return new UserService($http);
    }]);
     
    userModule.factory('userModel', ['userService', function(userService) {
      return new UserModel(userService);
    }]);
     
    userModule.controller('loginController', ['$scope', 'userModel', LoginController]);
     
    userModule.controller('registrationController', ['$scope', 'userModel', RegistrationController]);

    Если затем поместить UserModule.js в пользовательскую папку она становится «явным» объектом, используемым в этом модуле. Также было бы разумно, добавить некоторые директивы для загрузчика RequireJS или Browserify.

    Советы по общему код


    Каждое приложение имеет общий код, использущийся во многих модулях. Просто нужно место для него, которым может быть папка с именем «common» или «shared» или как угодно. В действительно больших приложениях имеется тенденция к частично совпадающей и перекрещивающейся функциональности. Существует несколько методов для управления всем этим:

    1. Если объекты вашего модуля требуют прямой доступ к нескольким «общим» объектам, создайте один или несколько фасадов для них. Это поможет сократить число нахлебников для каждого объекта, так как наличие слишком большого числа нахлебников, как правило, показывает, что код протух.
    2. Если ваша папка «общие» становится большим модулем, разделите его на подмодули для решения конкретных функциональных задач или проблем. Убедитесь, что прикладные модули используют только те «общие» модули, в которых они нуждаются. Это «Принцип разделения интерфейса» от SOLID.
    3. Добавьте служебные методы в $rootScope, чтобы они могли быть использованы в дочерних областях. Это избавит от внедрения одной и той же зависимости (например, «PermissionsModel») в каждый контроллер в приложении. Обратите внимание, что это нужно делать вдумчиво, чтобы избежать загромождения глобальной области и не сделать зависимости не очевидными.
    4. Используйте события, чтобы разделить компоненты, которые не требуют явной ссылки друг друга. В Ангуляре это делается с помощью методов $emit, $broadcast и $on в области видимости объекта. Контроллер может сгенерировать событие для выполнения некоторых действий, а затем получить уведомление, что действие завершено.

    Небольшое замечание по ресурсам и тестам


    Думаю, что можно более гибко подойти к организации HTML, CSS и изображений. Поместив их в папку «assets» вложенную в папку модуля, что, вероятно, улучшит баланс между ресурсами и зависящими от них модулями и не сделает структуру слишком громоздкой. Так же, думаю, будет разумным разделить папки верхнего уровня на папки для контента, содержащие структуру папок, отражающую структуру пакета приложения. Думаю, что это так же хорошо подойдет для тестирования.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 21

      +1
      Неплохо сделано в ExtJS — docs.sencha.com/extjs/4.2.0/#!/guide/application_architecture
      как показала практика, такой подход упрощает жизнь, прямо из коробки разработчик раскладывает всё по полочкам и потом придерживается установленного порядка.
        0
        Такой способ с его плюсами и минусами в статье описан.

        статью не читай комментарий оставляй?
          –2
          был опыт, мне подход показался удобным. подобную организацию кода использовал и с другим фреймворком, не ExtJS.

          статью не читай комментарий оставляй?

          вы сделали такой вывод основываясь на чём? :)
            0
            ну просто вы показали ровно то что есть в статье :)

            Вообще у меня был/есть опыт использования такого подхода (который по ссылке), и все же я соглашусь с выводом статьи что такая организация не совсем удобна когда нужно часть функциональности вырезать и вставить в другой проект. Если же этого не требуется, то такой подход весьма удобен и понятен, а главное прост в использовании.
              0
              ну просто вы показали ровно то что есть в статье :)


              именно так, показал, что где-то есть люди, которые это уже обдумали и воплотили в жизнь
          0
          Там разбито по интерфейсным компонентам (по большому счету). Думаю, для Ангуляра хороший формат, т.к. он как раз заточен на директивы и интерфейс. По крайней мере завести папочку «UI», было бы не плохо :-)
            +1
            Я вообще немного удивлён, что Ангуляр в шаблоне проекта свалил всё в кучу. Когда сами разработчики фреймворка не пытаются помочь с организацией проекта, это как-то странно…
              0
              Ну, они же пишут в документации, что это не эталон и чтобы разработчики организовывали исходя из своих потребностей. К сожалению (или к счастью) Ангуляр не настолько высокого уровня абстракции, чтобы решать за разработчиков, как им авторизацию делать, как модальное окно рисовать или как организовать товары… Поэтому они и не могут разбить иначе в шаблонном проекте
          +1
          Действительно модульный подход весьма логичный и наглядный, нежели абстрактный MVC, пробую применять уже в некоторых проектах своих… Есть Components для Node.js, где вообще инкапсулируются css, html и js файлы в отдельные реюзабельные компоненты.
            +3
            Спасибо за статью, благодаря ней я узнал два новых для меня решения («стопки на полу» и «ящик для носков» в терминах статьи).
              0
              как вы это все в итоге подключаете к проекту? используете amd?
                0
                Я потом собираю все в один файл, заодно и минимизирую. Можно делать это например с помощью grunt.js, но так как я использую .net, то мне проще использовать новую штуку от Microsoft, которая называется bundles.
                  0
                  grunt + concat (пример подсмотрел в грантфайле самого ангулара, безумно понравилось, использую на всех проектах). Туда же можно и через clusure compiller прогнать, и через imagemin и много чего еще.
                  +1
                  Хороший подход, использую его, только с одной маленькой но удобной модификацией: имена файлов. Вместо

                  UserContract.js
                  UserController.js
                  UserModel.js
                  UserService.js
                  UserView.aspx
                  ...
                  

                  называем файлы по шаблону {Entity}.{concept}.{language}:

                  User.contract.js
                  User.controller.js
                  User.model.js
                  User.service.js
                  User.view.aspx
                  


                  Что это даёт? Например, пожалуй самое для меня удобное: в VisualStudio я поставил плагин TabStudio, который позволяет группировать открытые табы по имени. В результате вместо 4-х открытых табов с длинными заголовками и необходимостью скроллить и искать в толпе других открытых табов

                  __| UserContract.js (x) || UserController.js (x) || UserModel.js (x) || UserService.js (x) || UserView.aspx (x) |____

                  получается лишь один скромный «групповой» таб с общим именем и 4-мя расширениями, типа такого

                  __| User .contract.js .controller.js .model.js .service.js .view.aspx (x) |____

                  что сильно экономит место и время.
                    0
                    Направление верное, мы такое давно используем в БЭМ проектах
                    ru.bem.info/method/filesystem/
                      0
                      Ага, давным давно смотрел вашу презентацию. Логичный подход к проектированию компонентов. Правда, дальнейшее углубление взорвало мозг и решил отложить)
                      0
                      А зачем «models/», их же как бы нет в angular там scope?
                        0
                        model более общее понятие. scope уже привязывается к модели или к ее части. Например, если выводить список с помощью ngRepeat, то помимо области видимости на весь список, создадутся дочерние на каждый элемент списка.… Но, вообще, понятия не имею, как они организуют код в этой папочке. Не json-файлы же там хранят)
                          0
                          model более общее понятие. scope уже привязывается к модели или к ее части. Например, если выводить список с помощью ngRepeat, то помимо области видимости на весь список, создадутся дочерние на каждый элемент списка.…


                          Ну это придется ручками делать, ведь scope создается один на контроллер, и он биндится к шаблону. Если нужны дочерние, то их придется создать вручную.
                            0
                            Неправильно сказал. Не область видимости привязывается к модели, а модель к области видимости. Точнее, scope это всего лишь ссылка на модель: habrahabr.ru/post/181882/. Область видимости, да, одна на контроллер, но может быть несколько на элементе, причем они так же будут вложенными, а не параллельными. На практике, создавать дочерние области приходится совсем не часто. Стандартные директивы и так их создают, когда нужно.
                        0
                        удалено

                        Only users with full accounts can post comments. Log in, please.