23 июля 2014 в 00:27

С большой силой приходит и большая ответственность — техника безопасности в AngularJS

image

Несомненно, ангуляр даёт вам силу. Но пользоваться ей нужно с умом. Я постарался сформулировать три простых правила, которые я много раз нарушал и страдал от этого.

1. Делайте копию объекта, если он может подвергнуться нежелательным изменениям.


В ангуляре данные по умолчанию едины, и если вы измените их намеренно, случайно или в результате ошибки, под угрозой окажутся все места его использования. Например, у нас есть фабрика пустых сущностей, которые будут использованы для заполнения форм.
module.factory("emptyEntity", function() {
	var emptyObject = {
		name:"",
		surname:"",
		address:{
			city:""
			street:"",
		}
	};

    return {
        createEmptyEntity: function(){
            return emptyObject;
        }
    };

});

И далее, в контроллере формы мы создаём в $scope этот пустой объект и используем его как модель формы.
   $scope.model = mapper.createEmptyPetition();

Что будет, если при вводе формы эта модель изменится, а потом вызвать mapper.createEmptyPetition() снова для другой формы? Так, как везде используется один и тот же экземпляр объекта emptyObject, изменения будут отражены в нём, и при следующем вызове mapper.createEmptyPetition() мы получим грязный и использованный объект. Подобных моментов при разработке может возникать великое множество, и нужно осторожно относиться к раздаче ссылок на объекты направо и налево. В данном случае следовало бы сделать вот так — возвращать копию объекта, чтобы её изменения не касались оригинального объекта:
 createEmptyEntity: function(){
     return angular.copy(emptyObject);
 }

2. Не теряйте ссылку на объект/массив, если не хотите потерять синхронизацию данных


Простой пример.
У нас есть контроллер, в $scope которого лежит массив, и есть функция для очищения массива:
module.controller("NewPetitionController", ["$scope", function($scope) {
        $scope.myArray = [1,2,3,4];

        $scope.cleanArray = function(){
             $scope.myArray = [];
       }
    }
]);

И где-то во вьюшке вы отдаёте массив в какую-нибудь директиву, например, которая его отрисует.
<div my-array-viewer array="myArray"></div>

Что будет, если вызвать функцию cleanArray? Директива спокойно продолжит отображать старый добрый полный массив, потому что у неё осталась ссылка на него. А кодом "$scope.myArray = []" мы только создали новый массив и записали ссылку на него в свойство myArray, на что директиве my-array-viewer абсолютно параллельно. Чтобы занулить массив, не потеряв на него ссылку, нужно просто вызвать $scope.myArray.length = 0;
То же касается объектов. Нельзя просто взять и присвоить переменной новый объект, нужно изменить старый, чтобы остальные части прилоежния, имеющие ссылку на этот объект, не потеряли её.
module.controller("NewPetitionController", ["$scope", function($scope) {
        $scope.myObj = {foo: "bar"};

        $scope.setObj = function(newObj){
             //$scope.myObj = newObj; //Так делать нельзя, это приведёт к утере ссылки
             angular.extend($scope.myObj, newObj); //нужно вот так, чтобы изменился исходный объект
       }
    }
]);

3. Будьте внимательны с дочерними $scope


Многие директивы, такие как ng-if, ng-include создают дочерний $scope. Что это значит? У этих директив будет создан новый экземпляр $scope, в свойстве prototype которого будет родительский скоуп — стандартной javascript-наследование. Из этого следует, что изменение простых свойств (string, number, boolean etc.) в дочернем скоупе НЕ БУДЕТ затрагивать родительский скоуп, так как простые свойства при наследовании копируются. В отличие от них, объекты при прототипном наследовании передаются ссылками, поэтому изменение свойств объектов будет отображаться в родительском скоупе.
Поэтому так делать не следует, это не будет работать:
<div ng-if="true">
   <a ng-click="showSecondBlock = true">Показать второй блок</a>
</div>

<div ng-if="showSecondBlock">
Второй блок отображается!
</div>

Вместо этого, нужно иметь для таких дел специальный объект в $scope, назовём его viewModel
app.controller("MainCtrl", function($scope) {
  $scope.viewModel = {};
});

<div ng-if="true">
   <a ng-click="viewModel.showSecondBlock = true">Показать второй блок</a>
</div>

<div ng-if="viewModel.showSecondBlock">
Второй блок отображается!
</div>


Пишите в комментариях, о какие ещё особенности ангуляр-way вам довелось набить шишек.
Андрей @Houston
карма
42,0
рейтинг 0,0
Похожие публикации

Комментарии (22)

  • +5
    Первые два пункта, скорее, относятся даже не к конкретно AngularJS или javascript'у, а к программированию в принципе.
    • +2
      Без сомнений, но в AngularJS как в ярком представителе MVC фреймворка с двусторонним биндингом это очень заметно, потому что тут вся работа производится с данными — массивами, объектами и их свойствами.
  • +2
    По второму пункту было бы неплохо привести пример тех самых «не безопасных директив». Скажем если директива использует скоуп и смотрит за ним, то проблем по идее не должно быть.

    jsfiddle.net/XAZFr/ — это то что я смог сходу придумать… но буду рад увидеть более адекватные примеры.
  • –15
    Никакой особенной силы ангуляр не даёт. Он даёт возможность написать статью с пафосным заголовком, в который можно вставить смешную картинку.
    • +9
      И не говорите, все эти языки, фреймворки, инструменты…
      Почему нельзя по старинке дырочки в бумажке делать?
      • –1
        Самое смешное то, что все эти клоуны, которые считали что ангуляром можно пользоваться, сейчас пользуют реакт. Но минусы свои не заберут, ггг. Русское IT такое русское.
        • +2
          все эти клоуны, которые считали что ангуляром можно пользоваться, сейчас пользуют реакт.

          Какое интересное утверждение подкрепленное лишь фантазией автора. Действительно, русское IT такое русское.
  • +2
    Только недавно была статья про использование директивы Ace, так опять пишут про основы JS и часть документации про изолированные скоупы. К сожалению, самое интересное в посте — картинка. А жаль.
    • +1
      Проблема мэйнстрим-технологий: слишком много шума вокруг, слишком мало людей реально читают документацию. Можете просто посмотреть на количество тупых вопросов связанных с AngularJS которых по хорошему не должно возникать, если человек на нормальном уровне знает JS и хотя бы немного поразбирался с инструментом который использует. Но без развернутого описания, с чем такое поведение связано, ценность статьи снижается.
  • 0
    Вообще, при работе с формой, не следует отправлять на сервер объект, связанный с полями формы. Стоит отправить его копию. Простой пример:

    В форме используется ресурс
    user: {
      email: a@a.ru,
      password: 123
    }
    

    делаете user.$save(), получаете в ответ что-то вроде {data: true} (мало ли что может вернуть бэкенд), user становится
    user: {
      data: true
    }
    

    и поля формы внезапно очищаются.

    Чтобы этого не происходило нужно отделять друг от друга объект формы и ресурса
    • 0
      а если бэкенд изменяет свойства объекта? например, форматирует текст, приводит номер телефона в нужный формат, вставляет айдишники (мало ли что может делать бэкенд). Тогда нужно опять вручную прокидывать изменения в модель?..
      • 0
        Вообще имеет смысл использовать нормальные объекты для хранения данных внутри приложения. То есть у нас есть объект Mymodel, который умеет из данных приходящих с сервера формировать данные для нашего приложения, у нас есть метод serializer/marshal/toJSON, который подготавливает данные для отправки на сервер.

        Для своих проектов коллега реализовал небольшой враппер для моделей и коллекций оных по аналогии с backbone. Сейчас обкатываем на проекте.
        • 0
          Слава богу, мы не стали городить таких костылей, не смотря на то, что переписали всё с Бекбона. Обычный ангуляровский ресурс отлично справляется и логика модель=объект отлично ложится на приложение
      • 0
        Странно, конечно. Но если на бэкенде такая логика, то будет одна модель, да.
  • +1
    В Angular 1.2 появились alias для контроллеров.

    <div ng-controller="StoreController as store">{{store.name}}</div>
    

    app.controller('StoreController', function() {
    	this.name = 'this.name';
    });
    

    Пример использования jsfiddle.net/5UW2X/
    Используя alias, аргумент $scope для контроллера становится не обязательным, но в дочерних контроллерах, что бы получить доступ родительскому контроллеру необходимо обратиться через $scope.[aliasName].some

    P.S. $scope.$watch возращает функцию, при вызове которой слежение прекратиться
    var unwatch = $scope.$watch("some", function () {
    	//only one time callback
    	unwatch();
    });
    

  • +1
    Вторая проблема, как и третья, решаются одинаково: $scope — это не модель, а контейнер моделей. Столько копий уже об это сломано.

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

    $scope.model.array

    Раньше еще часто замечали, что в ng-model должна быть минимум одна точка, тогда никогда не словите эту проблему и не будете иметь геморроя с хаками вроде array.length = 0…
    По опыту: рано или поздно, если нарушать это правило, проблему не только словите, но и пропустите в продакшн, не заметив где-нибудь, и вот тогда уже перестаните это делать.
    • 0
      Например, как в этом видео www.youtube.com/watch?v=DTx23w4z6Kc (кстати очень советую остальные видео из этого курса, лучшего введения в Angular я еще не видел).
  • 0
    Я вообще-то пользуюсь точкой обычно, но чисто ради интереса — а как очистить объект?
    • 0
      В нутри ngResource есть что-то похожее — функция shallowClearAndCopy. Посмотрите ее содержимое.
  • 0
    По первому пункту: зачем вообще нужна var emptyObject = {...}? Почему не переместить код инициализации пустого объекта в тело createEmptyEntity() и избавиться от необходимости что-то копировать и помнить об этом?
    • 0
      Я намеревался как можно проще проиллюстрировать ситуацию, когда один экземпляр объекта может использоваться много раз.
  • 0
    По второму пункту — если не нужна цепочка прототипов можно сделать так:
    angular.copy($scope.myObj, newObj); 
    

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.