使用Context.Tag定义服务
https://github.com/typeonce-dev/effect-getting-started-course
WARNINGGenericTag 的局限性
使用
GenericTag定义服务时存在一些隐藏问题:
- 我们正在定义、引用和导出单独的值(维护复杂且更容易出错)
/// 1️⃣ 定义服务接口 export interface PokeApi { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } /// 2️⃣ 为服务定义 `Context` export const PokeApi = Context.GenericTag<PokeApi>("PokeApi"); /// 3️⃣ 定义实现 export const PokeApiLive = PokeApi.of({ getPokemon: Effect.gen(function* () { const baseUrl = yield* Config.string("BASE_URL"); const response = yield* Effect.tryPromise({ try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`), catch: () => new FetchError(), }); if (!response.ok) { return yield* new FetchError(); } const json = yield* Effect.tryPromise({ try: () => response.json(), catch: () => new JsonError(), }); return yield* Schema.decodeUnknown(Pokemon)(json); }), });
index.ts
import { Effect } from "effect"; // 👇 为服务定义和实现分别/多次导入 import { PokeApi, PokeApiLive } from "./PokeApi"; const program = Effect.gen(function* () { const pokeApi = yield* PokeApi; return yield* pokeApi.getPokemon; }); const runnable = program.pipe(Effect.provideService(PokeApi, PokeApiLive));
- 我们面临服务类型之间冲突的风险(具有相同结构的服务)
export interface PokeApi { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } // ⛔️ 这是 2 个不同的导出,但它们引用相同的 `PokeApi` 接口 export const PokeApi1 = Context.GenericTag<PokeApi>("PokeApi1"); export const PokeApi2 = Context.GenericTag<PokeApi>("PokeApi2");
如果我们想要 100% 确保避免冲突,我们需要添加另一个 interface,使用 Context.GenericTag 的第一个类型参数使每个服务唯一:
// 👇 `Symbol` 使这个实例唯一 interface _PokeApi1 { readonly _: unique symbol; } export const PokeApi1 = Context.GenericTag<_PokeApi1, PokeApi>("PokeApi1"); // 👇 `Symbol` 使这个实例唯一 interface _PokeApi2 { readonly _: unique symbol; } export const PokeApi2 = Context.GenericTag<_PokeApi2, PokeApi>("PokeApi2");
当然,Effect 为此提供了解决方案:class 和 Context.Tag。
使用Context.Tag的服务类
我们可以使用 class 和 Context.Tag 更轻松地定义服务:
服务标签 “PokeApi”
第一个类型参数与
class名称相同(PokeApi)第二个类型参数是服务的签名(
PokeApiImpl)
interface PokeApiImpl { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } export class PokeApi extends Context.Tag("PokeApi")<PokeApi, PokeApiImpl>() {}
TIPContext.Tag 的优势
我们将完整的服务定义简化为一个值:
由于
PokeApi是一个class,它既可以作为值也可以作为类型
Context.Tag确保服务是唯一的(内部)我们可以在
class内部定义方法和属性,这些将对服务的任何实例都可访问。我们使用这个来将服务实现定义为static属性
interface PokeApiImpl { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } export class PokeApi extends Context.Tag("PokeApi")<PokeApi, PokeApiImpl>() { static readonly Live = PokeApi.of({ getPokemon: Effect.gen(function* () { const baseUrl = yield* Config.string("BASE_URL"); const response = yield* Effect.tryPromise({ try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`), catch: () => new FetchError(), }); if (!response.ok) { return yield* new FetchError(); } const json = yield* Effect.tryPromise({ try: () => response.json(), catch: () => new JsonError(), }); return yield* Schema.decodeUnknown(Pokemon)(json); }), }); }
Context.Tag内部的static readonly Live是 Effect 中常见且推荐的模式。
它允许直接从 PokeApi 引用所有实现(Live、Test、Mock)只需单次导入。
有了这个,我们只需导入 PokeApi:
index.ts
import { Effect } from "effect"; import { PokeApi } from "./PokeApi"; const program = Effect.gen(function* () { const pokeApi = yield* PokeApi; return yield* pokeApi.getPokemon; }); const runnable = program.pipe(Effect.provideService(PokeApi, PokeApi.Live)); const main = runnable.pipe( Effect.catchTags({ FetchError: () => Effect.succeed("Fetch error"), JsonError: () => Effect.succeed("Json error"), ParseError: () => Effect.succeed("Parse error"), }) ); Effect.runPromise(main).then(console.log);
使用
Context.Tag而不是Context.GenericTag是最佳实践。
再次,我们为一个开始时”简单”的 API 请求添加了大量代码。
PokeApi.ts
import { Config, Context, Effect, Schema, type ParseResult } from "effect"; import type { ConfigError } from "effect/ConfigError"; import { FetchError, JsonError } from "./errors"; import { Pokemon } from "./schemas"; interface PokeApiImpl { readonly getPokemon: Effect.Effect< Pokemon, FetchError | JsonError | ParseResult.ParseError | ConfigError >; } export class PokeApi extends Context.Tag("PokeApi")<PokeApi, PokeApiImpl>() { static readonly Live = PokeApi.of({ getPokemon: Effect.gen(function* () { const baseUrl = yield* Config.string("BASE_URL"); const response = yield* Effect.tryPromise({ try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`), catch: () => new FetchError(), }); if (!response.ok) { return yield* new FetchError(); } const json = yield* Effect.tryPromise({ try: () => response.json(), catch: () => new JsonError(), }); return yield* Schema.decodeUnknown(Pokemon)(json); }), }); }
index.ts
import { Effect } from "effect"; import { PokeApi } from "./PokeApi"; const program = Effect.gen(function* () { const pokeApi = yield* PokeApi; return yield* pokeApi.getPokemon; }); const runnable = program.pipe(Effect.provideService(PokeApi, PokeApi.Live)); const main = runnable.pipe( Effect.catchTags({ FetchError: () => Effect.succeed("Fetch error"), JsonError: () => Effect.succeed("Json error"), ParseError: () => Effect.succeed("Parse error"), }) ); Effect.runPromise(main).then(console.log);
事实证明,服务仍然可能过于有限。如果我们有更多相互依赖的服务会怎样?
我们不想多次创建它们或在任何地方手动使用 Effect.provideService。
欢迎使用 Layer!让我们跳到下一个模块来学习如何使用它!