入门
安装与引导
AngularJS(1.x)通过 script 标签引入。ng-app 引导应用程序启动,ng-controller 将控制器连接到 DOM 子树。双花括号 {{ }} 是 Angular 表达式,用于渲染模型数据。
<!-- 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。
<!-- 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 应用,每个应用对应自己的根元素。
<!-- 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' 的属性,{{ }} 实时渲染它。这是最简单的双向数据绑定形式。
<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){}] 保证控制器在压缩后仍然安全。创建模块时务必显式传入依赖数组。
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;
};
}]);模块
创建与获取模块
双参数形式 angular.module(name, deps) 创建一个新模块。单参数形式 angular.module(name) 获取先前定义的模块。误传 [] 两次会静默覆盖模块的所有注册内容。
// 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 等内置模块是可选的,必须显式列出(且需加载对应的脚本)。子模块让你可以将大型应用拆分为可复用的部分。
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 模式或注册自定义验证器。
app.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/home', { templateUrl: 'home.html' })
.when('/about', { templateUrl: 'about.html' })
.otherwise({ redirectTo: '/home' });
}]);运行块
run() 在注入器创建且所有 provider 配置完成后执行——是应用的'main'入口。注入的是实例(而非 provider)。常用于注册全局 $rootScope 监听器,处理路由变更或身份验证检查。
app.run(['$rootScope', 'AuthService', function($rootScope, AuthService) {
$rootScope.$on('$routeChangeStart', function(event, next) {
if (!AuthService.isAuthenticated()) {
event.preventDefault();
}
});
}]);常量与值
constant() 注册的值在 config() 块中也可注入——非常适合 provider 需要的配置。value() 仅在运行阶段的构造(服务、控制器)中可注入。需要早期可用的配置应使用 constant。
// 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 */ } };
}]);控制器
基础控制器
控制器设置 $scope 的初始状态并添加行为(函数)。保持控制器精简——业务逻辑应放在服务中。控制器构造函数在每次需要新实例时(例如每个 ng-controller 使用处)运行一次。
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 继承的陷阱,并在嵌套视图中明确属性来自哪个控制器。
<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。
// 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 自动化)。
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 来避免此问题。
<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。
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
});
}]);$scope
$scope 基础
$scope 是控制器与视图之间的粘合剂——它持有模板绑定的模型和行为。Angular 为每个控制器创建一个新的作用域;模板中的表达式针对该作用域(及其父级)求值。
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)可使写入沿链向上传播。
<!-- 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 是作用域层次结构的顶部;所有其他作用域都从它派生。放在这里的属性在所有模板中可见。谨慎使用,仅用于真正的全局值(应用名、会话标志)——滥用会产生隐藏耦合,使状态难以追踪。
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 时取消绑定监听器以避免泄漏。
// 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 并避免内存泄漏。
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: {} 创建一个不通过原型继承父级的隔离作用域——对可复用组件至关重要。父级只能通过声明的绑定(@、=、&)进行通信。这防止组件意外读取或污染周围作用域。
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>表达式
基础表达式
Angular 表达式类似 JavaScript,但针对 $scope(而非 window)求值。支持算术、字符串拼接、成员访问和三元 运算。与 JS 不同,undefined/null 显示为空字符串(不会显示 'undefined' 文本)。可选链操作符不受支持。
<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)。这使模板安全且声明式。
<!-- 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 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。
// 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 或计算属性过滤以获得更好性能。
<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 编译完成前隐藏元素。
<!-- 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>内置指令
ng-model(双向绑定)
ng-model 将表单输入绑定到作用域属性,实现双向同步:输入变化更新模型,模型变化更新输入。适用于 input、textarea、select 和 checkbox/radio。始终绑定到带点的属性(user.name)以避免原始类型遮蔽问题。
<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 节点——这避免重新渲染所有内容并修复重复键错误。
<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。
<!-- 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 属性——这些用于状态驱动的样式。
<!-- 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 在元素初始化时求值一次表达式——适合演示但在实际应用中不鼓励,因为它把逻辑放在了标记中。应在控制器中设置初始状态。
<!-- 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 属性——非常适合在表单无效或请求进行中时禁用提交按钮。
<!-- 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>自定义指令
基础指令
指令扩展 HTML。restrict: 'E' 使其成为自定义元素(<greeting>);'A' 为属性。最简单的指令只返回一个模板。默认使用元素/属性 restrict;类/注释 restrict 很少见。现代最佳实践倾向于对基于模板的指令使用组件。
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 中。
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 */ }
};
});隔离作用域绑定(@ = &)
@ 将属性作为字符串读取(单向,父->子,带插值)。= 创建到父级表达式的双向绑定。& 暴露一个在父作用域中执行表达式的函数——用于事件回调。这三个绑定是可复用指令组件的核心。
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 是规范化的属性。
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。
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)可以包装调用方提供的任意 内容。
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>过滤器
内置过滤器
Angular 内置 number、currency、date、lowercase、uppercase、json、limitTo、orderBy 和 filter。冒号后的参数配置过滤器。currency 接受 ISO 符号或自定义字符。过滤器每次 digest 都重新运行,因此缓存重计算或在控制器中计算。
<p>{{ 1234.5 | number:2 }}</p> <!-- 1,234.50 -->
<p>{{ 9.99 | currency:'USD' }}</p> <!-- $9.99 -->
<p>{{ 128 | currency:'€' }}</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(完整星期)。
<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 重新运行。
<!-- 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。
<!-- 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 注册过滤器工厂。工厂返回一个转换输入(管道前的值)及任意额外参数的函数。过滤器必须是纯函数、无副作用。它们是可注入的,因此可以在工厂内部依赖其他服务。
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 重新运行过滤器,并让你复用结果。当你需要在逻辑中(而非仅视图中)使用过滤后的值时很有用。
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, '$');
}]);服务($http、$location 等)
$http 基础
$http 执行 XHR 请求并返回 Promise。resolve 的值是包含 data、status、headers 和 config 的响应对象。.then 处理成功,.catch 处理错误。自 Angular 1.6 起,.success/.error 回调 被移除——统一使用 promise 的 .then/.catch。
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 解析)。
$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> 标签。
// 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 作为第三个参数。
// $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,以便测试可以模拟它们,代码保持环境无关。
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 的服务。
// 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工厂、服务与 Provider
factory()
factory() 接受一个返回服务实例的函数——工厂运行一次(单例),返回值被共享。这是创建服务最常见的方式:非常适合将私有状态封装在简洁的 API 后。返回的任何内容(对象、函数、原始值)都成为该服务。
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()。
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()。
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。
// 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。组件只消费,所以选择是内部的。
// 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 是原始实例,你可以包装、扩展或替换它。用于横切关注点(日志、缓存、错误报告)而无需触碰原始服务。装饰器是惰性的,仅在服务首次被注入时运行。
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
}]);路由(ngRoute 与 ui-router)
ngRoute 设置
ngRoute 是内置路由器(在单独的 angular-route.js 文件中)。用 .when(path, config) 注册路由,用 .otherwise() 设置回退。ng-view 是匹配的模板+控制器渲染的出口。ngRoute 仅支持一个出口和简单的 :param URL——嵌套视图请用 ui-router。
<!-- 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('') 配置。
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。
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 强大。
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——对数据预加载很有用。
$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 无法实现。
$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 -->表单与验证
带 ng-model 的表单
给表单一个 name 属性以在作用域上暴露其验证状态。novalidate 关闭浏览器内置的提示气泡,让 Angular 控制验证。每个命名的输入发布自己的状态(myForm.email.$valid、$dirty、$error)。ng-model 是验证跟踪输入所必需的。
<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 后显示消息,以免在用户输入前就提示。
<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 正则需转义反斜杠。
<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。
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' 引入。它与内置和自定义验证器键都能良好配合。
<!-- 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 禁用按钮以防止重复提交。
<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
};
}]);事件(ng-click、ng-change 等)
ng-click
ng-click 在元素被点击时求值一个表达式。$event 是原生 DOM 事件,因此可以在其上调用 stopPropagation/preventDefault——不过表单优先用 ng-submit,能用内置指令的地方尽量用。ng-click 适用于任何元素,不只是按钮。
<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 监听器。
<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 可用。
<!-- 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,而非在模板中散布处理程序。
<!-- 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 在每个事件指令中都可用。
<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 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();
};
}]);$watch / $digest / $apply
$scope.$watch
$watch 注册一个在 digest 期间被监听值变化时运行的监听器。第一个参数可以是字符串表达式或函数;回调接收 (newVal, oldVal)。$watch 返回一个取消注册函数——调用它以停止监听并避免泄漏,尤其在指令中。
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)便宜得多。用于原始值数组或只关心列表成员资格而非深度内容的场景。
$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。仅在确实需要的小对象上保留深度监听。
$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。
// 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。
// $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。
// 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.组件(.component())
.component() 基础
.component() 是指令的语法糖,为基于模板的组件优化。它默认 restrict 'E'、隔离作用域和 controllerAs '$ctrl'——因此用 this(别名 $ctrl)而非 $scope。bindings 替代 scope:{...};自 1.5 起输入优先用 '<'(单向)而非 '='。
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: ... })。
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(清理)。用它们替代把逻辑塞进构造函数——它们让组件行为显式且可测试。
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。
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),其主体由父级提供。
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 操作。
// 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(); } };
});$q Promise 服务
$q.defer() 基础
$q 是 Angular 的 Promise 实现(Promises/A+)。用 $q.defer() 创建,然后 resolve(value) 或 reject(reason)。promise 是感知 digest 的:resolve 会触发 $digest 使视图更新。需要 Angular 自动 digest 时优先用 $q 而非原生 Promise;否则原生 Promise 也可。
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 的。
// $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 内运行,因此作用域更新自动生效。
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 行为的方式。
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 时视图更新自动触发。
// 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。
// 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动画(ngAnimate)
ngAnimate 设置
ngAnimate 是一个单独的模块(自己的 JS 文件)。一旦作为依赖添加,它会自动增强内置指令,使它们在进入/离开/移动过渡期间添加 CSS 类——你只需写 CSS。常见情况无需 JS。没有 ngAnimate,这些指令会瞬间改变 DOM。
<!-- 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 移除两者。这是最简单、最高性能的动画方法。
/* .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),因为动画驱动整个序列。适合弹跳/弹簧效果。
/* 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 延迟每个项目——非常适合列表显现效果。
/* 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。
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 让父级选择让其子级一起动画(默认父级动画会跳过子级动画)。
<!-- 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测试(Karma 与 Jasmine)
Karma 配置草图
Karma 是 AngularJS 的标准测试运行器:它启动真实浏览器,加载你的脚本及 angular-mocks,并报告 Jasmine 结果。angular-mocks 提供 module() 和 inject() 辅助函数及模拟服务($httpBackend)。singleRun:true 在一次运行后 退出(CI);省略以进入监听模式。
// 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.jsJasmine 基础
Jasmine 提供 describe(套件)、it(用例)、beforeEach/afterEach(设置/拆卸)和带匹配器的 expect(toBe、toEqual、toContain、toBeTruthy、toThrow 等)。xit/xdescribe 跳过用例。测试默认同步;异步用 done 回调或 Jasmine 的 async/await 支持。
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 绑定的方法并检查返回的实例。
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 的服务无需真实服务器即可完全测试。
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() 测试隔离作用域。
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* 以捕获不匹配。
// 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();依赖注入
依赖注入基础
Angular 的 DI 通过检查函数参数名来解析依赖。这种'隐式'风格很简洁但在压缩后(参数名被缩短)会失效。原型/演示可用。生产环境请使用注解风格之一——内联数组或 $inject——或用 ng-annotate 自动化。
// 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 插件可自动添加。
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。
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 内部有用。
// 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 infong-annotate(构建时)
ng-annotate 是一个构建时工具,自动添加内联数组注解,因此你写干净的隐式 DI 并发布压缩安全的代码。用 /* @ngInject */ 消除棘手情况的歧义。它与 gulp、grunt、webpack、babel 集成。后继者 ng-annotate-patched 维护对现代工具链的支持。
// 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 使隐式(未注解)依赖注入在运行时抛出错误,因此你在开发而非生产中捕获压缩不安全的代码。在开发和测试中保持开启。它略微影响性能(额外检查),因此通常在生产构建中移除。
<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.最佳实践与性能
使用 controllerAs
controllerAs 明确每个属性属于哪个控制器,避免作用域继承的原始类型遮蔽,并让你更接近 Angular 2+ 的'组件'模型。绑定到 this(别名)而非 $scope。对于组件,$ctrl 是默认别名——一致使用它。
<!-- 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 应用中最大的单一性能提升。
<!-- 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' 错误。
<!-- 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;服务是单例、可注入且可单元测试的。将业务逻辑、数据访问和共享状态推入服务,让控制器专注于将视图连接到这些服务。这种分离也让多个控制器共享同一逻辑。
// 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 数量。
<!-- 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>模块组织
按功能(而非文件类型)组织,使每个功能成为自包含模块,包含其控制器、服务、模板、路由和测试。根应用模块只组合功能模块。这让相关代码在一起,使功能可跨应用复用,扩展性远优于单个巨型模块。
// 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相关 AngularJS 代码片段
Copy-paste ready code for common tasks.
Module & Controller
Define a module and a controller with scope methods.
Scope Inheritance
Prototypal scope inheritance between parent and child controllers.
Custom Directives
Attribute directive and element directive with isolated scope.
Services & Factories
Factory and service singletons for shared state and logic.
Routing (ngRoute)
Configure routes with templates, controllers, and resolve guards.
Custom Filters
Chainable filters for formatting values in templates.
Forms & Validation
Form with required, minlength, email validation, and disabled submit.
$http Service
Promise-based HTTP requests with config object and error handling.
这篇内容对您有帮助吗?