个性化阅读
专注于IT技术分析

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

本文概述

单页应用程序要求前端开发人员成为更好的软件工程师。 CSS和HTML不再是最大的问题, 实际上, 不再仅仅是一个问题。前端开发人员需要处理XHR, 应用程序逻辑(模型, 视图, 控制器), 性能, 动画, 样式, 结构, SEO以及与外部服务的集成。从所有这些结合中得出的结果是用户体验(UX), 应始终优先考虑。

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

常见的AngularJS错误

常见错误1:通过DOM访问范围

建议对生产进行一些优化调整。其中之一是禁用调试信息。

DebugInfoEnabled是默认设置为true的设置, 并允许通过DOM节点进行范围访问。如果要通过JavaScript控制台进行尝试, 请选择一个DOM元素并使用以下命令访问其范围:

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

即使不将jQuery与CSS结合使用, 它也很有用, 但不应在控制台之外使用。原因是当$ compileProvider.debugInfoEnabled设置为false时, 在DOM节点上调用.scope()将返回未定义。

这是为生产推荐的少数几种选择之一。

请注意, 即使在生产中, 你仍然可以通过控制台访问示波器。从控制台调用angular.reloadWithDebugInfo(), 应用程序将执行此操作。

常见错误2:那里没有小点

你可能已经读过, 如果你的ng模型中没有点, 那你做错了。关于继承, 该陈述通常是正确的。范围具有JavaScript特有的继承原型模型, 而嵌套范围是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:无法充分利用控制器

关于如何公开控制器对象有一些警告。就像普通模型一样, 它基本上是在控制器范围内设置的对象。

如果需要监视控制器对象的属性, 则可以监视函数, 但不是必需的。这是一个例子:

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 testForsrcmini() {
  return {
    controller: 'AnotherController as AC', template: '<p>{{ AC.text }}</p>'
  };
}

angular.module('myApp', [])
.controller('AnotherController', AnotherController)
.directive('testForsrcmini', testForsrcmini);

另一种注释方式也是有效的, 尽管不够简洁:

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

常见错误5:不使用具名的视图和UI-ROUTER供电”

迄今为止, AngularJS的事实上的路由解决方案一直是ui-router。 ngRoute模块从核心中删除之前, 对于更复杂的路由来说太基本了。

即将推出新的NgRouter, 但作者仍认为生产尚为时过早。当我写这篇文章时, 稳定的Angular是1.3.15, 而ui路由器是岩石。

主要原因:

  • 很棒的状态嵌套
  • 路线抽象
  • 可选参数和必需参数

在这里, 我将介绍状态嵌套以避免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'
    }
  }
});

通过将状态定义移至也是抽象的父视图, 我们可以在切换通常会影响子兄弟姐妹的网址时避免子视图重载。当然, 小部件可以是一个简单的指令。但是关键是, 它也可能是另一个复杂的嵌套状态。

还有一种方法可以通过使用$ urlRouterProvider.deferIntercept()来实现, 但是我认为使用状态配置实际上更好。如果你对拦截路由感兴趣, 我在StackOverflow上写了一个小教程。

常见错误#6:使用匿名函数声明Angular世界中的所有内容

这个错误的口径较轻, 并且比避免AngularJS错误消息更多的是样式问题。你可能之前已经注意到, 我很少将匿名函数传递给angular内部的声明。我通常只先定义一个函数, 然后将其传递。

这不仅仅是功能。我从阅读风格指南中获得了这种方法, 尤其是Airbnb和Todd Motto的指南。我相信它有很多优点, 几乎没有缺点。

首先, 如果将函数和对象分配给变量, 则可以更轻松地对其进行操作和变异。其次, 代码更简洁, 可以轻松拆分为文件。这意味着可维护性。如果你不想污染全局名称空间, 请将每个文件包装在IIFE中。第三个原因是可测试性。考虑以下示例:

'use strict';

function yoda() {

  var privateMethod = function () {
    // this function is not exposed
  };

  var publicMethod1 = function () {
    // this function is exposed, but it's internals are not exposed
    // some logic...
  };

  var publicMethod2 = function (arg) {
    // THE BELOW CALL CANNOT BE SPIED ON WITH JASMINE
    publicMethod1('someArgument');
  };

  // IF THE LITERAL IS RETURNED THIS WAY, IT CAN'T BE REFERRED TO FROM INSIDE
  return {
    publicMethod1: function () {
      return publicMethod1();
    }, publicMethod2: function (arg) {
      return publicMethod2(arg);
    }
  };
}

angular.module('app', [])
.factory('yoda', yoda);

所以现在我们可以模拟publicMethod1了, 但是为什么要公开它呢?仅仅监视现有方法是否会更容易?但是, 该方法实际上是另一个功能-薄包装器。看一下这种方法:

function yoda() {

  var privateMethod = function () {
    // this function is not exposed
  };

  var publicMethod1 = function () {
    // this function is exposed, but it's internals are not exposed
    // some logic...
  };

  var publicMethod2 = function (arg) {
    // the below call cannot be spied on
    publicMethod1('someArgument');

    // BUT THIS ONE CAN!
    hostObject.publicMethod1('aBetterArgument');
  };

  var hostObject = {
    publicMethod1: function () {
      return publicMethod1();
    }, publicMethod2: function (arg) {
      return publicMethod2(arg);
    }
  };

  return hostObject;
}

这不仅与样式有关, 因为实际上代码更可重用且惯用。开发人员具有更强的表达能力。将所有代码拆分为独立的块只会更容易。

常见错误7:使用工人在AKA角AKA中进行大量处理

在某些情况下, 可能需要通过将一组复杂的对象通过一组过滤器, 修饰器以及最后的排序算法来处理它们。一种用例是应用程序应脱机工作或显示数据的性能至关重要。而且由于JavaScript是单线程的, 因此冻结浏览器相对容易。

Web工作者也很容易避免这种情况。似乎没有专门针对AngularJS的流行库。不过, 这可能是最好的, 因为实现起来很容易。

首先, 让我们设置服务:

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

    return deferred.promise;
  };
  var hostObject = {
    scoreItems: function (items, weights) {
      return scoreItems(items, weights);
    }
  };

  return hostObject;

}

angular.module('app.worker')
.factory('scoringService', scoringService);

现在, 工人:

'use strict';

function scoringFunction(items, weights) {
  var itemsArray = [];
  for (var i = 0; i < items.length; i++) {
    // some heavy processing
    // itemsArray is populated, etc.
  }

  itemsArray.sort(function (a, b) {
    if (a.sum > b.sum) {
      return -1;
    } else if (a.sum < b.sum) {
      return 1;
    } else {
      return 0;
    }
  });

  return itemsArray;
}

self.addEventListener('message', function (e) {
  var reply = {
    ready: true
  };
  if (e.data && e.data.items && e.data.items.length) {
    reply.items = scoringFunction(e.data.items, e.data.weights);
  }
  self.postMessage(reply);
}, false);

现在, 像往常一样注入服务, 并像对待返回诺言的任何服务方法一样对待scoringService.scoreItems()。繁重的处理将在单独的线程上执行, 并且不会对UX造成损害。

注意事项:

  • 产卵多少似乎没有普遍的规律。一些开发人员声称8是一个很好的数字, 但是使用在线计算器并适合自己
  • 检查与旧版浏览器的兼容性
  • 将数字0从服务传递给工作程序时遇到问题。我在传递的属性上应用了.toString(), 它可以正常工作。

常见错误8:解决过度使用和误解

解决会增加额外的时间来加载视图。我认为, 前端应用程序的高性能是我们的主要目标。在应用程序等待来自API的数据时, 呈现视图的某些部分应该不是问题。

考虑以下设置:

function resolve(index, timeout) {
  return {
    data: function($q, $timeout) {
      var deferred = $q.defer();
      $timeout(function () {
        deferred.resolve(console.log('Data resolve called ' + index));
      }, timeout);
      return deferred.promise;
    }
  };
}

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

控制台输出将是:

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

这基本上意味着:

  • 解析是异步执行的
  • 我们不能依赖执行顺序(或至少需要灵活一些)
  • 所有状态都被阻塞, 直到所有解析都做完为止, 即使它们不是抽象的。

这意味着在用户看到任何输出之前, 他/她必须等待所有依赖项。当然, 我们需要这些数据。如果绝对有必要在视图之前使用它, 请将其放在.run()块中。否则, 只需从控制器对服务进行调用, 并妥善处理半载状态。看到正在进行的工作-并且控制器已经执行了, 所以实际上就是进度-比停止应用程序更好。

常见错误9:未优化应用-三个示例

a)导致过多的摘要循环, 例如将滑块附加到模型

这是一个普遍的问题, 可能会导致AngularJS错误, 但我将在滑块示例中进行讨论。我使用了这个滑块库, 即角度范围滑块, 因为我需要扩展功能。该指令在最低版本中具有以下语法:

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

考虑控制器中的以下代码:

this.maxPrice = '100';
this.price = '55';

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

这样工作就很慢。临时解决方案是在输入上设置超时。但这并不总是很方便, 有时我们真的不想在所有情况下都延迟实际的模型更改。

因此, 我们将添加一个临时模型, 以在超时时更改工作模型:

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

并在控制器中:

this.maxPrice = '100';
this.price = '55';
this.priceTemporary = '55';

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

var timeoutInstance;
$scope.$watch('MC.priceTemporary', function (newVal) {
  if (!isNaN(newVal)) {
    if (timeoutInstance) {
      $timeout.cancel(timeoutInstance);
    }

    timeoutInstance = $timeout(function () {
      $scope.MC.price = newVal;
    }, 144);
    
  }
});

b)不使用$ applyAsync

AngularJS没有调用$ digest()的轮询机制。之所以执行它, 是因为我们使用了指令(例如ng-click, input), 服务($ timeout, $ http)和方法($ watch)来评估代码并随后调用摘要。

。$ applyAsync()的作用是将表达式的解析延迟到下一个$ digest()周期, 该周期在0超时后触发, 实际上是〜10ms。

现在有两种使用applyAsync的方法。 $ http请求的自动方式, 其余的手动方式。

要使所有大约同时返回的http请求在一个摘要中解析, 请执行以下操作:

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

手动方式显示其实际工作方式。考虑一些在原始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-分离的DOM树

很多时候你可能会听说不建议将jQuery与AngularJS一起使用, 应该避免使用它。必须理解这些陈述背后的原因。据我所知, 至少有三个原因, 但它们都不是真正的阻碍因素。

原因1:执行jQuery代码时, 你需要自己调用$ digest()。在很多情况下, 都有针对AngularJS量身定制的AngularJS解决方案, 在Angular中比jQuery(例如ng-click或事件系统)更好地使用。

原因2:关于构建应用程序的思考方法。如果你一直在向浏览器中重新加载的JavaScript添加到网站中, 则不必担心内存消耗过多。使用单页应用程序时, 你不必担心。如果你不进行清理, 则在你的应用上花费超过几分钟的用户可能会遇到性能问题。

原因3:清理实际上并不是最容易做和分析的事情。无法从脚本(在浏览器中)调用垃圾回收器。你可能最终会得到分离的DOM树。我创建了一个示例(jQuery已加载到index.html中):

<section>
  <test-for-srcmini></test-for-srcmini>
  <button ng-click="MC.removeDirective()">remove directive</button>
</section>
function MainController($rootScope, $scope) {
  this.removeDirective = function () {
    $rootScope.$emit('destroyDirective');
  };
}

function testForsrcmini($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('testForsrcmini', testForsrcmini);

这是一个简单的指令, 可以输出一些文本。它下面有一个按钮, 它将只是手动销毁该指令。

因此, 删除指令后, 在scope.toBeDetached中仍然有对DOM树的引用。在chrome dev工具中, 如果你访问”配置文件”标签, 然后”获取堆快照”, 则会在输出中看到:

你可以忍受一些, 但是如果你有很多东西, 那就不好了。尤其是由于某种原因(例如在示例中)将其存储在合并范围内。将在每个摘要上评估整个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:过度使用隔离范围

每当你需要一个可以在单个位置使用或者不希望与所使用的环境冲突的指令时, 就无需使用隔离范围。最近, 有创建可重用组件的趋势, 但是你是否知道核心的角度指令根本不使用隔离范围?

主要原因有两个:无法对元素应用两个独立的作用域指令, 并且嵌套/继承/事件处理可能会遇到问题。尤其是对于包含-效果可能不是你所期望的。

因此, 这将失败:

<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 style="font-size: 21px;">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()。对于每个绑定{{模型}}, AngularJS都会创建一个观察者。在每个摘要阶段, 将评估每个此类绑定并将其与先前的值进行比较。这就是所谓的脏检查, 这就是$ digest所做的。如果自上次检查以来该值已更改, 则会触发观察者回调。如果该观察者回调修改了模型($ scope变量), 则在引发异常时会触发一个新的$ digest循环(最多10个)。

除非表达式复杂, 否则即使有成千上万的绑定, 浏览器也不会出现问题。关于”有多少观察员可以接受”的常见答案是2000。

那么, 我们如何限制观察者的数量呢?当我们不期望范围模型发生变化时, 不观察范围模型。从AngularJS 1.3开始, 这非常容易, 因为一次性绑定现在已经成为核心。

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

评估一次averageArray和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:不依赖自动化, 或者过于依赖自动化

如果你遵循前端开发中的趋势并且有点像我一样懒惰-那么你可能会尝试不做任何事情。跟踪所有依赖项, 以不同的方式处理文件集, 在每次保存文件后重新加载浏览器-开发不仅要做编码, 还有很多事情要做。

因此, 你可能会使用凉亭, 也许会使用npm, 这取决于你为应用程序提供服务的方式。你可能会使用咕unt, 吞咽或早午餐。还是bash, 也很酷。实际上, 你可能已经使用Yeoman发电机启动了你的最新项目!

这就引出了一个问题:你是否了解基础架构真正完成的整个过程?你是否需要拥有的东西, 特别是如果你只是花了几个小时尝试修复连接网络服务器livereload功能时?

花一点时间评估你的需求。所有这些工具仅在这里为你提供帮助, 使用它们没有其他回报。我交谈的更有经验的开发人员倾向于简化事情。

常见错误16:未在TDD模式下运行单元测试

测试不会使你的代码脱离AngularJS错误消息。他们会做的是确保你的团队不会一直遇到回归问题。

我在这里专门写关于单元测试的信息, 不是因为我觉得它们比e2e测试更重要, 而是因为它们执行得更快。我必须承认, 我将要描述的过程是一个非常愉快的过程。

测试驱动开发, 例如gulp-karmaRunner, 基本上在每个文件保存上运行所有单元测试。我最喜欢编写测试的方式是, 我首先要写空保证:

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

之后, 我编写或重构实际代码, 然后返回测试并使用实际测试代码填写保证书。

在终端中运行TDD任务可以使该过程加快大约100%。即使有很多单元测试, 它们也只需几秒钟即可执行。只需保存测试文件, 跑步者就会选择它, 评估你的测试并立即提供反馈。

使用e2e测试, 该过程要慢得多。我的建议-将e2e测试分成多个测试套件, 一次只能运行一个。量角器支持它们, 下面是我用于测试任务的代码(我喜欢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服务器, 以便可以从本地网络访问它。在设置中启用Web检查器。然后, 使用计算机的ip(而不是常规的” localhost”)将设备连接到桌面并访问本地开发页面。仅此而已, 你现在应该可以通过桌面浏览器使用你的设备了。

这是针对Android的详细说明。对于iOS, 可以通过Google轻松找到非官方指南。

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

常见错误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语言, 这是一笔很好的投资。它是Internet的语言。比赛超级激烈!我们有一个重点-用户体验。为了成功, 我们需要涵盖所有内容。

这些示例中使用的源代码可以从GitHub下载。随时下载并制作自己的。

我想感谢四个给我最大启发的出版商:

  • 本·纳德尔
  • 托德·格托
  • 帕斯卡·普雷希特(Pascal Precht)
  • 桑迪普熊猫

我还想感谢FreeNode #angularjs和#javascript频道上的所有出色人士, 他们进行了许多精彩的对话, 并不断提供支持。

最后, 请始终记住:

// when in doubt, comment it out! :)
赞(0)
未经允许不得转载:srcmini » AngularJS开发人员最常犯的18个错误

评论 抢沙发

评论前必须登录!