18 Kesalahan Paling Umum yang Dilakukan Pengembang AngularJS

Diterbitkan: 2022-03-11

Aplikasi satu halaman menuntut pengembang front-end untuk menjadi insinyur perangkat lunak yang lebih baik. CSS dan HTML tidak lagi menjadi perhatian terbesar, bahkan tidak ada lagi hanya satu perhatian. Pengembang front-end perlu menangani XHR, logika aplikasi (model, tampilan, pengontrol), kinerja, animasi, gaya, struktur, SEO, dan integrasi dengan layanan eksternal. Hasil yang muncul dari semua kombinasi tersebut adalah User Experience (UX) yang harus selalu diutamakan.

AngularJS adalah kerangka kerja yang sangat kuat. Ini adalah repositori paling berbintang ketiga di GitHub. Tidak sulit untuk mulai menggunakan, tetapi tujuan yang dimaksudkan untuk mencapai pemahaman permintaan. Pengembang AngularJS tidak lagi dapat mengabaikan konsumsi memori, karena itu tidak akan mengatur ulang navigasi lagi. Ini adalah garda depan pengembangan web. Mari kita merangkulnya!

Kesalahan umum AngularJS

Kesalahan Umum #1: Mengakses Lingkup Melalui DOM

Ada beberapa tweak pengoptimalan yang direkomendasikan untuk produksi. Salah satunya adalah menonaktifkan info debug.

DebugInfoEnabled adalah pengaturan yang defaultnya adalah true, dan memungkinkan akses cakupan melalui node DOM. Jika Anda ingin mencobanya melalui konsol JavaScript, pilih elemen DOM dan akses cakupannya dengan:

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

Ini dapat berguna bahkan ketika tidak menggunakan jQuery dengan CSS-nya, tetapi tidak boleh digunakan di luar konsol. Alasannya adalah ketika $compileProvider.debugInfoEnabled disetel ke false, memanggil .scope() pada node DOM akan mengembalikan undefined .

Itu adalah salah satu dari sedikit pilihan yang direkomendasikan untuk produksi.

Harap dicatat bahwa Anda masih dapat mengakses ruang lingkup melalui konsol, bahkan saat produksi. Panggil angular.reloadWithDebugInfo() dari konsol dan aplikasi akan melakukannya.

Kesalahan Umum #2: Tidak Memiliki Titik Di Sana

Anda mungkin telah membaca bahwa jika Anda tidak memiliki titik di ng-model , Anda salah melakukannya. Dalam hal warisan, pernyataan itu sering benar. Cakupan memiliki model pewarisan prototipe, tipikal untuk JavaScript, dan cakupan bersarang umum untuk AngularJS. Banyak arahan membuat cakupan anak seperti ngRepeat , ngIf , dan ngController . Saat menyelesaikan model, pencarian dimulai pada cakupan saat ini dan melewati setiap cakupan induk, hingga $rootScope .

Tapi, ketika menetapkan nilai baru, apa yang terjadi tergantung pada jenis model (variabel) yang ingin kita ubah. Jika modelnya primitif, ruang lingkup anak hanya akan membuat model baru. Tetapi jika perubahannya adalah properti dari objek model, pencarian pada cakupan induk akan menemukan objek yang direferensikan dan mengubah properti sebenarnya. Model baru tidak akan disetel pada cakupan saat ini, jadi tidak ada penyembunyian yang akan terjadi:

 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>

Mengklik tombol berlabel "Set primitif" akan mengatur foo di lingkup dalam ke 2, tetapi tidak akan mengubah foo di lingkup luar.

Mengklik tombol berlabel "Ubah objek" akan mengubah properti bilah dari cakupan induk. Karena tidak ada variabel pada lingkup bagian dalam, tidak ada bayangan yang akan terjadi, dan nilai yang terlihat untuk bilah akan menjadi 3 di kedua lingkup.

Cara lain untuk melakukannya adalah dengan memanfaatkan fakta bahwa cakupan induk dan Cakupan root direferensikan dari setiap cakupan. Objek $parent dan $root masing-masing dapat digunakan untuk mengakses parent scope dan $rootScope , langsung dari view. Ini mungkin cara yang ampuh, tetapi saya bukan penggemarnya karena masalah dengan menargetkan cakupan tertentu di aliran. Ada cara lain untuk mengatur dan mengakses properti khusus untuk lingkup - menggunakan sintaks controllerAs .

Kesalahan Umum #3: Tidak Menggunakan controllerAs Syntax

Cara alternatif dan paling efisien untuk menetapkan model untuk menggunakan objek pengontrol alih-alih $scope yang disuntikkan. Alih-alih menyuntikkan cakupan, kita dapat mendefinisikan model seperti ini:

 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>

Ini jauh lebih membingungkan. Terutama ketika ada banyak cakupan bersarang, seperti halnya dengan status bersarang.

Ada lebih banyak sintaks controllerAs.

Kesalahan Umum #4: Tidak Sepenuhnya Memanfaatkan controllerAs Syntax

Ada beberapa peringatan tentang bagaimana objek pengontrol diekspos. Ini pada dasarnya adalah objek yang diatur pada lingkup pengontrol, seperti model normal.

Jika Anda perlu menonton properti dari objek pengontrol, Anda dapat menonton suatu fungsi tetapi Anda tidak diharuskan. Berikut ini contohnya:

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

Lebih mudah untuk hanya melakukan:

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

Artinya juga di rantai lingkup, Anda dapat mengakses MC dari pengontrol anak:

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

Namun, untuk dapat melakukannya, Anda harus konsisten dengan akronim yang Anda gunakan untuk controllerAs. Setidaknya ada tiga cara untuk mengaturnya. Anda sudah melihat yang pertama:

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

Namun, jika Anda menggunakan ui-router , menentukan pengontrol seperti itu rentan terhadap kesalahan. Untuk status, pengontrol harus ditentukan dalam konfigurasi status:

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

Ada cara lain untuk memberi anotasi:

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

Anda dapat melakukan hal yang sama dalam arahan:

 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);

Cara lain untuk membubuhi keterangan juga valid, meskipun kurang ringkas:

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

Kesalahan Umum #5: Tidak Menggunakan Tampilan Bernama Dengan UI-ROUTER Untuk Daya”

Solusi perutean de facto untuk AngularJS, sampai sekarang, adalah ui-router . Dihapus dari inti beberapa waktu lalu, modul ngRoute, terlalu mendasar untuk perutean yang lebih canggih.

Ada NgRouter baru dalam prosesnya, tetapi penulis masih menganggapnya terlalu dini untuk produksi. Saat saya menulis ini, Angular yang stabil adalah 1.3.15, dan ui-router berbatu.

Alasan utama:

  • bersarang negara yang luar biasa
  • abstraksi rute
  • parameter opsional dan yang diperlukan

Di sini saya akan membahas state nesting untuk menghindari kesalahan AngularJS.

Anggap ini sebagai kasus penggunaan yang kompleks namun standar. Ada sebuah aplikasi, yang memiliki tampilan beranda dan tampilan produk. Tampilan produk memiliki tiga bagian terpisah: intro, widget, dan konten. Kami ingin widget tetap ada dan tidak dimuat ulang saat beralih antar status. Tetapi konten harus dimuat ulang.

Perhatikan struktur halaman indeks produk HTML berikut ini:

 <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>

Ini adalah sesuatu yang bisa kita dapatkan dari pembuat kode HTML, dan sekarang perlu memisahkannya menjadi file dan status. Saya biasanya mengikuti konvensi bahwa ada status MAIN abstrak, yang menyimpan data global jika diperlukan. Gunakan itu sebagai ganti $rootScope. Status Utama juga akan menyimpan HTML statis yang diperlukan di setiap halaman. Saya menjaga index.html tetap bersih.

 <!— 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>

Kemudian mari kita lihat halaman indeks produk:

 <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>

Seperti yang Anda lihat, halaman indeks produk memiliki tiga tampilan bernama. Satu untuk intro, satu untuk widget, dan satu untuk produk. Kami memenuhi spesifikasi! Jadi sekarang mari kita siapkan perutean:

 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);

Itu akan menjadi pendekatan pertama. Sekarang, apa yang terjadi ketika beralih antara main.product.index dan main.product.details ? Konten dan widget dimuat ulang, tetapi kami hanya ingin memuat ulang konten. Ini bermasalah, dan pengembang benar-benar membuat router yang hanya mendukung fungsionalitas itu. Salah satu nama untuk ini adalah tampilan lengket . Untungnya, ui-router mendukungnya dengan penargetan tampilan bernama absolut .

 // 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' } } });

Dengan memindahkan definisi status ke tampilan induk, yang juga abstrak, kita dapat mempertahankan tampilan anak agar tidak dimuat ulang saat mengganti url yang biasanya memengaruhi saudara kandung anak tersebut. Tentu saja, widget bisa berupa arahan sederhana. Tapi intinya adalah, itu juga bisa menjadi keadaan bersarang kompleks lainnya.

Ada cara lain untuk melakukan ini melalui penggunaan $urlRouterProvider.deferIntercept() , tetapi saya pikir menggunakan konfigurasi status sebenarnya lebih baik. Jika Anda tertarik untuk mencegat rute, saya menulis tutorial kecil di StackOverflow.

Kesalahan Umum #6: Mendeklarasikan Segalanya di Dunia Sudut Menggunakan Fungsi Anonim

Kesalahan ini kaliber lebih ringan, dan lebih merupakan masalah gaya daripada menghindari pesan kesalahan AngularJS. Anda mungkin telah memperhatikan bahwa saya jarang meneruskan fungsi anonim ke deklarasi internal sudut. Saya biasanya hanya mendefinisikan fungsi terlebih dahulu dan kemudian meneruskannya.

Ini lebih dari sekedar fungsi. Saya mendapatkan pendekatan ini dari membaca panduan gaya, terutama Motto Airbnb dan Todd. Saya percaya ada beberapa keuntungan dan hampir tidak ada kekurangannya.

Pertama-tama, Anda dapat memanipulasi dan mengubah fungsi dan objek dengan lebih mudah jika ditugaskan ke variabel. Kedua, kode lebih bersih dan dapat dengan mudah dipecah menjadi file. Itu berarti rawatan. Jika Anda tidak ingin mengotori namespace global, bungkus setiap file dalam IIFE. Alasan ketiga adalah testability. Pertimbangkan contoh ini:

 '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);

Jadi sekarang kita bisa mengejek publicMethod1 , tapi kenapa kita harus melakukannya karena sudah diekspos? Bukankah lebih mudah memata-matai metode yang ada? Namun, metode ini sebenarnya adalah fungsi lain - pembungkus tipis. Lihatlah pendekatan ini:

 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; }

Ini bukan hanya tentang gaya, karena pada dasarnya kode lebih dapat digunakan kembali dan idiomatis. Pengembang mendapatkan kekuatan yang lebih ekspresif. Memisahkan semua kode menjadi blok mandiri membuatnya lebih mudah.

Kesalahan Umum #7: Melakukan Pemrosesan Berat Di Angular AKA Menggunakan Pekerja

Dalam beberapa skenario, mungkin diperlukan untuk memproses sejumlah besar objek kompleks dengan melewatkannya melalui serangkaian filter, dekorator, dan akhirnya algoritma pengurutan. Salah satu kasus penggunaan adalah ketika aplikasi harus bekerja secara offline atau di mana kinerja menampilkan data adalah kuncinya. Dan karena JavaScript adalah single-threaded, relatif mudah untuk membekukan browser.

Juga mudah untuk menghindarinya dengan pekerja web. Sepertinya tidak ada perpustakaan populer yang menangani itu khusus untuk AngularJS. Ini mungkin yang terbaik, karena implementasinya mudah.

Pertama, mari kita siapkan layanannya:

 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);

Sekarang, pekerja:

 '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);

Sekarang, masukkan layanan seperti biasa dan perlakukan scoringService.scoreItems() seperti yang Anda lakukan pada metode layanan apa pun yang mengembalikan janji. Pemrosesan berat akan dilakukan pada utas terpisah, dan tidak ada kerusakan yang akan dilakukan pada UX.

Apa yang harus diwaspadai:

  • sepertinya tidak ada aturan umum untuk berapa banyak pekerja yang akan ditelurkan. Beberapa pengembang mengklaim bahwa 8 adalah angka yang bagus, tetapi gunakan kalkulator online dan sesuaikan dengan diri Anda sendiri
  • periksa kompatibilitas dengan browser lama
  • Saya mengalami masalah saat meneruskan angka 0 dari layanan ke pekerja. Saya menerapkan .toString() pada properti yang diteruskan, dan itu berfungsi dengan benar.

Kesalahan Umum #8: Penyelesaian Penggunaan yang Berlebihan dan Kesalahpahaman

Menyelesaikan menambah waktu ekstra untuk memuat tampilan. Saya percaya bahwa kinerja tinggi dari aplikasi front-end adalah tujuan utama kami. Seharusnya tidak menjadi masalah untuk merender beberapa bagian tampilan saat aplikasi menunggu data dari API.

Pertimbangkan pengaturan ini:

 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) }); }

Output konsol akan menjadi:

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

Yang pada dasarnya berarti bahwa:

  • Penyelesaian dieksekusi secara tidak sinkron
  • Kami tidak dapat mengandalkan perintah eksekusi (atau setidaknya perlu sedikit melenturkan)
  • Semua status diblokir sampai semua penyelesaian melakukan tugasnya, bahkan jika itu tidak abstrak.

Ini berarti bahwa sebelum pengguna melihat output apa pun, dia harus menunggu semua dependensi. Kita perlu memiliki data itu, tentu, oke. Jika benar-benar perlu untuk memilikinya sebelum tampilan, letakkan di blok .run() . Jika tidak, cukup lakukan panggilan ke layanan dari pengontrol dan tangani status setengah terisi dengan anggun. Melihat pekerjaan yang sedang berlangsung - dan pengontrol sudah dijalankan, jadi itu sebenarnya adalah kemajuan - lebih baik daripada menghentikan aplikasi.

Kesalahan Umum #9: Tidak Mengoptimalkan Aplikasi - Tiga Contoh

a) Menyebabkan terlalu banyak loop intisari, seperti memasang penggeser ke model

Ini adalah masalah umum yang dapat menyebabkan kesalahan AngularJS, tetapi saya akan membahasnya pada contoh slider. Saya menggunakan pustaka penggeser ini, penggeser rentang sudut, karena saya membutuhkan fungsionalitas yang diperluas. Arahan itu memiliki sintaks ini dalam versi minimal:

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

Perhatikan kode berikut di controller:

 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'); } } });

Jadi itu bekerja lambat. Solusi kasualnya adalah dengan menetapkan batas waktu pada input. Tapi itu tidak selalu berguna, dan terkadang kita tidak benar-benar ingin menunda perubahan model yang sebenarnya dalam semua kasus.

Jadi kami akan menambahkan model sementara yang terikat untuk mengubah model kerja pada waktu habis:

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

dan di pengontrol:

 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) Tidak menggunakan $applyAsync

AngularJS tidak memiliki mekanisme polling untuk memanggil $digest() . Itu hanya dieksekusi karena kita menggunakan arahan (misalnya ng-click , input ), services ( $timeout , $http ), dan metode ( $watch ) yang mengevaluasi kode kita dan memanggil intisari setelahnya.

Apa .$applyAsync() adalah menunda resolusi ekspresi hingga siklus $digest() berikutnya, yang dipicu setelah batas waktu 0, yang sebenarnya ~10 md.

Ada dua cara untuk menggunakan applyAsync sekarang. Cara otomatis untuk permintaan $http , dan cara manual untuk sisanya.

Untuk membuat semua permintaan http yang kembali dalam waktu yang hampir bersamaan diselesaikan dalam satu intisari, lakukan:

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

Cara manual menunjukkan cara kerjanya. Pertimbangkan beberapa fungsi yang berjalan pada panggilan balik ke pendengar acara Vanilla JS atau jQuery .click() , atau beberapa perpustakaan eksternal lainnya. Setelah dijalankan dan mengubah model, jika Anda belum membungkusnya dalam $apply() Anda perlu memanggil $scope.$root.$digest() ( $rootScope.$digest() ), atau setidaknya $scope.$digest() . Jika tidak, Anda tidak akan melihat perubahan.

Jika Anda melakukannya beberapa kali dalam satu aliran, itu mungkin mulai berjalan lambat. Pertimbangkan untuk memanggil $scope.$applyAsync() pada ekspresi sebagai gantinya. Ini akan mengatur panggilan hanya satu siklus intisari untuk semuanya.

c) Melakukan pemrosesan gambar yang berat

Jika Anda mengalami kinerja yang buruk, Anda dapat menyelidiki alasannya dengan menggunakan Timeline dari Alat Pengembang Chrome. Saya akan menulis lebih banyak tentang alat ini dalam kesalahan #17. Jika grafik garis waktu Anda didominasi dengan warna hijau setelah perekaman, masalah kinerja Anda mungkin terkait dengan pemrosesan gambar. Ini tidak sepenuhnya terkait dengan AngularJS, tetapi dapat terjadi di atas masalah kinerja AngularJS (yang sebagian besar akan berwarna kuning pada grafik). Sebagai insinyur front-end, kita perlu memikirkan proyek akhir yang lengkap.

Luangkan waktu sejenak untuk menilai:

  • Apakah Anda menggunakan paralaks?
  • Apakah Anda memiliki beberapa lapisan konten yang saling tumpang tindih?
  • Apakah Anda memindahkan gambar Anda?
  • Apakah Anda menskalakan gambar (misalnya dengan ukuran latar belakang)?
  • Apakah Anda mengubah ukuran gambar dalam loop, dan mungkin menyebabkan loop intisari saat mengubah ukuran?

Jika Anda menjawab "ya" untuk setidaknya tiga hal di atas, pertimbangkan untuk menguranginya. Mungkin Anda dapat menyajikan berbagai ukuran gambar dan tidak mengubah ukuran sama sekali. Mungkin Anda bisa menambahkan "transform: translateZ(0)" force hack pemrosesan GPU. Atau gunakan requestAnimationFrame untuk penangan.

Kesalahan Umum #10: jQuerying - Pohon DOM Terpisah

Sering kali Anda mungkin mendengar bahwa tidak disarankan untuk menggunakan jQuery dengan AngularJS, dan itu harus dihindari. Sangat penting untuk memahami alasan di balik pernyataan ini. Setidaknya ada tiga alasan, sejauh yang saya bisa lihat, tetapi tidak satupun dari mereka adalah pemblokir yang sebenarnya.

Alasan 1: Saat Anda menjalankan kode jQuery, Anda perlu memanggil $digest() sendiri. Untuk banyak kasus, ada solusi AngularJS yang disesuaikan untuk AngularJS dan dapat digunakan lebih baik di dalam Angular daripada jQuery (misalnya ng-klik atau sistem acara).

Alasan 2: Metode pemikiran tentang membangun aplikasi. Jika Anda telah menambahkan JavaScript ke situs web, yang dimuat ulang saat bernavigasi, Anda tidak perlu terlalu khawatir tentang konsumsi memori. Dengan aplikasi satu halaman, Anda harus khawatir. Jika Anda tidak membersihkannya, pengguna yang menghabiskan lebih dari beberapa menit di aplikasi Anda mungkin mengalami peningkatan masalah kinerja.

Alasan 3: Membersihkan sebenarnya bukan hal termudah untuk dilakukan dan dianalisis. Tidak ada cara untuk memanggil pengumpul sampah dari skrip (di browser). Anda mungkin berakhir dengan pohon DOM yang terlepas. Saya membuat contoh (jQuery dimuat di 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);

Ini adalah arahan sederhana yang menampilkan beberapa teks. Ada tombol di bawahnya, yang hanya akan menghancurkan arahan secara manual.

Jadi ketika direktif dihapus, tetap ada referensi ke pohon DOM di scope.toBeDetached. Di alat dev chrome, jika Anda mengakses tab "profil" dan kemudian "mengambil heap snapshot", Anda akan melihat di output:

Anda dapat hidup dengan beberapa, tetapi buruk jika Anda memiliki banyak. Apalagi jika karena alasan tertentu, seperti pada contoh, Anda menyimpannya di ruang lingkup. Seluruh DOM akan dievaluasi pada setiap intisari. Pohon DOM terpisah yang bermasalah adalah pohon dengan 4 simpul. Jadi bagaimana ini bisa diselesaikan?

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

Pohon DOM yang terpisah dengan 4 entri dihapus!

Dalam contoh ini, direktif menggunakan cakupan yang sama, dan menyimpan elemen DOM pada cakupan. Lebih mudah bagi saya untuk menunjukkannya seperti itu. Itu tidak selalu menjadi seburuk itu, karena Anda bisa menyimpannya dalam sebuah variabel. Namun, itu masih akan memakan memori jika penutupan apa pun yang telah mereferensikan variabel itu atau lainnya dari lingkup fungsi yang sama tetap ada.

Kesalahan Umum #11: Terlalu Banyak Menggunakan Lingkup Terisolasi

Kapan pun Anda membutuhkan arahan yang Anda tahu akan digunakan di satu tempat, atau yang tidak Anda harapkan akan berkonflik dengan lingkungan apa pun yang digunakan, tidak perlu menggunakan ruang lingkup yang terisolasi. Akhir-akhir ini, ada tren untuk membuat komponen yang dapat digunakan kembali, tetapi tahukah Anda bahwa arahan sudut inti tidak menggunakan ruang lingkup yang terisolasi sama sekali?

Ada dua alasan utama: Anda tidak dapat menerapkan dua arahan lingkup yang terisolasi ke suatu elemen, dan Anda mungkin mengalami masalah dengan pemrosesan nesting/warisan/event. Terutama mengenai transklusi - efeknya mungkin tidak seperti yang Anda harapkan.

Jadi ini akan gagal:

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

Dan bahkan jika Anda hanya menggunakan satu direktif, Anda akan melihat bahwa baik model lingkup yang terisolasi maupun peristiwa yang disiarkan di IsolatedScopeDirective tidak akan tersedia untuk AnotherController. Sayangnya, Anda dapat melenturkan dan menggunakan sihir transklusi untuk membuatnya bekerja - tetapi untuk sebagian besar kasus penggunaan, tidak perlu mengisolasi.

 <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>

Jadi, dua pertanyaan sekarang:

  1. Bagaimana Anda bisa memproses model cakupan induk dalam arahan lingkup yang sama?
  2. Bagaimana Anda bisa memberi contoh nilai model baru?

Ada dua cara, di keduanya Anda memberikan nilai ke atribut. Pertimbangkan MainController ini:

 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]; }

Itu mengontrol tampilan ini:

 <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>

Perhatikan bahwa "atribut arloji" tidak diinterpolasi. Semuanya berfungsi, karena sihir JS. Berikut adalah definisi direktif:

 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' }; }

Perhatikan bahwa attrs.watchAttribute diteruskan ke scope.$watch() tanpa tanda kutip! Itu berarti apa yang sebenarnya diteruskan ke $watch adalah string MC.foo ! Namun, itu berhasil, karena string apa pun yang diteruskan ke $watch() akan dievaluasi berdasarkan cakupannya dan MC.foo tersedia pada cakupannya. Itu juga cara paling umum untuk melihat atribut dalam arahan inti AngularJS.

Lihat kode di github untuk template, dan lihat ke $parse dan $eval untuk lebih mengagumkan lagi.

Kesalahan Umum #12: Tidak Membersihkan Diri Sendiri - Pengamat, Interval, Batas Waktu, dan Variabel

AngularJS melakukan beberapa pekerjaan atas nama Anda, tetapi tidak semua. Berikut ini perlu dibersihkan secara manual:

  • Setiap pengamat yang tidak terikat pada cakupan saat ini (misalnya terikat pada $rootScope)
  • Interval
  • Waktu habis
  • Variabel yang mereferensikan DOM dalam arahan
  • Plugin jQuery yang cerdik, misalnya yang tidak memiliki penangan yang bereaksi terhadap acara $destroy JavaScript

Jika Anda tidak melakukannya secara manual, Anda akan menemukan perilaku tak terduga dan kebocoran memori. Lebih buruk lagi - ini tidak akan langsung terlihat, tetapi pada akhirnya akan merayap. Hukum Murphy.

Hebatnya, AngularJS menyediakan cara praktis untuk menangani semua itu:

 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. }); };

Perhatikan acara jQuery $destroy . Ini disebut seperti AngularJS, tetapi ditangani secara terpisah. Lingkup $watchers tidak akan bereaksi terhadap acara jQuery.

Kesalahan Umum #13: Menjaga Terlalu Banyak Pengamat

Ini seharusnya cukup sederhana sekarang. Ada satu hal yang perlu dipahami di sini: $digest() . Untuk setiap pengikatan {{ model }} , AngularJS membuat pengamat. Pada setiap fase digest, setiap pengikatan tersebut dievaluasi dan dibandingkan dengan nilai sebelumnya. Itu disebut pemeriksaan kotor, dan itulah yang dilakukan $digest. Jika nilainya berubah sejak pemeriksaan terakhir, callback pengamat diaktifkan. 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. Itu banyak! 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. Read about it here.

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.

Pertama, mulai server web Anda di host 0.0.0.0 sehingga dapat diakses dari jaringan lokal Anda. Aktifkan inspektur web di pengaturan. Kemudian sambungkan perangkat Anda ke desktop dan akses halaman pengembangan lokal Anda, menggunakan ip mesin Anda alih-alih "localhost" biasa. Itu saja yang diperlukan, perangkat Anda sekarang harus tersedia untuk Anda dari browser desktop Anda.

Berikut adalah petunjuk rinci untuk Android Dan untuk iOS, panduan tidak resmi dapat ditemukan dengan mudah melalui google.

Saya baru-baru ini memiliki beberapa pengalaman keren dengan browserSync. Ini bekerja dengan cara yang mirip dengan livereload, tetapi sebenarnya juga menyinkronkan semua browser yang melihat halaman yang sama melalui browserSync. Itu termasuk interaksi pengguna seperti menggulir, mengklik tombol, dll. Saya melihat output log aplikasi iOS sambil mengontrol halaman di iPad dari desktop saya. Itu bekerja dengan baik!

Kesalahan Umum #18: Tidak Membaca Kode Sumber Pada Contoh NG-INIT

Ng-init , dari bunyinya pasti mirip ng-if dan ng-repeat kan? Pernahkah Anda bertanya-tanya mengapa ada komentar di dokumen yang tidak boleh digunakan? IMHO itu mengejutkan! Saya mengharapkan arahan untuk menginisialisasi model. Itu juga yang dilakukannya, tapi… itu diimplementasikan dengan cara yang berbeda, yaitu, tidak melihat nilai atribut. Anda tidak perlu menelusuri kode sumber AngularJS - izinkan saya membawanya kepada Anda:

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

Kurang dari yang Anda harapkan? Cukup mudah dibaca, selain sintaks direktif yang canggung, bukan? Baris keenam adalah tentang semuanya.

Bandingkan dengan 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 }); }); } }; }];

Sekali lagi, baris keenam. Ada $watch di sana, itulah yang membuat direktif ini dinamis. Dalam kode sumber AngularJS, sebagian besar dari semua kode adalah komentar yang menjelaskan kode yang sebagian besar dapat dibaca dari awal. Saya percaya ini adalah cara yang bagus untuk belajar tentang AngularJS.

Kesimpulan

Panduan ini mencakup kesalahan AngularJS yang paling umum hampir dua kali lebih panjang dari panduan lainnya. Ternyata seperti itu secara alami. Permintaan untuk teknisi front end JavaScript berkualitas tinggi sangat tinggi. AngularJS sangat populer saat ini , dan telah memegang posisi stabil di antara alat pengembangan paling populer selama beberapa tahun. Dengan AngularJS 2.0 di jalan, itu mungkin akan mendominasi selama bertahun-tahun yang akan datang.

Apa yang hebat tentang pengembangan front-end adalah bahwa hal itu sangat bermanfaat. Pekerjaan kami terlihat langsung, dan orang-orang berinteraksi langsung dengan produk yang kami berikan. Waktu yang dihabiskan untuk mempelajari JavaScript, dan saya yakin kita harus fokus pada bahasa JavaScript, adalah investasi yang sangat bagus. Ini adalah bahasa Internet. Persaingannya sangat kuat! Ada satu fokus bagi kami - pengalaman pengguna. Untuk menjadi sukses, kita perlu menutupi semuanya.

Kode sumber yang digunakan dalam contoh ini dapat diunduh dari GitHub. Jangan ragu untuk mengunduhnya dan membuatnya menjadi milik Anda.

Saya ingin memberikan penghargaan kepada empat pengembang penerbitan yang paling menginspirasi saya:

  • Ben Nadel
  • Moto Todd
  • Pascal Precht
  • Sandeep Panda

Saya juga ingin berterima kasih kepada semua orang hebat di saluran FreeNode #angularjs dan #javascript untuk banyak percakapan luar biasa, dan dukungan berkelanjutan.

Dan akhirnya, selalu ingat:

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