AngularJS 开发人员最常犯的 18 个错误

已发表: 2022-03-11

单页应用程序要求前端开发人员成为更好的软件工程师。 CSS 和 HTML 不再是最大的关注点,事实上,不再只是一个关注点。 前端开发人员需要处理 XHR、应用程序逻辑(模型、视图、控制器)、性能、动画、样式、结构、搜索引擎优化以及与外部服务的集成。 从所有这些组合中产生的结果是用户体验(UX),它应该始终被优先考虑。

AngularJS 是一个非常强大的框架。 它是 GitHub 上排名第三的存储库。 开始使用并不难,但它旨在完成需求理解的目标。 AngularJS 开发人员不能再忽略内存消耗,因为它不会再在导航时重置。 这是Web开发的先锋。 让我们拥抱它!

常见的 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 中没有点,那么您做错了。 当涉及到继承时,这种说法通常是正确的。 Scopes 有一个原型继承模型,这是 JavaScript 的典型模型,嵌套的 scopes 是 AngularJS 常见的。 许多指令创建子作用域,例如ngRepeatngIfngController 。 解析模型时,查找从当前范围开始,并遍历每个父范围,一直到$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.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 世界中的所有内容

这个错误的口径较轻,更多的是风格问题,而不是避免 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-clickinput )、服务( $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>

所以,现在有两个问题:

  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 的变量
  • 狡猾的 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 个)。

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. 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. 内存泄漏? 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 上启动您的 Web 服务器,以便可以从您的本地网络访问它。 在设置中启用网络检查器。 然后将您的设备连接到您的桌面并访问您的本地开发页面,使用您机器的 ip 而不是常规的“localhost”。 就是这样,您的设备现在应该可以从您的桌面浏览器中使用了。

以下是 Android 和 iOS 的详细说明,可通过 google 轻松找到非官方指南。

我最近对 ​​browserSync 有了一些很酷的体验。 它的工作方式与 livereload 类似,但实际上它还通过 browserSync 同步查看同一页面的所有浏览器。 这包括用户交互,例如滚动、单击按钮等。我在从桌面控制 iPad 上的页面时正在查看 iOS 应用程序的日志输出。 效果很好!

常见错误 #18:不阅读 NG-INIT 示例的源代码

Ng-init听上去应该和ng-ifng-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! :)