AngularJS 依赖注入:让代码更灵活、更易维护
在开发前端应用时,我们经常需要在不同的模块之间传递数据或共享功能。如果每个组件都自己去创建依赖对象,代码会变得混乱、耦合度高,修改起来也容易出错。而 AngularJS 提供了一种强大的机制——依赖注入(Dependency Injection,简称 DI),它就像一个“中央调度中心”,自动帮你管理对象的创建和传递。
想象一下,你是一家公司的行政主管,每天要为不同部门安排会议室、打印资料、联系供应商。如果每个部门都自己去采购文具、联系物业,那效率一定很低,还容易重复浪费。而如果你设立一个行政团队,统一协调资源,每个部门只需要“申请”所需服务,就能快速获得支持——这就是依赖注入的核心思想。
在 AngularJS 中,依赖注入让开发者不再手动创建对象,而是通过声明“我需要什么”,框架自动为你提供。这种模式不仅提升了代码的可维护性,也极大方便了单元测试和模块化开发。
什么是依赖注入?为什么它重要?
依赖注入是一种设计模式,它的核心理念是:不主动去创建依赖项,而是由外部容器来提供。换句话说,你不需要知道某个服务是如何创建的,只需要声明“我需要它”,框架会自动帮你完成。
举个例子,假设你有一个用户管理模块,需要调用一个“日志服务”来记录操作。没有依赖注入时,你可能这样写:
// 错误示范:手动创建依赖
function UserService() {
var logger = new Logger(); // 手动创建日志服务
this.logUserAction = function (action) {
logger.log(action); // 使用日志服务
};
}
问题来了:如果将来要更换日志服务的实现(比如从控制台输出改成写入文件),你就得修改所有使用 Logger 的地方。这违背了“开闭原则”——对扩展开放,对修改关闭。
而使用 AngularJS 依赖注入后,你可以这样写:
// 正确方式:声明依赖,由框架提供
app.service('UserService', function(Logger) {
// Logger 是由 AngularJS 注入进来的,无需手动创建
this.logUserAction = function(action) {
Logger.log(action); // 直接使用注入的实例
};
});
这里的关键是:Logger 并不是你创建的,而是 AngularJS 框架根据名称自动查找并传入的。这种解耦方式,让代码更清晰、更易维护。
如何注册服务?从 provider 到 factory
AngularJS 提供了多种方式来注册服务,它们各有用途,但最终目的都是让依赖可以被注入。
使用 provider 注册服务
provider 是最灵活的注册方式,它允许你在应用启动前进行配置。它提供了一个 $get 方法,用于定义如何创建服务实例。
app.provider('ConfigService', function() {
var apiUrl = 'https://api.example.com'; // 默认配置
// 可以在配置阶段修改
this.setApiUrl = function(url) {
apiUrl = url;
};
// 定义如何创建服务实例
this.$get = function() {
return {
getApiUrl: function() {
return apiUrl;
}
};
};
});
// 在配置阶段使用
app.config(function(ConfigServiceProvider) {
ConfigServiceProvider.setApiUrl('https://dev-api.example.com');
});
说明:
app.provider('ConfigService', ...):注册一个名为ConfigService的提供者。this.$get:定义服务的构造函数,返回实际可用的对象。app.config(...):在配置阶段调用setApiUrl,修改默认行为。
使用 factory 注册服务
factory 是更简洁的注册方式,适合大多数情况。它直接返回一个对象,不需要显式定义 $get。
app.factory('DataService', function() {
var data = [];
return {
add: function(item) {
data.push(item);
},
getAll: function() {
return data;
}
};
});
说明:
app.factory('DataService', ...):注册一个名为DataService的工厂。- 返回的是一个对象,这个对象会被注入到需要它的组件中。
- 每次注入时,都会返回同一个实例(单例模式)。
使用 service 注册服务
service 本质上是 factory 的语法糖。它通过构造函数的方式注册服务。
app.service('UserService', function() {
this.users = [];
this.addUser = function(user) {
this.users.push(user);
};
this.getUsers = function() {
return this.users;
};
});
说明:
app.service('UserService', ...):注册服务。- 使用
this指向服务实例。 - 内部自动创建实例,等价于
factory返回一个new出来的对象。
| 注册方式 | 特点 | 适用场景 |
|---|---|---|
| provider | 最灵活,支持配置阶段修改 | 需要动态配置的服务,如 API 地址、密钥等 |
| factory | 简洁,返回对象 | 大多数业务逻辑服务 |
| service | 语法更直观,使用 this |
面向对象风格的服务 |
依赖注入的注入方式:构造函数注入 vs 数组注入
AngularJS 支持两种注入语法,推荐使用数组注入,尤其在压缩代码时更安全。
构造函数注入(不推荐用于生产)
app.controller('MainController', function(Logger, UserService) {
// Logger 和 UserService 会自动注入
this.message = 'Hello from controller';
this.log = function() {
Logger.log('Controller loaded');
};
});
这种方式简洁,但在压缩代码后,参数名会被重命名(如 Logger 变成 a),导致注入失败。
数组注入(推荐,适用于生产环境)
app.controller('MainController', ['$scope', 'Logger', 'UserService', function($scope, Logger, UserService) {
$scope.message = 'Hello from controller';
$scope.log = function() {
Logger.log('Controller loaded');
};
$scope.users = UserService.getUsers();
}]);
说明:
- 第一个元素是依赖列表,明确写出需要注入的名称。
- 最后一个元素是函数体,参数顺序必须与依赖列表一致。
- 即使代码被压缩,依赖名称也不会被改变,因为它们是以字符串形式存在的。
✅ 小贴士:数组注入是生产环境的首选,能有效避免 minify 后的注入失败问题。
实战案例:构建一个完整的用户管理模块
我们来搭建一个完整的示例,展示依赖注入在实际项目中的应用。
// 创建 AngularJS 应用
var app = angular.module('userApp', []);
// 注册日志服务
app.service('Logger', function() {
this.log = function(message) {
console.log('LOG: ' + message);
};
});
// 注册数据服务
app.factory('UserDataService', function(Logger) {
var users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
return {
getAll: function() {
Logger.log('Fetching all users');
return users;
},
add: function(user) {
user.id = users.length + 1;
users.push(user);
Logger.log('User added: ' + user.name);
}
};
});
// 注册控制器
app.controller('UserController', ['$scope', 'UserDataService', 'Logger', function($scope, UserDataService, Logger) {
$scope.users = UserDataService.getAll();
$scope.newUser = { name: '', email: '' };
$scope.addUser = function() {
UserDataService.add($scope.newUser);
$scope.users = UserDataService.getAll();
$scope.newUser = { name: '', email: '' }; // 清空表单
Logger.log('User added via form');
};
}]);
运行逻辑说明:
Logger服务被注入到UserDataService,用于记录操作。UserDataService和Logger被注入到UserController。- 控制器调用服务方法,完成增删改查逻辑。
- 所有依赖都由 AngularJS 自动注入,无需手动创建。
这个结构清晰、职责分明,方便后期扩展和测试。
常见问题与最佳实践
1. 依赖注入不是“魔法”,它需要注册
你不能随便在控制器里注入一个服务,除非先在模块中注册。比如:
app.controller('MyController', function(NonExistentService) {
// ❌ 报错:NonExistentService 未注册
});
必须先:
app.service('NonExistentService', function() { ... });
2. 单例模式:服务实例是共享的
在 AngularJS 中,factory 和 service 注册的服务,在整个应用生命周期中只有一个实例。这适合需要全局状态的服务,比如用户登录状态、配置信息等。
3. 避免在服务中直接依赖 DOM
服务应该尽量保持“无状态”和“无 DOM 依赖”。如果需要操作 DOM,应该放在指令(directive)中,而不是服务。
4. 使用命名空间组织服务
建议将服务按功能分组,如 UserService、AuthService、StorageService,避免命名冲突。
结语
AngularJS 依赖注入不仅是一种技术,更是一种工程思想。它让代码更加模块化、可测试、可维护。当你习惯了“声明依赖,由框架提供”的方式后,你会发现开发效率大幅提升,调试也更轻松。
从今天开始,不要再手动 new 对象了。学会使用 factory、service 注册服务,用数组注入语法编写控制器,你就能真正掌握 AngularJS 依赖注入的精髓。
无论是构建小型应用,还是大型企业级系统,依赖注入都是一把利器。掌握它,就是掌握了一种更优雅、更专业的前端开发方式。