1. 为什么需要Service:AngularJS的模块化设计哲学
在AngularJS的世界里,Service不是可有可无的装饰品,而是框架设计哲学的核心体现。我第一次接触AngularJS时,曾把所有逻辑都堆在控制器里,结果代码很快变成了难以维护的"意大利面条"。直到理解了Service的真正价值,才明白AngularJS团队为何如此强调"关注点分离"。
Service本质上是一种可注入的单例对象,它的生命周期与整个应用保持一致。这与控制器形成鲜明对比——控制器会随着路由切换不断创建和销毁。想象你正在开发一个电商平台:商品数据获取、购物车操作、用户认证这些核心业务逻辑,如果写在控制器里,每次页面刷新都需要重新初始化,不仅性能低下,还难以保持状态一致性。
我曾在实际项目中遇到过这样的场景:需要在多个控制器中共享用户登录状态。最初尝试用$rootScope广播事件,结果导致事件监听器像野草一样疯长,调试时根本理不清事件流向。后来改用Service封装用户状态管理,所有控制器通过依赖注入获取同一实例,代码立刻变得清晰可维护。
关键经验:当发现多个控制器需要相同功能时,就是提取Service的最佳时机。这符合DRY(Don't Repeat Yourself)原则,也是AngularJS模块化设计的精髓。
2. Service的四种实现方式与适用场景
AngularJS提供了service()、factory()、provider()和value()四种创建Service的方式,新手常常被这些选择搞得晕头转向。通过几个真实项目案例,我来帮你理清它们的区别。
2.1 factory():灵活度最高的首选方案
factory()是我最常用的方式,它的核心特点是返回一个对象或函数。去年开发实时聊天应用时,我用factory创建了SocketService:
javascript复制angular.module('chatApp').factory('SocketService', ['$rootScope', function($rootScope) {
const socket = io.connect('/');
return {
on: function(eventName, callback) {
socket.on(eventName, function() {
const args = arguments;
$rootScope.$apply(function() {
callback.apply(socket, args);
});
});
},
emit: function(eventName, data, callback) {
socket.emit(eventName, data, function() {
const args = arguments;
$rootScope.$apply(function() {
callback.apply(socket, args);
});
});
}
};
}]);
这种模式的优势在于:
- 可以包含私有变量(如内部的socket连接)
- 显式返回公共API
- 支持依赖注入其他服务
2.2 service():面向对象风格的实现
service()通过构造函数创建实例,适合熟悉面向对象开发的团队。在开发企业级CRM系统时,我们这样封装客户服务:
javascript复制angular.module('crmApp').service('CustomerService', ['$http', function($http) {
this.customers = [];
this.loadCustomers = function() {
return $http.get('/api/customers').then(response => {
this.customers = response.data;
});
};
this.getCustomerById = function(id) {
return this.customers.find(c => c.id === id);
};
}]);
注意service()内部使用this添加属性和方法,AngularJS会帮我们处理new操作。这种方式在团队有Java/C#背景时特别受欢迎。
2.3 provider():配置阶段的高级玩法
provider()是唯一可以在config阶段配置的服务类型。开发多租户SaaS平台时,我们需要根据不同子域名动态配置API端点:
javascript复制angular.module('saasApp').provider('ApiConfig', function() {
let apiPrefix = '/default-api';
return {
setApiPrefix: function(prefix) {
apiPrefix = prefix;
},
$get: ['$http', function($http) {
return {
get: function(endpoint) {
return $http.get(apiPrefix + endpoint);
}
};
}]
};
});
// 在config阶段配置
angular.module('saasApp').config(['ApiConfigProvider', function(ApiConfigProvider) {
const subdomain = window.location.hostname.split('.')[0];
ApiConfigProvider.setApiPrefix(`/${subdomain}-api`);
}]);
避坑提示:provider必须包含$get方法,这是AngularJS实例化服务时实际调用的工厂函数。
2.4 value()和constant():简单值的封装
对于简单的配置值或常量,可以使用value()或constant()。两者的关键区别在于constant()可以在config阶段注入:
javascript复制angular.module('app').constant('APP_VERSION', '1.0.0');
angular.module('app').config(['APP_VERSION', function(version) {
console.log('App version:', version); // 可以正常使用
}]);
angular.module('app').value('API_KEY', '12345');
angular.module('app').config(['API_KEY', function(key) {
console.log(key); // 报错!value服务不能在config阶段使用
}]);
3. Service的依赖注入机制深度解析
AngularJS的依赖注入(DI)系统是Service工作的核心机制,但很多开发者只停留在表面使用。我曾花了整整两天时间追踪一个诡异的注入错误,最终发现是minify后的参数名混淆导致的。这次经历让我深刻理解了DI的工作原理。
3.1 依赖注入的三种写法
javascript复制// 1. 数组标注写法(推荐,安全)
angular.module('app').controller('MyCtrl', ['$scope', 'UserService', function($scope, UserService) {
// ...
}]);
// 2. $inject属性写法
function MyCtrl($scope, UserService) { /*...*/ }
MyCtrl.$inject = ['$scope', 'UserService'];
angular.module('app').controller('MyCtrl', MyCtrl);
// 3. 直接注入写法(危险!会被minify破坏)
angular.module('app').controller('MyCtrl', function($scope, UserService) {
// ...
});
第一种写法虽然略显冗长,但在生产环境使用uglify压缩代码时绝对安全。我曾见过一个项目因为使用第三种写法,上线后所有依赖注入全部失效,导致页面白屏。
3.2 依赖注入的内部工作原理
当AngularJS需要实例化一个组件时,它会:
- 解析组件的依赖列表(通过数组或$inject)
- 查找对应的provider(服务名+'Provider')
- 调用provider的$get方法获取服务实例
- 缓存该实例供后续注入使用
这个机制解释了为什么Service是单例的——每个服务只会调用一次$get方法,之后都返回缓存实例。
3.3 循环依赖的解决方案
在大型项目中,偶尔会遇到ServiceA依赖ServiceB,同时ServiceB又依赖ServiceA的情况。AngularJS会抛出$injector:cdep错误。通过$injector服务可以优雅解决:
javascript复制angular.module('app').factory('ServiceA', ['$injector', function($injector) {
// 延迟获取ServiceB
const getServiceB = () => $injector.get('ServiceB');
return {
methodA: function() {
const serviceB = getServiceB();
// ...
}
};
}]);
angular.module('app').factory('ServiceB', ['ServiceA', function(ServiceA) {
return {
methodB: function() {
ServiceA.methodA();
}
};
}]);
4. Service的单元测试策略
没有单元测试的Service就像没有安全网的杂技表演。在金融行业项目中,我们为每个Service都编写了完整的测试套件。以下是经过实战检验的测试模式。
4.1 测试工具链配置
javascript复制describe('UserService', function() {
let UserService, $httpBackend;
beforeEach(module('app'));
beforeEach(inject(function(_UserService_, _$httpBackend_) {
UserService = _UserService_;
$httpBackend = _$httpBackend_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
});
关键点:
- 使用beforeEach确保测试隔离性
- inject服务时使用下划线包裹命名避免冲突
- 验证没有未处理的HTTP请求
4.2 HTTP交互测试示例
测试一个获取用户列表的Service方法:
javascript复制it('should fetch users from API', function() {
const mockUsers = [{id: 1, name: 'John'}];
$httpBackend.expectGET('/api/users').respond(mockUsers);
let result;
UserService.getUsers().then(users => {
result = users;
});
$httpBackend.flush();
expect(result).toEqual(mockUsers);
});
4.3 模拟依赖的进阶技巧
当Service依赖其他复杂Service时,可以使用jasmine.createSpyObj创建模拟对象:
javascript复制describe('OrderService with mock dependencies', function() {
let OrderService, mockUserService;
beforeEach(function() {
mockUserService = jasmine.createSpyObj('UserService', ['getCurrentUser']);
mockUserService.getCurrentUser.and.returnValue({id: 123});
module('app', function($provide) {
$provide.value('UserService', mockUserService);
});
inject(function(_OrderService_) {
OrderService = _OrderService_;
});
});
it('should use current user when placing order', function() {
OrderService.placeOrder({items: [...]});
expect(mockUserService.getCurrentUser).toHaveBeenCalled();
});
});
5. Service性能优化实战经验
在日活百万级的应用中,Service的微小性能问题都会被无限放大。以下是几个经过实战检验的优化方案。
5.1 延迟初始化策略
不是所有Service都需要在应用启动时初始化。对于某些重量级服务,可以实现按需加载:
javascript复制angular.module('app').factory('HeavyService', ['$q', '$injector', function($q, $injector) {
let initialized = false;
let serviceInstance;
const initService = () => {
if (!initialized) {
initialized = true;
// 实际的重型初始化操作
serviceInstance = {
heavyMethod: function() { /*...*/ }
};
}
return $q.when(serviceInstance);
};
return {
ready: initService,
callWhenReady: function(methodName) {
return function() {
const args = arguments;
return initService().then(service => {
return service[methodName].apply(service, args);
});
};
}
};
}]);
5.2 缓存策略实现
对于数据类Service,合理的缓存可以大幅减少网络请求:
javascript复制angular.module('app').factory('ProductService', ['$http', '$q', function($http, $q) {
const cache = {};
const pendingRequests = {};
return {
getProduct: function(id) {
// 返回缓存数据
if (cache[id]) {
return $q.when(cache[id]);
}
// 避免重复请求
if (pendingRequests[id]) {
return pendingRequests[id];
}
const request = $http.get(`/api/products/${id}`).then(response => {
cache[id] = response.data;
delete pendingRequests[id];
return response.data;
});
pendingRequests[id] = request;
return request;
},
clearCache: function(id) {
if (id) {
delete cache[id];
} else {
Object.keys(cache).forEach(key => delete cache[key]);
}
}
};
}]);
5.3 内存泄漏预防
AngularJS应用中常见的内存泄漏源是未清理的事件监听器。在Service中要特别注意:
javascript复制angular.module('app').factory('EventService', ['$rootScope', function($rootScope) {
const listeners = [];
return {
onGlobalEvent: function(eventName, callback) {
const listener = $rootScope.$on(eventName, callback);
listeners.push(listener);
return listener;
},
cleanup: function() {
listeners.forEach(deregisterFn => deregisterFn());
listeners.length = 0;
}
};
}]);
// 在路由变更时清理
angular.module('app').run(['$rootScope', 'EventService', function($rootScope, EventService) {
$rootScope.$on('$destroy', EventService.cleanup);
}]);
6. AngularJS Service与Angular的对比
虽然现在Angular已经发展到全新版本,但理解AngularJS Service的设计对学习现代前端框架仍有重要意义。我在迁移老项目时总结出这些关键差异点。
6.1 依赖注入系统的演进
AngularJS使用字符串名称的依赖注入,而Angular采用基于类型的注入:
typescript复制// AngularJS
angular.module('app').service('UserService', ['$http', function($http) {
// ...
}]);
// Angular
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(private http: HttpClient) {}
}
Angular的注入系统更类型安全,但失去了运行时动态注入的灵活性。
6.2 生命周期管理的改进
AngularJS Service始终是单例,而Angular提供了更丰富的生命周期选项:
typescript复制@Injectable({
providedIn: 'root' // 应用级单例
// 或者
providedIn: SomeModule // 模块级单例
// 或者
providedIn: 'any' // 每个注入点创建新实例
})
export class ConfigService {}
6.3 服务通信模式的变化
AngularJS中常用$rootScope.$emit/$broadcast进行全局通信,而Angular推荐使用RxJS Subject:
typescript复制@Injectable()
export class MessageService {
private subject = new Subject<any>();
send(message: string) {
this.subject.next(message);
}
receive(): Observable<any> {
return this.subject.asObservable();
}
}
这种模式更符合响应式编程理念,也更易于测试和维护。
