使用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!让我们跳到下一个模块来学习如何使用它!