Angular 内存 Web API 实战:用 angular-in-memory-web-api 模拟 REST 服务,为 Demo 与测试提速
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
本指南聚焦 Angular 官方仓库中的angular-in-memory-web-api包:它在不启动任何真实后端的情况下,把HttpClient的请求拦截并转交给一个由你控制的"内存数据库",从而完整模拟一套 RESTy CRUD 接口。读完本文,你将掌握从零搭建InMemoryDbService、注册模块、调优配置参数,到利用命令、自定义解析器、HTTP 方法拦截器与响应拦截器扩展行为的一整套开发与测试技巧。
核心机制与典型应用场景
angular-in-memory-web-api是一个面向 Demo 与测试的内存 Web API。它拦截原本要发往远程服务器的 AngularHttp/HttpClient请求,将其重定向到由InMemoryDbService维护的内存数据仓库中,然后按照一套 RESTy 语义处理并返回响应。相关说明与用法记录在 packages/misc/angular-in-memory-web-api/README.md,完整实现位于该目录的 src 下。
值得强调,官方对该包定位有严格边界:
这是仅用于开发与测试的工具。内存数据仓库不会对请求施加任何身份验证或授权;请永远不要把它发布到生产应用中。
这一点必须作为团队约定固化下来——在 CI 流水线里它可以代替真实数据库完成端到端测试,但一旦进入生产构建,应像下文示例那样将模块剔除。
该工具最典型的应用场景包括:
- 演示应用:需要模拟 CRUD 数据持久化而又不想搭建、启动测试服务器;
- 快速原型与概念验证(PoC):从业务模型直通可交互界面;
- 社区示例分享:在网页编码环境(如 Plunker、CodePen)中分享可运行示例,为 Angular issue 和 Stack Overflow 回答配上活代码;
- 预演未就绪的数据接口:本地内存库先响应已实现的集合,尚未实现的部分通过"透传"转发到开发/测试服务器;
- 单元测试:避免手动拦截多次 HTTP 调用、拼装一串串响应。每次测试时数据仓库自动重建,天然消除跨测试的数据污染;
- 端到端测试:将应用切换到测试模式并启用内存 Web API,可避免扰动真实数据库,尤其适合 CI 持续集成构建。
作为配套的工程约束,还需注意:该包主要为 Angular 官方文档服务,不追求模拟全部真实世界的 Web API;它被官方明确标注为"始终处于实验状态",主版本之间可能出现破坏性变更(会通过CHANGELOG.md说明),使用时请把它当开发工具而非生产产品。
HTTP 请求处理模型:RESTy URL 约定
内存 Web API 处理一个 HTTP 请求后,会返回一个携带 HTTPResponse对象(底层依赖所使用的Http或HttpClient库)的Observable,行为与真实 RESTy Web API 保持一致。它原生支持如下 URI 形态:
:base/:collectionName/:id?对应到实际请求:
// 假设请求指向 api 基础路径,并从 'heroes' 集合取数据 GET api/heroes // 全部 heroes GET api/heroes/42 // id=42 的 hero GET api/heroes?name=^j // 'j' 为正则:返回 name 以 'j' 或 'J' 开头的 heroes GET api/heroes.json/42 // 忽略 ".json" 后缀这些请求会落到你在初始化阶段定义好的"数据库"——一组具名集合之上。默认解析对集合名做了特殊处理:解析时丢弃.之后的任何内容(例如customers.json中的json),因而 URL 中的.json扩展名会被自然忽略,对应实现在 backend-service.ts。
基础设置:实现 InMemoryDbService
编写 createDb 构建"数据库"
创建一个实现了InMemoryDbService的InMemoryDataService类,至少要实现createDb方法。该方法产出一个以集合名为 key、以集合对象数组为 value的哈希,作为后续查询与更新的数据源:
import {InMemoryDbService} from 'angular-in-memory-web-api'; export class InMemHeroService implements InMemoryDbService { createDb() { let heroes = [ {id: 1, name: 'Windstorm'}, {id: 2, name: 'Bombasto'}, {id: 3, name: 'Magneta'}, {id: 4, name: 'Tornado'}, ]; return {heroes}; } }需要注意的关键约束:
- 该库假定每个集合都存在名为
id的主键(见 interfaces.ts 中InMemoryDbService的定义与文档说明); createDb既可以同步也可以异步:当需要从 JSON 文件初始化数据库时必须异步。你可以返回数据库对象本身、该对象的Observable或Promise——仓库测试夹具HeroInMemDataService(hero-in-mem-data-service.ts)用returnType = 'object' | 'observable' | 'promise'三种分支演示了全部写法;- 官方对
createDb有一个容易被忽略的硬性要求(定义在 interfaces.ts):必须可被安全地反复调用,且每次返回全新的对象、新数组、新条目。这保证了内存后端可以在不触碰原始数据的情况下自由变更集合内容。
内存后端服务在两种时机调用你的createDb:
- 处理第一个HTTP 请求时(对应 backend-service.ts 中懒加载的
dbReady); - 收到
resetdb命令时。
第二种情况下,服务会传入一个RequestInfo对象,从而允许你的createDb逻辑按客户端请求调整行为。上面的测试夹具就演示了这一点:当 POSTcommands/resetDb请求体携带{clear: true}时清空所有集合,createDb通过reqInfo.utils.getJsonBody(reqInfo.req)读取请求体来感知该指令。
注册 InMemoryWebApiModule
在你的根AppModule.imports中,把数据仓库服务类注册进HttpClientInMemoryWebApiModule,通过静态方法forRoot传入服务类与可选的配置对象:
import { HttpClientModule } from '@angular/common/http'; import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemHeroService } from '../app/hero.service'; @NgModule({ imports: [ HttpClientModule, HttpClientInMemoryWebApiModule.forRoot(InMemHeroService), ... ], ... }) export class AppModule { ... }三条关键注意事项:
- 导入顺序:
HttpClientInMemoryWebApiModule必须放在HttpClientModule之后,确保内存后端 provider 覆盖 Angular 自带的后端。其原理可见 http-client-in-memory-web-api-module.ts:forRoot实际注册了HttpBackend的 factory provider,把HttpClient默认的后端整体替换为内存后端; - 懒加载模块:在懒加载的特性模块中,可像调用
forRoot一样调用.forFeature方法(它内部就是forRoot的别名,见同文件 forFeature,并且加载较晚的模块会遮蔽先加载模块的 provider); - 生产构建剔除:生产环境应让请求直达真实服务器。CLI 应用可按如下方式在 production 构建中排除内存 provider:
imports: [ HttpClientModule, environment.production ? [] : HttpClientInMemoryWebApiModule.forRoot(InMemHeroService) ... ]若项目同时兼容旧的
Http与新的HttpClient,可以改用 in-memory-web-api-module.ts 导出的InMemoryWebApiModule,二者 API 形态一致。另外源码注释提示:若项目使用FetchBackend,请保证forRoot位于 providers 列表靠后位置。
配置参数:InMemoryBackendConfigArgs 全解
官方把可选配置集中定义在InMemoryBackendConfigArgs接口中(interfaces.ts),作为forRoot的第二个参数传入:
InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 500 }),下表整理了全部配置项及其默认值(默认值取自 interfaces.ts 中InMemoryBackendConfig构造函数):
| 配置项 | 类型 | 默认值 | 作用说明 |
|---|---|---|---|
apiBase | string | undefined | API 基础路径,如'api/'。未指定时parseRequestUrl假定其为请求中的第一个路径段 |
caseSensitiveSearch | boolean | false | false时搜索匹配大小写不敏感(默认);置true后区分大小写 |
dataEncapsulation | boolean | false | false(默认)时内容直接放在响应体中;true时把内容封装进{ data: ... }再作为响应体返回 |
delay | number | 500 | 模拟延迟的毫秒数,配置为0可去掉延迟 |
delete404 | boolean | false | false(默认)删除不存在的对象时返回 204;true时返回 404 |
host | string | 应用所在 Web 服务器 host | 本服务运行的 host,构造函数中会根据当前 location 自动填充 |
passThruUnknownUrl | boolean | false | true时把无法识别的请求 URL 透传给原始后端;false(默认)返回 404 |
post204 | boolean | true | true(默认)POST 后不返回条目(204);false返回条目(200) |
post409 | boolean | false | false(默认)允许用 POST 覆盖已存在 id 的条目;true时对已存在 id 的 POST 返回 409 冲突 |
put204 | boolean | true | true(默认)PUT 后不返回条目(204);false返回条目(200) |
put404 | boolean | false | false(默认)PUT 目标不存在时创建新条目;true时返回 404 |
rootPath | string | 应用被服务的路径 | 任何 API 调用之前的根路径(如''),同样在构造函数中根据当前 location 自动填充 |
其中host与rootPath虽声明默认为undefined,但 backend-service.ts 的构造函数会基于getLocation('/')将二者初始化为应用实际 Web 服务器 host 与根路径,再用传入的 config 覆盖。
如果你不想走模块静态方法,也可以单独提供配置:provide(InMemoryBackendConfig, {useValue: {delay: 600}})。
请求评估顺序:一次请求的生命周期
服务对请求的评估顺序决定了一切行为分支。官方在 README 中给出了推理链,而实际逻辑完整实现在 backend-service.ts 的handleRequest_方法中:
- 若请求形如命令(apiBase 末尾为
commands),按命令处理(详见下文"命令"一节); - 若对应HTTP 方法被覆盖(
InMemoryDbService上定义了同名小写方法),尝试调用覆盖方法,有返回值则直接采用; - 若资源名(api base 之后的段)命中已配置的集合,交给
collectionHandler走 CRUD 处理; - 若未命中但
Config.passThruUnknownUrl为true,尝试透传给真实 XHR 后端; - 返回404。
HttpClientBackendService(http-client-backend-service.ts)的handle(req)首先进入handleRequest,而handleRequest会等待dbReady(数据库就绪)后才真正分发——这就是为什么首个请求到来时才触发createDb。
内建 CRUD 的状态码语义
当请求命中已知集合后,collectionHandler依据 HTTP 方法分发(backend-service.ts),其状态码语义与官方状态码常量(http-status-codes.ts)对照如下:
- GET:按
id精确查找或按查询串过滤;找到返回200 OK(body 经dataEncapsulation包装),找不到返回404; - POST(创建实体):请求体条目无
id时,用id或自动生成器补全;URL 中 id 与条目id不一致返回400;集合中不存在该 id 时插入并返回201 CREATED(响应头Location: <resourceUrl>/<id>);已存在时若post409为true返回409 CONFLICT,否则视为更新——随后依据post204返回204或200; - PUT(更新实体):请求体缺
id返回404;URL id 与条目 id 不符返回400;条目存在则覆盖,依put204返回204或200;条目不存在时若put404为true返回404,否则当作创建并返回201 CREATED; - DELETE:缺
id返回404;删除成功返回204;找不到目标时依delete404决定返回204(默认)还是404。
理解这些细节的价值在于:你可以只通过调配置参数(post204、post409、put204、put404、delete404)就让内存接口逼近真实后端的约定,从而让前端错误处理逻辑在联调前就得到真实演练。
默认延迟与自定延迟
默认情况下,服务给所有数据请求附加500ms 延迟以模拟网络往返时延。其实现位于 delay-response.ts:delayResponse会同时把next与error两个通道都推迟指定毫秒数;backend-service.ts 的addDelay则在delay === 0时直接跳过延迟。
命令请求(
commands)不附加任何延迟——它们处理的是内存服务自身的配置,无需模拟真实的数据访问往返。
按需修改或消除延迟:
InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 0 }), // 无延迟 InMemoryWebApiModule.forRoot(InMemHeroService, { delay: 1500 }), // 1.5 秒延迟查询字符串:正则风格的数据过滤
查询串允许你以"属性=正则"的方式传自定义过滤器。其解析实现在applyQuery(backend-service.ts):多个条件之间以AND逻辑组合,逐条用RegExp测试集合条目对应属性。
格式:
/app/heroes/?propertyName=regexPattern示例——匹配 heroes 集合中所有以j或J开头的名字:
/app/heroes/?name=^j两点补充:
- 匹配默认大小写不敏感(源码中
caseSensitiveSearch为false时正则会附加i标志); - 需要区分大小写时设置
config.caseSensitiveSearch = true。
注意查询匹配发生在"属性名"上,且实现仅针对集合条目的字符串属性做正则比对,这一点从源码注释也能确认,属于有意设计而非缺陷。
PassThru:透传给真实服务器
当现有、正在运行的远程服务器应接管那些不在内存数据库里的集合请求时,设置Config.passThruUnknownUrl: true。此后无法识别的请求会通过 Angular 默认的 XHR 后端转发到远程服务器(具体取决于你用的是Http还是HttpClient)。对于HttpClient,透传后端由 http-client-backend-service.ts 在注入上下文内 new 出一个HttpXhrBackend(依赖注入的XhrFactory)实现——它会在首次需要时被惰性创建,并且在commands/config更新配置后被清空重建。
Commands:运行时控制内存服务
客户端可发出命令请求,用于获取内存服务的配置状态、重新配置它,或重置内存数据库。当 API 基础路径的最后一段是commands时,collectionName被当作命令名对待。请求评估的第一步(/commands\/?$/i正则)就是为它准备的,且服务对命令的处理在 backend-service.ts。
示例 URL:
commands/resetdb // 将"数据库"重置为初始状态 commands/config // 获取或更新本服务的配置对象使用方式:
http.post('commands/resetdb', undefined); http.get('commands/config'); http.post('commands/config', '{"delay":1000}');命令请求不模拟真实远端数据访问,它们忽略延迟、尽可能即时响应(createResponse$的withDelay参数被置为false)。命令名大小写不敏感——服务内部会先做toLowerCase(),因此resetDb与resetdb等价。
resetDb命令会以RequestInfo对象回调你的InMemoryDbService.createDb(见 backend-service.ts 的resetDb:先置dbReadySubject为false,再执行createDb,待数据库就绪后置回true)。借助这一点,你可以让createDb依据客户端请求调整行为。下面示例在命令请求体中携带重置选项:
http // 用 clear 选项重置数据库集合 .post('commands/resetDb', { clear: true })) // 命令完成后,获取 heroes .concatMap( ()=> http.get<Data>('api/heroes') .map(data => data.data as Hero[]) ) // 顺序执行请求序列并处理 heroes .subscribe(...)上述代码块沿用了 README 中较旧的 RxJS 链式操作符写法;在 RxJS 6+ 中建议改用
.pipe(concatMap(...), map(...))。源码自身(如handleRequest)采用的就是pipe(concatMap(...))的 pipeable 风格。
resetDb的时序被用于夹具演示:在 hero-in-mem-data-service.ts 中,createDb(reqInfo)检查请求体,若body.clear === true则先清空各集合,再从body.returnType决定以对象 / Observable / Promise 之一返回数据库。
parseRequestUrl:URL 解析的定制
parseRequestUrl把请求 URL 解析为ParsedRequestUrl对象(定义见 interfaces.ts),其公开属性apiBase、collectionName、id、query、resourceUrl指导服务完成后续处理。默认实现依赖 config 中的apiBase、host与urlRoot三个值,源码位于 backend-service.ts。其中配置apiBase能带来最有意思的解析行为变化:
当
apiBase=undefined,url='http://localhost/api/customers/42':{apiBase: 'api/', collectionName: 'customers', id: '42', ...}当
apiBase='some/api/root/',url='http://localhost/some/api/root/customers':{ apiBase: 'some/api/root/', collectionName: 'customers', id: undefined, ... }当
apiBase='/',url='http://localhost/customers':{ apiBase: '/', collectionName: 'customers', id: undefined, ... }
实际的 api base 段内容被忽略,只有段的数量有意义——以下字符串等价:'a/b' ~ 'some/api/' ~ 'two/segments'。这也带来一个反直觉后果:能通过内存 Web API 的 URL,真实服务器未必接受。
要定制解析行为,在你的InMemoryDbService上实现自定义parseRequestUrl方法即可。服务会传入两个参数:
url——请求 URL 字符串;requestInfoUtils——RequestInfoUtilities工具对象中的一系列工具方法,包含默认解析器。注意部分字段(如id、collection)此刻尚未计算完成,因为它们依赖解析结果。
你的方法必须返回一个ParsedRequestUrl对象,或返回null/undefined——后者意味着交给默认解析器。这样你可以只拦截并解析部分 URL,其余留给默认逻辑。夹具 hero-in-mem-data-override-service.ts 就演示了把/foo/heroes改写为/heroes后再调用默认解析器。
自定义 genId:接管主键生成
集合条目默认应带id主键属性。新增条目时你可以显式指定id,此时服务会盲目使用该值、不校验唯一性;若不指定,则由genId方法生成。
默认生成逻辑genIdDefault(backend-service.ts)只支持数字型 id:取集合现有最大数字 id 加一。若集合 id 类型非数字或未知,会抛出错误:
Collection '<name>' id type is non-numeric or unknown. Can only generate numeric ids.要定制生成策略,在InMemoryDbService中实现名为genId的方法:它接收新增条目所属的 collection 与 collection name,返回生成的 id;若你的生成器返回null/undefined,服务退回默认生成器。测试夹具演示了"按集合区分策略"的典型写法(hero-in-mem-data-override-service.ts):对nobodies集合生成伪 GUID,其他集合则从 1000 起递增。
responseInterceptor:改写每个响应
responseInterceptor允许你改动服务内建 HTTP 方法返回的响应,典型用途是给应用期望的响应追加请求头。
实现方式:在InMemoryDbService类中添加responseInterceptor方法。服务以如下形式调用它(类型定义为ResponseInterceptor,见 interfaces.ts):
responseOptions = this.responseInterceptor(responseOptions, requestInfo);夹具里的拦截器(hero-in-mem-data-override-service.ts)给所有响应头附加了x-test: test-header。collectionHandler会在内建 CRUD 方法生成ResponseOptions后、真正发出响应前调用它,因此它能影响最终送达客户端的响应头、状态码与 body。
HTTP 方法拦截器:接管任意 HTTP 动词
当请求超出内存 Web API 的内建处理能力(例如需要特殊的业务语义),你可以通过实现同名小写方法来覆盖任意 HTTP 方法。方法名必须与 HTTP 方法拼写一致且全小写(如get),服务会以RequestInfo对象调用它:
yourInMemDbService"get"自定义 HTTP 方法必须返回以下二者之一:
Observable<Response>——说明你处理了该请求,响应由此 Observable 提供。官方建议它应为"cold"(惰性)Observable,以便和createResponse$保持一致的生命周期语义;null/undefined——表示你决定不干预(也许只想针对该 HTTP 方法的部分路径做拦截),服务将继续走默认处理流程。
RequestInfo是定义在src/in-mem/interfaces.ts(即 interfaces.ts)的公开接口。其完整成员(含文档所列各项)包括:
req: Request; // 来自客户端的请求对象 apiBase: string; // 解析出的 api 基础路径 collectionName: string; // 由请求 URL 计算出的集合名 collection: any; // 命中的集合(若存在) headers: HttpHeaders; // 响应默认头 method: string; // 小写的 HTTP 方法名 id: any; // 条目的 `id`(若指定) query: Map<string, string[]>; // 解析出的查询参数 resourceUrl: string; // 资源的有效 URL url: string; // 请求中的 URL utils: RequestInfoUtilities; // 辅助函数集合RequestInfo.utils(RequestInfoUtilities,见 interfaces.ts)为你提供一批辅助能力:createResponse$(按内存后端一致的方式从ResponseOptions工厂构造冷响应,可带延迟)、findById、getConfig、getDb、getJsonBody、getLocation、getPassThruBackend、isCollectionIdNumeric、parseRequestUrl。夹具get拦截器(hero-in-mem-data-override-service.ts)展示了完整用法:仅当collectionName === 'villains'时返回自定义villains集合数据(内存库之外的另一处静态数据),其余请求返回undefined交给默认 GET。
仓库内样例:从哪里读代码最快上手
- 测试夹具
HeroInMemDataService(hero-in-mem-data-service.ts):Hero 导向的InMemoryDbService,与 Angular 官方文档 HTTP 示例风格一致,演示了同步/异步createDb与基于reqInfo的动态重建; - 扩展夹具
HeroInMemDataOverrideService(hero-in-mem-data-override-service.ts):继承基础夹具并演示genId覆盖、HTTP GET 拦截、parseRequestUrl覆盖与responseInterceptor四种扩展点,是阅读后自己动手的最佳蓝本; - 行为测试http-client-backend-service_spec.ts 及配套夹具
hero-service.ts、http-client-hero-service.ts:以断言形式锁定了 CRUD、查询、命令、透传、拦截、响应封装等各类行为,是理解"某配置到底产生什么效果"的最直接参考资料。
使用建议与升级维护
最后回到两个工程实践要点:
- 变更排查:如果你遇到"过去能用、现在不工作"的情况,先检查是否升级了该库的新版本,阅读 CHANGELOG.md 中是否包含影响应用的破坏性变更;若仍未解决,再到 Angular 仓库提交 issue,最好附上一个最小可复现示例;
- 生产红线:无论内存 Web API 多方便,请始终遵守其"仅限开发与测试"的定位,并在生产构建中按上文的环境判断方式彻底移除该模块。
综上,angular-in-memory-web-api把"没有后端也能像有后端一样开发"这件事做成了一套小而完整的工程方案:读懂它的请求评估链、默认配置与各类扩展点之后,你既能高效为 Demo、测试与 CI 搭建可控的数据环境,也能在必要时以极小的成本定制出贴近真实业务的服务语义。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考