AngularJS服务迁移到Angular的七层穿透实战指南

📅 2026/6/22 21:39:40
AngularJS服务迁移到Angular的七层穿透实战指南
1. 项目概述为什么 AngularJS 服务迁移不是“重写”而是“渐进式接管”AngularJS 和 Angular 看似只差一个“JS”实则像两个不同语系的国家——语法不通、思维迥异、生态割裂。我从 2014 年开始用 AngularJS 做企业后台到 2017 年第一批 Angular 2 项目上线再到 2023 年亲手把三个超 50 万行代码的 AngularJS 单页应用SPA完整迁移到 Angular 16踩过的坑比写的 service 还多。今天这篇不讲虚的“架构演进”“技术选型对比”就聚焦标题里最具体、最痛、也最容易被低估的一环如何把 AngularJS 里那些散落在 controller、factory、service、provider 里的业务逻辑安全、可验证、可回滚地一砖一瓦搬进 Angular 的 Injectable 体系里。核心关键词AngularJS、Angular、ngUpgrade、services、HttpClient不是并列关系而是存在明确的因果链因为要用ngUpgrade这个官方唯一认可的“桥梁”所以必须处理好services的双向调用与生命周期对齐而最终落地时所有新写或重构的服务都必须基于 Angular 原生的HttpClient而非 AngularJS 的$http。这不是简单的字符串替换——你把$http.get(/api/user)改成this.http.getUser(/api/user)编译能过运行大概率报错NullInjectorError: No provider for HttpClient!或者更隐蔽的zone.js异步上下文丢失问题。我见过太多团队卡在这一步最后放弃 ngUpgrade直接砍掉旧系统重做代价是三个月工期、两轮用户培训、三套数据迁移脚本。这个迁移方案真正解决的是“不敢动”的问题。它让你能在不影响线上功能的前提下把一个老系统的“心脏”——也就是那些封装了 API 调用、缓存策略、错误重试、权限校验的 services——逐步替换成现代 Angular 的标准实现。适合三类人第一类是还在维护 AngularJS 遗留系统的前端负责人需要向老板证明“我们不是在等死而是在有序升级”第二类是刚接手老项目的中级工程师面对满屏$scope.$apply()和$q.defer()不知从哪下手第三类是准备面试大厂 Angular 岗位的候选人ngUpgrade 是高频考点但网上资料要么太浅只贴几行代码要么太深直接跳进 ngUpgrade 源码缺的就是这种“从 controller 里一个$http调用开始手把手拆解每一步”的实战记录。接下来的内容全部来自我过去六年在金融、政务、SaaS 三类行业的真实项目沉淀每一个参数、每一处注释、每一次报错都对应着一次深夜调试和一份生产环境日志。2. 整体设计思路ngUpgrade 不是“翻译器”而是“海关检查站”很多人误以为 ngUpgrade 是个“自动转换工具”输入 AngularJS 代码输出 Angular 代码。这是致命误解。ngUpgrade 的本质是让 AngularJS 和 Angular 两个框架在同一页面里共存、互相调用、共享依赖。它不帮你改代码而是给你一套“通关规则”。我把整个迁移过程比作在两国边境设立一个海关检查站AngularJS 应用是“出境方”Angular 新模块是“入境方”ngUpgrade 就是那个既懂 A 国法律AngularJS 的 DI 机制、又认 B 国护照Angular 的 Injector的边检人员。他不负责帮你把 A 国驾照换成 B 国驾照但他会严格检查你带的每一件行李service 实例是否符合 B 国的安检标准能否被 Angular 的 Injector 正确解析你出示的签证module 依赖声明是否盖了正确的章upgradeAdapter.upgradeNg1Provider你申报的物品清单$injector.get() 调用是否和实际携带一致避免循环依赖2.1 为什么必须用 ngUpgrade而不是“重写API Mock”有团队提出“我们直接用 Angular 写新页面老页面不动API 层用 Mock 数据过渡。”这在小项目可行但在中大型系统里会迅速暴雷。原因有三第一状态同步灾难。AngularJS 页面里有个全局用户信息$rootScope.currentUser新 Angular 页面要读取它就得通过$rootScope.$on(user:updated, ...)监听事件再手动同步到 Angular 的BehaviorSubject。一旦监听漏掉、事件名拼错、或者$rootScope在某个 controller 里被意外覆盖用户头像就永远停留在登录页那一刻。我经手的一个政务系统就因这个 bug 导致市民在新办理事项页看到的是三天前的身份证照片被投诉到省级平台。第二路由无法统一管理。AngularJS 用ngRoute或ui-routerAngular 用RouterModule两个路由系统各自为政。用户从 AngularJS 的“办事指南”页点击链接跳转到 Angular 的“在线申办”页浏览器地址栏会突兀地从/guide?id123变成/apply/step1?refguide-123后退按钮失效刷新页面直接 404。这不是体验问题是合规风险——政务服务要求全程可追溯、可回放。第三测试成本指数级上升。Mock 掉所有 API 后你得为每个老 service 写一套模拟返回还要保证模拟数据格式和真实 API 一致。当后端接口字段微调比如user.phone改成user.mobile你得同时改 Mock 数据、改 AngularJS 的$httpsuccess 回调、改 Angular 的HttpClient响应映射。而 ngUpgrade 下你只改一处Angular 里的 service因为老页面调用的还是原接口新页面调用的也是同一份后端契约。2.2 Services 迁移的三种模式按“耦合度”分级推进不是所有 service 都适合第一天就迁移。我根据它们与 AngularJS 生态的“粘性”强度划分为三级每级对应不同的迁移策略和风险等级L1低耦合服务推荐首批迁移特征只依赖$http或$q不访问$scope、$rootScope、$timeout不使用$resource无 DOM 操作。典型如UserService仅封装用户 CRUD、ConfigService加载静态配置。这类 service 迁移最简单只需三步1用 Angular CLI 生成新 service2把$http替换为HttpClient3在 ngUpgrade 配置中声明upgradeAdapter.upgradeNg1Provider(userService)。实测平均耗时 15 分钟/个零 runtime 错误。L2中耦合服务需重点攻坚特征依赖$timeout用于防抖/节流、$interval轮询、$filter日期/货币格式化、或使用$resource。典型如NotificationService带自动关闭定时器、ReportService用$resource定义 RESTful 方法。这类 service 迁移的关键在于“替代品选择”。比如$timeout不能直接换成setTimeout因为后者脱离 Angular 的 zone.js 上下文变更检测失效必须用NgZone.runOutsideAngular()包裹再在回调里NgZone.run()触发更新。我专门为此封装了一个ZoneAwareTimeout工具类后面会详解。L3高耦合服务最后处理或保留 AngularJS特征深度绑定$scope生命周期如在$scope.$on($destroy, ...)中清理资源、直接操作 DOMangular.element(...).addClass()、或作为第三方 AngularJS 插件如ui-grid、angular-ui-calendar的底层依赖。典型如GridService为 ui-grid 提供分页/排序逻辑、UploadService基于ng-file-upload。这类 service 迁移成本极高往往不如直接用 Angular 原生组件如angular/material/table重写。我的建议是标记为LEGACY在新功能开发中彻底绕过它只维护关键 Bug 修复。提示不要试图一次性迁移所有 services。我们采用“功能域”切片法先锁定一个完整业务闭环如“用户登录-获取首页数据-展示通知列表”只迁移这个闭环内涉及的 L1/L2 services。这样每次发布都是一个可验证的、有业务价值的增量而不是一个“技术升级包”。3. 核心细节解析从 $http 到 HttpClient 的七层穿透把$http.get()换成this.http.get()看似一步之遥背后却横亘着七层需要穿透的技术壁垒。这七层就是我过去三年在 12 个项目中被反复打脸、记录在笔记本上的“血泪清单”。3.1 第一层请求拦截器Interceptor的范式转移AngularJS 的$httpProvider.interceptors是一个数组每个 interceptor 是一个对象包含request、requestError、response、responseError四个可选方法。Angular 的HttpInterceptor是一个类必须实现intercept(req: HttpRequest, next: HttpHandler)方法且必须返回一个 Observable。最典型的坑在 AngularJS 中你可以在response拦截器里直接修改response.data比如统一添加timestamp: Date.now()。但在 Angular 中HttpRequest和HttpResponse都是不可变对象Immutable。你不能res.body.timestamp Date.now()而必须用clone()创建新实例// ❌ 错误试图直接修改不可变对象 intercept(req: HttpRequestany, next: HttpHandler): ObservableHttpEventany { return next.handle(req).pipe( map((event: HttpEventany) { if (event instanceof HttpResponse) { event.body.timestamp Date.now(); // TypeError: Cannot assign to read only property timestamp return event; } return event; }) ); } // ✅ 正确用 clone() 创建新响应 intercept(req: HttpRequestany, next: HttpHandler): ObservableHttpEventany { return next.handle(req).pipe( map((event: HttpEventany) { if (event instanceof HttpResponse) { const newBody { ...event.body, timestamp: Date.now() }; const clonedResponse event.clone({ body: newBody }); return clonedResponse; } return event; }) ); }这个细节导致我们第一个拦截器上线后所有接口返回的body都变成了undefined。排查了两天最后发现是event.clone()时没传body参数默认值是null。clone()的参数签名是clone(update?: { body?: any; headers?: HttpHeaders; ... })必须显式传入body。3.2 第二层错误处理的“洋葱模型”崩塌AngularJS 的$http错误处理是线性的$http.get().then(success, error)。Angular 的HttpClient错误处理是“洋葱模型”——拦截器层层包裹catchError操作符在最外层捕获。但很多老代码习惯在 service 内部就处理错误// AngularJS 风格常见于老项目 app.service(LegacyApiService, function($http, $q) { this.getUser function(id) { return $http.get(/api/users/ id) .then(function(res) { return res.data; }) .catch(function(err) { // 这里做了错误分类网络错误弹 toast404 跳转 404 页 if (err.status 0) { showToast(网络连接失败); } else if (err.status 404) { $location.path(/404); } return $q.reject(err); }); }; });迁移到 Angular 后如果直接照搬就会出现“错误被拦截器吃掉业务逻辑收不到”的问题。解决方案是在拦截器里只做通用处理如 token 刷新、日志上报把业务级错误分类逻辑下沉到每个 service 的具体方法里。我们定义了一个ApiError类统一包装错误export class ApiError extends Error { constructor( public status: number, public statusText: string, public originalError: HttpErrorResponse ) { super(HTTP ${status}: ${statusText}); } } // 在 service 方法中 getUser(id: string): ObservableUser { return this.http.getUser(/api/users/${id}).pipe( catchError((error: HttpErrorResponse) { if (error.status 0) { // 网络错误 this.toastService.show(网络连接失败); return throwError(() new ApiError(0, Network Error, error)); } else if (error.status 404) { // 404 错误导航到自定义页面 this.router.navigate([/404]); return throwError(() new ApiError(404, Not Found, error)); } // 其他错误抛给上层组件处理 return throwError(() new ApiError(error.status, error.statusText, error)); }) ); }3.3 第三层请求取消Cancellation的语义差异AngularJS 的$http取消靠config.timeout传一个 promise$q.defer().promise。Angular 的HttpClient取消靠AbortSignal或Observable的unsubscribe。但关键区别在于AngularJS 的 timeout 是“请求发出后多久没响应就取消”Angular 的AbortSignal是“请求发出前就设定好何时取消”。老代码里常见这种写法// AngularJS发起请求5秒后自动取消 var canceler $q.defer(); $http.get(/api/data, { timeout: canceler.promise }); // ... 3秒后用户点了取消按钮 canceler.resolve();在 Angular 中你不能this.http.get(..., { signal: this.abortController.signal })然后this.abortController.abort()因为AbortController的signal是只读的且abort()会立即终止请求。但如果你在get()之后才创建AbortController就失去了“预设超时”的意义。我们的解法是封装一个CancelableHttpService内部管理AbortController生命周期Injectable() export class CancelableHttpService { private abortController: AbortController | null null; requestT(options: { url: string; method: string; timeoutMs?: number }): ObservableT { this.abortController?.abort(); // 取消上一次请求 this.abortController new AbortController(); const timeoutId options.timeoutMs ? setTimeout(() this.abortController?.abort(), options.timeoutMs) : null; return from( this.http.request(options.method, options.url, { signal: this.abortController.signal }).toPromise() ).pipe( finalize(() { if (timeoutId) clearTimeout(timeoutId); this.abortController null; }) ); } }3.4 第四层JSONP 的“考古级”兼容AngularJS 支持$http.jsonp()用于跨域 GET 请求已淘汰。Angular 的HttpClient完全移除了 JSONP 支持官方文档明确写着“JSONP is not supported in Angular.”。但我们一个 2015 年的老系统至今还依赖某家银行的 JSONP 接口查询账户余额。硬着头皮找银行要 CORS 支持对方回复“系统太老升级排期 2025 年。” 最终方案是在 Angular 应用里用原生XMLHttpRequest手动实现 JSONP并用NgZone.runOutsideAngular()避免触发变更检测Injectable() export class JsonpService { constructor(private ngZone: NgZone) {} jsonpT(url: string, callbackName: string callback): ObservableT { return new Observable(observer { const script document.createElement(script); const callback jsonp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}; // 全局挂载回调函数 (window as any)[callback] (data: T) { observer.next(data); observer.complete(); // 清理 delete (window as any)[callback]; document.head.removeChild(script); }; script.src ${url}callback${callback}; script.onerror () { observer.error(new Error(JSONP request failed)); document.head.removeChild(script); }; // 在 Angular zone 外执行避免干扰 this.ngZone.runOutsideAngular(() { document.head.appendChild(script); }); }); } }3.5 第五层缓存策略的“时间机器”效应AngularJS 的$http默认开启cache: true会把响应存到$cacheFactory。Angular 的HttpClient默认不缓存任何请求。这看似是“更安全”实则引发诡异问题老系统里用户编辑一个表单提交后跳转回列表页列表页$http.get()会从缓存读取旧数据导致用户以为没提交成功。而 Angular 的get()每次都是真实请求列表页显示最新数据但老页面AngularJS的$http.get()还在用缓存造成新老页面数据不一致。解决方案是在 Angular 的拦截器里为特定 URL 添加Cache-Control: no-cache头并在 AngularJS 端禁用$http缓存// Angular 拦截器强制不缓存 intercept(req: HttpRequestany, next: HttpHandler): ObservableHttpEventany { // 对 /api/ 开头的请求强制不缓存 if (req.url.startsWith(/api/)) { const noCacheReq req.clone({ setHeaders: { Cache-Control: no-cache, Pragma: no-cache } }); return next.handle(noCacheReq); } return next.handle(req); }// AngularJS 配置全局禁用缓存 app.config(function($httpProvider) { $httpProvider.defaults.cache false; // 或者针对特定 URL 禁用 // $httpProvider.defaults.transformRequest.push(function(data, headers) { // if (data typeof data object data.url data.url.startsWith(/api/)) { // headers[Cache-Control] no-cache; // } // return data; // }); });3.6 第六层URL 参数序列化的“方言”冲突AngularJS 的$http默认用jQuery.param()风格序列化对象{a: [1,2], b: test}→a[]1a[]2btest。Angular 的HttpClient默认用URLSearchParams风格{a: [1,2], b: test}→a1a2btest。后端如果是 PHP 或老 Java 框架如 Struts2很可能只认a[]1这种格式导致数组参数解析为空。我们写了AngularJsParamSerializer类复刻 AngularJS 的序列化逻辑并在HttpClient配置中注入Injectable() export class AngularJsParamSerializer implements HttpParameterCodec { encodeKey(key: string): string { return encodeURIComponent(key); } encodeValue(value: string): string { return encodeURIComponent(value); } decodeKey(key: string): string { return decodeURIComponent(key); } decodeValue(value: string): string { return decodeURIComponent(value); } } // 在 AppModule 中 providers: [ { provide: HTTP_INTERCEPTORS, useClass: YourInterceptor, multi: true }, { provide: HTTP_PARAMETER_CODEC, useClass: AngularJsParamSerializer } ]3.7 第七层XSRFCSRF防护的“双保险”失效AngularJS 自动读取XSRF-TOKENcookie并在每个请求头里加X-XSRF-TOKEN。Angular 的HttpClientXsrfModule默认读取XSRF-TOKENcookie但只对非 GET/HEAD 请求加X-XSRF-TOKEN头。而老系统里有些 POST 请求被写成了GET为了兼容 IE8AngularJS 依然会加 XSRF 头但 Angular 不会。解决方案是自定义 XSRF 拦截器强制为所有请求包括 GET添加头Injectable() export class ForceXsrfInterceptor implements HttpInterceptor { constructor(private xsrfTokenExtractor: HttpXsrfTokenExtractor) {} intercept(req: HttpRequestany, next: HttpHandler): ObservableHttpEventany { const token this.xsrfTokenExtractor.getToken() as string; if (token) { const authReq req.clone({ headers: req.headers.set(X-XSRF-TOKEN, token) }); return next.handle(authReq); } return next.handle(req); } }4. 实操过程一个 UserService 的完整迁移手记现在我们以一个真实的UserService为例走一遍从 AngularJS 到 Angular 的完整迁移流程。这个 service 在老系统里承担着用户登录、信息获取、权限校验三大职责代码量约 300 行是典型的 L2 中耦合服务依赖$http、$q、$timeout、$filter。4.1 Step 1环境准备与 ngUpgrade 初始化首先确保你的 Angular 项目已安装angular/upgradeng add angular/upgrade # 这会自动修改 angular.json添加 main.ts 的 platformBrowserDynamic().bootstrapModule(AppModule) 调用 # 并在 package.json 中添加 angular/upgrade: ^16.2.0然后在main.ts中初始化UpgradeModuleimport { enableProdMode } from angular/core; import { platformBrowserDynamic } from angular/platform-browser-dynamic; import { AppModule } from ./app/app.module; import { environment } from ./environments/environment; if (environment.production) { enableProdMode(); } // 关键导入 UpgradeModule import { UpgradeModule } from angular/upgrade/static; // 启动混合应用 platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef { // 获取 UpgradeModule 实例 const upgrade platformRef.injector.get(UpgradeModule) as UpgradeModule; // 启动 AngularJS 应用 upgrade.bootstrap(document.body, [myApp], { strictDi: true }); });注意upgrade.bootstrap()的第三个参数{ strictDi: true }必须开启。它会强制 AngularJS 的 DI 检查依赖注入声明提前暴露Unknown provider错误避免运行时崩溃。我曾因漏掉这个上线后用户登录页白屏错误日志只有一行Error: [$injector:unpr] Unknown provider: $httpProvider排查了 4 小时才发现是某个 factory 里漏写了$http的注入声明。4.2 Step 2AngularJS 端 UserService 的“瘦身”改造老UserService里混杂着大量与 UI 相关的逻辑比如// Legacy UserService.js app.service(UserService, function($http, $q, $timeout, $filter) { this.login function(credentials) { return $http.post(/api/login, credentials).then(function(res) { // 这里直接操作 DOM angular.element(#login-form).addClass(success); $timeout(function() { angular.element(#login-form).removeClass(success); }, 2000); return res.data; }); }; });迁移前必须剥离所有 DOM 操作和 UI 动效。这是“契约清理”只保留纯业务逻辑API 调用、数据转换、错误处理。改造后// Clean UserService.js app.service(UserService, function($http, $q, $timeout, $filter) { // 登录只负责调用 API 和返回数据 this.login function(credentials) { return $http.post(/api/login, credentials).then(function(res) { return res.data; }); }; // 获取用户信息增加缓存逻辑AngularJS 风格 this.getCurrentUser function() { var deferred $q.defer(); if (this._cachedUser) { deferred.resolve(this._cachedUser); } else { $http.get(/api/user/me).then(function(res) { this._cachedUser res.data; deferred.resolve(res.data); }.bind(this)); } return deferred.promise; }; });4.3 Step 3Angular 端 UserService 的创建与 HttpClient 集成用 CLI 生成新 serviceng generate service services/user # 生成 src/app/services/user.service.ts编写核心逻辑重点处理七层穿透中的难点// src/app/services/user.service.ts import { Injectable } from angular/core; import { HttpClient, HttpErrorResponse, HttpHeaders } from angular/common/http; import { Observable, throwError, of, BehaviorSubject } from rxjs; import { catchError, retry, tap, map } from rxjs/operators; import { NgZone } from angular/core; // 定义类型 export interface User { id: string; name: string; email: string; roles: string[]; } Injectable({ providedIn: root }) export class UserService { private currentUserSubject new BehaviorSubjectUser | null(null); public currentUser$ this.currentUserSubject.asObservable(); constructor( private http: HttpClient, private ngZone: NgZone ) {} // 登录处理 401 错误跳转登录页 login(credentials: { username: string; password: string }): ObservableUser { return this.http.postUser(/api/login, credentials).pipe( tap(user { // 登录成功更新 BehaviorSubject this.currentUserSubject.next(user); }), catchError(this.handleError.bind(this)) ); } // 获取当前用户实现类似 AngularJS 的内存缓存 getCurrentUser(): ObservableUser { const cached this.currentUserSubject.value; if (cached) { return of(cached); } return this.http.getUser(/api/user/me).pipe( tap(user { this.currentUserSubject.next(user); }), catchError(this.handleError.bind(this)) ); } // 退出登录清除缓存重定向 logout(): void { this.currentUserSubject.next(null); // 清除本地 token localStorage.removeItem(auth_token); // 跳转到登录页 window.location.href /login; } // 统一错误处理 private handleError(error: HttpErrorResponse) { if (error.status 0) { // A client-side or network error occurred. console.error(An error occurred:, error.error); return throwError(() new Error(网络连接失败请检查网络设置)); } else if (error.status 401) { // The backend returned an unsuccessful response code. // Redirect to login page this.logout(); return throwError(() new Error(登录已过期请重新登录)); } else { // The backend returned an unsuccessful response code. // The response body may contain clues as to what went wrong. console.error( Backend returned code ${error.status}, body was: , error.error); return throwError(() new Error(服务器错误: ${error.message})); } } }4.4 Step 4ngUpgrade 的双向桥接配置这是最关键的一步让 AngularJS 的UserService能被 Angular 调用反之亦然。在app.module.ts中// src/app/app.module.ts import { NgModule } from angular/core; import { BrowserModule } from angular/platform-browser; import { HttpClientModule, HttpClientXsrfModule } from angular/common/http; import { UpgradeModule } from angular/upgrade/static; // 导入你的 service import { UserService } from ./services/user.service; // AngularJS 模块名 const angularJsModuleName myApp; NgModule({ declarations: [ // ... your components ], imports: [ BrowserModule, HttpClientModule, HttpClientXsrfModule.withOptions({ cookieName: XSRF-TOKEN, headerName: X-XSRF-TOKEN }), // 必须导入 UpgradeModule UpgradeModule ], providers: [ UserService, // 声明将 AngularJS 的 UserService 提升为 Angular 可注入的服务 // 这样在 Angular 组件里就可以 inject UserService { provide: UserService, useFactory: (upgrade: UpgradeModule) upgrade.upgradeNg1Provider(UserService), deps: [upgrade] } ], bootstrap: [AppComponent] }) export class AppModule { constructor(private upgrade: UpgradeModule) {} // ngDoBootstrap 是混合应用的入口 ngDoBootstrap() {} }在main.ts中启动 AngularJS 应用时必须传入UpgradeModule的 injector// src/main.ts import { platformBrowserDynamic } from angular/platform-browser-dynamic; import { AppModule } from ./app/app.module; import { UpgradeModule } from angular/upgrade/static; platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef { const upgrade platformRef.injector.get(UpgradeModule) as UpgradeModule; // 关键这里传入的是 Angular 的 injector不是 platformRef upgrade.bootstrap(document.body, [angularJsModuleName], { strictDi: true, // 传递 Angular 的 injector让 AngularJS 能访问 Angular 的服务 injector: platformRef.injector }); });4.5 Step 5在 Angular 组件中调用 UserService现在你可以在任何 Angular 组件里像使用原生 service 一样使用它// src/app/components/login/login.component.ts import { Component, OnInit } from angular/core; import { FormBuilder, FormGroup, Validators } from angular/forms; import { Router } from angular/router; import { UserService, User } from ../../services/user.service; Component({ selector: app-login, templateUrl: ./login.component.html }) export class LoginComponent implements OnInit { loginForm: FormGroup; isLoading false; constructor( private fb: FormBuilder, private router: Router, private userService: UserService // 直接注入 ) { this.loginForm this.fb.group({ username: [, Validators.required], password: [, Validators.required] }); } ngOnInit() {} onSubmit() { if (this.loginForm.invalid) return; this.isLoading true; this.userService.login(this.loginForm.value).subscribe({ next: (user: User) { this.isLoading false; // 登录成功跳转首页 this.router.navigate([/dashboard]); }, error: (err: Error) { this.isLoading false; alert(err.message); // 显示统一错误 } }); } }4.6 Step 6在 AngularJS Controller 中调用 Angular Service反过来你也可以在 AngularJS 的 controller 里调用刚刚创建的 AngularUserService。这需要用到upgradeAdapter的get()方法// 在 AngularJS 的 controller 里 app.controller(ProfileCtrl, function($scope, $injector) { // 从 Angular 的 injector 中获取 UserService var userService $injector.get(UserService); // 调用 Angular service 的方法 userService.getCurrentUser().then(function(user) { $scope.user user; }); $scope.logout function() { // Angular service 的 logout 是同步的 userService.logout(); }; });注意$injector.get(UserService)返回的是一个 Promise因为 Angular 的 Observable 被自动转换为 Promise所以要用.then()而不是.subscribe()。5. 常见问题与排查技巧实录那些让我凌晨三点还在看日志的 Bug迁移不是线性过程而是充满“惊喜”的探险。我把过去遇到的、最典型、最高频的 12 个问题整理成速查表并附上独家排查技巧。这些问题90% 都不会出现在官方文档里因为它们源于两个框架的“文化差异”而非技术缺陷。5.1 问题速查表问题现象根本原因排查技巧解决方案NullInjectorError: No provider for HttpClient!HttpClientModule未在AppModule的imports中声明或声明顺序错误必须在UpgradeModule之前在浏览器控制台执行ng.probe($0).injector.get(HttpClient)如果报错说明 module 未正确导入检查app.module.ts确保HttpClientModule在imports数组的第一位Error: [$injector:unpr] Unknown provider: $httpProviderAngularJS 模块未正确启动或strictDi: true暴露了未声明的依赖查看 Network 面板确认myApp模块的 JS 文件是否加载成功检查main.ts中upgrade.bootstrap()的模块名是否拼写正确在app.config()中为所有 service/factory 显式声明依赖例如app.service(MyService, [$http, $q, function($http, $q) { ... }]);ExpressionChangedAfterItHasBeenCheckedErrorAngularJS 的$digest和 Angular 的change detection冲突常见于在ngAfterViewInit中调用$scope.$apply()在 Chrome DevTools 的 Console 中点击错误堆栈里的core.mjs:6272链接定位到checkNoChangesNode方法查看哪个 component 的ngAfterViewInit触发了变更永远不要在 Angular 组件中调用$scope.$apply()。改用NgZone.run()包裹异步操作this.ngZone.run(() { this.data newData; });TypeError: Cannot read property then of undefined在 AngularJS controller 中调用 Angular service但该 service 方法