Skip to content

AngularJS 速查表

基于 JavaScript 的前端 Web 框架(Angular 1.x)。

01

入门

安装与引导

AngularJS(1.x)通过 script 标签引入。ng-app 引导应用程序启动,ng-controller 将控制器连接到 DOM 子树。双花括号 {{ }} 是 Angular 表达式,用于渲染模型数据。

angularjs
<!-- include AngularJS 1.8.x -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>

<body ng-app="myApp">
  <div ng-controller="MainCtrl">
    {{ message }}
  </div>
</body>

<script>
  var app = angular.module('myApp', []);
  app.controller('MainCtrl', function($scope) {
    $scope.message = 'Hello AngularJS!';
  });
</script>

ng-app 与自动引导

带模块名的 ng-app 会在 DOMContentLoaded 时自动引导 AngularJS。ng-strict-di 强制使用显式依赖注入注解,可在开发阶段及早发现压缩(minification)导致的 bug。每个页面只会自动引导一个 ng-app。

angularjs
<!-- ng-app on <html> makes the whole page an Angular app -->
<html ng-app="myApp" ng-strict-di>
  <head><title>App</title></head>
  <body>
    <div>{{ 1 + 2 }}</div>
  </body>
</html>

<!-- ng-strict-di enforces dependency injection annotations -->
<!-- (errors if a service uses implicit DI without annotation) -->

手动引导

当需要延迟启动时(例如等待异步模块加载或获取配置)使用 angular.bootstrap()。这种方式还允许在一个页面上引导多个 Angular 应用,每个应用对应自己的根元素。

angularjs
<!-- HTML has NO ng-app attribute -->
<div id="appRoot">
  {{ message }}
</div>

<script>
  angular.element(document).ready(function() {
    angular.bootstrap(document.getElementById('appRoot'), ['myApp']);
  });
</script>

第一个数据绑定

当 ng-app 不带模块名时,Angular 以'auto'模式运行并使用默认的 ng 模块。ng-model 将输入绑定到隐式作用域中名为 'name' 的属性,{{ }} 实时渲染它。这是最简单的双向数据绑定形式。

angularjs
<div ng-app>
  <label>Your name:
    <input type="text" ng-model="name">
  </label>
  <p>Hello {{ name || 'stranger' }}!</p>
</div>

模块与控制器基础

angular.module('name', [deps]) 创建模块;angular.module('name') 获取已存在的模块。内联数组注解 ['$scope', function($scope){}] 保证控制器在压缩后仍然安全。创建模块时务必显式传入依赖数组。

angularjs
var app = angular.module('myApp', []); // create module
//          ^name   ^dependencies

app.controller('GreetCtrl', ['$scope', function($scope) {
  $scope.user = { name: 'Ada' };
  $scope.greet = function() {
    return 'Hi ' + $scope.user.name;
  };
}]);
02

模块

创建与获取模块

双参数形式 angular.module(name, deps) 创建一个新模块。单参数形式 angular.module(name) 获取先前定义的模块。误传 [] 两次会静默覆盖模块的所有注册内容。

angularjs
// CREATE a module (second arg = dependency array)
var app = angular.module('myApp', []);

// RETRIEVE an existing module (NO second arg)
// var app = angular.module('myApp');

// common mistake: passing [] again overwrites the module

模块依赖

依赖数组列出的模块,其 provider 会被注入到当前模块。ngRoute 等内置模块是可选的,必须显式列出(且需加载对应的脚本)。子模块让你可以将大型应用拆分为可复用的部分。

angularjs
var app = angular.module('myApp', [
  'ngRoute',          // built-in routing
  'ngAnimate',        // animation hooks
  'ui.router',        // 3rd-party router
  'myApp.common',     // your own sub-module
]);

配置块

config() 在 provider 注册期间、任何服务实例创建之前运行。这是唯一可以注入并配置 Provider(例如 $routeProvider、$locationProvider)的地方。用于设置路由、启用 HTML5 模式或注册自定义验证器。

angularjs
app.config(['$routeProvider', function($routeProvider) {
  $routeProvider
    .when('/home', { templateUrl: 'home.html' })
    .when('/about', { templateUrl: 'about.html' })
    .otherwise({ redirectTo: '/home' });
}]);

运行块

run() 在注入器创建且所有 provider 配置完成后执行——是应用的'main'入口。注入的是实例(而非 provider)。常用于注册全局 $rootScope 监听器,处理路由变更或身份验证检查。

angularjs
app.run(['$rootScope', 'AuthService', function($rootScope, AuthService) {
  $rootScope.$on('$routeChangeStart', function(event, next) {
    if (!AuthService.isAuthenticated()) {
      event.preventDefault();
    }
  });
}]);

常量与值

constant() 注册的值在 config() 块中也可注入——非常适合 provider 需要的配置。value() 仅在运行阶段的构造(服务、控制器)中可注入。需要早期可用的配置应使用 constant。

angularjs
// constant: available in BOTH config and run phases
app.constant('API_BASE', 'https://api.example.com');

// value: available only in run phase (injectable as instance)
app.value('defaultPageSize', 20);

// usage in a service:
app.factory('UserSvc', ['API_BASE', function(API_BASE) {
  return { all: function() { /* use API_BASE */ } };
}]);
03

控制器

基础控制器

控制器设置 $scope 的初始状态并添加行为(函数)。保持控制器精简——业务逻辑应放在服务中。控制器构造函数在每次需要新实例时(例如每个 ng-controller 使用处)运行一次。

angularjs
app.controller('TodoCtrl', ['$scope', function($scope) {
  $scope.todos = [
    { text: 'Learn AngularJS', done: true },
    { text: 'Build an app', done: false }
  ];

  $scope.addTodo = function() {
    $scope.todos.push({ text: $scope.todoText, done: false });
    $scope.todoText = '';
  };

  $scope.remaining = function() {
    return $scope.todos.filter(function(t) { return !t.done; }).length;
  };
}]);

Controller As 语法

controllerAs 以别名(main)将控制器实例发布到作用域上。使用 this 而非 $scope 来定义属性和方法。这避免了 $scope 继承的陷阱,并在嵌套视图中明确属性来自哪个控制器。

angularjs
<div ng-controller="MainCtrl as main">
  <p>{{ main.title }}</p>
  <button ng-click="main.save()">Save</button>
</div>

<script>
app.controller('MainCtrl', function() {
  this.title = 'Controller As';
  this.save = function() { /* ... */ };
});
</script>

压缩安全的依赖注入

压缩工具会重命名函数参数,破坏 Angular 基于名称的依赖推断。内联数组注解在函数前以字符串形式列出服务名,使名称在压缩后仍能保留。始终使用此注解、ng-annotate 或 $inject。

angularjs
// SAFE: inline array annotation (parameter names are string literals)
app.controller('SafeCtrl', ['$scope', '$http', function($scope, $http) {
  $http.get('/api/items').then(function(res) { $scope.items = res.data; });
}]);

// UNSAFE: implicit DI breaks when minified
app.controller('UnsafeCtrl', function($scope, $http) { /* ... */ });

$inject 属性注解

内联数组的替代方案:在构造函数上附加 $inject 服务名数组。这让函数声明更清晰且可复用。两种风格等价——选一种保持一致(或用 ng-annotate 自动化)。

angularjs
function UserCtrl($scope, UserService) {
  UserService.list().then(function(users) {
    $scope.users = users;
  });
}
// explicit dependency list on the function
UserCtrl.$inject = ['$scope', 'UserService'];

app.controller('UserCtrl', UserCtrl);

嵌套控制器与继承

子作用域通过原型链继承父作用域,因此子控制器可读取父级属性。但在子级上赋值原始类型会创建一个新的本地属性,从而遮蔽父级而非更新它——这是一个常见陷阱。优先使用对象(点表示法)或 controllerAs 来避免此问题。

angularjs
<div ng-controller="ParentCtrl">
  <p>{{ shared }}</p>      <!-- sees parent's 'shared' -->
  <div ng-controller="ChildCtrl">
    <p>{{ shared }}</p>    <!-- inherits, can shadow -->
    <p>{{ childOnly }}</p>
  </div>
</div>

app.controller('ParentCtrl', ['$scope', function($scope) {
  $scope.shared = 'from parent';
}]);
app.controller('ChildCtrl', ['$scope', function($scope) {
  $scope.childOnly = 'child only';
  $scope.shared = 'overridden by child'; // shadows parent
}]);

控制器清理($destroy)

监听 $scope 上的 $destroy 事件,在控制器作用域被销毁时(例如导航离开)执行清理。取消 $interval/$timeout 计时器、解绑 window/监听器并释放引用以防内存泄漏。Angular 会自动清理自身的 scope watcher。

angularjs
app.controller('TimerCtrl', ['$scope', '$interval', function($scope, $interval) {
  var timer = $interval(function() { $scope.now = Date.now(); }, 1000);

  $scope.$on('$destroy', function() {
    $interval.cancel(timer); // free the resource
  });
}]);
04

$scope

$scope 基础

$scope 是控制器与视图之间的粘合剂——它持有模板绑定的模型和行为。Angular 为每个控制器创建一个新的作用域;模板中的表达式针对该作用域(及其父级)求值。

angularjs
app.controller('MsgCtrl', ['$scope', function($scope) {
  $scope.message = 'Hi';          // model on scope
  $scope.count = 0;
  $scope.inc = function() {
    $scope.count++;
  };
}]);

// In HTML: {{ message }}, {{ count }}, ng-click="inc()"

作用域继承(原型式)

子作用域通过 JavaScript 原型链继承父作用域。读取父级属性可以工作,但在子级上赋值原始类型(如 $scope.name = 'x')会创建一个本地副本遮蔽父级。绑定到对象(user.name)可使写入沿链向上传播。

angularjs
<!-- primitive on parent: child writes a NEW copy -->
<div ng-controller="ParentCtrl">
  <div ng-controller="ChildCtrl">
    <input ng-model="name"> <!-- shadows parent.name -->
  </div>
</div>

<!-- object avoids the shadowing gotcha -->
app.controller('ParentCtrl', ['$scope', function($scope) {
  $scope.user = { name: 'Ada' }; // child edits user.name -> updates parent
}]);

$rootScope

$rootScope 是作用域层次结构的顶部;所有其他作用域都从它派生。放在这里的属性在所有模板中可见。谨慎使用,仅用于真正的全局值(应用名、会话标志)——滥用会产生隐藏耦合,使状态难以追踪。

angularjs
app.run(['$rootScope', function($rootScope) {
  $rootScope.appName = 'MyApp';     // available everywhere
  $rootScope.version = '1.0';
}]);

<!-- any template -->
<footer>© {{ appName }} {{ version }}</footer>

作用域事件:$emit / $broadcast / $on

$emit 触发一个向上传播至 $rootScope 的事件;$broadcast 向下传播至所有子作用域。$on 注册监听器并返回一个取消注册的函数。通过第二个参数传递数据。在 $destroy 时取消绑定监听器以避免泄漏。

angularjs
// travel UP toward $rootScope
$scope.$emit('userLoggedOut', { userId: 7 });

// travel DOWN to all descendant scopes
$scope.$broadcast('itemAdded', { id: 99 });

// listen for an event
var unbind = $scope.$on('itemAdded', function(event, args) {
  console.log('item', args.id);
});
// stop listening when scope is destroyed
$scope.$on('$destroy', unbind);

$new、$id、$parent

$new(isolated, parent) 创建子(或隔离)作用域;$id 是唯一的数字 id;$parent 引用父作用域。这些主要用于指令内部。始终对手动创建的作用域调用 $destroy() 以移除 watcher 并避免内存泄漏。

angularjs
var child = $scope.$new();        // create a child scope
child.extra = 'child data';
console.log(child.$id);          // unique scope id
console.log(child.$parent === $scope); // true
// ...
child.$destroy();                // remove from hierarchy

隔离作用域(指令预览)

在指令中设置 scope: {} 创建一个不通过原型继承父级的隔离作用域——对可复用组件至关重要。父级只能通过声明的绑定(@、=、&)进行通信。这防止组件意外读取或污染周围作用域。

angularjs
app.directive('userCard', function() {
  return {
    scope: {              // ISOLATE scope: does NOT inherit parent
      user: '=',          // two-way binding
      onKick: '&'         // expression binding
    },
    template: '<p>{{ user.name }}</p>'
  };
});

<!-- usage -->
<user-card user="currentUser" on-kick="removeUser()"></user-card>
05

表达式

基础表达式

Angular 表达式类似 JavaScript,但针对 $scope(而非 window)求值。支持算术、字符串拼接、成员访问和三元运算。与 JS 不同,undefined/null 显示为空字符串(不会显示 'undefined' 文本)。可选链操作符不受支持。

angularjs
<p>{{ 1 + 2 }}</p>                <!-- 3 -->
<p>{{ 'Hello, ' + name }}</p>
<p>{{ user.name }}</p>
<p>{{ items.length }} items</p>
<p>{{ obj?.nested }}</p>           <!-- AngularJS does NOT support ?. -->

表达式与 JavaScript 的区别

表达式比 JS 更受限:不能有语句(if/for/while)、不能 new/throw、不能用逗号、不能定义函数、某些版本不支持位运算。它们以 $scope 为上下文求值,并静默吞掉错误(记录到 $exceptionHandler)。这使模板安全且声明式。

angularjs
<!-- ALLOWED in expressions -->
{{ a + b }}
{{ cond ? 'yes' : 'no' }}
{{ user.getName() }}
{{ [1,2,3].length }}

<!-- NOT allowed: control flow, new, throw, comma operator -->
{{ if (x) { ... } }}     <!-- SyntaxError -->
{{ throw new Error() }}  <!-- not allowed -->

一次性绑定(::)

:: 前缀创建一次性绑定:Angular 求值表达式,一旦它有定义(非 undefined)就移除 watcher。这大幅减少 digest 循环中的 watcher 数量,提升初始加载后不再变化的数据的性能。

angularjs
<!-- AngularJS 1.3+ -->
<p>{{ ::user.name }}</p>           <!-- evaluated once, then unwatched -->
<li ng-repeat="item in ::items">{{ ::item.label }}</li>

<!-- good for static data that never changes -->
<h1>{{ ::conferenceTitle }}</h1>

$eval 与 $parse

$scope.$eval(expr) 解析并对作用域运行表达式字符串。$parse 将表达式一次性编译为可复用的 getter/setter 函数——比反复求值字符串高效得多。在消费表达式绑定的指令/服务中使用 $parse。

angularjs
// evaluate an expression against $scope
var result = $scope.$eval('1 + 2 * count');   // 1 + 2*count
// $scope.$eval(expr, locals)

// compile once, reuse: more efficient for repeated eval
var getter = $parse('user.name');
var name = getter($scope);
getter.assign($scope, 'Ada'); // two-way: sets user.name

表达式中的过滤器

管道 | 对表达式应用过滤器;多个过滤器从左到右链式调用。参数通过冒号传递。过滤器很方便但每次 digest 都会重新运行——对于大型列表,在控制器中通过 $filter 或计算属性过滤以获得更好性能。

angularjs
<p>{{ price | currency }}</p>             <!-- $1.99 -->
<p>{{ today | date:'yyyy-MM-dd' }}</p>    <!-- 2025-01-01 -->
<p>{{ name | uppercase }}</p>
<p>{{ items | filter:query | orderBy:'name' }}</p>

<!-- chained with |, arguments after : -->

ng-bind 与 ng-cloak

在 Angular 引导之前,浏览器会显示原始的 {{ }} 文本('未编译内容的闪烁')。ng-bind 仅在 Angular 运行后将值写入元素的文本,避免闪烁。ng-cloak 结合 CSS 规则在 Angular 编译完成前隐藏元素。

angularjs
<!-- raw {{ }} may flash before Angular compiles -->
<p>{{ message }}</p>

<!-- ng-bind avoids the flash -->
<p ng-bind="message"></p>

<!-- ng-cloak hides elements until compiled -->
<style>[ng-cloak] { display: none; }</style>
<p ng-cloak>{{ message }}</p>
06

内置指令

ng-model(双向绑定)

ng-model 将表单输入绑定到作用域属性,实现双向同步:输入变化更新模型,模型变化更新输入。适用于 input、textarea、select 和 checkbox/radio。始终绑定到带点的属性(user.name)以避免原始类型遮蔽问题。

angularjs
<input type="text" ng-model="user.name">
<p>Hello, {{ user.name }}</p>

<!-- checkbox binds to boolean -->
<input type="checkbox" ng-model="agree"> I agree

<!-- select binds the chosen option's value -->
<select ng-model="color">
  <option value="r">Red</option>
  <option value="g">Green</option>
</select>

ng-repeat

ng-repeat 为集合中的每个项目克隆元素。暴露 $index、$first、$last、$middle、$even、$odd。始终使用 track by 给 Angular 一个稳定的标识,以便在数组变化时复用 DOM 节点——这避免重新渲染所有内容并修复重复键错误。

angularjs
<ul>
  <li ng-repeat="item in items track by item.id">
    {{ $index }}: {{ item.name }}
    <span ng-if="$first">(first)</span>
    <span ng-if="$last">(last)</span>
  </li>
</ul>

<!-- ng-repeat over object -->
<li ng-repeat="(key, value) in config">{{ key }} = {{ value }}</li>

ng-if / ng-show / ng-hide

ng-if 条件性地从 DOM 插入/移除元素(及其子作用域)——切换开销大但隐藏时成本低。ng-show/ng-hide 仅切换 CSS 类,将元素保留在 DOM 中——切换便宜但元素始终被编译。对于很少显示的内容优先使用 ng-if。

angularjs
<!-- ng-if: removes/adds element from DOM -->
<div ng-if="user.isAdmin">Admin tools</div>

<!-- ng-show/ng-hide: toggles display:none only -->
<div ng-show="isLoading">Loading...</div>
<div ng-hide="isLoggedIn">Please log in</div>

ng-class 与 ng-style

ng-class 接受对象(真值键成为类)、类名数组或字符串。ng-style 接受将 CSS 属性名映射到值的对象。两者每次 digest 都重新求值,因此静态样式优先使用普通 class 属性——这些用于状态驱动的样式。

angularjs
<!-- object form: keys are classes, truthy values apply them -->
<div ng-class="{ active: tab === 1, disabled: !enabled }"></div>

<!-- array form -->
<div ng-class="[styleA, styleB]"></div>

<!-- ng-style: object of CSS properties -->
<div ng-style="{ color: theme.color, 'font-size': theme.size + 'px' }">
  Styled text
</div>

ng-bind-template 与 ng-init

ng-bind-template 将多个表达式绑定到元素文本,无需原始 {{ }}。ng-init 在元素初始化时求值一次表达式——适合演示但在实际应用中不鼓励,因为它把逻辑放在了标记中。应在控制器中设置初始状态。

angularjs
<!-- bind multiple expressions to text -->
<p ng-bind-template="Hello {{ first }} {{ last }}"></p>

<!-- ng-init: set a local (use sparingly, prefer controller) -->
<div ng-init="qty = 1; cost = 5">
  Total: {{ qty * cost }}
</div>

ng-src / ng-href / ng-disabled

使用 ng-src 和 ng-href 而非 src='{{ }}',这样浏览器不会在 Angular 插值前对字面量 '{{ }}' URL 发起请求。ng-disabled 根据表达式切换 disabled 属性——非常适合在表单无效或请求进行中时禁用提交按钮。

angularjs
<!-- avoid broken requests before binding resolves -->
<img ng-src="{{ user.avatarUrl }}" alt="avatar">
<a ng-href="{{ profileUrl }}">Profile</a>

<button ng-disabled="form.$invalid || saving">
  {{ saving ? 'Saving...' : 'Save' }}
</button>
07

自定义指令

基础指令

指令扩展 HTML。restrict: 'E' 使其成为自定义元素(<greeting>);'A' 为属性。最简单的指令只返回一个模板。默认使用元素/属性 restrict;类/注释 restrict 很少见。现代最佳实践倾向于对基于模板的指令使用组件。

angularjs
app.directive('greeting', function() {
  return {
    restrict: 'E',              // E=element, A=attr, C=class, M=comment
    template: '<h1>Hello there!</h1>'
  };
});

<!-- usage -->
<greeting></greeting>
<!-- or as attribute: <div greeting></div> -->

指令定义对象(DDO)

指令定义对象配置各个方面:restrict、scope(是否隔离)、template/templateUrl、transclude、controller 和 link 函数。templateUrl 通过 $http 加载模板(带缓存);template 内联。DOM 操作放在 link 中,视图模型逻辑放在 controller 中。

angularjs
app.directive('card', function() {
  return {
    restrict: 'E',
    scope: {},                  // isolate scope
    templateUrl: 'card.html',
    transclude: true,
    controller: function() { /* ... */ },
    controllerAs: 'vm',
    bindToController: {},
    link: function(scope, el, attrs) { /* DOM work */ }
  };
});

隔离作用域绑定(@ = &)

@ 将属性作为字符串读取(单向,父->子,带插值)。= 创建到父级表达式的双向绑定。& 暴露一个在父作用域中执行表达式的函数——用于事件回调。这三个绑定是可复用指令组件的核心。

angularjs
app.directive('profile', function() {
  return {
    restrict: 'E',
    scope: {
      name: '@',          // read attribute as STRING (one-way)
      user: '=',          // two-way binding to expression
      onSave: '&'         // callback: evaluate expression in parent scope
    },
    template: '<p>{{ name }} / {{ user.email }}</p>'
  };
});

<profile name="{{ n }}" user="currentUser" on-save="save()"></profile>

link 函数

link 函数在指令元素编译后运行。用于直接 DOM 操作、事件监听(通过 jQuery Lite 的 .on)以及用 attrs.$observe 观察插值属性。scope 是指令的作用域,element 是 jqLite 包装的 DOM 节点,attrs 是规范化的属性。

angularjs
app.directive('highlight', function() {
  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
      element.on('mouseenter', function() {
        element.css('background', 'yellow');
      });
      element.on('mouseleave', function() {
        element.css('background', '');
      });
      attrs.$observe('highlight', function(val) {
        // react to attribute interpolation changes
      });
    }
  };
});

compile 函数

compile 函数在 Angular 为 ng-repeat 等克隆原始模板元素之前运行一次。用于不需要作用域的模板级转换。它返回一个 link 函数(或 pre/post link 对象)。大多数指令不需要 compile——实例工作优先用 link。

angularjs
app.directive('repeatN', function() {
  return {
    restrict: 'E',
    compile: function(tElement, tAttrs) {
      // runs ONCE on the TEMPLATE element, before cloning
      var n = parseInt(tAttrs.count, 10);
      for (var i = 1; i < n; i++) {
        tElement.after(tElement.clone());
      }
      // optionally return a (post) link function
      return function link(scope, el, attrs) {
        el.addClass('repeated');
      };
    }
  };
});

嵌入(Transclusion)

transclude: true 捕获原始元素内容并将其插入到 ng-transclude 出现的模板位置。被嵌入的内容保留其原始(父)作用域,而非指令的隔离作用域。这让包装指令(panel、modal、card)可以包装调用方提供的任意内容。

angularjs
app.directive('panel', function() {
  return {
    restrict: 'E',
    transclude: true,
    template: '<div class="panel"><div class="panel-body" ng-transclude></div></div>'
  };
});

<!-- the inner content lands inside ng-transclude -->
<panel>
  <h3>Title</h3>
  <p>Body content goes here.</p>
</panel>
08

过滤器

内置过滤器

Angular 内置 number、currency、date、lowercase、uppercase、json、limitTo、orderBy 和 filter。冒号后的参数配置过滤器。currency 接受 ISO 符号或自定义字符。过滤器每次 digest 都重新运行,因此缓存重计算或在控制器中计算。

angularjs
<p>{{ 1234.5 | number:2 }}</p>          <!-- 1,234.50 -->
<p>{{ 9.99 | currency:'USD' }}</p>     <!-- $9.99 -->
<p>{{ 128 | currency:'&euro;' }}</p>   <!-- €128.00 -->
<p>{{ 'hi' | uppercase }}</p>          <!-- HI -->
<p>{{ 'HI' | lowercase }}</p>          <!-- hi -->
<p>{{ 'a b c' | limitTo:2 }}</p>       <!-- a  -->

date 过滤器

date 过滤器格式化 Date 对象、ISO 8601 字符串或纪元毫秒。预定义格式包括 'short'、'medium'、'long'、'full'、'shortDate'、'mediumDate'、'shortTime'。自定义格式使用令牌:y(年)、M(月)、d(日)、H(时)、m(分)、s(秒)、EEEE(完整星期)。

angularjs
<p>{{ now | date }}</p>                       <!-- Jan 1, 2025 -->
<p>{{ now | date:'yyyy-MM-dd' }}</p>        <!-- 2025-01-01 -->
<p>{{ now | date:'shortTime' }}</p>         <!-- 3:45 PM -->
<p>{{ now | date:'EEEE, MMMM d, y' }}</p>   <!-- Wednesday, January 1, 2025 -->

<!-- accepts a Date, ISO string, or millis timestamp -->

filter 过滤器(数组搜索)

filter 过滤器选择匹配字符串、对象或谓词函数的数组项。对象匹配指定字段包含给定值的项目。可选的 true 第三个参数强制精确匹配。对于大型数组,在控制器中过滤以避免每次 digest 重新运行。

angularjs
<!-- match any field containing the query string -->
<li ng-repeat="u in users | filter:query">{{ u.name }}</li>

<!-- match a specific field with an object -->
<li ng-repeat="u in users | filter:{ role: 'admin' }">{{ u.name }}</li>

<!-- strict equality on a field -->
<li ng-repeat="u in users | filter:{ id: 5 }:true">{{ u.name }}</li>

orderBy 过滤器

orderBy 按字段名(前缀 '-' 为降序)、此类字段数组或比较器函数对数组排序。它每次 digest 创建一个新的排序数组——对于大型列表,在控制器中排序一次并复用。与 filter 结合可构建搜索+排序 UI。

angularjs
<!-- ascending by 'name' -->
<li ng-repeat="u in users | orderBy:'name'">{{ u.name }}</li>

<!-- descending -->
<li ng-repeat="u in users | orderBy:'-name'">{{ u.name }}</li>

<!-- multiple fields -->
<li ng-repeat="u in users | orderBy:['role','-age']">{{ u.name }}</li>

<!-- by a function -->
<li ng-repeat="u in users | orderBy:scoreFn">{{ u.name }}</li>

自定义过滤器

用 app.filter 注册过滤器工厂。工厂返回一个转换输入(管道前的值)及任意额外参数的函数。过滤器必须是纯函数、无副作用。它们是可注入的,因此可以在工厂内部依赖其他服务。

angularjs
app.filter('capitalize', function() {
  return function(input) {
    if (!input) return '';
    input = String(input);
    return input.charAt(0).toUpperCase() + input.slice(1).toLowerCase();
  };
});

<!-- usage -->
<p>{{ 'hELLo' | capitalize }}</p>   <!-- Hello -->

控制器中的 $filter 服务

注入 $filter 以在 JS(控制器、服务)中应用过滤器。调用 $filter('name')(value, arg1, arg2)。这避免了每次 digest 重新运行过滤器,并让你复用结果。当你需要在逻辑中(而非仅视图中)使用过滤后的值时很有用。

angularjs
app.controller('ListCtrl', ['$scope', '$filter', function($scope, $filter) {
  $scope.users = [/* ... */];

  // run the orderBy filter programmatically
  $scope.sorted = $filter('orderBy')($scope.users, 'name');

  // currency filter to format a number
  $scope.priceText = $filter('currency')(99.5, '$');
}]);
09

服务($http、$location 等)

$http 基础

$http 执行 XHR 请求并返回 Promise。resolve 的值是包含 data、status、headers 和 config 的响应对象。.then 处理成功,.catch 处理错误。自 Angular 1.6 起,.success/.error 回调被移除——统一使用 promise 的 .then/.catch。

angularjs
app.controller('ApiCtrl', ['$scope', '$http', function($scope, $http) {
  $http.get('/api/users')
    .then(function(response) {
      $scope.users = response.data;       // response: {data, status, headers, config}
    })
    .catch(function(error) {
      console.error('failed', error.status);
    });
}]);

$http POST/PUT 配置

get/post/put/delete/patch/jsonp/head 都有便捷方法。要完全控制,传入配置对象:params 成为查询字符串,headers 设置请求头,responseType 控制响应体如何解析。$http 自动应用默认转换(JSON 解析)。

angularjs
$http.post('/api/users', { name: 'Ada' })
  .then(function(res) { console.log('created', res.data); });

// with headers / query params / response type
$http({
  method: 'GET',
  url: '/api/report',
  params: { format: 'pdf' },
  headers: { 'X-Auth': token },
  responseType: 'arraybuffer'
}).then(function(res) { /* res.data is binary */ });

$location 服务

$location 是 window.location 的包装,与浏览器 URL 和 digest 循环保持同步。用于读取或修改 path、search 参数和 hash。要启用 HTML5 干净 URL,配置 $locationProvider.html5Mode(true) 并添加 <base> 标签。

angularjs
// read the URL
$location.path();          // '/users/42'
$location.search();        // { tab: 'edit' }
$location.hash();          // 'top'
$location.absUrl();        // full URL

// change without full reload
$location.path('/dashboard');
$location.search('q', 'angular');   // ?q=angular
$location.hash('section-2');

$timeout 与 $interval

$timeout 和 $interval 是 setTimeout/setInterval 的感知 digest 包装——它们触发 $apply 以便作用域更新被捕获。始终在 $destroy 时取消它们以防泄漏和'在已销毁作用域上 digest'错误。如不需要 digest,传 false 作为第三个参数。

angularjs
// $timeout: like setTimeout but triggers a $digest
var promise = $timeout(function() {
  $scope.status = 'ready';
}, 1000);

// cancel
$timeout.cancel(promise);

// $interval: like setInterval, also digest-aware
var ticker = $interval(function() { $scope.now = Date.now(); }, 5000);
$scope.$on('$destroy', function() { $interval.cancel(ticker); });

$log 与 $window / $document

$log 是 console 的轻量包装,支持 log/info/warn/error/debug,可被装饰用于生产日志。$window 和 $document 包装全局对象——始终注入它们而非直接调用 window/document,以便测试可以模拟它们,代码保持环境无关。

angularjs
app.controller('LogCtrl', ['$log', '$window', function($log, $window) {
  $log.log('plain');
  $log.info('info');
  $log.warn('warning');
  $log.error('error');

  $window.alert('Hi');      // safer than bare alert()
  $window.innerWidth;
}]);

// $document wraps document; $window wraps window.
// Injecting them (instead of using globals) makes testing easy.

$rootScope.broadcast 与全局事件

使用 $rootScope.$broadcast 发送应用级事件(登录/登出、主题变更)。任何作用域上的监听器都会收到它。对于只需 $rootScope 监听器听到的事件,$rootScope.$emit 更便宜(不下传)。避免滥用全局事件——大多数通信优先使用带显式 API 的服务。

angularjs
// anywhere: broadcast a global event
$rootScope.$broadcast('auth:logout');

// in a controller: listen
$scope.$on('auth:logout', function() {
  $scope.currentUser = null;
});

// $emit on $rootScope = effectively global too
$rootScope.$emit('something', data); // only $rootScope.$on hears this
10

工厂、服务与 Provider

factory()

factory() 接受一个返回服务实例的函数——工厂运行一次(单例),返回值被共享。这是创建服务最常见的方式:非常适合将私有状态封装在简洁的 API 后。返回的任何内容(对象、函数、原始值)都成为该服务。

angularjs
app.factory('UserService', ['$http', function($http) {
  var current = null;            // private state

  return {
    login: function(creds) {
      return $http.post('/login', creds).then(function(res) {
        current = res.data;
        return current;
      });
    },
    getCurrent: function() { return current; },
    isLoggedIn: function() { return !!current; }
  };
}]);

service()

service() 用 new 实例化构造函数,因此你将属性附加到 this。等同于返回新实例的工厂——两者都是单例。当你偏好构造函数/类风格时使用 service();需要返回特定对象或包装某物时使用 factory()。

angularjs
app.service('Counter', function() {
  // invoked with 'new' -> 'this' is the instance
  this.count = 0;
  this.inc = function() { this.count++; };
  this.reset = function() { this.count = 0; };
});

// usage: Counter.inc(); Counter.count;

provider()

provider() 是最底层的配方:它暴露一个配置时对象(带 setter)和一个构建实例的 $get 工厂。只有 provider 可注入到 config() 块中(后缀 'Provider')。当服务需要实例化前配置时使用 provider();否则优先用 factory()。

angularjs
app.provider('Greeter', function() {
  // config-time: defaults settable from .config()
  this.greeting = 'Hello';        // default

  this.setGreeting = function(g) { this.greeting = g; };

  // factory that produces the service instance
  this.$get = function() {
    var self = this;
    return {
      greet: function(name) { return self.greeting + ', ' + name + '!'; }
    };
  };
});

// configure in a config block:
app.config(['GreeterProvider', function(GreeterProvider) {
  GreeterProvider.setGreeting('Bonjour');
}]);

constant() 与 value()

两者都注册一个简单的可注入值。constant() 在配置阶段也可用(因此 provider 可使用)且不可变。value() 仅在运行阶段可用。经验法则:provider 依赖的配置用 constant;共享运行时数据或辅助函数用 value。

angularjs
// constant: injectable in config AND run phases
app.constant('MAX_USERS', 1000);

// value: injectable in run phase only (not config)
app.value('toastConfig', { duration: 3000 });

// value can hold any type, even a function
app.value('logger', function(msg) { console.log(msg); });

factory、service 与 provider 对比

底层每种配方都是 provider:factory 的函数成为 $get,service 是 new-up 构造函数的工厂。按可读性选择:大多数情况用 factory,构造函数风格类用 service,需要实例化前配置时用 provider。组件只消费,所以选择是内部的。

angularjs
// all three are singletons; the difference is HOW the instance is built

// factory: function returns the instance
app.factory('A', function() { return { x: 1 }; });

// service: constructor is 'new'-ed
app.service('B', function() { this.x = 1; });

// provider: $get returns the instance (configurable)
app.provider('C', function() {
  this.$get = function() { return { x: 1 }; };
});

$provide.decorator()

decorator() 拦截现有服务的实例化——$delegate 是原始实例,你可以包装、扩展或替换它。用于横切关注点(日志、缓存、错误报告)而无需触碰原始服务。装饰器是惰性的,仅在服务首次被注入时运行。

angularjs
app.decorator('$log', ['$delegate', function($delegate) {
  // $delegate is the original $log instance
  var origError = $delegate.error;
  $delegate.error = function(msg) {
    // e.g., send to error tracking, then call original
    trackError(msg);
    origError.apply($delegate, arguments);
  };
  return $delegate;   // return the (modified) service
}]);
11

路由(ngRoute 与 ui-router)

ngRoute 设置

ngRoute 是内置路由器(在单独的 angular-route.js 文件中)。用 .when(path, config) 注册路由,用 .otherwise() 设置回退。ng-view 是匹配的模板+控制器渲染的出口。ngRoute 仅支持一个出口和简单的 :param URL——嵌套视图请用 ui-router。

angularjs
<!-- include angular-route.js (separate file in 1.x) -->
<script src="angular-route.js"></script>

var app = angular.module('myApp', ['ngRoute']);

app.config(['$routeProvider', function($routeProvider) {
  $routeProvider
    .when('/home', { templateUrl: 'home.html', controller: 'HomeCtrl' })
    .when('/users/:id', { templateUrl: 'user.html', controller: 'UserCtrl' })
    .otherwise({ redirectTo: '/home' });
}]);

<!-- where the matched view renders -->
<div ng-view></div>

ngRoute 路由参数

$routeParams 将 URL 段(:id)暴露为字符串键对象——值始终是字符串,因此数字需用 parseInt 转换。它在路由变更后更新。动态链接使用 ng-href。默认 hashbang 前缀是 #!(自 1.6 起);用 $locationProvider.hashPrefix('') 配置。

angularjs
app.controller('UserCtrl', ['$routeParams', 'UserSvc',
  function($routeParams, UserSvc) {
    // URL /users/42  ->  $routeParams.id === '42' (always a string)
    this.user = UserSvc.get($routeParams.id);
  }]);

<!-- links -->
<a href="#!/users/42">View user 42</a>
<!-- with HTML5 mode, use a plain href="/users/42" -->

ngRoute 生命周期事件

ngRoute 广播 $routeChangeStart(可通过 event.preventDefault 取消)、$routeChangeSuccess 和 $routeChangeError。'next'/'current' 参数携带路由配置(含 resolve 结果)。用于身份验证守卫、加载指示器和面包屑标题。resolve 拒绝会触发 $routeChangeError。

angularjs
app.run(['$rootScope', function($rootScope) {
  $rootScope.$on('$routeChangeStart', function(event, next, current) {
    if (next.requireAuth && !AuthService.isLoggedIn()) {
      event.preventDefault();             // cancel navigation
    }
  });

  $rootScope.$on('$routeChangeSuccess', function(event, current, previous) {
    $rootScope.pageTitle = current.$$route.title;
  });

  $rootScope.$on('$routeChangeError', function(event, current, previous, rejection) {
    console.error('route failed', rejection);
  });
}]);

ui-router 状态

ui-router 将应用组织成命名状态(而非 URL 路径)。ui-sref 按状态名和参数链接,ui-view 是出口。状态可嵌套并有自己的 URL。这解耦了导航与 URL,并支持多个/命名视图——对真实应用远比 ngRoute 强大。

angularjs
var app = angular.module('myApp', ['ui.router']);

app.config(['$stateProvider', function($stateProvider) {
  $stateProvider
    .state('home', { url: '/home', templateUrl: 'home.html' })
    .state('users', { url: '/users', templateUrl: 'users.html' })
    .state('users.detail', {
      url: '/:id',
      templateUrl: 'user-detail.html',
      controller: 'UserDetailCtrl'
    });
}]);

<!-- outlet -->
<div ui-view></div>
<!-- links: state name instead of URL -->
<a ui-sref="users.detail({ id: 42 })">User 42</a>

ui-router resolve 与嵌套状态

resolve 将键映射到 promise;在所有 resolve 完成前不会实例化控制器。如果某个 resolve 拒绝,会触发 $stateChangeError 且不进入该状态。resolve 的值按键可注入到控制器中。子状态继承并可访问父级的 resolve——对数据预加载很有用。

angularjs
$stateProvider.state('users.detail', {
  url: '/:id',
  templateUrl: 'user-detail.html',
  controller: 'UserDetailCtrl',
  controllerAs: 'vm',
  resolve: {
    // injected into the controller; waits for these before activating
    user: ['UserSvc', '$stateParams', function(UserSvc, $stateParams) {
      return UserSvc.get($stateParams.id);   // returns a promise
    }]
  }
});

app.controller('UserDetailCtrl', ['user', function(user) {
  this.user = user;   // already resolved
}]);

ui-router 多命名视图

ui-router 可为每个状态渲染多个命名 ui-view 出口。views 映射的键匹配模板中的 ui-view 名称;空字符串键指向未命名出口。这让单个状态可组合多个区域(header、sidebar、main)——ngRoute 的单一 ng-view 无法实现。

angularjs
$stateProvider.state('dashboard', {
  url: '/dashboard',
  views: {
    '':        { templateUrl: 'dashboard/main.html' },
    'sidebar': { templateUrl: 'dashboard/sidebar.html', controller: 'SideCtrl' },
    'header':  { templateUrl: 'dashboard/header.html' }
  }
});

<!-- index.html -->
<div ui-view="header"></div>
<div ui-view="sidebar"></div>
<div ui-view></div>          <!-- the unnamed '' view -->
12

表单与验证

带 ng-model 的表单

给表单一个 name 属性以在作用域上暴露其验证状态。novalidate 关闭浏览器内置的提示气泡,让 Angular 控制验证。每个命名的输入发布自己的状态(myForm.email.$valid、$dirty、$error)。ng-model 是验证跟踪输入所必需的。

angularjs
<form name="myForm" novalidate>
  <input name="email" type="email" ng-model="user.email" required>
  <input name="age" type="number" ng-model="user.age" min="18" required>

  <button ng-disabled="myForm.$invalid" ng-click="save()">Save</button>
</form>

<!-- novalidate disables the browser's native validation so Angular handles it -->

验证状态

每个输入暴露 $pristine/$dirty(是否改变)、$touched/$untouched(是否失焦)、$valid/$invalid 和 $error(失败验证器的映射)。表单聚合这些:任一输入无效则 myForm.$invalid 为 true。仅在 $touched 或 $dirty 后显示消息,以免在用户输入前就提示。

angularjs
<input name="email" ng-model="user.email" required>
<p ng-show="myForm.email.$touched && myForm.email.$error.required">
  Email is required.
</p>

<!-- states available on each input / the form -->
<!-- $pristine, $dirty, $touched, $untouched, $valid, $invalid, $error -->

内置验证器

内置验证器:required、type=email/url/number、min/max、ng-minlength/ng-maxlength、ng-pattern。每个失败的验证器在 $error 中设置一个键(如 $error.required、$error.minlength、$error.pattern)。HTML 属性中的 ng-pattern 正则需转义反斜杠。

angularjs
<input name="email" type="email" ng-model="user.email" required>
<input name="age" type="number" ng-model="user.age" min="18" max="120">
<input name="code" ng-model="user.code"
       ng-minlength="4" ng-maxlength="8" required>
<input name="url" type="url" ng-model="user.url">
<input name="zip" ng-model="user.zip" ng-pattern="/^\d{5}$/">

自定义验证器(指令)

通过 require ngModel 并在 ctrl.$validators 上注册函数的指令添加自定义验证器。返回 true 为有效,false 为无效——失败出现在 $error 中你的键下(此处为 'even')。异步验证使用 ctrl.$asyncValidators 返回 promise。

angularjs
app.directive('evenNumber', function() {
  return {
    require: 'ngModel',
    link: function(scope, el, attrs, ctrl) {
      ctrl.$validators.even = function(modelValue, viewValue) {
        if (ctrl.$isEmpty(modelValue)) return true;   // let 'required' handle empty
        return parseInt(modelValue, 10) % 2 === 0;
      };
    }
  };
});

<input ng-model="n" even-number>
<p ng-show="form.n.$error.even">Must be even!</p>

ngMessages

ngMessages 显示第一个匹配的错误(或用 ng-messages-multiple 显示全部)。比堆叠 ng-if 处理每个验证器清晰得多。将共享消息提取到局部文件并用 ng-messages-include='messages.html' 引入。它与内置和自定义验证器键都能良好配合。

angularjs
<!-- include angular-messages.js, add module 'ngMessages' -->
<script src="angular-messages.js"></script>

<div ng-messages="myForm.email.$error" ng-if="myForm.email.$touched">
  <div ng-message="required">Email is required.</div>
  <div ng-message="email">Enter a valid email.</div>
  <div ng-message="server">That email is taken.</div>
</div>

<!-- a template can be reused across fields with ng-messages-include -->

表单提交

ng-submit 在表单提交时(回车键或 type='submit' 按钮)触发,且仅在 ng-model 验证通过时(除非覆盖)。将 myForm.$valid 传给处理程序作为安全检查。novalidate 确保 Angular(而非浏览器)驱动验证。用 ng-disabled 禁用按钮以防止重复提交。

angularjs
<form name="myForm" ng-submit="save(myForm.$valid)" novalidate>
  <input name="email" ng-model="user.email" required>
  <button type="submit" ng-disabled="myForm.$invalid">Save</button>
</form>

app.controller('FormCtrl', ['$scope', function($scope) {
  $scope.save = function(isValid) {
    if (!isValid) return;
    // ...submit to server
  };
}]);
13

事件(ng-click、ng-change 等)

ng-click

ng-click 在元素被点击时求值一个表达式。$event 是原生 DOM 事件,因此可以在其上调用 stopPropagation/preventDefault——不过表单优先用 ng-submit,能用内置指令的地方尽量用。ng-click 适用于任何元素,不只是按钮。

angularjs
<button ng-click="count = count + 1">Inc</button>
<span>{{ count }}</span>

<!-- call a scope method -->
<button ng-click="save()">Save</button>

<!-- pass the native event with $event -->
<button ng-click="save($event)">Save</button>
<button ng-click="save($event); $event.stopPropagation()">Save</button>

ng-change

ng-change 在 ng-model 更新模型后触发——所以 $scope.country 已经是新值。它要求同一元素上有 ng-model。用于响应输入/选择/复选框变化(级联下拉、实时过滤)而无需自己设置 change 监听器。

angularjs
<select ng-model="country" ng-change="onCountryChange()">
  <option value="us">US</option>
  <option value="uk">UK</option>
</select>

app.controller('LocCtrl', ['$scope', function($scope) {
  $scope.onCountryChange = function() {
    // fires AFTER ng-model updates $scope.country
    $scope.cities = CityService.forCountry($scope.country);
  };
}]);

ng-submit 与 ng-focus/blur 系列

ng-submit 处理表单提交。Angular 还为许多 DOM 事件提供事件指令:ng-focus、ng-blur、ng-change、ng-copy、ng-cut、ng-paste、ng-keydown/up/press、ng-mousedown/up/enter/over/leave/move 和 ng-dblclick。每个求值一个表达式,$event 可用。

angularjs
<!-- ng-submit: on form submission -->
<form ng-submit="search()">
  <input ng-model="query">
</form>

<!-- the ng-focus / ng-blur / ng-copy / ng-cut / ng-paste directives -->
<input ng-model="q" ng-focus="onFocus()" ng-blur="onBlur()">
<input ng-copy="copied = true">

键盘与鼠标事件

Angular 提供 ng-keydown/ng-keypress/ng-keyup 和 ng-mousedown/up/enter/leave/over/move 以及 ng-dblclick。每个都传递 $event,因此可检查 $event.keyCode 或 $event.shiftKey。对于应用级快捷键,在服务中绑定 $document 或 $window,而非在模板中散布处理程序。

angularjs
<!-- keyboard events -->
<input ng-keydown="onKey($event)">
<input ng-keypress="onKey($event)">

<!-- mouse events -->
<div ng-mouseenter="hover = true" ng-mouseleave="hover = false">
  {{ hover ? 'Hovering' : 'Not hovering' }}
</div>
<button ng-dblclick="open()">Double-click</button>

$event 对象

$event 是转发给处理程序的原生 DOM 事件。可读取 target、坐标、修饰键,并调用 preventDefault/stopPropagation。抵制在点击处理程序中放重逻辑——改为调用控制器/服务上的方法。$event 在每个事件指令中都可用。

angularjs
<button ng-click="onClick($event)">Click</button>

app.controller('ClickCtrl', ['$scope', function($scope) {
  $scope.onClick = function(event) {
    console.log(event.target);    // clicked element
    console.log(event.clientX, event.clientY);
    event.preventDefault();
    event.stopPropagation();
  };
}]);

事件修饰符(手动)

与 Vue/Angular 2+ 不同,AngularJS 没有 .stop/.prevent/.once 修饰符——你在处理程序内部自行调用事件方法。这更冗长但显式。对于经常需要的行为(提交时自动 preventDefault),ng-submit 已为表单做了这件事。

angularjs
<!-- AngularJS has NO built-in modifiers like .stop / .prevent.
     Call the method on $event instead. -->
<a href="/delete" ng-click="del($event)">Delete</a>

app.controller('DelCtrl', ['$scope', function($scope) {
  $scope.del = function(event) {
    event.preventDefault();   // stop the link navigation
    $scope.removeItem();
  };
}]);
14

$watch / $digest / $apply

$scope.$watch

$watch 注册一个在 digest 期间被监听值变化时运行的监听器。第一个参数可以是字符串表达式或函数;回调接收 (newVal, oldVal)。$watch 返回一个取消注册函数——调用它以停止监听并避免泄漏,尤其在指令中。

angularjs
app.controller('WatchCtrl', ['$scope', function($scope) {
  $scope.user = { name: 'Ada' };

  // watch a path expression (string)
  var unbind = $scope.$watch('user.name', function(newVal, oldVal) {
    console.log('name changed', oldVal, '->', newVal);
  });

  // stop watching (frees resources)
  unbind();
}]);

$watchCollection

$watchCollection 监视数组或对象的一层:在添加/移除/替换项目时触发,但不会深度比较项目属性。比深度 $watch(第三个参数 true)便宜得多。用于原始值数组或只关心列表成员资格而非深度内容的场景。

angularjs
$scope.items = [1, 2, 3];

// watch array/object shallowly: detects add/remove/replace but NOT deep mutation
$scope.$watchCollection('items', function(newItems, oldItems) {
  console.log('collection changed', newItems);
});

// fires on push/splice, or when an item is replaced,
// but NOT when an item's own properties change.

深度 $watch(第三个参数)

将 true 作为第三个参数使 $watch 进行深度值比较——任何嵌套属性变化时触发。这很昂贵(每次 digest 遍历整个对象)。优先监听特定路径或使用 $watchCollection。仅在确实需要的小对象上保留深度监听。

angularjs
$scope.config = { ui: { theme: 'dark' } };

// third arg true -> deep comparison by value (expensive!)
$scope.$watch('config', function(n, o) {
  console.log('config (deep) changed');
}, true);

// prefer watching a specific path when possible:
$scope.$watch('config.ui.theme', function(n, o) { /* cheaper */ });

$scope.$apply

Angular 之外的代码(裸 setTimeout、不带 $http 的 XHR、jQuery 事件、Promise 回调)更新模型但 Angular 不知道——调用 $apply() 触发 $digest。函数形式包装并异常处理变更,运行一次 digest。在 Angular 服务($http、$timeout)内 $apply 已完成——不要重复 apply。

angularjs
// NON-Angular async callback (e.g., a 3rd-party library) won't trigger a digest
externalLib.onReady(function() {
  $scope.message = 'Ready!';

  // tell Angular to run a digest so the view updates
  $scope.$apply();
});

// or wrap the change so errors are caught and digest runs once
$scope.$apply(function() {
  $scope.message = 'Ready!';
  $scope.ready = true;
});

$evalAsync 与 $timeout(安全的 apply)

当你不确定 digest 是否正在进行时,优先用 $evalAsync 或 $timeout(fn, 0) 而非手动 $apply——它们避免 'digest already in progress' 错误。$evalAsync 批处理进当前 digest;$timeout 延迟到下一个 tick。当你还需要 DOM 已渲染时用 $timeout。

angularjs
// $evalAsync: schedule work later in the SAME digest if one is running,
//             otherwise start a new one. Safer than $apply.
$scope.$evalAsync(function() {
  $scope.computed = heavyCompute();
});

// $timeout(..., 0) always runs in a fresh digest — useful when you
// need the DOM updated before your code runs.
$timeout(function() {
  $scope.deferred = true;
}, 0);

$digest 循环详解

$digest 是将模型变更传播到视图的循环:它重新求值每个 watcher 直到值不再变化(脏检查),最多 10 次迭代。由于每个 watcher 每次 digest 都运行,性能随 watcher 数量变化。通过一次性绑定(::)、track by 和将计算移入控制器来减少 watcher。

angularjs
// a $digest iterates over ALL watchers on $scope and its children:
//   1. read each watched value, compare to previous
//   2. if changed, call the listener
//   3. listeners may change other watched values -> re-run
//   4. repeat until stable (max 10 iterations -> error)

// so: keep watchers cheap and few.
// track by, one-time binding (::), and compute in controllers help.
15

组件(.component())

.component() 基础

.component() 是指令的语法糖,为基于模板的组件优化。它默认 restrict 'E'、隔离作用域和 controllerAs '$ctrl'——因此用 this(别名 $ctrl)而非 $scope。bindings 替代 scope:{...};自 1.5 起输入优先用 '<'(单向)而非 '='。

angularjs
app.component('heroDetail', {
  template: '<h2>{{$ctrl.hero.name}}</h2><p>{{$ctrl.hero.power}}</p>',
  bindings: {
    hero: '<'        // one-way ('<'), two-way ('='), string ('@'), expr ('&')
  }
});

<!-- usage -->
<hero-detail hero="selectedHero"></hero-detail>

带控制器与绑定的组件

组件的控制器使用 this(模板中别名 $ctrl)。bindings 声明输入('<'、'=')和输出('&')。输入优先用单向 '<'。输出用 '&'——以参数对象调用它们,该对象成为父级表达式的局部变量:onChange({ value: ... })。

angularjs
app.component('counter', {
  bindings: {
    start: '<',
    onChange: '&'
  },
  template: `
    <button ng-click="$ctrl.dec()">-</button>
    <span>{{ $ctrl.count }}</span>
    <button ng-click="$ctrl.inc()">+</button>
  `,
  controller: function() {
    this.$onInit = function() { this.count = this.start || 0; };
    this.inc = function() { this.count++; this.onChange({ value: this.count }); };
    this.dec = function() { this.count--; this.onChange({ value: this.count }); };
  }
});

组件生命周期钩子

AngularJS 1.5+ 的组件钩子镜像 Angular 2+ 的生命周期:$onInit(初始化)、$onChanges(绑定变化,带 isFirstChange)、$doCheck(每次 digest)、$postLink(子 DOM 链接后)、$onDestroy(清理)。用它们替代把逻辑塞进构造函数——它们让组件行为显式且可测试。

angularjs
app.component('editor', {
  bindings: { doc: '<', onClose: '&' },
  controller: function() {
    this.$onInit = function() { /* after bindings, before first render */ };
    this.$onChanges = function(changes) {
      // changes.doc: { currentValue, previousValue, isFirstChange() }
    };
    this.$doCheck = function() { /* every digest: custom dirty checking */ };
    this.$onDestroy = function() { /* cleanup: timers, listeners */ };
    this.$postLink = function() { /* after child linking, DOM ready */ };
  }
});

$onChanges 详解

$onChanges 在单向('<')绑定的引用变化时触发,带一个 changes 对象:每个键有 currentValue、previousValue 和 isFirstChange()。它不会因同一对象的变异触发(因为引用未变)——那种情况用 $doCheck。初始化时总会触发一次,isFirstChange() 为 true。

angularjs
app.component('userView', {
  bindings: { user: '<' },
  controller: function() {
    this.$onChanges = function(changesObj) {
      // changesObj is keyed by binding name
      if (changesObj.user) {
        var c = changesObj.user;
        console.log(c.previousValue, '->', c.currentValue);
        console.log('first time?', c.isFirstChange());
      }
    };
  }
});

组件中的嵌入

设置 transclude: true 并在模板中包含 ng-transclude 以让调用方注入内容。在组件中,还支持多槽嵌入,通过 transclude: { slotName: 'elementName' }。嵌入让你构建可复用的包装器(panel、modal、card),其主体由父级提供。

angularjs
app.component('modal', {
  transclude: true,
  template: `
    <div class="modal">
      <div class="modal-body" ng-transclude></div>
    </div>
  `,
  bindings: { title: '@' }
});

<!-- caller's content lands in ng-transclude -->
<modal title="Confirm">
  <p>Are you sure?</p>
  <button>Yes</button>
</modal>

组件与指令对比

渲染模板的一切都用组件——它们更简单,并推动你走向 Angular 2+ 模型(隔离作用域、this/controllerAs、生命周期钩子)。将指令保留给无模板的属性行为(autofocus、tooltip 触发器、输入掩码)和 link 中的直接 DOM 操作。

angularjs
// COMPONENT: template-based, isolate scope, controllerAs by default
app.component('userCard', {
  template: '...',
  bindings: { user: '<' }
});

// DIRECTIVE: still needed for attribute behavior / DOM manipulation
app.directive('autoFocus', function() {
  return { restrict: 'A', link: function(scope, el) { el[0].focus(); } };
});
16

$q Promise 服务

$q.defer() 基础

$q 是 Angular 的 Promise 实现(Promises/A+)。用 $q.defer() 创建,然后 resolve(value) 或 reject(reason)。promise 是感知 digest 的:resolve 会触发 $digest 使视图更新。需要 Angular 自动 digest 时优先用 $q 而非原生 Promise;否则原生 Promise 也可。

angularjs
app.factory('AsyncWork', ['$q', function($q) {
  return function() {
    var deferred = $q.defer();

    setTimeout(function() {
      if (success) deferred.resolve('done');
      else         deferred.reject(new Error('failed'));
    }, 100);

    return deferred.promise;     // { then, catch, finally }
  };
}]);

// consume
AsyncWork().then(function(v) { console.log(v); })
          .catch(function(e) { console.error(e); });

$q.when / $q.all / $q.reject

$q.when 将普通值或非 $q 的 promise 包装为 Angular promise。$q.all 等待数组或对象的 promise 并以相同形状的结果 resolve。$q.reject(reason) 是返回已拒绝 promise 的便捷方式(例如链中早期验证)。这些都是感知 digest 的。

angularjs
// $q.when: wrap a value (or foreign promise) in an Angular promise
$q.when(42).then(function(v) { /* v === 42 */ });

// $q.all: wait for many promises in parallel
$q.all([getUser(), getPosts(), getFriends()]).then(function(results) {
  var user = results[0], posts = results[1], friends = results[2];
});

// $q.all with an object (named results)
$q.all({ user: getUser(), posts: getPosts() }).then(function(r) {
  r.user; r.posts;
});

// $q.reject: shortcut to return a rejected promise
return $q.reject(new Error('bad input'));

Promise 链式调用

从 .then 返回值会传给下一个处理程序;返回 promise 会让链等待。单个 .catch 处理上游任何拒绝。.finally 在成功和失败时都运行——非常适合隐藏加载器。每个处理程序在 Angular 的 digest 内运行,因此作用域更新自动生效。

angularjs
getUser(id)
  .then(function(user) {
    return getOrders(user);    // return a promise -> chain waits
  })
  .then(function(orders) {
    return orders.filter(paid); // return a value -> next step
  })
  .then(function(paidOrders) {
    $scope.paid = paidOrders;
  })
  .catch(function(err) {
    // any rejection above lands here
    console.error(err);
  })
  .finally(function() {
    $scope.loading = false;    // runs regardless
  });

$http 返回 Promise

$http 已返回 $q promise,因此链式 .then 转换响应(通常提取 .data)并返回更干净的值。将 $http 包装在服务中给调用方整洁的 API,并集中 URL/基础 URL/错误处理。拦截器是另一种应用横切 $http 行为的方式。

angularjs
app.factory('UserApi', ['$http', function($http) {
  return {
    list: function() { return $http.get('/users').then(function(r) { return r.data; }); },
    get:  function(id) { return $http.get('/users/' + id).then(function(r) { return r.data; }); }
  };
}]);

// caller doesn't deal with response.data, just the user
UserApi.get(42).then(function(user) { $scope.user = user; });

$q 构造函数(ES6 风格)

自 1.6 起,$q 支持 ES6 Promise 构造函数形式:$q(function(resolve, reject){...})。这比 $q.defer() 更简洁且匹配原生 Promise 语法。包装单个异步 API 时使用。生成的 promise 仍感知 digest,因此 resolve 时视图更新自动触发。

angularjs
// Angular 1.6+ supports the resolver-style constructor, like native Promise
app.factory('loadImg', ['$q', function($q) {
  return function(src) {
    return $q(function(resolve, reject) {
      var img = new Image();
      img.onload  = function() { resolve(img); };
      img.onerror = function() { reject(new Error('load failed')); };
      img.src = src;
    });
  };
}]);

$q 与原生 Promise 对比

关键区别:$q 在 resolve 时触发 $digest,原生 Promise 不会。在运行于现代浏览器的 AngularJS 应用中,可以使用原生 Promise 但必须将模型更新包装在 $apply(或 $evalAsync)中。为一致性和便利性,Angular 代码内部优先用 $q。

angularjs
// native Promise: no auto-digest; you must $apply manually
Promise.resolve().then(function() {
  $scope.x = 1;
  $scope.$apply();           // otherwise the view won't update
});

// $q promise: resolves inside a $digest automatically
$q.when().then(function() { $scope.x = 1; });   // view updates
17

动画(ngAnimate)

ngAnimate 设置

ngAnimate 是一个单独的模块(自己的 JS 文件)。一旦作为依赖添加,它会自动增强内置指令,使它们在进入/离开/移动过渡期间添加 CSS 类——你只需写 CSS。常见情况无需 JS。没有 ngAnimate,这些指令会瞬间改变 DOM。

angularjs
<!-- include angular-animate.js, then depend on 'ngAnimate' -->
<script src="angular-animate.js"></script>

var app = angular.module('myApp', ['ngAnimate']);

<!-- ngAnimate now adds classes automatically to:
     ng-repeat (enter/leave/move), ng-if, ng-show, ng-hide,
     ng-class, ng-view, ng-include, ui-view -->

CSS 过渡类

ngAnimate 为每个事件添加两个类:一个起始类(如 .ng-enter)和一帧后的 -active 类(.ng-enter-active)。在起始类上定义 CSS 过渡,在 active 类上定义结束状态——过渡结束时 Angular 移除两者。这是最简单、最高性能的动画方法。

angularjs
/* .ng-enter / .ng-leave / .ng-move are added automatically */
.fade.ng-enter        { transition: opacity 0.3s; opacity: 0; }
.fade.ng-enter-active { opacity: 1; }

.fade.ng-leave        { transition: opacity 0.3s; opacity: 1; }
.fade.ng-leave-active { opacity: 0; }

<!-- apply the .fade class to your ng-if / ng-repeat element -->
<div class="fade" ng-if="show">Hello</div>

CSS 关键帧动画

可以使用 CSS @keyframes 动画替代过渡——ngAnimate 切换相同的 .ng-enter/.ng-leave/.ng-move 类,关键帧运行。使用关键帧只需单个起始类(不需要 -active),因为动画驱动整个序列。适合弹跳/弹簧效果。

angularjs
/* use @keyframes; ngAnimate still toggles the classes */
.pop.ng-enter { animation: pop-in 0.3s; }

@keyframes pop-in {
  0%   { transform: scale(0); }
  100% { transform: scale(1); }
}

<div class="pop" ng-if="visible">Pop!</div>

ng-repeat 的进入/离开/移动

ng-repeat 项目获得 .ng-enter(新增)、.ng-leave(移除)和 .ng-move(重排)类。使用 track by 让 Angular 能在变化间匹配项目并正确动画化移动。stagger 动画可用 .ng-enter-stagger / .ng-leave-stagger 延迟每个项目——非常适合列表显现效果。

angularjs
/* list items animate when added, removed, or reordered */
.list-item.ng-enter        { transition: all 0.3s; opacity: 0; transform: translateY(-10px); }
.list-item.ng-enter-active { opacity: 1; transform: translateY(0); }

.list-item.ng-leave-active { opacity: 0; transform: translateX(20px); }

/* .ng-move animates reordering (track by recommended) */
.list-item.ng-move        { transition: transform 0.3s; }
.list-item.ng-move-active { }

<ul><li class="list-item" ng-repeat="t in todos track by t.id">{{ t.text }}</li></ul>

$animate 服务(JS 动画)

app.animation(cssSelector, factory) 为匹配选择器的元素注册 JS 动画。enter/leave/move/class 方法接收元素和 done 回调。当 CSS 不够时(例如 jQuery.animate 或物理库)有用。返回取消函数用于清理。JS 动画比 CSS 重——可能时优先用 CSS。

angularjs
app.animation('.fade-js', ['$animate', function($animate) {
  return {
    enter: function(element, done) {
      element.css('opacity', 0);
      element.animate({ opacity: 1 }, 300, done);
      return function(cancelled) { /* cleanup if cancelled */ };
    },
    leave: function(element, done) {
      element.animate({ opacity: 0 }, 300, done);
    }
  };
}]);

<div class="fade-js" ng-if="show">Hello</div>

禁用动画

用 $animate.enabled 全局或按子树关闭动画,或通过 ng-animate-disabled 类。这对大型列表性能或测试时禁用动画很有用。ng-animate-children 让父级选择让其子级一起动画(默认父级动画会跳过子级动画)。

angularjs
<!-- ng-animate-ref / ng-animate-sref control animation between routes -->

<!-- opt out per-element -->
<div ng-if="x" ng-animate-children>...</div>
<div ng-if="y" class="ng-animate-disabled">...</div>

// programmatically
$animate.enabled(false);                    // globally off
$animate.enabled(false, element);           // off for subtree
$animate.enabled(true, element);            // back on
18

测试(Karma 与 Jasmine)

Karma 配置草图

Karma 是 AngularJS 的标准测试运行器:它启动真实浏览器,加载你的脚本及 angular-mocks,并报告 Jasmine 结果。angular-mocks 提供 module() 和 inject() 辅助函数及模拟服务($httpBackend)。singleRun:true 在一次运行后退出(CI);省略以进入监听模式。

angularjs
// karma.conf.js (sketch)
module.exports = function(config) {
  config.set({
    frameworks: ['jasmine'],
    files: [
      'node_modules/angular/angular.js',
      'node_modules/angular-mocks/angular-mocks.js',
      'src/**/*.js',
      'test/**/*.spec.js'
    ],
    browsers: ['ChromeHeadless'],
    singleRun: true
  });
};

// run: karma start karma.conf.js

Jasmine 基础

Jasmine 提供 describe(套件)、it(用例)、beforeEach/afterEach(设置/拆卸)和带匹配器的 expect(toBe、toEqual、toContain、toBeTruthy、toThrow 等)。xit/xdescribe 跳过用例。测试默认同步;异步用 done 回调或 Jasmine 的 async/await 支持。

angularjs
describe('UserService', function() {
  beforeEach(function() { /* setup */ });
  afterEach(function() { /* teardown */ });

  it('isLoggedIn is false by default', function() {
    var svc = makeService();
    expect(svc.isLoggedIn()).toBe(false);
  });

  it('greets by name', function() {
    expect(greet('Ada')).toEqual('Hi, Ada');
  });

  // pending / disabled
  xit('not yet implemented', function() {});
});

测试控制器

module('myApp') 加载 Angular 模块;inject() 解析服务(通常用前后下划线别名)。用 $controller('Name', { $scope: scope }) 创建控制器并传入假依赖。然后对作用域断言。此模式也适用于 controllerAs:传递 this 绑定的方法并检查返回的实例。

angularjs
describe('TodoCtrl', function() {
  beforeEach(module('myApp'));

  var $controller, $scope;
  beforeEach(inject(function(_$controller_, _$rootScope_) {
    $scope = _$rootScope_.$new();
    $controller = _$controller_;
  }));

  it('starts with two todos', function() {
    $controller('TodoCtrl', { $scope: $scope });
    expect($scope.todos.length).toBe(2);
  });

  it('addTodo pushes and clears the input', function() {
    $controller('TodoCtrl', { $scope: $scope });
    $scope.todoText = 'New';
    $scope.addTodo();
    expect($scope.todos.length).toBe(3);
    expect($scope.todoText).toBe('');
  });
});

测试服务

注入被测服务和 angular-mocks 的 $httpBackend。expectGET(url).respond(data) 设置假响应;flush() 同步解析待处理请求。afterEach 中的 verifyNoOutstandingExpectation() 捕获未满足的期望。这使基于 $http 的服务无需真实服务器即可完全测试。

angularjs
describe('UserService', function() {
  var UserService, $httpBackend;

  beforeEach(module('myApp'));
  beforeEach(inject(function(_UserService_, _$httpBackend_) {
    UserService = _UserService_;
    $httpBackend = _$httpBackend_;
  }));

  afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); });

  it('list() fetches users', function() {
    $httpBackend.expectGET('/api/users').respond([{ name: 'Ada' }]);
    var users;
    UserService.list().then(function(u) { users = u; });
    $httpBackend.flush();          // resolve the pending request
    expect(users.length).toBe(1);
  });
});

测试指令

用 $compile(html)(scope) 编译一段 HTML,然后运行 scope.$digest() 求值绑定并链接指令。对生成的 jqLite 元素断言(el.find、el.text、el.attr)。对于 templateUrl,用 $httpBackend.expectGET 提供模板,然后在 digest 前 flush。通过 el.isolateScope() 测试隔离作用域。

angularjs
describe('greeting directive', function() {
  var $compile, $rootScope;
  beforeEach(module('myApp'));
  beforeEach(inject(function(_$compile_, _$rootScope_) {
    $compile = _$compile_; $rootScope = _$rootScope_;
  }));

  it('renders the template', function() {
    var scope = $rootScope.$new();
    scope.name = 'Ada';
    var el = $compile('<greeting name="name"></greeting>')(scope);
    scope.$digest();              // process watchers / bindings
    expect(el.find('h1').text()).toContain('Ada');
  });
});

$httpBackend 模拟

expectGET(及 expectPOST 等)断言请求被发起,若未发起则失败;whenGET 在被调用时静默返回数据,不调用则忽略。用 expect 做'应该发生'的断言,用 when 做设置。flush() 按顺序解析待处理请求。始终在 afterEach 中调用 verifyNoOutstanding* 以捕获不匹配。

angularjs
// EXPECT: a specific request must happen (asserts it was made)
$httpBackend.expectGET('/api/users').respond(200, [{ id: 1 }]);
// WHEN:  just stub a request if it happens (no assertion)
$httpBackend.whenGET('/api/users').respond(200, [{ id: 1 }]);

// flush triggers all pending requests:
$httpBackend.flush();

// verify nothing was left unflushed:
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
19

依赖注入

依赖注入基础

Angular 的 DI 通过检查函数参数名来解析依赖。这种'隐式'风格很简洁但在压缩后(参数名被缩短)会失效。原型/演示可用。生产环境请使用注解风格之一——内联数组或 $inject——或用 ng-annotate 自动化。

angularjs
// services are injected by NAME into the constructor's parameters
app.controller('A', function($scope, $http, UserService) { /* ... */ });

// Angular inspects the parameter names to resolve dependencies.
// works in development but BREAKS after minification (see below).

内联数组注解

最常见的压缩安全风格:传入一个数组,其元素是服务名(字符串),构造函数在最后。字符串在压缩后保留,因此 Angular 正确解析。任何将被压缩的代码都应使用此方式。手动维护较繁琐——ng-annotate 或 babel 插件可自动添加。

angularjs
app.controller('A', ['$scope', '$http', 'UserService',
  function($scope, $http, UserService) {
    // parameter names no longer matter; the strings drive DI
    $scope.users = UserService.list();
  }
]);

$inject 属性注解

在构造函数上附加 $inject 服务名数组。Angular 读取它解析依赖,忽略参数名。这让函数声明更清晰且同一构造函数可复用。ng-annotate 可在构建时从参数名自动生成 $inject。

angularjs
function ACtrl($scope, UserService) {
  $scope.users = UserService.list();
}
ACtrl.$inject = ['$scope', 'UserService'];   // explicit list

app.controller('A', ACtrl);

// equivalent to the inline array form; useful when you want
// a separately-declared, reusable constructor function.

$injector 服务

$injector 是 DI 容器:.get(name) 返回单例,.annotate(fn) 返回依赖列表,.instantiate/.invoke 构建/调用带依赖的函数。应用代码中很少需要,但在动态场景(按名加载服务)和自定义指令或路由 resolve 内部有用。

angularjs
// resolve and instantiate on demand
var svc = $injector.get('UserService');          // retrieve singleton
var fn  = $injector.annotate(MyCtrl);            // [' $scope', ...] via reflection
var inst = $injector.instantiate(MyCtrl);        // 'new' with DI
var res  = $injector.invoke(MyCtrl, context);    // call with DI

// list all registered service names (debug)
// $injector has modules / providers info

ng-annotate(构建时)

ng-annotate 是一个构建时工具,自动添加内联数组注解,因此你写干净的隐式 DI 并发布压缩安全的代码。用 /* @ngInject */ 消除棘手情况的歧义。它与 gulp、grunt、webpack、babel 集成。后继者 ng-annotate-patched 维护对现代工具链的支持。

angularjs
// Write clean code with a hint comment:
app.controller('A', /* @ngInject */ function($scope, UserService) {
  $scope.users = UserService.list();
});

// ng-annotate (run by your build tool) rewrites it to:
app.controller('A', ['$scope', 'UserService', function($scope, UserService) {
  $scope.users = UserService.list();
}]);

严格 DI 模式

ng-strict-di 使隐式(未注解)依赖注入在运行时抛出错误,因此你在开发而非生产中捕获压缩不安全的代码。在开发和测试中保持开启。它略微影响性能(额外检查),因此通常在生产构建中移除。

angularjs
<html ng-app="myApp" ng-strict-di>
  ...
</html>

// or when bootstrapping manually:
angular.bootstrap(root, ['myApp'], { strictDi: true });

// in strict-DI mode, any service using IMPLICIT (unannotated) DI
// throws an error, catching minification bugs during development.
20

最佳实践与性能

使用 controllerAs

controllerAs 明确每个属性属于哪个控制器,避免作用域继承的原始类型遮蔽,并让你更接近 Angular 2+ 的'组件'模型。绑定到 this(别名)而非 $scope。对于组件,$ctrl 是默认别名——一致使用它。

angularjs
<!-- prefer controllerAs over $scope -->
<div ng-controller="UsersCtrl as users">
  <li ng-repeat="u in users.list">{{ u.name }}</li>
</div>

app.controller('UsersCtrl', function(UserService) {
  // 'this' is exposed as 'users' in the template
  this.list = UserService.list();
  this.remove = function(id) { UserService.remove(id); };
});

一次性绑定(::)

用 :: 前缀表达式创建一次性绑定:Angular 求值一次,定义后移除其 watcher。对于不变的数据(标题、时间戳、静态列表),这显著减少 watcher 数量——是 AngularJS 应用中最大的单一性能提升。

angularjs
<!-- static data: bind once, drop the watcher -->
<h1>{{ ::conference.name }}</h1>
<span>{{ ::user.createdAt | date }}</span>

<!-- ng-repeat over a static list -->
<li ng-repeat="item in ::staticItems">{{ item.label }}</li>

<!-- less watchers = faster digest for the whole app -->

ng-repeat 中的 track by

没有 track by,Angular 使用内部 $$hashKey 匹配项目——数组变化时可能为实际未变的项目重新渲染并重建 DOM,丢失子状态。track by u.id 给出稳定标识让 Angular 复用节点。这对大型列表性能至关重要,并修复 'ng-repeat duplicates' 错误。

angularjs
<!-- BAD: Angular uses $$hashKey, re-renders everything on change -->
<li ng-repeat="u in users">{{ u.name }}</li>

<!-- GOOD: stable identity -> Angular reuses DOM nodes -->
<li ng-repeat="u in users track by u.id">{{ u.name }}</li>

<!-- for primitives, use $index (only if list doesn't reorder) -->
<li ng-repeat="n in nums track by $index">{{ n }}</li>

将逻辑移至服务

控制器难以测试且绑定到 $scope;服务是单例、可注入且可单元测试的。将业务逻辑、数据访问和共享状态推入服务,让控制器专注于将视图连接到这些服务。这种分离也让多个控制器共享同一逻辑。

angularjs
// BAD: logic crammed in the controller
app.controller('BadCtrl', function($scope) {
  $scope.calc = function() { /* 50 lines of business logic */ };
});

// GOOD: controller delegates to a testable service
app.controller('GoodCtrl', function(PriceService) {
  this.total = PriceService.calcTotal(this.items);   // thin
});

app.factory('PriceService', function() {
  return { calcTotal: function(items) { /* ... */ } };
});

绑定一次/减少 watcher

每个 {{ }} 和大多数绑定都添加一个每次 digest 运行的 watcher。避免在绑定中调用函数(即使输入不变每次 digest 也重新运行)——预计算并存储结果。结合一次性绑定(::)、track by 和控制器端计算,在大页面上保持低 watcher 数量。

angularjs
<!-- compute once in the controller, render many times -->
app.controller('Ctrl', function() {
  this.sortedUsers = this.users.slice().sort(byName); // do it once
});

<!-- avoid function calls in bindings: they re-run each digest -->
<!-- BAD:  {{ heavyCompute() }} -->
<!-- GOOD: compute once, bind the result -->
<p>{{ cachedResult }}</p>

模块组织

按功能(而非文件类型)组织,使每个功能成为自包含模块,包含其控制器、服务、模板、路由和测试。根应用模块只组合功能模块。这让相关代码在一起,使功能可跨应用复用,扩展性远优于单个巨型模块。

angularjs
// split features into sub-modules, each self-contained
angular.module('myApp.users', ['myApp.common']);
angular.module('myApp.posts', ['myApp.common']);
angular.module('myApp', ['myApp.users', 'myApp.posts', 'ngRoute']);

// file layout per feature:
//   users/users.module.js
//   users/users.controller.js
//   users/users.service.js
//   users/users.html
//   users/users.routes.js
//   users/users.spec.js

这篇内容对您有帮助吗?