AngularJS 開發人員最常犯的 18 個錯誤
已發表: 2022-03-11單頁應用程序要求前端開發人員成為更好的軟件工程師。 CSS 和 HTML 不再是最大的關注點,事實上,不再只是一個關注點。 前端開發人員需要處理 XHR、應用程序邏輯(模型、視圖、控制器)、性能、動畫、樣式、結構、搜索引擎優化以及與外部服務的集成。 從所有這些組合中產生的結果是用戶體驗(UX),它應該始終被優先考慮。
AngularJS 是一個非常強大的框架。 它是 GitHub 上排名第三的存儲庫。 開始使用並不難,但它旨在完成需求理解的目標。 AngularJS 開發人員不能再忽略內存消耗,因為它不會再在導航時重置。 這是Web開發的先鋒。 讓我們擁抱它!
常見錯誤 #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 中沒有點,那麼您做錯了。 當涉及到繼承時,這種說法通常是正確的。 Scopes 有一個原型繼承模型,這是 JavaScript 的典型模型,嵌套的 scopes 是 AngularJS 常見的。 許多指令創建子作用域,例如ngRepeat
、 ngIf
和ngController
。 解析模型時,查找從當前範圍開始,並遍歷每個父範圍,一直到$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>
單擊標有“設置原語”的按鈕會將內部範圍內的 foo 設置為 2,但不會更改外部範圍內的 foo。
單擊標有“更改對象”的按鈕將從父範圍更改 bar 屬性。 由於內部範圍內沒有變量,因此不會發生陰影,並且 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.index
和main.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 世界中的所有內容
這個錯誤的口徑較輕,更多的是風格問題,而不是避免 AngularJS 錯誤消息。 您之前可能已經註意到,我很少將匿名函數傳遞給 angular internal 的聲明。 我通常只是先定義一個函數,然後將其傳入。
這不僅僅涉及功能。 我從閱讀風格指南中得到了這種方法,尤其是 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 中使用工人進行繁重的處理
在某些情況下,可能需要通過一組過濾器、裝飾器和最後的排序算法來處理大量複雜對象。 一個用例是應用程序應該離線工作或顯示數據的性能是關鍵的地方。 而且由於 JavaScript 是單線程的,因此凍結瀏覽器相對容易。
使用網絡工作者也很容易避免它。 似乎沒有任何流行的庫專門為 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()
。 繁重的處理將在單獨的線程上進行,不會對用戶體驗造成傷害。
要注意什麼:
- 似乎沒有關於產生多少工人的一般規則。 一些開發人員聲稱 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()
循環,該循環在 0 超時後觸發,實際上是 ~10 毫秒。
現在有兩種使用applyAsync
的方法。 $http
請求的自動方式,其餘的手動方式。
要使幾乎同時返回的所有 http 請求在一個摘要中解析,請執行以下操作:
mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });
手動方式顯示了它的實際工作方式。 考慮一些在對 vanilla JS 事件偵聽器或 jQuery .click()
或其他一些外部庫的回調上運行的函數。 在它執行並更改模型之後,如果您還沒有將其包裝在$apply()
中,則需要調用$scope.$root.$digest()
( $rootScope.$digest()
),或者至少調用 $ $scope.$digest()
。 否則,您將看不到任何變化。
如果您在一個流程中多次執行此操作,它可能會開始運行緩慢。 考慮改為在表達式上調用$scope.$applyAsync()
。 它將為所有這些設置只調用一個摘要循環。
c) 對圖像進行大量處理
如果您遇到性能不佳的問題,您可以使用 Chrome 開發者工具中的時間線來調查原因。 我將在錯誤 #17 中寫更多關於這個工具的信息。 如果您的時間線圖在錄製後以綠色為主,則您的性能問題可能與圖像處理有關。 這與 AngularJS 並不嚴格相關,但可能發生在 AngularJS 性能問題之上(在圖表上大部分為黃色)。 作為前端工程師,我們需要考慮完整的端項目。
花點時間評估一下:
- 你使用視差嗎?
- 您是否有多個相互重疊的內容層?
- 你會移動你的圖像嗎?
- 你是否縮放圖像(例如背景尺寸)?
- 您是否在循環中調整圖像大小,並可能在調整大小時導致摘要循環?
如果您對上述至少三個回答“是”,請考慮放寬它。 也許您可以提供各種圖像大小而根本不調整大小。 也許您可以添加“transform: translateZ(0)”強制 GPU 處理技巧。 或者使用 requestAnimationFrame 作為處理程序。
常見錯誤 #10:jQuerying It - 分離的 DOM 樹
很多時候您可能聽說不建議將 jQuery 與 AngularJS 一起使用,並且應該避免使用。 必須了解這些陳述背後的原因。 據我所知,至少有三個原因,但沒有一個是真正的阻礙因素。
原因一:執行jQuery代碼時,需要自己調用$digest()
。 在許多情況下,有一個為 AngularJS 量身定制的 AngularJS 解決方案,並且可以在 Angular 中比 jQuery 更好地使用(例如 ng-click 或事件系統)。
原因2:構建app的思路。 如果您一直在向網站添加 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 樹的引用。 在 chrome 開發工具中,如果您訪問選項卡“配置文件”然後“獲取堆快照”,您將在輸出中看到:
你可以和幾個人住在一起,但如果你有一噸,那就不好了。 特別是如果出於某種原因,例如在示例中,您將其存儲在範圍內。 將在每個摘要上評估整個 DOM。 有問題的分離 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 指令根本不使用隔離範圍嗎?
有兩個主要原因:您不能將兩個獨立的作用域指令應用於一個元素,並且您可能會遇到嵌套/繼承/事件處理的問題。 特別是關於嵌入 - 效果可能不是您所期望的。
所以這會失敗:
<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>
所以,現在有兩個問題:
- 如何在相同範圍指令中處理父範圍模型?
- 如何實例化新的模型值?
有兩種方法,在這兩種方法中,您都將值傳遞給屬性。 考慮這個 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 的變量
- 狡猾的 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 創建一個觀察者。 在每個摘要階段,都會評估每個此類綁定並與之前的值進行比較。 這被稱為臟檢查,這就是 $digest 所做的。 如果自上次檢查後值發生變化,則觸發觀察者回調。 如果該觀察者回調修改了模型($scope 變量),則在拋出異常時會觸發一個新的 $digest 循環(最多 10 個)。
瀏覽器即使有數千個綁定也不會有問題,除非表達式很複雜。 “可以擁有多少觀察者”的常見答案是 2000。
那麼,我們如何限制觀察者的數量呢? 當我們不期望它們發生變化時,不關注示波器模型。 從 AngularJS 1.3 開始,這相當容易,因為一次性綁定現在是核心。
<li ng-repeat="item in ::vastArray">{{ ::item.velocity }}</li>
在評估了一次vastArray
和item.velocity
之後,它們將永遠不會再改變。 您仍然可以將過濾器應用於數組,它們會正常工作。 只是數組本身不會被評估。 在許多情況下,這是一場胜利。
常見錯誤 #14:誤解文摘
這個 AngularJS 錯誤已經部分包含在錯誤 9.b 和 13 中。這是一個更徹底的解釋。 AngularJS 通過對觀察者的回調函數來更新 DOM。 每個綁定,即指令{{ someModel }}
設置觀察者,但也為許多其他指令設置觀察者,如ng-if
和ng-repeat
。 看看源代碼就知道了,可讀性很強。 觀察者也可以手動設置,你自己可能至少做過幾次。
$watch()
ers 綁定到範圍。 $Watchers
可以接受字符串,這些字符串是根據$watch()
綁定到的範圍進行評估的。 他們還可以評估功能。 他們也接受回調。 因此,當調用$rootScope.$digest()
時,所有註冊的模型(即$scope
變量)都會被評估並與它們之前的值進行比較。 如果值不匹配,則執行$watch()
的回調。
重要的是要了解,即使模型的值已更改,回調也不會觸發,直到下一個摘要階段。 它被稱為“階段”是有原因的——它可以由幾個摘要周期組成。 如果只有觀察者更改了範圍模型,則執行另一個摘要循環。
但是$digest()
沒有被輪詢。 它是從核心指令、服務、方法等調用的。如果您從不調用.$apply
、 .$applyAsync
、 .$evalAsync
或任何其他最終調用$digest()
的自定義函數更改模型,則綁定不會更新。
順便說一句, $digest()
的源代碼實際上相當複雜。 儘管如此,它還是值得一讀的,因為有趣的警告彌補了這一點。
常見錯誤 #15:不依賴自動化,或過度依賴自動化
如果你跟隨前端開發的趨勢並且有點懶惰——像我一樣——那麼你可能會盡量不要手工做所有事情。 跟踪所有依賴項,以不同方式處理文件集,在每個文件保存後重新加載瀏覽器 - 開發不僅僅是編碼。
因此,您可能正在使用 bower,也可能使用 npm,具體取決於您為應用程序提供服務的方式。 您可能正在使用 grunt、gulp 或 brunch。 或者 bash,這也很酷。 事實上,您可能已經使用一些 Yeoman 生成器開始了您的最新項目!
這就引出了一個問題:你了解你的基礎設施真正做什麼的整個過程嗎? 你需要你擁有的東西嗎,特別是如果你只是花了幾個小時試圖修復你的連接網絡服務器 livereload 功能?
花點時間評估一下你需要什麼。 所有這些工具只是為了幫助你,使用它們沒有其他回報。 與我交談的更有經驗的開發人員傾向於簡化事情。
常見錯誤 #16:未在 TDD 模式下運行單元測試
測試不會使您的代碼沒有 AngularJS 錯誤消息。 他們要做的是確保您的團隊不會一直遇到回歸問題。
我在這裡專門寫單元測試,不是因為我覺得它們比 e2e 測試更重要,而是因為它們執行得更快。 我必須承認,我將要描述的過程是一個非常愉快的過程。
測試驅動開發作為 gulp-karma runner 的實現,基本上在每個文件保存時運行所有單元測試。 我最喜歡的編寫測試的方式是,我只是先寫空保證:
describe('some module', function () { it('should call the name-it service…', function () { // leave this empty for now }); ... });
之後,我編寫或重構實際代碼,然後我回到測試並用實際測試代碼填寫保證。
在終端中運行 TDD 任務可將流程加速約 100%。 即使您有很多單元測試,它們也會在幾秒鐘內執行。 只需保存測試文件,跑步者就會拿起它,評估您的測試,並立即提供反饋。
使用 e2e 測試,這個過程要慢得多。 我的建議 - 將 e2e 測試拆分為測試套件,一次只運行一個。 Protractor 支持它們,下面是我用於測試任務的代碼(我喜歡 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);
常見錯誤 #17:不使用可用的工具
A - 鉻斷點
Chrome 開發工具允許您指向加載到瀏覽器中的任何文件中的特定位置,在該點暫停代碼執行,並讓您與從該點開始的所有可用變量進行交互。 那是很多! 該功能根本不需要您添加任何代碼,一切都發生在開發工具中。
您不僅可以訪問所有變量,還可以查看調用堆棧、打印堆棧跟踪等。 您甚至可以將其配置為使用縮小文件。 在這裡閱讀它。
還有其他方法可以獲得類似的運行時訪問,例如通過添加console.log()
調用。 但是斷點更複雜。
AngularJS 還允許您通過 DOM 元素訪問範圍(只要啟用了debugInfo
),並通過控制台注入可用服務。 在控制台中考慮以下內容:
$(document.body).scope().$root
或指向檢查器中的一個元素,然後:
$($0).scope()
即使未啟用 debugInfo,您也可以執行以下操作:
angular.reloadWithDebugInfo()
並在重新加載後可用:
要從控制台注入服務並與之交互,請嘗試:
var injector = $(document.body).injector(); var someService = injector.get('someService');
B - 鉻時間軸
開發工具附帶的另一個很棒的工具是時間線。 這將允許您在使用應用程序時記錄和分析應用程序的實時性能。 除其他外,輸出顯示內存使用情況、幀速率以及佔用 CPU 的不同進程的剖析:加載、腳本、渲染和繪畫。
如果您遇到應用程序的性能下降,您很可能能夠通過時間線選項卡找到原因。 只需記錄您導致性能問題的行為,然後看看會發生什麼。 看客太多? 您會看到黃色條佔用大量空間。 內存洩漏? 您可以在圖表上查看隨時間消耗的內存量。
詳細說明:https://developer.chrome.com/devtools/docs/timeline
C - 在 iOS 和 Android 上遠程檢查應用程序
如果您正在開發混合應用程序或響應式 Web 應用程序,則可以通過 Chrome 或 Safari 開發工具訪問設備的控制台、DOM 樹和所有其他可用工具。 這包括 WebView 和 UIWebView。
首先,在主機 0.0.0.0 上啟動您的 Web 服務器,以便可以從您的本地網絡訪問它。 在設置中啟用網絡檢查器。 然後將您的設備連接到您的桌面並訪問您的本地開發頁面,使用您機器的 ip 而不是常規的“localhost”。 就是這樣,您的設備現在應該可以從您的桌面瀏覽器中使用了。
以下是 Android 和 iOS 的詳細說明,可通過 google 輕鬆找到非官方指南。
我最近對 browserSync 有了一些很酷的體驗。 它的工作方式與 livereload 類似,但實際上它還通過 browserSync 同步查看同一頁面的所有瀏覽器。 這包括用戶交互,例如滾動、單擊按鈕等。我在從桌面控制 iPad 上的頁面時正在查看 iOS 應用程序的日誌輸出。 效果很好!
常見錯誤 #18:不閱讀 NG-INIT 示例的源代碼
Ng-init
聽上去應該和ng-if
和ng-repeat
類似吧? 你有沒有想過為什麼文檔中有一條不應該使用它的評論? 恕我直言,這令人驚訝! 我希望該指令能夠初始化模型。 這也是它的作用,但是……它以不同的方式實現,即它不監視屬性值。 你不需要瀏覽 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 下載。 隨意下載它並使其成為您自己的。
我想感謝四位對我啟發最大的出版開發者:
- 本·納德爾
- 托德座右銘
- 帕斯卡·普雷希特
- 桑迪普熊貓
我還要感謝 FreeNode #angularjs 和 #javascript 頻道上的所有偉大人物,感謝他們進行了許多出色的對話和持續的支持。
最後,永遠記住:
// when in doubt, comment it out! :)