Effect -- Platform (1)

一、Effect 平台介绍

@effect/platform 是一个用于在 Node.js、Deno、Bun 和浏览器等环境中构建平台无关抽象的库。

通过 @effect/platform,你可以将文件系统(FileSystem)或终端(Terminal)等抽象服务集成到程序中。在组装最终应用时,你可以使用对应的包为目标平台提供特定层:

  • 适用于 Node.js 或 Deno 的 @effect/platform-node
  • 适用于 Bun 的 @effect/platform-bun
  • 适用于浏览器的 @effect/platform-browser

稳定模块

以下模块已稳定,其文档可在本网站查阅:

模块描述状态
Command(命令)提供与命令行交互的方式稳定
FileSystem(文件系统)用于文件系统操作的模块稳定
KeyValueStore(键值存储)管理用于数据存储的键值对稳定
Path(路径)用于处理文件路径的工具类稳定
PlatformLogger(平台日志器)使用文件系统 API 将日志消息记录到文件稳定
Runtime(运行时)运行程序,内置错误处理和日志记录功能稳定
Terminal(终端)用于终端交互的工具稳定

不稳定模块

@effect/platform 中的部分模块仍处于开发阶段或标记为实验性模块,这些功能可能会发生变化。

模块描述状态
Http API(HTTP 接口)提供声明式的 HTTP 接口定义方式不稳定
Http Client(HTTP 客户端)用于发送 HTTP 请求的客户端不稳定
Http Server(HTTP 服务器)用于处理 HTTP 请求的服务器不稳定
Socket(套接字)用于基于套接字通信的模块不稳定
Worker(工作线程)用于在独立工作线程中运行任务的模块不稳定

安装

如需安装测试版,请执行以下命令:

包管理器安装命令
npmnpm install @effect/platform
pnpmpnpm add @effect/platform
Yarnyarn add @effect/platform
Bunbun add @effect/platform
Denodeno add npm:@effect/platform

跨平台编程入门

以下是使用 Path 模块创建文件路径的基础示例,该示例可在不同环境中运行:

示例(跨平台路径处理)

index.ts

import { Path } from "@effect/platform"
import { Effect } from "effect"

const program = Effect.gen(function* () {
  // 访问 Path 服务
  const path = yield* Path.Path

  // 拼接路径片段以创建完整的文件路径
  const mypath = path.join("tmp", "file.txt")

  console.log(mypath)
})

在 Node.js 或 Deno 中运行程序

首先,安装适用于 Node.js 的特定包:

包管理器安装命令
npmnpm install @effect/platform-node
pnpmpnpm add @effect/platform-node
Yarnyarn add @effect/platform-node
Denodeno add npm:@effect/platform-node

更新程序以加载 Node.js 特定上下文:

示例(提供 Node.js 上下文)

index.ts

import { Path } from "@effect/platform"
import { Effect } from "effect"
import { NodeContext, NodeRuntime } from "@effect/platform-node"

const program = Effect.gen(function* () {
  // 访问 Path 服务
  const path = yield* Path.Path

  // 拼接路径片段以创建完整的文件路径
  const mypath = path.join("tmp", "file.txt")

  console.log(mypath)
})

NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))

最后,使用 tsx 在 Node.js 中运行程序,或直接在 Deno 中运行:

运行环境运行命令输出结果
npmnpx tsx index.tstmp/file.txt
pnpmpnpm dlx tsx index.tstmp/file.txt
Yarnyarn dlx tsx index.tstmp/file.txt
Denodeno run index.ts 或 deno run -RE index.ts(无需提示即可授予所需的读取和环境权限)tmp/file.txt

在 Bun 中运行程序

要在 Bun 中运行同一个程序,首先安装适用于 Bun 的特定包:

bun add @effect/platform-bun

更新程序以使用 Bun 特定上下文:

示例(提供 Bun 上下文)

index.ts

import { Path } from "@effect/platform"
import { Effect } from "effect"
import { BunContext, BunRuntime } from "@effect/platform-bun"

const program = Effect.gen(function* () {
  // 访问 Path 服务
  const path = yield* Path.Path

  // 拼接路径片段以创建完整的文件路径
  const mypath = path.join("tmp", "file.txt")

  console.log(mypath)
})

BunRuntime.runMain(program.pipe(Effect.provide(BunContext.layer)))

在 Bun 中运行程序:

bun index.ts

输出结果:tmp/file.txt

二、命令(Command)

@effect/platform/Command 模块提供了一种方式,可创建并运行带有指定进程名称和可选参数列表的命令。

创建命令

Command.make 函数会生成一个命令对象,包含进程名称、参数、环境变量等详情。

示例(定义目录列表命令)

import { Command } from "@effect/platform"

const command = Command.make("ls", "-al")
console.log(command)
/*
{
  _id: '@effect/platform/Command',
  _tag: 'StandardCommand',
  command: 'ls',
  args: [ '-al' ],
  env: {},
  cwd: { _id: 'Option', _tag: 'None' },
  shell: false,
  gid: { _id: 'Option', _tag: 'None' },
  uid: { _id: 'Option', _tag: 'None' }
}
*/

该命令对象需通过执行器(executor)触发后才会执行。

运行命令

需通过 CommandExecutor 运行命令,它支持以字符串、行、流等多种格式捕获输出。

示例(运行命令并打印输出)

import { Command } from "@effect/platform"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const command = Command.make("ls", "-al")

// 程序依赖 CommandExecutor
const program = Effect.gen(function* () {
  // 运行命令并以字符串形式返回输出
  const output = yield* Command.string(command)
  console.log(output)
})

// 提供所需的 CommandExecutor
NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))

输出格式

可通过不同方法处理命令输出,具体如下:

方法描述
string运行命令,以字符串形式返回输出(支持指定编码)
lines运行命令,以行数组形式返回输出(支持指定编码)
stream运行命令,以 Uint8Array 块流的形式返回输出
streamLines运行命令,以行流的形式返回输出(支持指定编码)

退出码(exitCode)

若仅需获取命令的退出码,可使用 Command.exitCode

示例(获取退出码)

import { Command } from "@effect/platform"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const command = Command.make("ls", "-al")

const program = Effect.gen(function* () {
  const exitCode = yield* Command.exitCode(command)
  console.log(exitCode)
})

NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))
// 输出:0

自定义环境变量

可通过 Command.env 为命令自定义环境变量,适用于命令执行需特定变量的场景。

示例(设置环境变量)

本示例中,命令在 shell 中运行,以确保环境变量能正确解析。

import { Command } from "@effect/platform"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const command = Command.make("echo", "-n", "$MY_CUSTOM_VAR").pipe(
  Command.env({
    MY_CUSTOM_VAR: "Hello, this is a custom environment variable!"
  }),
  // 在 Windows 和类 Unix 系统中,通过 shell 确保变量正确解析
  Command.runInShell(true)
)

const program = Effect.gen(function* () {
  const output = yield* Command.string(command)
  console.log(output)
})

NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))
// 输出:Hello, this is a custom environment variable!

向命令输入数据

通过 Command.feed 函数,可直接向命令的标准输入(stdin)发送数据。

示例(向命令的标准输入发送数据)

import { Command } from "@effect/platform"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

const command = Command.make("cat").pipe(Command.feed("Hello"))

const program = Effect.gen(function* () {
  console.log(yield* Command.string(command))
})

NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))
// 输出:Hello

获取进程详情

可获取运行中进程的详情,如 exitCode(退出码)、stdout(标准输出)、stderr(标准错误输出)等。

示例(获取运行中进程的退出码和流)

import { Command } from "@effect/platform"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect, Stream, String, pipe } from "effect"

// 辅助函数:将流输出收集为字符串
const runString = <E, R>(
  stream: Stream.Stream<Uint8Array, E, R>
): Effect.Effect<string, E, R> =>
  stream.pipe(
    Stream.decodeText(),
    Stream.runFold(String.empty, String.concat)
  )

const program = Effect.gen(function* () {
  const command = Command.make("ls")

  const [exitCode, stdout, stderr] = yield* pipe(
    // 启动命令并返回运行中进程的句柄
    Command.start(command),
    Effect.flatMap((process) =>
      Effect.all(
        [
          // 等待进程退出并返回命令的退出码
          process.exitCode,
          // 进程的标准输出流
          runString(process.stdout),
          // 进程的标准错误流
          runString(process.stderr)
        ],
        { concurrency: 3 }
      )
    )
  )
  console.log({ exitCode, stdout, stderr })
})

NodeRuntime.runMain(
  Effect.scoped(program).pipe(Effect.provide(NodeContext.layer))
)

将 stdout 流式传输到 process.stdout

可通过以下方式,将命令的 stdout(标准输出)直接流式传输到 process.stdout

示例(将命令输出直接流式传输到标准输出)

import { Command } from "@effect/platform"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

// 创建命令:对文件执行 `cat` 命令并继承 stdout
const program = Command.make("cat", "./some-file.txt").pipe(
  Command.stdout("inherit"), // 将 stdout 流式传输到 process.stdout
  Command.exitCode // 获取退出码
)

NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))

三、文件系统(FileSystem)

@effect/platform/FileSystem 模块提供了一套用于读取和写入文件系统的操作方法。

基础用法

该模块提供唯一的 FileSystem 标签,作为与文件系统交互的入口。

示例(访问文件系统操作)

import { FileSystem } from "@effect/platform"
import { Effect } from "effect"

const program = Effect.gen(function* () {
  const fs = yield* FileSystem.FileSystem

  // 使用 `fs` 执行文件系统操作
})

文件系统操作列表

FileSystem 接口包含以下操作方法,详细说明如下:

操作方法描述
access检查文件是否可访问,可选择指定访问权限级别
copy将文件或目录从 fromPath 复制到 toPath,等效于 cp -r 命令
copyFile将文件从 fromPath 复制到 toPath
chmod修改文件权限
chown修改文件的所有者和所属组
exists检查路径是否存在
link创建从 fromPath 到 toPath 的硬链接
makeDirectory在指定 path 处创建目录,可选择指定权限模式(mode)和是否递归创建嵌套目录
makeTempDirectory创建临时目录,默认在系统默认临时目录内创建
makeTempDirectoryScoped在作用域(scope)内创建临时目录,功能与 makeTempDirectory 一致,但作用域关闭时会自动删除该目录
makeTempFile创建临时文件,目录创建逻辑与 makeTempDirectory 一致,文件名随机生成
makeTempFileScoped在作用域内创建临时文件,功能与 makeTempFile 一致,但作用域关闭时会自动删除该文件
open以指定 options 打开 path 处的文件,作用域关闭时文件句柄会自动关闭
readDirectory列出目录内容,设置 recursive 选项可递归列出嵌套目录内容
readFile读取文件内容
readFileString以字符串形式读取文件内容
readLink读取符号链接的目标路径
realPath将路径解析为规范化的绝对路径
remove删除文件或目录,设置 recursive: true 可递归删除嵌套目录
rename重命名文件或目录
sink为指定 path 创建可写入的 Sink(接收器)
stat获取 path 处文件的相关信息
stream为指定 path 创建可读的 Stream(流)
symlink创建从 fromPath 到 toPath 的符号链接
truncate将文件截断至指定长度,未指定 length 时默认截断为 0 长度
utimes修改 path 处文件的文件系统时间戳
watch监听目录或文件的变化
writeFile向 path 处的文件写入数据
writeFileString向 path 处的文件写入字符串

示例(以字符串形式读取文件)

import { FileSystem } from "@effect/platform"
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

//      ┌─── Effect<void, PlatformError, FileSystem>
//      ▼
const program = Effect.gen(function* () {
  const fs = yield* FileSystem.FileSystem

  // 读取当前代码所在文件的内容
  const content = yield* fs.readFileString("./index.ts", "utf8")
  console.log(content)
})

// 提供必要的上下文并运行程序
NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))

模拟文件系统

在测试环境中,你可能需要模拟文件系统以避免执行实际的磁盘操作。FileSystem.layerNoop 提供了 FileSystem 服务的空操作(无实际效果)实现。

FileSystem.layerNoop 中的大多数操作会返回失败结果(例如,文件不存在时返回 Effect.fail)或缺陷(例如,未实现的功能返回 Effect.die)。不过,你可以向 FileSystem.layerNoop 传入一个对象,为选定的方法定义自定义返回值,从而覆盖特定行为。

示例(自定义行为的文件系统模拟)

import { FileSystem } from "@effect/platform"
import { Effect } from "effect"

const program = Effect.gen(function* () {
  const fs = yield* FileSystem.FileSystem

  const exists = yield* fs.exists("/some/path")
  console.log(exists)

  const content = yield* fs.readFileString("/some/path")
  console.log(content)
})

//      ┌─── Layer<FileSystem.FileSystem, never, never>
//      ▼
const customMock = FileSystem.layerNoop({
  readFileString: () => Effect.succeed("mocked content"),
  exists: (path) => Effect.succeed(path === "/some/path")
})

// 提供自定义的 FileSystem 模拟实现
Effect.runPromise(program.pipe(Effect.provide(customMock)))
/*
输出:
true
mocked content
*/

四、键值存储(KeyValueStore)

@effect/platform/KeyValueStore 模块提供了一个健壮且支持 Effect 特性的键值对管理接口。它支持异步操作,确保数据完整性和一致性,并包含内存存储、文件系统存储和模式验证存储等内置实现。

基础用法

该模块暴露唯一的服务 KeyValueStore,作为与存储系统交互的入口。

示例(访问 KeyValueStore 服务)

import { KeyValueStore } from "@effect/platform"
import { Effect } from "effect"

const program = Effect.gen(function* () {
  const kv = yield* KeyValueStore.KeyValueStore

  // 使用 `kv` 执行存储相关操作
})

键值存储操作列表

KeyValueStore 接口包含以下操作方法,详细说明如下:

操作方法描述
get若指定键存在,返回其字符串格式的值
getUint8Array若指定键存在,返回其 Uint8Array 格式的值
set设置指定键的值
remove删除指定键
clear删除所有条目
size返回条目数量
modify若指定键存在,更新其值
modifyUint8Array若指定键存在,更新其 Uint8Array 格式的值
has检查键是否存在
isEmpty检查存储是否为空
forSchema为指定模式(schema)创建 SchemaStore(模式化存储)

示例(键值存储的基础操作)

import {
  KeyValueStore,
  layerMemory
} from "@effect/platform/KeyValueStore"
import { Effect } from "effect"

const program = Effect.gen(function* () {
  const kv = yield* KeyValueStore

  // 存储初始为空
  console.log(yield* kv.size)

  // 设置键值对
  yield* kv.set("key", "value")
  console.log(yield* kv.size)

  // 获取值
  const value = yield* kv.get("key")
  console.log(value)

  // 删除键
  yield* kv.remove("key")
  console.log(yield* kv.size)
})

// 使用内存存储实现运行程序
Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
/*
输出:
0
1
{ _id: 'Option', _tag: 'Some', value: 'value' }
0
*/

内置实现

该模块包含两种 KeyValueStore 接口的内置实现,均以 “层(layer)” 的形式提供,可注入到支持 Effect 的程序中。

实现类型描述
内存存储(In-Memory Store)layerMemory 提供简单的内存键值存储,适用于轻量级场景或测试环境
文件系统存储(File System Store)layerFileSystem 提供基于文件的存储,满足持久化存储需求

处理非字符串值

默认情况下,KeyValueStore 支持 string 和 Uint8Array 类型的值。若需存储对象、数字、布尔值等其他类型,可使用 forSchema 方法创建 SchemaStore(模式化存储)。

SchemaStore 利用模式(schema)验证和转换值,内部通过 JSON.stringify 序列化数据,通过 JSON.parse 反序列化数据。

示例(使用模式存储类型化对象)

import {
  KeyValueStore,
  layerMemory
} from "@effect/platform/KeyValueStore"
import { Effect, Schema } from "effect"

// 定义 JSON 兼容的模式
const Person = Schema.Struct({
  name: Schema.String,
  age: Schema.Number
})

const program = Effect.gen(function* () {
  // 基于模式创建类型化存储
  const kv = (yield* KeyValueStore).forSchema(Person)

  // 存储类型化值
  const value = { name: "Alice", age: 30 }
  yield* kv.set("user1", value)
  console.log(yield* kv.size)

  // 获取值
  console.log(yield* kv.get("user1"))
})

// 本示例使用内存存储
Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
/*
输出:
1
{ _id: 'Option', _tag: 'Some', value: { name: 'Alice', age: 30 } }
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值