作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
michael Mikolajczyk的头像

Michal Mikolajczyk

Toptal Warsaw的b区块链物联网初创公司创始人/首席执行官和社区领导者, Michal拥有广泛的全栈经验.

Years of Experience

8

Share

单页应用程序要求前端开发人员成为更好的软件工程师. CSS和HTML不再是最大的关注点,事实上,不再是单一的关注点. 前端开发人员需要处理xhr, 应用程序逻辑(模型), views, controllers), performance, animations, styles, structure, SEO, 以及与外部服务的集成. 所有这些结合起来的结果是用户体验(UX),这应该始终是优先考虑的.

AngularJS是一个非常强大的框架. 它是GitHub上第三个最受欢迎的存储库. 开始使用并不难, 但是它所要达到的目标需要理解. AngularJS开发者再也不能忽视内存消耗了, 因为它不会在导航上重置. 这是先锋 web development. Let’s embrace it!

常见的AngularJS错误

常见错误#1:通过DOM访问作用域

这里有一些推荐用于生产的优化调整. 其中之一是禁用调试信息.

DebugInfoEnabled 是一个默认为true的设置,允许通过DOM节点访问作用域. 如果你想通过JavaScript控制台试试, 选择一个DOM元素并使用以下命令访问它的作用域:

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

即使不使用,它也很有用 jQuery 使用CSS,但不应该在控制台之外使用. 原因是当 $compileProvider.debugInfoEnabled 设置为false,调用 .scope() 将返回一个DOM节点 undefined.

这是少数推荐用于生产的选项之一.

请注意,即使在生产环境中,您仍然可以通过控制台访问作用域. Call angular.reloadWithDebugInfo () 从控制台和应用程序就会这样做.

常见错误#2:没有点

如果你是的话,你可能已经读过了 在你的n -模型中没有点你做错了. 当涉及到继承时,这种说法通常是正确的. 范围具有继承的原型模型, 典型的JavaScript, 和嵌套作用域在AngularJS中很常见. 许多指令创建子作用域,比如 ngRepeat, ngIf, and ngController. 解析模型时, 查找从当前作用域开始,遍历每个父作用域, all the way to $rootScope.

But, 设置新值时, 会发生什么取决于我们想要改变的模型(变量)类型. 如果模型是一个原语,子作用域将只创建一个新模型. 但如果改变的是模型对象的属性, 对父作用域的查找将找到被引用的对象并更改其实际属性. 新模型不会在当前范围上设置,因此不会发生屏蔽:

MainController($scope) {
  $scope.foo = 1;
  $scope.bar = {innerProperty: 2};
}

angular.module('myApp', [])
.控制器(MainController, MainController);

OUTER SCOPE:

{{ foo }}

{{ bar.innerProperty }}

INNER SCOPE

{{ foo }}

{{ bar.innerProperty }}

点击“Set primitive”按钮会将内部作用域中的foo设置为2, 但是不会在外部作用域中改变foo.

单击标有“更改对象”的按钮将从父作用域更改bar属性. 因为内部作用域中没有变量, 没有影子会发生, bar的可见值在两个作用域中都是3.

另一种方法是利用从每个作用域引用父作用域和根作用域的事实. The $parent and $root 对象可用于访问父作用域和 $rootScope,分别直接从视图. 这可能是一种强有力的方式, 但我不喜欢它,因为它存在瞄准特定范围的问题. 还有另一种方法可以设置和访问特定于作用域的属性——使用 controllerAs syntax.

常见错误#3:不使用控制器语法

分配模型使用控制器对象而不是注入的$作用域的另一种最有效的方法. 除了注入作用域,我们可以这样定义模型:

MainController($scope) {
  this.foo = 1;
  var that = this;
  var setBar = function () {
    // that.bar = {someProperty: 2};
    this.bar = {someProperty: 2};
  };
  setBar.call(this);
  //还有其他约定: 
  // var MC = this;
  // setBar.call(this); when using 'this' inside setBar()
}

OUTER SCOPE:

{{ MC.foo }}

{{ MC.bar.someProperty }}

INNER SCOPE

{{ MC.foo }}

{{ MC.bar.someProperty }}

这就不那么令人困惑了. 特别是当有许多嵌套作用域时,就像嵌套状态一样.

controllerAs语法还有更多内容.

常见错误#4:没有充分利用控制器语法

关于如何公开控制器对象,有一些注意事项. 它基本上是在控制器作用域中设置的对象,就像普通模型一样.

如果你需要观察控制器对象的属性, 你可以观看一个函数,但你不需要这样做. Here is an example:

MainController($scope) {
  this.title = '某些标题';
  $scope.$watch(angular.Bind (this, function () {
    return this.title;
  }), function (newVal, oldVal) {
    // handle changes
  });
}

更简单的做法是:

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

也就是说,在作用域链中,你可以从子控制器访问MC:

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

然而,为了能够做到这一点,您需要与用于controllera的缩略词保持一致. 至少有三种设置方法. 你们已经看过第一个了:

However, if you use ui-router,以这种方式指定控制器很容易出错. 对于状态,控制器应该在状态配置中指定:

angular.module('myApp', [])
.配置(函数($ statprovider) {
  $stateProvider
  .state('main', {
    url: '/',
    ` MainController as MC';
    templateUrl: / /模板/路径.html'
  })
}).
控制器('MainController', function(){…});

还有另一种注释方式:

(…)
.state('main', {
  url: '/',
  控制器:“MainController”,
  controllerAs: 'MC',
  templateUrl: / /模板/路径.html'
})

你可以在指令中做同样的事情:

函数AnotherController() {
  this.text = 'abc';
}

函数testForToptal() {
  return {
    控制器:'AnotherController as AC',
    template: '

{{ AC.text }}

' }; } angular.module('myApp', []) .控制器(AnotherController, AnotherController) .指令(testForToptal, testForToptal);

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

函数testForToptal() {
  return {
    控制器:“AnotherController”,
    controllerAs: 'AC',
    template: '

{{ AC.text }}

' }; }

常见错误#5:在UI-ROUTER中不使用命名视图

到目前为止,AngularJS事实上的路由解决方案一直是 ui-router. 不久前从core中移除的ngRoute模块,对于更复杂的路由来说太基础了.

There is a new NgRouter 正在进行中,但作者仍然认为它用于生产还为时过早. 当我写这个的时候,稳定的Angular是1.3.15, and ui-router rocks.

The main reasons:

  • 很棒的状态嵌套
  • route abstraction
  • 可选参数和必需参数

这里我将介绍状态嵌套以避免AngularJS错误.

可以把它看作是一个复杂但标准的用例. 有一个应用程序,它有主页视图和产品视图. 产品视图有三个独立的部分:介绍、小部件和内容. 我们希望小部件在切换状态时保持持久化,而不是重新加载. 但是内容应该重新加载.

考虑以下HTML产品索引页面结构:


  

SOME PRODUCT SPECIFIC INTRO

Product title

Context-specific content

这是我们可以从HTML编码器得到的东西, 现在需要把它分成文件和状态. 我通常遵循一个抽象的MAIN状态的惯例, 如果需要,它会保存全局数据. 使用它代替$rootScope. The Main state还将保留每个页面所需的静态HTML. I keep index.html clean.



  

然后让我们看看产品索引页:

如您所见,产品索引页有三个命名视图. 一个用于介绍,一个用于小部件,一个用于产品. We meet the specs! 现在让我们设置路由:

函数配置($ statprovider) {
  $stateProvider
    //主抽象状态,总是打开
    .state('main', {
      abstract: true,
      url: '/',
      ` MainController as MC';
      templateUrl:“/ routing-demo /主要.html'
    })
    // A SIMPLE HOMEPAGE
    .state('main.homepage', {
      url: '',
      控制器:'HomepageController as HC',
      templateUrl:“routing-demo /主页.html'
    })
    //以上都是好的,这里有麻烦
    //复杂的产品页面
    .state('main.product', {
      abstract: true,
      url: ':id',
      控制器:'ProductController as PC',
      templateUrl:“routing-demo /产品.html',
    })
    //产品默认子状态
    .state('main.product.index', {
      url: '',
      views: {
        'widget': {
          控制器:'WidgetController as PWC',
          templateUrl:“routing-demo /小部件.html' 
        },
        'intro': {
          控制器:'IntroController as PIC',
          templateUrl:“/ routing-demo /介绍.html' 
        },
        'content': {
          控制器:'ContentController as PCC',
          templateUrl:“routing-demo /内容.html'
        }
      }
    })
    //产品详细信息子状态
    .state('main.product.details', {
      url: '/details',
      views: {
        'widget': {
          控制器:'WidgetController as PWC',
          templateUrl:“routing-demo /小部件.html' 
        },
        'content': {
          控制器:'ContentController as PCC',
          templateUrl:“routing-demo /内容.html'
        }
      }
    });
}

angular.模块(“articleApp”,
  'ui.router'
])
.config(config);

这将是第一种方法. 现在,当两者切换时会发生什么 main.product.index and main.product.details? 内容和小部件被重新加载,但我们只想重新加载内容. This was problematic, 开发人员实际上创造了支持这种功能的路由器. 其中一个名字是 sticky views. Fortunately, ui-router 支持开箱即用 绝对命名视图定位.

//复杂的产品页面
//没有更多的麻烦
.state('main.product', {
  abstract: true,
  url: ':id',
  views: {
    //瞄准main中的未命名视图.HTML
    '@main': {
      控制器:'ProductController as PC',
      templateUrl:“routing-demo /产品.html' 
    },
    //针对product中的小部件视图.HTML
    //通过在这里定义一个子视图,我们确保它不会在子状态改变时重新加载
    'widget@main.product': {
      控制器:'WidgetController as PWC',
      templateUrl:“routing-demo /小部件.html' 
    }
  }
})
//产品默认子状态
.state('main.product.index', {
  url: '',
  views: {
    'intro': {
      控制器:'IntroController as PIC',
      templateUrl:“/ routing-demo /介绍.html' 
    },
    'content': {
      控制器:'ContentController as PCC',
      templateUrl:“routing-demo /内容.html'
    }
  }
})
//产品详细信息子状态
.state('main.product.details', {
  url: '/details',
  views: {
    'content': {
      控制器:'ContentController as PCC',
      templateUrl:“routing-demo /内容.html'
    }
  }
});

通过将状态定义移动到父视图, 这也是抽象的, 我们可以避免在切换url时重新加载子视图,而这通常会影响到该子视图的兄弟视图. 当然,这个小部件也可以是一个简单的指令. 但关键是,它也可能是另一种复杂的嵌套状态.

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

常见错误#6:用匿名函数声明Angular世界中的一切

This mistake 是一个轻量级的问题,更多的是一个风格问题,而不是避免AngularJS错误消息. 您可能已经注意到,我很少将匿名函数传递给angular内部声明. 我通常只是先定义一个函数,然后把它传递进去.

这不仅仅涉及函数. 我从阅读风格指南中获得了这种方法,尤其是Airbnb和Todd Motto的风格指南. 我认为它有几个优点,几乎没有缺点.

First of all, 如果将函数和对象赋值给变量,则可以更容易地操作和修改它们. 其次,代码更清晰,可以很容易地分割成文件. 这意味着可维护性. 如果不想污染全局命名空间,请将每个文件包装在iife中. 第三个原因是可测试性. 考虑一下这个例子:

'use strict';

function yoda() {

  var privateMethod = function () {
    //该函数不公开
  };

  var publicMethod1 = function () {
    //这个函数是公开的,但它的内部没有公开
    // some logic...
  };

  var publicMethod2 = function (arg) {
    //下面的调用不能被jasmine监视
    publicMethod1(“someArgument”);
  };

  //如果以这种方式返回字面值,则不能从内部引用它
  return {
    publicMethod1: function () {
      返回publicMethod1 ();
    },
    publicMethod2: function (arg) {
      返回publicMethod2 (arg);
    }
  };
}

angular.module('app', [])
.工厂(尤达,尤达);

现在我们可以嘲笑 publicMethod1但是既然已经暴露了,我们为什么还要这样做呢? 监视现有的方法不是更容易吗? 然而,该方法实际上是另一个函数——一个瘦包装器. 看看这个方法:

function yoda() {

  var privateMethod = function () {
    //该函数不公开
  };

  var publicMethod1 = function () {
    //这个函数是公开的,但它的内部没有公开
    // some logic...
  };

  var publicMethod2 = function (arg) {
    //下面的调用不能被监视
    publicMethod1(“someArgument”);

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

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

  return hostObject;
}

这不仅仅是关于风格,因为实际上代码更易于重用和习惯. 开发人员获得了更强的表达能力. 将所有代码分割成自包含的块只会使它更容易.

常见错误#7:在Angular中做繁重的处理,也就是使用worker

In some scenarios, 可能需要通过一组过滤器来处理大量复杂对象, decorators, 最后是排序算法. 一个用例是应用程序应该离线工作,或者显示数据的性能是关键. 由于JavaScript是单线程的,因此冻结浏览器相对容易.

网络工作者也很容易避免这种情况. 似乎没有任何流行的库专门为AngularJS处理这个问题. 但这可能是最好的选择,因为实现起来很容易.

首先,让我们设置服务:

($q) {
  
  var scoreItems = function(条目,权重){
    var deferred = $q.defer();
    var worker = new worker (/worker-demo/scoring . net.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:函数(项目,权重){
      返回scoreItems(items, weights);
    }
  };

  return hostObject;

}

angular.module('app.worker')
.工厂(scoringService, scoringService);

Now, the worker:

'use strict';

函数scoingfunction(项目,权重){
  var itemsArray = [];
  for (var i = 0; i < items.length; i++) {
    //一些繁重的处理
    // itemsArray填充,等等.
  }

  itemsArray.Sort(函数(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(.data.items, e.data.weights);
  }
  self.postMessage(reply);
}, false);

现在,像往常一样注入服务并处理 scoringService.scoreItems() 就像任何返回承诺的服务方法一样. 繁重的处理将在单独的线程上进行,不会对用户体验造成伤害.

注意事项:

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

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

解析会给视图的加载增加额外的时间. 我相信前端应用的高性能是我们的首要目标. 当应用程序等待来自API的数据时,渲染视图的某些部分应该没有问题.

Consider this setup:

函数解析(索引,超时){
  return {
    数据:函数($q, $timeout) {
      var deferred = $q.defer();
      $timeout(function () {
        deferred.resolve(console.log('数据解析被调用' + index));
      }, timeout);
      return deferred.promise;
    }
  };
}

函数configResolves($ stateprovider) {
  $stateProvider
    //主抽象状态,总是打开
    .state('main', {
      url: '/',
      ` MainController as MC';
      templateUrl:“/ routing-demo /主要.html',
      解析:解析(1,1597)
    })
    //复杂的产品页面
    .state('main.product', {
      url: ':id',  
      控制器:'ProductController as PC',
      templateUrl:“routing-demo /产品.html',
      解析:解析(2,2584)
    })
    //产品默认子状态
    .state('main.product.index', {
      url: '',
      views: {
        'intro': {
          控制器:'IntroController as PIC',
          templateUrl:“/ routing-demo /介绍.html'
        },
        'content': {
          控制器:'ContentController as PCC',
          templateUrl:“routing-demo /内容.html'
        }
      },
      解析:解析(3,987)
    });
}

控制台输出将是:

数据解析称为3
数据解析称为1
数据解析称为2
主控制器执行
产品控制器执行
执行的控制器

这基本上意味着:

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

这意味着在用户看到任何输出之前,他/她必须等待所有依赖项. 当然,我们需要这些数据. 如果绝对有必要将它放在视图之前,请将它放在 .run() block. Otherwise, 只需从控制器调用服务并优雅地处理半加载状态. 看到正在进行的工作-并且控制器已经执行, 所以这实际上是一种进步——比让应用停滞不前要好.

常见错误#9:未优化应用——三个例子

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

这是一个会导致AngularJS错误的普遍问题, 但我会在滑块的例子中讨论它. 我使用这个滑块库,角范围滑块,因为我需要扩展功能. 该指令在最小版本中有这样的语法:


  

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

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

$scope.$watch('MC.price', function (newVal) {
  如果(newVal || newVal === 0) {
    for (var i = 0; i < 987; i++) {
      console.log(‘你所有的基地都属于我们’);
    }
  }
});

So that works slow. 临时的解决方案是在输入上设置一个超时. 但这并不总是很方便, 有时候我们并不想在所有情况下都推迟实际的模型变化.

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


  

在控制器中:

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(‘你所有的基地都属于我们’);
    }
  }
});

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

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

b)不使用$applyAsync

AngularJS没有轮询机制来调用 $digest(). 它之所以被执行,只是因为我们使用了指令(例如.g. ng-click, input), services ($timeout, $http), and methods ($watch),它计算我们的代码,然后调用摘要.

What .$applyAsync() 它是否将表达式的解析延迟到下一个 $digest() 周期,在0超时后触发,实际上是~10ms.

有两种使用方法 applyAsync now. 一个自动化的方法 $http 请求,其余部分用手动方式处理.

要使所有在同一时间返回的http请求在一个摘要中解析,请这样做:

mymodule.配置(function ($httpProvider) {
  $httpProvider.useApplyAsync(真正的);
});

手动方式显示了它实际是如何工作的. 考虑一些运行在回调到普通JS事件监听器或jQuery上的函数 .click(),或者其他外部库. 在它执行并更改模型之后,如果您还没有将它包装在 $apply() you need to call $scope.$root.$digest() ($rootScope.$digest()), or at least $scope.$digest(). 否则,您将看不到任何更改.

如果在一个流中多次这样做,它可能会开始运行缓慢. Consider calling $scope.$applyAsync() 而是在表达式上. 它将为所有这些调用只设置一个摘要周期.

c)对图像进行大量处理

如果你的表现不佳, 你可以通过使用Chrome开发者工具的时间轴来调查原因. 我将在错误#17中详细介绍这个工具. 如果您的时间轴图形在录制后以绿色为主, 您的性能问题可能与图像处理有关. 这与AngularJS没有严格的关系, 但可能会发生在AngularJS性能问题之上(图中大部分是黄色的). 作为前端工程师,我们需要考虑完整的最终项目.

花点时间评估一下:

  • Do you use parallax?
  • 你是否有多个相互重叠的内容层?
  • 你会移动你的图像吗?
  • 你缩放图像吗.g. background-size)?
  • 你是否在循环中调整图像的大小,并且可能在调整大小时引起摘要循环?

如果你对以上至少三个问题的回答是肯定的,那就考虑放松一下. 也许您可以提供各种大小的图像,而不调整大小. 也许你可以添加“transform: translateZ(0)”强制GPU处理hack. 或者使用requestAnimationFrame作为处理程序.

常见错误#10:jquery它-分离的DOM树

你可能多次听说不建议在AngularJS中使用jQuery, 应该避免. 理解这些说法背后的原因是必要的. 至少有三个原因,据我所知,但没有一个是真正的阻碍.

Reason 1: 在执行jQuery代码时,需要调用 $digest() yourself. 在许多情况下,有一个 AngularJS solution 它是为AngularJS量身定制的,可以在Angular中比jQuery更好地使用(e.g. (点击事件系统).

Reason 2: 构建应用程序的方法. 如果你一直在给网站添加JavaScript, 导航时重新加载, 您不必担心内存消耗过多. 对于单页应用,你确实需要担心. 如果你不打扫的话, 在你的应用上花费超过几分钟的用户可能会遇到越来越多的性能问题.

Reason 3: 清理实际上并不是最容易做和分析的事情. 没有办法从脚本(在浏览器中)调用垃圾收集器。. 最终可能会得到分离的DOM树. 我创建了一个例子(jQuery是加载在索引.html):

MainController($rootScope, $scope) {
  this.removeDirective = function () {
    $rootScope.美元发出(“destroyDirective”);
  };
}

testForToptal($rootScope, $timeout) {
  return {
    链接:函数(作用域,元素,属性){

      var destroyListener = $rootScope.$on('destroyDirective', function () {
        scope.$destroy();
      });

      //添加一个DOM准备就绪的超时
      $timeout(function () {
        scope.toBeDetached =元素.find('p');
      });

      scope.$on('$destroy', function () {
        destroyListener();
        element.remove();
      });
    },
    template: '

I AM DIRECTIVE

' }; } angular.module('app', []) .控制器(MainController, MainController) .指令(testForToptal, testForToptal);

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

因此,当指令被删除时,作用域中仍然有对DOM树的引用.toBeDetached. In chrome dev tools, 如果您访问选项卡" profiles ",然后选择" take heap snapshot ", 您将在输出中看到:

你可以有几个,但如果你有很多,那就不好了. 特别是如果出于某种原因,就像在这个例子中一样,您将它存储在作用域中. 整个DOM将在每个摘要上求值. 有问题的分离DOM树是具有4个节点的树. 那么如何解决这个问题呢?

scope.$on('$destroy', function () {

  //将模型设置为null
  //将解决问题.
  scope.toBeDetached = null;

  destroyListener();
  element.remove();
});

带有4个条目的分离DOM树被删除!

在这个例子中,该指令使用相同的作用域,并将DOM元素存储在该作用域中. 这样对我来说更容易证明. 它并不总是那么糟糕,因为您可以将其存储在变量中. However, 如果存在任何引用该变量的闭包或来自同一函数作用域的其他闭包,则仍然会占用内存.

常见错误11:过度使用孤立作用域

当你需要一个指令,你知道将在一个地方使用, 或者您不希望与使用它的任何环境发生冲突, 没有必要使用隔离作用域. Lately, 创建可重用组件是一种趋势, 但是你知道核心的angular指令根本不使用隔离作用域吗?

有两个主要原因:你不能对一个元素应用两个独立的作用域指令, 您可能会遇到嵌套/继承/事件处理方面的问题. 特别是关于透血-效果可能不是你所期望的.

So this would fail:

即使你只使用一个指令, 你会注意到,无论是隔离的作用域模型,还是在isolatedScopeDirective中广播的事件,都不会对AnotherController可用. That being sad, 您可以灵活地使用透传魔法使其工作——但对于大多数用例来说, 没有必要孤立.

如果隔离作用域在这里不可用,请查看:{{isolatedModel}}

现在有两个问题:

  1. 如何在相同作用域指令中处理父作用域模型?
  2. 如何实例化新的模型值?

有两种方式,两种方式都是将值传递给属性. 考虑这个MainController:

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

它控制着这个视图:



  

Attributes test

注意,没有插入“watch-attribute”. 这一切都工作,由于JS的魔力. 下面是指令的定义:

testDirective() {
  var postLink = function (scope, element, attrs) {
    scope.$watch(attrs.watchAttribute, function (newVal) {
      if (newVal) {
        //查看控制台
        //我们不能直接使用该属性
        console.log(attrs.watchAttribute);

        //计算newVal,并使用它
        scope.modifiedFooBar = newVal.bar * 10;
      }
    }, true);

    attrs.$observe('observeAttribute', function (newVal) {
      scope.observed = newVal;
    });
  };

  return {
    link: postLink,
    templateUrl:“/ attributes-demo / test-directive.html'
  };
}

Notice that attrs.watchAttribute is passed into scope.$watch() 没有引号! 这意味着实际传递给$watch的是字符串 MC.foo! 但是,它确实有效,因为传入的任何字符串 $watch() 根据作用域和求值 MC.foo 可以在瞄准镜上看到吗. 这也是AngularJS核心指令中最常见的监视属性的方式.

请参阅github上的模板代码,并查看 $parse and $eval 为了更棒.

常见错误#12:不自己清理——观察者、间隔、超时和变量

AngularJS为你做了一些工作,但不是全部. 以下需要手工清理:

  • 任何未绑定到当前作用域的监视器(例如.g. bound to $rootScope)
  • Intervals
  • Timeouts
  • 在指令中引用DOM的变量
  • 狡猾的jQuery插件.g. 那些没有处理程序对JavaScript作出反应的 $destroy event

如果您不手动执行此操作,您将遇到意外行为和内存泄漏. 更糟糕的是,这些问题不会立即显现出来,但它们最终会慢慢出现. Murphy’s law.

令人惊讶的是,AngularJS提供了方便的方法来处理所有这些:

函数cleanMeUp($interval, $rootScope, $timeout) {
  var postLink = function (scope, element, attrs) {
    var rootModelListener = $rootScope.$watch('someModel', function () {
      // do something
    });

    var myInterval = $interval(function () {
      //每隔一段时间做一些事情
    }, 2584);

    var myTimeout = $timeout(function () {
      //在这里延迟一些操作
    }, 1597);

    scope.domElement =元素;

    $timeout(function () {
      //手动调用$destroy进行测试
      scope.$destroy();
    }, 987);

    //这里是清理发生的地方
    scope.$on('$destroy', function () {
      //关闭监听器
      rootModelListener();

      //取消interval和timeout
      $interval.cancel(myInterval);
      $timeout.cancel(myTimeout);

      //取消dom绑定模型
      scope.domElement = null;
    });

    element.On ('$destroy', function () {
      //这是一个jQuery事件
      //清除这里所有的JavaScript / jQuery构件

      //尊重jQuery插件有$destroy处理程序,
      //这是触发此事件的原因...
      //遵循标准.
    });

  };

Notice the jQuery $destroy event. 它的调用方式与AngularJS的类似,但它是单独处理的. Scope $watchers不会对jQuery事件做出反应.

常见错误13:拥有太多观察者

现在应该很简单了. 这里有一件事要明白: $digest(). For every binding {{ model }}, AngularJS创建了一个监视器. 在每个摘要阶段,对每个这样的绑定进行评估,并与前一个值进行比较. 这就是所谓的脏检查,这就是$digest所做的. 如果自上次检查以来值发生了变化,则触发监视程序回调. 如果这个观察者回调修改了一个模型($scope variable), 当抛出异常时,将触发一个新的$digest循环(最多10个).

即使有数千个绑定,浏览器也不会有问题,除非表达式很复杂. 对于“有多少观察者是可以的”这个问题,通常的答案是2000个.

那么,我们如何限制观察者的数量呢? 当我们不期望范围模型发生变化时,不去观察它们. 从AngularJS 1开始,这相当容易.3、既然一次性绑定现在是核心.

  • {{ ::item.velocity }}
  • After vastArray and item.velocity 被评估一次,它们就不会再改变了吗. 你仍然可以对数组应用过滤器,它们会工作得很好. 只是数组本身不会被求值. 在很多情况下,这是一种胜利.

    常见错误14:误解文摘

    这个AngularJS错误在错误9中已经被部分覆盖了.b and in 13. 这是一个更彻底的解释. AngularJS通过对观察者的回调函数来更新DOM. 每个绑定,都是指令 {{ someModel }} 设置了监视器,但是也为许多其他指令设置了监视器,比如 ng-if and ng-repeat. 只要看一下源代码,它是非常可读的. 观察者也可以手动设置,你自己可能至少做过几次.

    $watch()er被绑定到作用域. $Watchers 可以接受字符串,这些字符串根据 $watch() was bound to. 它们也可以计算函数. 他们也接受回调. So, when $rootScope.$digest() 被调用时,所有注册的模型(即 $scope 变量)被计算并与它们之前的值进行比较. 如果值不匹配,则回调到 $watch() is executed.

    理解这一点很重要,即使模型的值被改变了, 回调直到下一个摘要阶段才会触发. 它被称为“阶段”是有原因的——它可以由几个消化循环组成. 如果只有监视程序更改了范围模型,则执行另一个摘要循环.

    But $digest() is not polled for. 它可以从核心指令、服务、方法等调用. 如果您从不调用的自定义函数更改模型 .$apply, .$applyAsync, .$evalAsync,或者其他任何最终调用的东西 $digest(),则不会更新绑定.

    的源代码 $digest() 其实很复杂. 尽管如此,它还是值得一读,因为滑稽的警告弥补了这一点.

    常见错误15:不依赖自动化,或过度依赖自动化

    如果你跟随前端开发的趋势,并且有点懒惰——像我一样——那么你可能会尝试不要手工完成所有的事情. 跟踪所有依赖项, 以不同的方式处理文件集, 每次保存文件后都要重新加载浏览器——开发不仅仅是编码.

    你可能会使用bower,也可能是npm,这取决于你如何服务你的应用. 你可能会使用grunt, gulp或brunch. 或者bash,也很酷. 事实上,您可能已经开始使用一些Yeoman生成器来启动您的最新项目!

    这就引出了一个问题:您是否了解基础设施的整个过程? 你需要你所拥有的吗, 特别是如果你只是花了几个小时试图修复你的连接web服务器的肝脏负载功能?

    花点时间评估一下你需要什么. 所有这些工具都只是为了帮助你,使用它们没有其他奖励. 与我交谈过的更有经验的开发者倾向于简化事情.

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

    测试不会让你的代码没有AngularJS错误消息. 他们要做的是确保您的团队不会一直遇到回归问题.

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

    测试驱动开发作为e的实现.g. Gulp-karma运行器,基本上在每个保存的文件上运行所有的单元测试. 我最喜欢的编写测试的方式是,我只是先写空保证:

    描述('某些模块',函数(){
      它('应该调用命名服务…',function () {
        //暂时不写
      });
      ...
    });
    

    After that, 我编写或重构实际的代码, 然后我回到测试,用实际的测试代码填充保证.

    在终端中运行TDD任务可以使整个过程加快大约100%. 单元测试在几秒钟内执行,即使您有很多单元测试. 只需保存测试文件,运行程序就会拾取它, evaluate your tests, 并立即提供反馈.

    使用端到端测试,这个过程要慢得多. 我的建议是——将端到端测试分成测试套件,每次只运行一个. 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() {
      //确保返回流
      //注意:使用fake './foobar'以运行文件
      // listed in karma.conf.. js而不是传递给
      // gulp.src !
      return gulp.src('./foobar')
        .pipe(karma({
          configFile:“测试/业力.conf.js',
          action: 'run'
        }))
        .On ('error', function(err) {
          //确保失败的测试导致gulp以非零退出
          // console.log(err);
          this.emit('end'); //instead of erroring the stream, end it
        });
    }
    
    function tdd() {
      return gulp.src('./foobar')
        .pipe(karma({
          configFile:“测试/业力.conf.js',
          action: 'start'
        }))
        .On ('error', function(err) {
          //确保失败的测试导致gulp以非零退出
          // console.log(err);
          // this.emit('end'); // not ending the stream here
        });
    }
    
    函数runProtractor () {
    
      var argument = args.suite || 'all';
      
      //注意:使用fake './foobar'以运行文件
      //在protractor中列出.conf.Js,而不是传递给
      // gulp.src
      return gulp.src('./foobar')
        .pipe(protractor({
          configFile:“测试/量角器.conf.js',
          参数:['——suite',参数]
        }))
        .On ('error', function (err) {
          //确保失败的测试导致gulp以非零退出
          throw err;
        })
        .On ('end', function () {
          //关闭浏览器同步服务器
          browserSync.exit();
        });
    }
    
    gulp.task('tdd', tdd);
    gulp.task('test', test);
    gulp.task('test-e2e', ['webdriver-update'], runProtractor);
    gulp.任务(webdriver-update, webdriverUpdate);
    

    常见错误17:不使用可用的工具

    A - chrome断点

    Chrome开发工具允许您在加载到浏览器中的任何文件中指向特定位置, 在该点暂停代码执行, 让你从这一点开始与所有可用的变量交互. That is a lot! 该功能根本不需要添加任何代码,一切都在开发工具中进行.

    你不仅可以访问所有的变量, 您还可以看到调用堆栈, print stack traces, and more. 您甚至可以配置它来使用 minified files. Read about it here.

    还有其他方法可以获得类似的运行时访问权限.g. by adding console.log() calls. 但是断点更为复杂.

    AngularJS还允许你通过DOM元素访问作用域(只要 debugInfo ),并通过控制台注入可用的服务. 在控制台中考虑以下内容:

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

    或者指向检视器中的一个元素,然后:

    $($0).scope()
    

    即使没有启用debugInfo,你也可以这样做:

    angular.reloadWithDebugInfo ()
    

    并在重新加载后可用:

    要从控制台注入服务并与之交互,请尝试:

    Var注入器= $(文档.body).injector();
    var someService =注入器.get('someService');
    

    B - chrome timeline

    开发工具附带的另一个重要工具是时间轴. 这将允许你在使用应用时记录和分析应用的实时性能. The output shows, among others, memory usage, frame rate, 以及对占用CPU的不同进程的剖析:加载, scripting, rendering, and painting.

    如果你的应用程序的性能下降, 你很有可能通过时间轴标签找到原因. 只需记录导致性能问题的操作,然后看看会发生什么. Too many watchers? 你会看到黄色条占据了很多空间. Memory leaks? 您可以在图表上看到随着时间的推移消耗了多少内存.

    详细说明: http://developer.chrome.com/devtools/docs/timeline

    C -在iOS和Android上远程检查应用程序

    如果你正在开发一个混合应用程序或响应式web应用程序, 您可以访问设备的控制台, DOM tree, 以及所有其他可用的工具,无论是通过Chrome或 Safari dev tools. 包括WebView和UIWebView.

    首先,在主机0上启动web服务器.0.0.0,以便可以从本地网络访问. 在设置中启用web检查器. 然后将设备连接到桌面并访问本地开发页面, 使用机器的IP而不是常规的“localhost”. 这就是所有需要做的,你的设备现在应该可以从你的桌面浏览器中访问.

    Here Android和iOS的详细说明是什么, 通过谷歌可以很容易地找到非官方指南.

    我最近有了一些很酷的经历 browserSync. 它的工作原理与肝脏负荷相似, 但它实际上也通过browserSync同步所有浏览同一页面的浏览器. 这包括用户交互,如滚动、点击按钮等. 当我从桌面控制iPad上的页面时,我正在查看iOS应用程序的日志输出. It worked nicely!

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

    Ng-init从它的发音来看,应该是相似的 ng-if and ng-repeat, right? 你有没有想过为什么在文档中有一个不应该使用它的注释? 恕我直言,这很令人惊讶! 我希望这个指令初始化一个模型. 这也是它的作用, 但是……它是以不同的方式实现的, that is, 它不监视属性值. 你不需要浏览AngularJS的源代码——让我给你看看:

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

    比你想象的要少? 除了令人尴尬的指令语法之外,它还很好读,不是吗? 第六行是它的全部内容.

    将其与ng-show进行比较:

    var ngShowDirective = ['$animate', function($animate) {
      return {
        restrict: 'A',
        multiElement: true,
        链接:函数(作用域,元素,attr) {
          scope.$watch(attr.ngShowWatchAction(value) {
            //我们为ng-hide添加了一个临时的,特定于动画的类
            //我们可以控制元素在屏幕上显示的时间,而不需要
            //有一个全局的/贪婪的CSS选择器,当其他动画运行时中断.
            //读取:http://github.com/angular/angular.js /问题/ 9103 # issuecomment - 58335845
            $animate[value ? 'removeClass': 'addClass'](元素,NG_HIDE_CLASS, {
              tempClasses: NG_HIDE_IN_PROGRESS_CLASS
            });
          });
        }
      };
    }];
    

    还是第六行. There is a $watch 这就是为什么这个指令是动态的. 在AngularJS源代码中, 所有代码的很大一部分都是描述代码的注释,这些代码从一开始就很容易读懂. 我相信这是学习AngularJS的好方法.

    Conclusion

    本指南涵盖了AngularJS最常见的错误,几乎是其他指南的两倍长. 结果很自然. 对高质量JavaScript前端工程师的需求非常高. AngularJS is so hot right now几年来,它一直在最流行的开发工具中保持着稳定的地位. With AngularJS 2.在未来的几年里,它可能会占据主导地位.

    前端开发的伟大之处在于它是非常有益的. 我们的工作是即时可见的,人们直接与我们交付的产品进行交互. 学习所花费的时间 JavaScript,我相信我们应该专注于JavaScript语言,这是一个非常好的投资. 它是互联网的语言. 竞争非常激烈! 我们有一个重点——用户体验. 要想成功,我们需要包罗万象.

    这些示例中使用的源代码可以从 GitHub. 请随意下载并将其作为您自己的工具.

    我想要感谢4位给了我最大启发的发行开发者:

    我还想感谢FreeNode #angularjs和#javascript频道上的所有伟大的人们,他们进行了许多精彩的对话, 以及持续的支持.

    最后,永远记住:

    //当有疑问时,注释掉它! :)
    
    就这一主题咨询作者或专家.
    Schedule a call
    michael Mikolajczyk的头像
    Michal Mikolajczyk

    Located in Warszawa, Poland

    Member since September 17, 2014

    About the author

    Toptal Warsaw的b区块链物联网初创公司创始人/首席执行官和社区领导者, Michal拥有广泛的全栈经验.

    Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

    Years of Experience

    8

    世界级的文章,每周发一次.

    订阅意味着同意我们的 privacy policy

    世界级的文章,每周发一次.

    订阅意味着同意我们的 privacy policy

    Toptal Developers

    Join the Toptal® community.