Logo of unctx

unctx

纯 JS 中的 Composables

Vanilla JS 中的 Composition API

unctx 是什么?

Vue.js 引入了一种名为 Composition API 的出色模式,它允许通过将复杂逻辑拆分为可重用函数并按逻辑顺序分组来组织代码。unctx 使您能够轻松地在 JavaScript 库中实现 Composition API 模式,而无需麻烦。

用法

在您的出色库中

yarn add unctx
# or
npm install unctx
import { createContext } from "unctx";

const ctx = createContext();

export const useAwesome = ctx.use;

// ...
ctx.call({ test: 1 }, () => {
  // This is similar to the vue setup function
  // Any function called here can use `useAwesome` to get { test: 1 }
});

用户代码

import { useAwesome } from "awesome-lib";

// ...
function setup() {
  const ctx = useAwesome();
}

注意:当没有提供上下文时,ctx.use 将抛出错误。对于容错用法(返回可为空的上下文),请使用 ctx.tryUse

使用命名空间

为避免库多个版本的问题,unctx 提供了一个安全的全局命名空间,通过键(保存在 globalThis 中)访问上下文。重要提示:请为键使用一个详细的名称,以避免与其他 JavaScript 库冲突。建议使用 npm 包名。使用 Symbol 没有效果,因为它仍然会导致多个上下文问题。

import { useContext, getContext } from "unctx";

const useAwesome = useContext("awesome-lib");

// or
// const awesomeContext = getContext('awesome-lib')

您也可以使用 createNamespace 工具为更高级的用例创建内部命名空间。

异步上下文

上下文只能在非异步用法中以及在第一个 await 语句之前使用。这是为了确保上下文不会在并发调用之间共享。

async function setup() {
  console.log(useAwesome()); // Returns context
  setTimeout(() => {
    console.log(useAwesome());
  }, 1); // Returns null
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log(useAwesome()); // Returns null
}

一个简单的解决方案是将上下文缓存到局部变量中

async function setup() {
  const ctx = useAwesome(); // We can directly access cached version of ctx
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log(ctx);
}

这并非总是通过创建变量并传递变量来解决问题的优雅且简单的方法。毕竟,`unctx` 的目的就是确保上下文在所有可组合函数中都能神奇地可用!

原生异步上下文

Unctx 支持 Node.js 的 AsyncLocalStorage,作为保留和跟踪异步上下文的原生方式。要启用此模式,您需要设置 asyncContext: true 选项,并提供 AsyncLocalStorage 的实现(或提供 globalThis.AsyncLocalStorage polyfill)。

请参阅 tc39 异步上下文提案cloudflare 文档 以获取相关平台特定文档。

import { createContext } from "unctx";
import { AsyncLocalStorage } from "node:async_hooks";

const ctx = createContext({
  asyncContext: true,
  AsyncLocalStorage,
});

ctx.call("123", () => {
  setTimeout(() => {
    // Prints 123
    console.log(ctx.use());
  }, 100);
});

异步转换

由于原生异步上下文尚未在所有平台中得到支持,`unctx` 提供了一个构建时解决方案,可转换异步语法,以便在每个 async/await 语句后自动恢复上下文。这需要使用 Rollup、Vite 或 Webpack 等打包工具。

导入并注册转换插件

import { unctxPlugin } from "unctx/plugin";

// Rollup
// TODO: Add to rollup configuration
unctxPlugin.rollup();

// Vite
// TODO: Add to vite configuration
unctxPlugin.vite();

// Webpack
// TODO: Add to webpack configuration
unctxPlugin.webpack();

使用 ctx.callAsync 而不是 ctx.call

await ctx.callAsync("test", setup);

任何需要上下文的异步函数都应该用 withAsyncContext 包裹。

import { withAsyncContext } from "unctx";

const setup = withAsyncContext(async () => {
  console.log(useAwesome()); // Returns context
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log(useAwesome()); // Still returns context with dark magic!
});

单例模式

如果您确定使用共享实例是安全的(不依赖于请求),您也可以使用 ctx.setctx.unset 来实现单例模式

注意:您不能将 setcall 结合使用。在替换实例之前,务必使用 unset,否则您将收到 Context conflict 错误。

import { createContext } from "unctx";

const ctx = createContext();
ctx.set(new Awesome());

// Replacing instance without unset
// ctx.set(new Awesome(), true)

export const useAwesome = ctx.use;

类型化上下文

所有工具上都存在一个泛型类型,用于为 TypeScript 支持设置实例/上下文类型。

// Return type of useAwesome is Awesome | null
const { use: useAwesome } = createContext<Awesome>();

内部原理

通过临时上下文注入,函数组合成为可能。当调用 ctx.call(instance, cb) 时,instance 参数将被存储在一个临时变量中,然后 cb 被调用。cb 内部的任何函数都可以通过使用 ctx.use(或 useAwesome)隐式地访问该实例。

常见问题

上下文只能在第一个 await 之前使用:

请查看 异步上下文 部分。

Context conflict 错误:

在您的库中,您应该只保持一个 call() 同时运行(除非第一个参数使用相同的引用进行调用)。

例如,这会引发错误

ctx.call({ test: 1 }, () => {
  ctx.call({ test: 2 }, () => {
    // Throws error!
  });
});

许可

MIT。倾情打造 💖