AngularJS 개발자가 저지르는 가장 흔한 실수 18가지

게시 됨: 2022-03-11

단일 페이지 앱을 사용하려면 프론트엔드 개발자가 더 나은 소프트웨어 엔지니어가 되어야 합니다. CSS와 HTML은 더 이상 가장 큰 관심사가 아닙니다. 사실 더 이상 단일 관심사가 아닙니다. 프론트엔드 개발자는 XHR, 애플리케이션 로직(모델, 뷰, 컨트롤러), 성능, 애니메이션, 스타일, 구조, SEO 및 외부 서비스와의 통합을 처리해야 합니다. 이 모든 것이 결합된 결과는 항상 우선시되어야 하는 사용자 경험(UX)입니다.

AngularJS는 매우 강력한 프레임워크입니다. GitHub에서 세 번째로 별표가 많은 리포지토리입니다. 사용을 시작하는 것은 어렵지 않지만 요구 이해를 달성하기위한 목표입니다. 더 이상 탐색 시 재설정되지 않으므로 AngularJS 개발자는 더 이상 메모리 소비를 무시할 수 없습니다. 이것이 웹 개발의 선봉입니다. 포용하자!

일반적인 AngularJS 실수

일반적인 실수 #1: DOM을 통한 범위 액세스

프로덕션에 권장되는 몇 가지 최적화 조정이 있습니다. 그 중 하나는 디버그 정보를 비활성화하는 것입니다.

DebugInfoEnabled 는 기본값이 true이고 DOM 노드를 통한 범위 액세스를 허용하는 설정입니다. JavaScript 콘솔을 통해 시도하려면 DOM 요소를 선택하고 다음을 사용하여 해당 범위에 액세스하십시오.

 angular.element(document.body).scope()

CSS와 함께 jQuery를 사용하지 않는 경우에도 유용할 수 있지만 콘솔 외부에서 사용하면 안 됩니다. 그 이유는 $compileProvider.debugInfoEnabled 가 false로 설정되어 있을 때 DOM 노드에서 .scope() 를 호출하면 undefined 가 반환되기 때문입니다.

이것은 생산에 권장되는 몇 안 되는 옵션 중 하나입니다.

프로덕션 환경에서도 콘솔을 통해 스코프에 계속 액세스할 수 있습니다. 콘솔에서 angular.reloadWithDebugInfo() 를 호출하면 앱이 바로 그 작업을 수행합니다.

일반적인 실수 #2: 점이 없는 경우

ng-model 에 점이 없으면 잘못하고 있다는 것을 읽었을 것입니다. 상속과 관련하여 그 진술은 종종 사실입니다. 범위에는 JavaScript에 일반적인 상속의 프로토타입 모델이 있으며 중첩 범위는 AngularJS에 일반적입니다. 많은 지시문은 ngRepeat , ngIfngController 와 같은 자식 범위를 만듭니다. 모델을 확인할 때 조회는 현재 범위에서 시작하여 모든 상위 범위를 거쳐 $rootScope 까지 진행됩니다.

그러나 새 값을 설정할 때 변경하려는 모델(변수)의 종류에 따라 상황이 달라집니다. 모델이 기본 모델인 경우 하위 범위는 새 모델을 생성합니다. 그러나 변경 사항이 모델 개체의 속성인 경우 상위 범위에 대한 조회는 참조된 개체를 찾고 실제 속성을 변경합니다. 새 모델은 현재 범위에 설정되지 않으므로 마스킹이 발생하지 않습니다.

 function MainController($scope) { $scope.foo = 1; $scope.bar = {innerProperty: 2}; } angular.module('myApp', []) .controller('MainController', MainController);
 <div ng-controller="MainController"> <p>OUTER SCOPE:</p> <p>{{ foo }}</p> <p>{{ bar.innerProperty }}</p> <div ng-if="foo"> <!— ng-if creates a new scope —> <p>INNER SCOPE</p> <p>{{ foo }}</p> <p>{{ bar.innerProperty }}</p> <button ng-click="foo = 2">Set primitive</button> <button ng-click="bar.innerProperty = 3">Mutate object</button> </div> </div>

"Set primitive"라고 표시된 버튼을 클릭하면 내부 범위의 foo가 2로 설정되지만 외부 범위의 foo는 변경되지 않습니다.

"개체 변경"이라고 표시된 버튼을 클릭하면 상위 범위에서 막대 속성이 변경됩니다. 내부 스코프에 변수가 없기 때문에 섀도잉이 발생하지 않고 bar의 가시적 값은 두 스코프 모두에서 3이 됩니다.

이를 수행하는 또 다른 방법은 상위 범위와 루트 범위가 모든 범위에서 참조된다는 사실을 활용하는 것입니다. $parent$root 개체를 사용하여 뷰에서 직접 부모 범위 및 $rootScope 에 각각 액세스할 수 있습니다. 그것은 강력한 방법일 수 있지만 스트림의 특정 범위를 대상으로 하는 문제로 인해 나는 그것을 좋아하지 않습니다. controllerAs 구문을 사용하여 범위에 특정한 속성을 설정하고 액세스하는 또 다른 방법이 있습니다.

일반적인 실수 #3: controllerAs 구문을 사용하지 않음

주입된 $scope 대신 컨트롤러 개체를 사용하도록 모델을 할당하는 가장 효율적인 대안입니다. 범위를 주입하는 대신 다음과 같이 모델을 정의할 수 있습니다.

 function MainController($scope) { this.foo = 1; var that = this; var setBar = function () { // that.bar = {someProperty: 2}; this.bar = {someProperty: 2}; }; setBar.call(this); // there are other conventions: // var MC = this; // setBar.call(this); when using 'this' inside setBar() }
 <div> <p>OUTER SCOPE:</p> <p>{{ MC.foo }}</p> <p>{{ MC.bar.someProperty }}</p> <div ng-if="test1"> <p>INNER SCOPE</p> <p>{{ MC.foo }}</p> <p>{{ MC.bar.someProperty }}</p> <button ng-click="MC.foo = 3">Change MC.foo</button> <button ng-click="MC.bar.someProperty = 5">Change MC.bar.someProperty</button> </div> </div>

이것은 훨씬 덜 혼란 스럽습니다. 특히 중첩된 상태의 경우와 같이 중첩된 범위가 많은 경우에 그렇습니다.

controllerAs 구문에는 더 많은 것이 있습니다.

일반적인 실수 #4: controllerAs 구문을 완전히 활용하지 않음

컨트롤러 개체가 노출되는 방식에는 몇 가지 주의 사항이 있습니다. 기본적으로 일반 모델과 마찬가지로 컨트롤러의 범위에 설정된 개체입니다.

컨트롤러 객체의 속성을 관찰해야 하는 경우 함수를 볼 수 있지만 반드시 볼 필요는 없습니다. 다음은 예입니다.

 function MainController($scope) { this.title = 'Some title'; $scope.$watch(angular.bind(this, function () { return this.title; }), function (newVal, oldVal) { // handle changes }); }

다음을 수행하는 것이 더 쉽습니다.

 function MainController($scope) { this.title = 'Some title'; $scope.$watch('MC.title', function (newVal, oldVal) { // handle changes }); }

범위 체인 아래에서도 하위 컨트롤러에서 MC에 액세스할 수 있음을 의미합니다.

 function NestedController($scope) { if ($scope.MC && $scope.MC.title === 'Some title') { $scope.MC.title = 'New title'; } }

그러나 그렇게 하려면 controllerAs에 사용하는 약어와 일치해야 합니다. 설정하는 방법은 적어도 세 가지가 있습니다. 당신은 이미 첫 번째 것을 보았습니다:

 <div ng-controller="MainController as MC"> … </div>

그러나 ui-router 를 사용하는 경우 컨트롤러를 이러한 방식으로 지정하면 오류가 발생하기 쉽습니다. 상태의 경우 상태 구성에서 컨트롤러를 지정해야 합니다.

 angular.module('myApp', []) .config(function ($stateProvider) { $stateProvider .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/path/to/template.html' }) }). controller('MainController', function () { … });

주석을 추가하는 또 다른 방법이 있습니다.

 (…) .state('main', { url: '/', controller: 'MainController', controllerAs: 'MC', templateUrl: '/path/to/template.html' })

지시문에서도 동일한 작업을 수행할 수 있습니다.

 function AnotherController() { this.text = 'abc'; } function testForToptal() { return { controller: 'AnotherController as AC', template: '<p>{{ AC.text }}</p>' }; } angular.module('myApp', []) .controller('AnotherController', AnotherController) .directive('testForToptal', testForToptal);

덜 간결하지만 주석을 추가하는 다른 방법도 유효합니다.

 function testForToptal() { return { controller: 'AnotherController', controllerAs: 'AC', template: '<p>{{ AC.text }}</p>' }; }

일반적인 실수 #5: 전원을 위해 UI-ROUTER와 함께 명명된 보기를 사용하지 않음"

AngularJS를 위한 사실상의 라우팅 솔루션은 지금까지 ui-router 였습니다. 얼마 전에 코어에서 제거된 ngRoute 모듈은 더 정교한 라우팅을 하기에는 너무 기본적이었습니다.

새로운 NgRouter 가 진행 중이지만 작성자는 아직 생산하기에는 너무 이르다고 생각합니다. 내가 이것을 쓰고 있을 때 안정적인 Angular는 1.3.15이고 ui-router 는 흔들립니다.

주요 이유:

  • 멋진 상태 중첩
  • 경로 추상화
  • 선택 및 필수 매개변수

여기에서는 AngularJS 오류를 피하기 위한 상태 중첩에 대해 설명합니다.

이것을 복잡하지만 표준적인 사용 사례로 생각하십시오. 홈페이지 보기와 상품 보기가 있는 앱이 있습니다. 제품 보기에는 소개, 위젯 및 콘텐츠의 세 가지 개별 섹션이 있습니다. 위젯이 상태를 전환할 때 다시 로드되지 않고 유지되기를 원합니다. 그러나 콘텐츠는 다시 로드해야 합니다.

다음 HTML 제품 색인 페이지 구조를 고려하십시오.

 <body> <header> <!-- SOME STATIC HEADER CONTENT --> </header> <section class="main"> <div class="page-content"> <div class="row"> <div class="col-xs-12"> <section class="intro"> <h2>SOME PRODUCT SPECIFIC INTRO</h2> </section> </div> </div> <div class="row"> <div class="col-xs-3"> <section class="widget"> <!-- some widget, which should never reload --> </section> </div> <div class="col-xs-9"> <section class="content"> <div class="product-content"> <h2>Product title</h2> <span>Context-specific content</span> </div> </section> </div> </div> </div> </section> <footer> <!-- SOME STATIC HEADER CONTENT --> </footer> </body>

이것은 HTML 코더에서 얻을 수 있는 것이며 이제 파일과 상태로 분리해야 합니다. 나는 일반적으로 필요한 경우 전역 데이터를 유지하는 추상 MAIN 상태가 있다는 규칙을 따릅니다. $rootScope 대신 사용하십시오. Main 상태는 또한 모든 페이지에 필요한 정적 HTML을 유지합니다. index.html을 깨끗하게 유지합니다.

 <!— index.html —> <body> <div ui-view></div> </body>
 <!— main.html —> <header> <!-- SOME STATIC HEADER CONTENT --> </header> <section class="main"> <div ui-view></div> </section> <footer> <!-- SOME STATIC HEADER CONTENT --> </footer>

그런 다음 제품 색인 페이지를 보겠습니다.

 <div class="page-content"> <div class="row"> <div class="col-xs-12"> <section class="intro"> <div ui-view="intro"></div> </section> </div> </div> <div class="row"> <div class="col-xs-3"> <section class="widget"> <div ui-view="widget"></div> </section> </div> <div class="col-xs-9"> <section class="content"> <div ui-view="content"></div> </section> </div> </div> </div>

보시다시피 제품 색인 페이지에는 세 개의 명명된 보기가 있습니다. 하나는 소개용, 하나는 위젯용, 하나는 제품용입니다. 우리는 사양을 충족! 이제 라우팅을 설정해 보겠습니다.

 function config($stateProvider) { $stateProvider // MAIN ABSTRACT STATE, ALWAYS ON .state('main', { abstract: true, url: '/', controller: 'MainController as MC', templateUrl: '/routing-demo/main.html' }) // A SIMPLE HOMEPAGE .state('main.homepage', { url: '', controller: 'HomepageController as HC', templateUrl: '/routing-demo/homepage.html' }) // THE ABOVE IS ALL GOOD, HERE IS TROUBLE // A COMPLEX PRODUCT PAGE .state('main.product', { abstract: true, url: ':id', controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html', }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'widget': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' }, 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }) // PRODUCT DETAILS SUBSTATE .state('main.product.details', { url: '/details', views: { 'widget': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }); } angular.module('articleApp', [ 'ui.router' ]) .config(config);

그것이 첫 번째 접근일 것입니다. 이제 main.product.indexmain.product.details 사이를 전환하면 어떻게 됩니까? 콘텐츠와 위젯이 다시 로드되지만 우리는 콘텐츠만 다시 로드하기를 원합니다. 이것은 문제가 있었고 개발자는 실제로 해당 기능만 지원하는 라우터를 만들었습니다. 이것 의 이름 중 하나 는 고정 보기 입니다 . 다행스럽게도 ui-router 는 이름이 지정된 절대 뷰 타겟팅으로 즉시 이를 지원합니다.

 // A COMPLEX PRODUCT PAGE // WITH NO MORE TROUBLE .state('main.product', { abstract: true, url: ':id', views: { // TARGETING THE UNNAMED VIEW IN MAIN.HTML '@main': { controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html' }, // TARGETING THE WIDGET VIEW IN PRODUCT.HTML // BY DEFINING A CHILD VIEW ALREADY HERE, WE ENSURE IT DOES NOT RELOAD ON CHILD STATE CHANGE '[email protected]': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' } } }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }) // PRODUCT DETAILS SUBSTATE .state('main.product.details', { url: '/details', views: { 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } });

상태 정의를 추상적인 상위 보기로 이동하면 일반적으로 해당 하위 형제에 영향을 미치는 URL을 전환할 때 하위 보기가 다시 로드되지 않도록 보호할 수 있습니다. 물론 위젯은 간단한 지시문일 수 있습니다. 그러나 요점은 이것이 또 다른 복잡한 중첩 상태일 수도 있다는 것입니다.

$urlRouterProvider.deferIntercept() 를 사용하여 이를 수행하는 또 다른 방법이 있지만 실제로는 상태 구성을 사용하는 것이 더 낫다고 생각합니다. 경로 가로채기에 관심이 있다면 StackOverflow에 대한 작은 자습서를 작성했습니다.

일반적인 실수 #6: Angular World에서 Anonymous Functions를 사용하여 모든 것을 선언하기

실수 는 더 가벼운 것이며 AngularJS 오류 메시지를 피하는 것보다 스타일의 문제입니다. 내가 Angular 내부 선언에 익명 함수를 거의 전달하지 않는다는 것을 이전에 알아차렸을 것입니다. 나는 일반적으로 먼저 함수를 정의한 다음 전달합니다.

이것은 단순한 기능 이상을 의미합니다. 나는 스타일 가이드, 특히 Airbnb와 Todd Motto를 읽으면서 이 접근 방식을 얻었습니다. 몇 가지 장점이 있고 거의 단점이 없다고 생각합니다.

우선, 함수와 객체를 변수에 할당하면 훨씬 쉽게 조작하고 변경할 수 있습니다. 둘째, 코드가 더 깨끗하고 파일로 쉽게 분할할 수 있습니다. 유지보수성을 의미합니다. 전역 네임스페이스를 오염시키지 않으려면 모든 파일을 IIFE로 래핑하십시오. 세 번째 이유는 테스트 가능성입니다. 다음 예를 고려하십시오.

 'use strict'; function yoda() { var privateMethod = function () { // this function is not exposed }; var publicMethod1 = function () { // this function is exposed, but it's internals are not exposed // some logic... }; var publicMethod2 = function (arg) { // THE BELOW CALL CANNOT BE SPIED ON WITH JASMINE publicMethod1('someArgument'); }; // IF THE LITERAL IS RETURNED THIS WAY, IT CAN'T BE REFERRED TO FROM INSIDE return { publicMethod1: function () { return publicMethod1(); }, publicMethod2: function (arg) { return publicMethod2(arg); } }; } angular.module('app', []) .factory('yoda', yoda);

이제 publicMethod1 을 조롱할 수 있지만 노출된 이후에 그렇게 해야 하는 이유는 무엇입니까? 그냥 기존 방식을 염탐하는 것이 더 쉽지 않을까요? 그러나 이 방법은 실제로 또 다른 기능인 얇은 래퍼입니다. 이 접근 방식을 살펴보십시오.

 function yoda() { var privateMethod = function () { // this function is not exposed }; var publicMethod1 = function () { // this function is exposed, but it's internals are not exposed // some logic... }; var publicMethod2 = function (arg) { // the below call cannot be spied on publicMethod1('someArgument'); // BUT THIS ONE CAN! hostObject.publicMethod1('aBetterArgument'); }; var hostObject = { publicMethod1: function () { return publicMethod1(); }, publicMethod2: function (arg) { return publicMethod2(arg); } }; return hostObject; }

실제로 코드가 더 재사용 가능하고 관용적이기 때문에 이것은 스타일에 관한 것만이 아닙니다. 개발자는 더 많은 표현력을 얻습니다. 모든 코드를 자체 포함된 블록으로 분할하면 더 쉽습니다.

일반적인 실수 #7: 작업자를 사용하여 Angular AKA에서 무거운 처리 수행

일부 시나리오에서는 일련의 필터, 데코레이터 및 마지막으로 정렬 알고리즘을 통해 복잡한 개체의 대규모 배열을 처리해야 할 수 있습니다. 한 가지 사용 사례는 앱이 오프라인으로 작동해야 하거나 데이터 표시 성능이 중요한 경우입니다. 그리고 자바스크립트는 단일 쓰레드이기 때문에 브라우저를 멈추는 것은 상대적으로 쉽습니다.

웹 작업자와 함께 피하기도 쉽습니다. AngularJS에 대해 특별히 처리하는 인기 있는 라이브러리는 없는 것 같습니다. 구현이 쉽기 때문에 최선일 수 있습니다.

먼저 서비스를 설정해 보겠습니다.

 function scoringService($q) { var scoreItems = function (items, weights) { var deferred = $q.defer(); var worker = new Worker('/worker-demo/scoring.worker.js'); var orders = { items: items, weights: weights }; worker.postMessage(orders); worker.onmessage = function (e) { if (e.data && e.data.ready) { deferred.resolve(e.data.items); } }; return deferred.promise; }; var hostObject = { scoreItems: function (items, weights) { return scoreItems(items, weights); } }; return hostObject; } angular.module('app.worker') .factory('scoringService', scoringService);

이제 작업자:

 'use strict'; function scoringFunction(items, weights) { var itemsArray = []; for (var i = 0; i < items.length; i++) { // some heavy processing // itemsArray is populated, etc. } itemsArray.sort(function (a, b) { if (a.sum > b.sum) { return -1; } else if (a.sum < b.sum) { return 1; } else { return 0; } }); return itemsArray; } self.addEventListener('message', function (e) { var reply = { ready: true }; if (e.data && e.data.items && e.data.items.length) { reply.items = scoringFunction(e.data.items, e.data.weights); } self.postMessage(reply); }, false);

이제 평소와 같이 서비스를 주입하고 프라미스를 반환하는 서비스 메서드와 마찬가지로 scoringService.scoreItems() 를 처리합니다. 무거운 처리는 별도의 스레드에서 수행되며 UX에 해를 끼치 지 않습니다.

주의할 사항:

  • 스폰할 일꾼의 수에 대한 일반적인 규칙은 없는 것 같습니다. 일부 개발자는 8이 좋은 숫자라고 주장하지만 온라인 계산기를 사용하고 자신에게 적합합니다.
  • 이전 브라우저와의 호환성 확인
  • 서비스에서 작업자에게 숫자 0을 전달할 때 문제가 발생합니다. 전달된 속성에 .toString() 을 적용했는데 제대로 작동했습니다.

일반적인 실수 #8: 남용과 오해가 해결합니다

해결은 보기 로드에 추가 시간을 추가합니다. 프론트 엔드 앱의 고성능이 우리의 주요 목표라고 생각합니다. 앱이 API의 데이터를 기다리는 동안 보기의 일부를 렌더링하는 것은 문제가 되지 않습니다.

다음 설정을 고려하십시오.

 function resolve(index, timeout) { return { data: function($q, $timeout) { var deferred = $q.defer(); $timeout(function () { deferred.resolve(console.log('Data resolve called ' + index)); }, timeout); return deferred.promise; } }; } function configResolves($stateProvide) { $stateProvider // MAIN ABSTRACT STATE, ALWAYS ON .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/routing-demo/main.html', resolve: resolve(1, 1597) }) // A COMPLEX PRODUCT PAGE .state('main.product', { url: ':id', controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html', resolve: resolve(2, 2584) }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } }, resolve: resolve(3, 987) }); }

콘솔 출력은 다음과 같습니다.

 Data resolve called 3 Data resolve called 1 Data resolve called 2 Main Controller executed Product Controller executed Intro Controller executed

기본적으로 다음을 의미합니다.

  • 해결은 비동기적으로 실행됩니다.
  • 우리는 실행 순서에 의존할 수 없습니다(또는 최소한 상당히 유연해야 함).
  • 모든 상태는 추상적이지 않더라도 모든 해결이 제 역할을 할 때까지 차단됩니다.

이것은 사용자가 출력을 보기 전에 모든 종속성을 기다려야 함을 의미합니다. 우리는 그 데이터가 필요합니다. 보기 전에 반드시 필요한 경우 .run() 블록에 넣으십시오. 그렇지 않으면 컨트롤러에서 서비스를 호출하고 절반만 로드된 상태를 정상적으로 처리합니다. 진행 중인 작업을 보는 것(컨트롤러가 이미 실행되어 실제로 진행 중인 것)은 앱을 정지시키는 것보다 낫습니다.

일반적인 실수 #9: 앱을 최적화하지 않음 - 세 가지 예

a) 슬라이더를 모델에 연결하는 것과 같이 너무 많은 다이제스트 루프 발생

이것은 AngularJS 오류가 발생할 수 있는 일반적인 문제이지만 슬라이더의 예에서 설명하겠습니다. 확장된 기능이 필요했기 때문에 이 슬라이더 라이브러리인 각도 범위 슬라이더를 사용하고 있었습니다. 해당 지시문에는 최소 버전에서 다음 구문이 있습니다.

 <body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.price" > </div> </body>

컨트롤러에서 다음 코드를 고려하십시오.

 this.maxPrice = '100'; this.price = '55'; $scope.$watch('MC.price', function (newVal) { if (newVal || newVal === 0) { for (var i = 0; i < 987; i++) { console.log('ALL YOUR BASE ARE BELONG TO US'); } } });

그래서 느리게 작동합니다. 캐주얼한 해결책은 입력에 시간 초과를 설정하는 것입니다. 그러나 이것이 항상 편리한 것은 아니며 모든 경우에 실제 모델 변경을 지연하고 싶지 않을 때도 있습니다.

따라서 시간 초과 시 작업 모델을 변경하기 위해 바인딩된 임시 모델을 추가합니다.

 <body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.priceTemporary" > </div> </body>

컨트롤러에서:

 this.maxPrice = '100'; this.price = '55'; this.priceTemporary = '55'; $scope.$watch('MC.price', function (newVal) { if (!isNaN(newVal)) { for (var i = 0; i < 987; i++) { console.log('ALL YOUR BASE ARE BELONG TO US'); } } }); var timeoutInstance; $scope.$watch('MC.priceTemporary', function (newVal) { if (!isNaN(newVal)) { if (timeoutInstance) { $timeout.cancel(timeoutInstance); } timeoutInstance = $timeout(function () { $scope.MC.price = newVal; }, 144); } });

b) $applyAsync를 사용하지 않음

AngularJS에는 $digest() 를 호출하는 폴링 메커니즘이 없습니다. 코드를 평가하고 나중에 다이제스트를 호출하는 지시문(예: ng-click , input ), 서비스( $timeout , $http ) 및 메서드( $watch )를 사용하기 때문에 실행됩니다.

.$applyAsync() 가 하는 일은 다음 $digest() 주기까지 표현식의 해석을 지연시키는 것입니다. 이것은 실제로 ~10ms인 0 타임아웃 후에 트리거됩니다.

이제 applyAsync 를 사용하는 두 가지 방법이 있습니다. $http 요청을 위한 자동화된 방법과 나머지를 위한 수동 방법.

거의 같은 시간에 반환되는 모든 http 요청이 하나의 다이제스트에서 해결되도록 하려면 다음을 수행하십시오.

 mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });

수동 방식은 실제로 어떻게 작동하는지 보여줍니다. 바닐라 JS 이벤트 리스너나 jQuery .click() 또는 기타 외부 라이브러리에 대한 콜백에서 실행되는 일부 함수를 고려하십시오. 모델을 실행하고 변경한 후 $apply() 로 래핑하지 않은 경우 $scope.$root.$digest() ( $rootScope.$digest() ) 또는 최소한 $scope.$digest() 를 호출해야 합니다. $scope.$digest() . 그렇지 않으면 변경 사항이 표시되지 않습니다.

한 흐름에서 여러 번 수행하면 느리게 실행될 수 있습니다. 대신 표현식에서 $scope.$applyAsync() 를 호출하는 것을 고려하십시오. 모두에 대해 하나의 다이제스트 주기만 호출하도록 설정합니다.

c) 많은 이미지 처리

성능이 저하되면 Chrome 개발자 도구의 타임라인을 사용하여 원인을 조사할 수 있습니다. 이 도구에 대한 자세한 내용은 실수 #17에서 작성하겠습니다. 기록 후 타임라인 그래프가 녹색으로 지배되는 경우 성능 문제는 이미지 처리와 관련될 수 있습니다. 이것은 AngularJS와 엄격하게 관련이 없지만 AngularJS 성능 문제(그래프에서 대부분 노란색)와 관련하여 발생할 수 있습니다. 프론트엔드 엔지니어로서 우리는 완전한 최종 프로젝트에 대해 생각해야 합니다.

잠시 시간을 내어 다음을 평가하십시오.

  • 시차를 사용합니까?
  • 서로 겹치는 여러 레이어의 콘텐츠가 있습니까?
  • 이미지를 이리저리 옮기나요?
  • 이미지의 크기를 조정합니까(예: 배경 크기로)?
  • 루프에서 이미지 크기를 조정하고 크기 조정 시 다이제스트 루프를 일으킬 수 있습니까?

위의 세 가지 이상에 "예"라고 대답했다면 완화를 고려하십시오. 아마도 다양한 이미지 크기를 제공하고 크기를 전혀 조정하지 않을 수 있습니다. "transform: translateZ(0)" 강제 GPU 처리 해킹을 추가할 수 있습니다. 또는 핸들러에 requestAnimationFrame을 사용하십시오.

일반적인 실수 #10: jQuerying - 분리된 DOM 트리

AngularJS와 함께 jQuery를 사용하는 것은 권장되지 않으며 피해야 한다는 말을 많이 들었을 것입니다. 이러한 진술 뒤에 숨은 이유를 이해하는 것이 필수적입니다. 내가 아는 한 적어도 세 가지 이유가 있지만 그 중 어느 것도 실제 차단기가 아닙니다.

이유 1: jQuery 코드를 실행할 때 $digest() 를 직접 호출해야 합니다. 많은 경우에 AngularJS에 맞게 조정된 AngularJS 솔루션이 있으며 jQuery보다 Angular 내부에서 더 잘 사용할 수 있습니다(예: ng-click 또는 이벤트 시스템).

이유 2: 앱 구축에 대한 생각의 방식. 탐색할 때 다시 로드되는 웹 사이트에 JavaScript를 추가했다면 메모리 소모에 대해 너무 걱정할 필요가 없었습니다. 단일 페이지 앱을 사용하면 걱정할 필요가 있습니다. 정리하지 않으면 앱에서 몇 분 이상을 보내는 사용자에게 성능 문제가 커질 수 있습니다.

이유 3: 정리는 실제로 수행하고 분석하기 가장 쉬운 일이 아닙니다. 스크립트(브라우저에서)에서 가비지 수집기를 호출할 방법이 없습니다. 분리된 DOM 트리로 끝날 수 있습니다. 예제를 만들었습니다(jQuery는 index.html에 로드됨).

 <section> <test-for-toptal></test-for-toptal> <button ng-click="MC.removeDirective()">remove directive</button> </section>
 function MainController($rootScope, $scope) { this.removeDirective = function () { $rootScope.$emit('destroyDirective'); }; } function testForToptal($rootScope, $timeout) { return { link: function (scope, element, attributes) { var destroyListener = $rootScope.$on('destroyDirective', function () { scope.$destroy(); }); // adding a timeout for the DOM to get ready $timeout(function () { scope.toBeDetached = element.find('p'); }); scope.$on('$destroy', function () { destroyListener(); element.remove(); }); }, template: '<div><p>I AM DIRECTIVE</p></div>' }; } angular.module('app', []) .controller('MainController', MainController) .directive('testForToptal', testForToptal);

이것은 일부 텍스트를 출력하는 간단한 지시문입니다. 그 아래에 지시어를 수동으로 제거하는 버튼이 있습니다.

따라서 지시문이 제거되면 scope.toBeDetached에 DOM 트리에 대한 참조가 남아 있습니다. 크롬 개발 도구에서 "프로필" 탭에 액세스한 다음 "힙 스냅샷 찍기"에 액세스하면 출력에 다음이 표시됩니다.

몇 마리로 살 수도 있지만 한 톤 있으면 좋지 않습니다. 특히 예와 같이 어떤 이유로 범위에 저장하는 경우. 전체 DOM은 모든 다이제스트에서 평가됩니다. 문제가 되는 detached DOM 트리는 노드가 4개인 트리입니다. 그러면 어떻게 해결할 수 있습니까?

 scope.$on('$destroy', function () { // setting this model to null // will solve the problem. scope.toBeDetached = null; destroyListener(); element.remove(); });

4개의 항목이 있는 분리된 DOM 트리가 제거되었습니다!

이 예에서 지시문은 동일한 범위를 사용하고 범위에 DOM 요소를 저장합니다. 그런 식으로 보여주기가 더 쉬웠어요. 변수에 저장할 수 있으므로 항상 그렇게 나쁜 것은 아닙니다. 그러나 해당 변수를 참조한 클로저 또는 동일한 함수 범위의 다른 클로저가 지속되는 경우 여전히 메모리를 차지합니다.

일반적인 실수 #11: 고립된 범위의 남용

한 곳에서 사용된다는 것을 알고 있거나 사용되는 환경과 충돌하지 않을 것으로 예상되는 지시문이 필요할 때마다 격리된 범위를 사용할 필요가 없습니다. 최근에는 재사용 가능한 컴포넌트를 생성하는 경향이 있지만, 핵심 Angular 디렉티브는 격리된 범위를 전혀 사용하지 않는다는 사실을 알고 계셨습니까?

두 가지 주요 이유가 있습니다. 두 개의 격리된 범위 지시문을 요소에 적용할 수 없고 중첩/상속/이벤트 처리에 문제가 발생할 수 있습니다. 특히 transclusion과 관련하여 - 효과가 기대한 것과 다를 수 있습니다.

그래서 이것은 실패할 것입니다:

 <p isolated-scope-directive another-isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"></p>

그리고 하나의 지시문만 사용하더라도 격리된 범위 모델이나 isolatedScopeDirective에서 브로드캐스트된 이벤트 모두 AnotherController에서 사용할 수 없음을 알 수 있습니다. 슬프게도, 변환 마법을 사용하여 작동하게 할 수 있지만 대부분의 사용 사례에서는 격리할 필요가 없습니다.

 <p isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"> <div ng-controller="AnotherController"> … the isolated scope is not available here, look: {{ isolatedModel }} </div> </p>

이제 두 가지 질문이 있습니다.

  1. 동일한 범위 지시문에서 상위 범위 모델을 어떻게 처리할 수 있습니까?
  2. 새 모델 값을 어떻게 인스턴스화할 수 있습니까?

두 가지 방법 모두에서 속성에 값을 전달합니다. 이 MainController를 고려하십시오.

 function MainController($interval) { this.foo = { bar: 1 }; this.baz = 1; var that = this; $interval(function () { that.foo.bar++; }, 144); $interval(function () { that.baz++; }, 144); this.quux = [1,2,3]; }

이 보기를 제어합니다.

 <body ng-controller="MainController as MC"> <div class="cyan-surface"> <h1>Attributes test</h1> <test-directive watch-attribute="MC.foo" observe-attribute="current index: {{ MC.baz }}"></test-directive> </div> </body>

"watch-attribute"는 보간되지 않습니다. JS 마술로 인해 모두 작동합니다. 다음은 지시문 정의입니다.

 function testDirective() { var postLink = function (scope, element, attrs) { scope.$watch(attrs.watchAttribute, function (newVal) { if (newVal) { // take a look in the console // we can't use the attribute directly console.log(attrs.watchAttribute); // the newVal is evaluated, and it can be used scope.modifiedFooBar = newVal.bar * 10; } }, true); attrs.$observe('observeAttribute', function (newVal) { scope.observed = newVal; }); }; return { link: postLink, templateUrl: '/attributes-demo/test-directive.html' }; }

attrs.watchAttribute 는 따옴표 없이 scope.$watch() 에 전달됩니다! 즉, 실제로 $watch에 전달된 것은 MC.foo 문자열이었습니다! 그러나 $watch() 에 전달된 모든 문자열은 범위에 대해 평가되고 MC.foo 는 범위에서 사용할 수 있기 때문에 작동합니다. 이는 AngularJS 핵심 지시문에서 속성을 관찰하는 가장 일반적인 방법이기도 합니다.

템플릿에 대한 github의 코드를 참조하고 $parse$eval 에서 더 멋진 기능을 살펴보세요.

일반적인 실수 #12: 자신을 정리하지 않는 것 - 감시자, 간격, 시간 초과 및 변수

AngularJS는 사용자를 대신하여 일부 작업을 수행하지만 전부는 아닙니다. 다음은 수동으로 정리해야 합니다.

  • 현재 범위에 바인딩되지 않은 모든 감시자(예: $rootScope에 바인딩)
  • 간격
  • 시간 초과
  • 지시문에서 DOM을 참조하는 변수
  • Dodgy jQuery 플러그인, 예를 들어 JavaScript $destroy 이벤트에 반응하는 핸들러가 없는 플러그인

수동으로 수행하지 않으면 예기치 않은 동작과 메모리 누수가 발생합니다. 더 나쁜 것은 - 이것들은 즉시 눈에 띄지 않을 것이지만 결국에는 서서히 나타날 것입니다. 머피의 법칙.

놀랍게도 AngularJS는 다음과 같은 모든 것을 처리할 수 있는 편리한 방법을 제공합니다.

 function cleanMeUp($interval, $rootScope, $timeout) { var postLink = function (scope, element, attrs) { var rootModelListener = $rootScope.$watch('someModel', function () { // do something }); var myInterval = $interval(function () { // do something in intervals }, 2584); var myTimeout = $timeout(function () { // defer some action here }, 1597); scope.domElement = element; $timeout(function () { // calling $destroy manually for testing purposes scope.$destroy(); }, 987); // here is where the cleanup happens scope.$on('$destroy', function () { // disable the listener rootModelListener(); // cancel the interval and timeout $interval.cancel(myInterval); $timeout.cancel(myTimeout); // nullify the DOM-bound model scope.domElement = null; }); element.on('$destroy', function () { // this is a jQuery event // clean up all vanilla JavaScript / jQuery artifacts here // respectful jQuery plugins have $destroy handlers, // that is the reason why this event is emitted... // follow the standards. }); };

jQuery $destroy 이벤트에 주목하십시오. AngularJS와 같이 호출되지만 별도로 처리됩니다. 범위 $watchers는 jQuery 이벤트에 반응하지 않습니다.

흔한 실수 #13: 감시자를 너무 많이 두는 것

이것은 이제 아주 간단해야 합니다. 여기서 한 가지 이해해야 할 것이 있습니다: $digest() . 모든 바인딩 {{ model }} 에 대해 AngularJS는 감시자를 만듭니다. 모든 다이제스트 단계에서 이러한 각 바인딩이 평가되고 이전 값과 비교됩니다. 이것을 더티 체킹(dirty-checking)이라고 하며 이것이 $digest가 하는 일입니다. 마지막 확인 이후 값이 변경되면 감시자 콜백이 시작됩니다. If that watcher callback modifies a model ($scope variable), a new $digest cycle is fired (up to a maximum of 10) when an exception is thrown.

Browsers don't have problems even with thousands of bindings, unless the expressions are complex. The common answer for “how many watchers are ok to have” is 2000.

So, how can we limit the number of watchers? By not watching scope models when we don't expect them to change. It is fairly easy onwards from AngularJS 1.3, since one-time bindings are in core now.

 <li ng-repeat="item in ::vastArray">{{ ::item.velocity }}</li>

After vastArray and item.velocity are evaluated once, they will never change again. You can still apply filters to the array, they will work just fine. It is just that the array itself will not be evaluated. In many cases, that is a win.

Common Mistake #14: Misunderstanding The Digest

This AngularJS error was already partly covered in mistakes 9.b and in 13. This is a more thorough explanation. AngularJS updates DOM as a result of callback functions to watchers. Every binding, that is the directive {{ someModel }} sets up watchers, but watchers are also set for many other directives like ng-if and ng-repeat . Just take a look at the source code, it is very readable. Watchers can also be set manually, and you have probably done that at least a few times yourself.

$watch() ers are bound to scopes. $Watchers can take strings, which are evaluated against the scope that the $watch() was bound to. They can also evaluate functions. And they also take callbacks. So, when $rootScope.$digest() is called, all the registered models (that is $scope variables) are evaluated and compared against their previous values. If the values don't match, the callback to the $watch() is executed.

It is important to understand that even though a model's value was changed, the callback does not fire until the next digest phase. It is called a “phase” for a reason - it can consist of several digest cycles. If only a watcher changes a scope model, another digest cycle is executed.

But $digest() is not polled for . It is called from core directives, services, methods, etc. If you change a model from a custom function that does not call .$apply , .$applyAsync , .$evalAsync , or anything else that eventually calls $digest() , the bindings will not be updated.

By the way, the source code for $digest() is actually quite complex. It is nevertheless worth reading, as the hilarious warnings make up for it.

Common Mistake #15: Not Relying On Automation, Or Relying On It Too Much

If you follow the trends within front end development and are a bit lazy - like me - then you probably try to not do everything by hand. Keeping track of all your dependencies, processing sets of files in different ways, reloading the browser after every file save - there is a lot more to developing than just coding.

So you may be using bower, and maybe npm depending on how you serve your app. There is a chance that you may be using grunt, gulp, or brunch. Or bash, which also is cool. In fact, you may have started your latest project with some Yeoman generator!

This leads to the question: do you understand the whole process of what your infrastructure really does? Do you need what you have, especially if you just spent hours trying to fix your connect webserver livereload functionality?

Take a second to assess what you need. All those tools are only here to aid you, there is no other reward for using them. The more experienced developers I talk to tend to simplify things.

Common Mistake #16: Not Running The Unit Tests In TDD Mode

Tests will not make your code free of AngularJS error messages. What they will do is assure that your team doesn't run into regression issues all the time.

I am writing specifically about unit tests here, not because I feel they are more important than e2e tests, but because they execute much faster. I must admit that the process I am about to describe is a very pleasurable one.

Test Driven Development as an implementation for eg gulp-karma runner, basically runs all your unit tests on every file save. My favorite way to write tests is, I just write empty assurances first:

 describe('some module', function () { it('should call the name-it service…', function () { // leave this empty for now }); ... });

After that, I write or refactor the actual code, then I come back to the tests and fill in the assurances with actual test code.

Having a TDD task running in a terminal speeds up the process by about 100%. Unit tests execute in a matter of a few seconds, even if you have a lot of them. Just save the test file and the runner will pick it up, evaluate your tests, and provide feedback instantly.

With e2e tests, the process is much slower. My advice - split e2e tests up into test suites and just run one at a time. Protractor has support for them, and below is the code I use for my test tasks (I like gulp).

 'use strict'; var gulp = require('gulp'); var args = require('yargs').argv; var browserSync = require('browser-sync'); var karma = require('gulp-karma'); var protractor = require('gulp-protractor').protractor; var webdriverUpdate = require('gulp-protractor').webdriver_update; function test() { // Be sure to return the stream // NOTE: Using the fake './foobar' so as to run the files // listed in karma.conf.js INSTEAD of what was passed to // gulp.src ! return gulp.src('./foobar') .pipe(karma({ configFile: 'test/karma.conf.js', action: 'run' })) .on('error', function(err) { // Make sure failed tests cause gulp to exit non-zero // console.log(err); this.emit('end'); //instead of erroring the stream, end it }); } function tdd() { return gulp.src('./foobar') .pipe(karma({ configFile: 'test/karma.conf.js', action: 'start' })) .on('error', function(err) { // Make sure failed tests cause gulp to exit non-zero // console.log(err); // this.emit('end'); // not ending the stream here }); } function runProtractor () { var argument = args.suite || 'all'; // NOTE: Using the fake './foobar' so as to run the files // listed in protractor.conf.js, instead of what was passed to // gulp.src return gulp.src('./foobar') .pipe(protractor({ configFile: 'test/protractor.conf.js', args: ['--suite', argument] })) .on('error', function (err) { // Make sure failed tests cause gulp to exit non-zero throw err; }) .on('end', function () { // Close browser sync server browserSync.exit(); }); } gulp.task('tdd', tdd); gulp.task('test', test); gulp.task('test-e2e', ['webdriver-update'], runProtractor); gulp.task('webdriver-update', webdriverUpdate);

Common Mistake #17: Not Using The Available Tools

A - chrome breakpoints

Chrome dev tools allow you to point at a specific place in any of the files loaded into the browser, pause code execution at that point, and let you interact with all the variables available from that point. That is a lot! That functionality does not require you to add any code at all, everything happens in the dev tools.

Not only you get access to all the variables, you also see the call stack, print stack traces, and more. You can even configure it to work with minified files. 여기에서 읽어보세요.

There are other ways you can get similar run-time access, eg by adding console.log() calls. But breakpoints are more sophisticated.

AngularJS also allows you to access scope through DOM elements (as long as debugInfo is enabled), and inject available services through the console. Consider the following in the console:

 $(document.body).scope().$root

or point at an element in the inspector, and then:

 $($0).scope()

Even if debugInfo is not enabled, you can do:

 angular.reloadWithDebugInfo()

And have it available after reload:

To inject and interact with a service from the console, try:

 var injector = $(document.body).injector(); var someService = injector.get('someService');

B - chrome timeline

Another great tool that comes with dev tools is the timeline. That will allow you to record and analyse your app's live performance as you are using it. The output shows, among others, memory usage, frame rate, and the dissection of the different processes that occupy the CPU: loading, scripting, rendering, and painting.

If you experience that your app's performance degrades, you will most likely be able to find the cause for that through the timeline tab. Just record your actions which led to performance issues and see what happens. Too many watchers? You will see yellow bars taking a lot of space. Memory leaks? You can see how much memory was consumed over time on a graph.

A detailed description: https://developer.chrome.com/devtools/docs/timeline

C - inspecting apps remotely on iOS and Android

If you are developing a hybrid app or a responsive web app, you can access your device's console, DOM tree, and all other tools available either through Chrome or Safari dev tools. That includes the WebView and UIWebView.

먼저 로컬 네트워크에서 액세스할 수 있도록 호스트 0.0.0.0에서 웹 서버를 시작합니다. 설정에서 웹 인스펙터를 활성화하세요. 그런 다음 일반 "localhost" 대신 컴퓨터의 IP를 사용하여 장치를 데스크톱에 연결하고 로컬 개발 페이지에 액세스합니다. 그것이 전부입니다. 이제 데스크탑 브라우저에서 장치를 사용할 수 있습니다.

다음은 Android에 대한 자세한 지침이며 iOS의 경우 비공식 가이드는 Google을 통해 쉽게 찾을 수 있습니다.

저는 최근에 browserSync에 대한 멋진 경험을 했습니다. livereload와 유사한 방식으로 작동하지만 실제로는 browserSync를 통해 동일한 페이지를 보고 있는 모든 브라우저를 동기화합니다. 여기에는 스크롤, 버튼 클릭 등과 같은 사용자 상호 작용이 포함됩니다. 데스크탑에서 iPad의 페이지를 제어하면서 iOS 앱의 로그 출력을 보고 있었습니다. 그것은 잘 작동했습니다!

일반적인 실수 #18: NG-INIT 예제에서 소스 코드를 읽지 않음

Ng-init 는 소리부터 ng-ifng-repeat 와 비슷해야 겠죠? 문서에 사용해서는 안 된다는 주석이 있는 이유가 궁금하신가요? IMHO 놀랐습니다! 지시문이 모델을 초기화할 것으로 예상합니다. 그것이 또한 하는 일이지만… 다른 방식으로 구현됩니다. 즉, 속성 값을 감시하지 않습니다. AngularJS 소스 코드를 탐색할 필요가 없습니다. 가져오겠습니다.

 var ngInitDirective = ngDirective({ priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } }; } });

예상보다 적습니까? 어색한 지시문 구문을 제외하고는 꽤 가독성이 좋지 않습니까? 여섯 번째 줄은 모든 것입니다.

ng-show와 비교하십시오.

 var ngShowDirective = ['$animate', function($animate) { return { restrict: 'A', multiElement: true, link: function(scope, element, attr) { scope.$watch(attr.ngShow, function ngShowWatchAction(value) { // we're adding a temporary, animation-specific class for ng-hide since this way // we can control when the element is actually displayed on screen without having // to have a global/greedy CSS selector that breaks when other animations are run. // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, { tempClasses: NG_HIDE_IN_PROGRESS_CLASS }); }); } }; }];

다시, 여섯 번째 줄. 거기에 $watch 가 있습니다. 이것이 이 지시문을 동적으로 만드는 것입니다. AngularJS 소스 코드에서 모든 코드의 큰 부분은 처음부터 대부분 읽을 수 있었던 코드를 설명하는 주석입니다. AngularJS에 대해 배울 수 있는 좋은 방법이라고 생각합니다.

결론

가장 일반적인 AngularJS 실수를 다루는 이 가이드는 다른 가이드보다 거의 두 배나 깁니다. 자연스럽게 그렇게 되었습니다. 고품질 JavaScript 프론트 엔드 엔지니어에 대한 수요는 매우 높습니다. AngularJS는 현재 매우 인기가 있으며 몇 년 동안 가장 인기 있는 개발 도구 중에서 안정적인 위치를 유지하고 있습니다. AngularJS 2.0이 출시되면 아마도 앞으로 몇 년 동안 지배적일 것입니다.

프론트 엔드 개발의 장점은 매우 보람 있다는 것입니다. 우리의 작업은 즉시 볼 수 있으며 사람들은 우리가 제공하는 제품과 직접 상호 작용합니다. JavaScript를 배우는 데 보낸 시간과 JavaScript 언어에 집중해야 한다고 생각하는 것은 매우 좋은 투자입니다. 인터넷의 언어입니다. 경쟁이 매우 강력합니다! 사용자 경험이라는 한 가지 초점이 있습니다. 성공하려면 모든 것을 커버해야 합니다.

이 예제에 사용된 소스 코드는 GitHub에서 다운로드할 수 있습니다. 자유롭게 다운로드하여 자신의 것으로 만드십시오.

나에게 가장 큰 영감을 준 4명의 퍼블리싱 개발자에게 크레딧을 주고 싶었습니다.

  • 벤 나델
  • 토드 모토
  • 파스칼 프레히트
  • 산딥 판다

또한 많은 훌륭한 대화와 지속적인 지원에 대해 FreeNode #angularjs 및 #javascript 채널의 모든 훌륭한 사람들에게 감사드립니다.

마지막으로 항상 기억하십시오.

 // when in doubt, comment it out! :)