AngularJS 依赖注入(完整教程)

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

运行逻辑说明

  1. Logger 服务被注入到 UserDataService,用于记录操作。
  2. UserDataServiceLogger 被注入到 UserController
  3. 控制器调用服务方法,完成增删改查逻辑。
  4. 所有依赖都由 AngularJS 自动注入,无需手动创建。

这个结构清晰、职责分明,方便后期扩展和测试。


常见问题与最佳实践

1. 依赖注入不是“魔法”,它需要注册

你不能随便在控制器里注入一个服务,除非先在模块中注册。比如:

app.controller('MyController', function(NonExistentService) {
    // ❌ 报错:NonExistentService 未注册
});

必须先:

app.service('NonExistentService', function() { ... });

2. 单例模式:服务实例是共享的

在 AngularJS 中,factoryservice 注册的服务,在整个应用生命周期中只有一个实例。这适合需要全局状态的服务,比如用户登录状态、配置信息等。

3. 避免在服务中直接依赖 DOM

服务应该尽量保持“无状态”和“无 DOM 依赖”。如果需要操作 DOM,应该放在指令(directive)中,而不是服务。

4. 使用命名空间组织服务

建议将服务按功能分组,如 UserServiceAuthServiceStorageService,避免命名冲突。


结语

AngularJS 依赖注入不仅是一种技术,更是一种工程思想。它让代码更加模块化、可测试、可维护。当你习惯了“声明依赖,由框架提供”的方式后,你会发现开发效率大幅提升,调试也更轻松。

从今天开始,不要再手动 new 对象了。学会使用 factoryservice 注册服务,用数组注入语法编写控制器,你就能真正掌握 AngularJS 依赖注入的精髓。

无论是构建小型应用,还是大型企业级系统,依赖注入都是一把利器。掌握它,就是掌握了一种更优雅、更专业的前端开发方式。