在 Nodejs 中复制 React Context

Nic*_*owr 6 javascript node.js reactjs react-context

我想在 Nodejs 中复制 React Context 的行为,但我正在努力解决它。

在反应,通过创建只有一个背景下,我可以提供,在我的组件占用不同的值,取决于value给予<Provider/>。所以以下工作:

const MyContext = React.createContext(0);

const MyConsumer = () => { 
  return (
    <MyContext.Consumer>
      {value => {
        return <div>{value}</div>
      }}
    </MyContext.Consumer>
  )
}

const App = () => 
    <React.Fragment>
      <MyContext.Provider value={1}>
        <MyConsumer/>
      </MyContext.Provider>
      <MyContext.Provider value={2}>
        <MyConsumer/>
      </MyContext.Provider>
    </React.Fragment>;

ReactDOM.render(
  <App/>,
  document.getElementById("react")
);
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Run Code Online (Sandbox Code Playgroud)

但是我不知道如何在 Nodejs 中实现它。我已经查看了 React Context 的源代码,但它没有多大帮助......这是我到目前为止所得到的:

// context.js
export const createContext = (defaultValue: number) => {
  const context = {
    value: defaultValue,
    withContext: null,
    useContext: null,
  };
  function withContext(value: number, callback: (...args: any[]) => any) {
    context.value = value;
    return callback;
  }
  function useContext() {
    return context;
  }

  context.withContext = withContext;
  context.useContext = useContext;

  return context;
};

// functions.js
import { context } from "./index";
export function a() {
  const result = context.useContext();

  console.log(result);
}

export function b() {
  const result = context.useContext();

  console.log(result);
}

// index.js
import { createContext } from "./context";
import { a, b } from "./functions";

export const context = createContext(0);

const foo = context.withContext(1, a);
const bar = context.withContext(2, b);

console.log("foo", foo());
console.log("bar", bar());
Run Code Online (Sandbox Code Playgroud)

显然,value被覆盖并被2记录两次。

任何帮助都感激不尽!

She*_*aff 11

2022年更新

NodeJS 提出了一个新的内置函数来实现这一点:异步上下文跟踪

感谢@Emmanuel Meric de Bellefon 指出了这一点。


定义所需的行为

如果您只需要它用于同步代码,您可以做一些相对简单的事情。您所需要的只是指定边界在哪里。

在 React 中,您可以使用 JSX 来完成此操作

<Context.Provider value={2}>
  <MyComponent />
</Context.Provider>
Run Code Online (Sandbox Code Playgroud)

在这个例子中, 的值Context将是2forMyComponent但在它的范围之外<Context.Provider>将是之前的值。

如果我用普通 JS 翻译它,我可能希望它看起来像这样:

<Context.Provider value={2}>
  <MyComponent />
</Context.Provider>
Run Code Online (Sandbox Code Playgroud)

在此示例中,我希望 的值context在其2范围内myFunction但在其范围之外context.provider()将是之前设置的任何值。

如何解决问题

最基本的是,这可以通过全局对象来解决

const myFunctionWithContext = context.provider(2, myFunction)
myFunctionWithContext('an argument')
Run Code Online (Sandbox Code Playgroud)

现在我们知道污染全局范围globalThiswindow. 因此我们可以使用Symbol来代替,以确保不会出现任何命名冲突:

// we define a "context"
globalThis.context = 'initial value'

function a() {
  // we can access the context
  const currentValue = globalThis.context
  console.log(`context value in a: ${currentValue}`)

  // we can modify the context for the "children"
  globalThis.context = 'value from a'
  b()

  // we undo the modification to restore the context
  globalThis.context = currentValue
}

function b() {
  console.log(`context value in b: ${globalThis.context}`)
}

a()
Run Code Online (Sandbox Code Playgroud)

然而,即使这个解决方案永远不会与全局范围产生冲突,它仍然不理想,并且不能很好地适应多个上下文。那么让我们创建一个“上下文工厂”模块:

const context = Symbol()

globalThis[context] = 'initial value'

function a() {
  console.log(`context value in a: ${globalThis[context]}`)
}

a()
Run Code Online (Sandbox Code Playgroud)

现在,只要您仅将其用于同步代码,只需在回调之前覆盖一个值并在(in provider)之后重置它即可。

同步代码的解决方案

如果您想像 React 一样构建代码,那么使用几个单独的模块会如下所示:

// in createContext.js
const contextMap = new Map() // all of the declared contexts, one per `createContext` call

/* export default */ function createContext(value) {
    const key = Symbol('context') // even though we name them the same, Symbols can never conflict
    contextMap.set(key, value)

    function provider(value, callback) {
        const old = contextMap.get(key)
        contextMap.set(key, value)
        callback()
        contextMap.set(key, old)
    }

    function consumer() {
        return contextMap.get(key)
    }

    return {
        provider,
        consumer,
    }
}

// in index.js
const contextOne = createContext('initial value')
const contextTwo = createContext('other context') // we can create multiple contexts without conflicts

function a() {
    console.log(`value in a: ${contextOne.consumer()}`)
    contextOne.provider('value from a', b)
    console.log(`value in a: ${contextOne.consumer()}`)
}

function b() {
    console.log(`value in b: ${contextOne.consumer()}`)
    console.log(`value in b: ${contextTwo.consumer()}`)
}

a()
Run Code Online (Sandbox Code Playgroud)

更进一步:异步代码的问题

如果是异步函数,上面提出的解决方案将不起作用b(),因为一旦b返回,上下文值就会重置为其值a()(这就是provider工作原理)。例如:

// in createContext.js
const contextMap = new Map()

/* export default */ function createContext(value) {
    const key = Symbol('context')
    contextMap.set(key, value)
    return {
        provider(value, callback) {
            const old = contextMap.get(key)
            contextMap.set(key, value)
            callback()
            contextMap.set(key, old)
        },
        consumer() {
            return contextMap.get(key)
        }
    }
}

// in myContext.js
/* import createContext from './createContext.js' */
const myContext = createContext('initial value')
/* export */ const provider = myContext.provider
/* export */ const consumer = myContext.consumer

// in a.js
/* import { provider, consumer } from './myContext.js' */
/* import b from './b.js' */
/* export default */ function a() {
    console.log(`value in a: ${consumer()}`)
    provider('value from a', b)
    console.log(`value in a: ${consumer()}`)
}

// in b.js
/* import { consumer } from './myContext.js' */
/* export default */ function b() {
    console.log(`value in b: ${consumer()}`)
}

// in index.js
/* import a from './a.js' */
a()
Run Code Online (Sandbox Code Playgroud)

到目前为止,我还没有真正了解如何正确管理异步函数的问题,但我打赌可以通过使用Symbol,this和来完成Proxy

用于this通过context

在开发同步代码解决方案时,我们发现我们可以“负担得起”向“不属于我们的”对象添加属性,只要我们使用Symbol键来执行此操作(就像我们globalThis在第一个示例中所做的那样)例子)。我们还知道函数总是使用隐式this参数调用,该参数是

  • 全局范围 ( globalThis),
  • 父作用域(当调用obj.func(), inside func, thiswill be 时obj
  • 任意范围对象(当使用.bind,.call或 时.apply
  • 在某些情况下,任意原始值(仅在严格模式下可能)

此外,javascript 允许我们将代理定义为对象与使用该对象的任何脚本之间的接口。在 a 中Proxy,我们可以定义一组陷阱,每个陷阱将处理使用我们的对象的特定方式。对于我们的问题来说,有趣的是apply,它捕获函数调用并让我们访问this该函数将被调用的对象。

知道了这一点,我们可以“增强”this使用上下文提供者调用的函数context.provider(value, myFunction),并使用引用上下文的符号:

const contextMap = new Map()
function createContext(value) {
    const key = Symbol('context')
    contextMap.set(key, value)

    function provider(value, callback) {
        const old = contextMap.get(key)
        contextMap.set(key, value)
        callback()
        contextMap.set(key, old)
    }
    
    function consumer() {
        return contextMap.get(key)
    }

    return {
        provider,
        consumer
    }
}

const { provider, consumer } = createContext('initial value')
function a() {
    console.log(`value in a: ${consumer()}`)
    provider('value from a', b)
    console.log(`value in a: ${consumer()}`)
}

async function b() {
  await new Promise(resolve => setTimeout(resolve, 1000))
  console.log(`value in b: ${consumer()}`) // we want this to log 'value from a', but it logs 'initial value'
}

a()
Run Code Online (Sandbox Code Playgroud)

Reflect将调用设置target为且参数来自的函数thisscopeargumentsList

只要我们“存储”的内容this允许我们获取范围的“当前”值(context.provider()调用的值),那么我们应该能够从内部访问该值myFunction,并且不需要设置/重置就像我们为同步解决方案所做的那样的唯一对象。

第一个异步解决方案:浅上下文

将所有这些放在一起,这是对类似反应上下文的异步解决方案的初步尝试。然而,与原型链不同的是,this当从另一个函数中调用一个函数时,它不会自动继承。因此,以下解决方案中的上下文只能在 1 级函数调用中幸存:

{
    apply: (target, thisArg = {}, argumentsList) => {
        const scope = Object.assign({}, thisArg, {[id]: key}) // augment `this`
        return Reflect.apply(target, scope, argumentsList) // call function
    }
}
Run Code Online (Sandbox Code Playgroud)

第二种异步方案:上下文转发

要使上下文在另一个函数调用中的函数调用中幸存下来,一个潜在的解决方案可能是必须将上下文显式转发到任何函数调用(这可能很快就会变得很麻烦)。从上面的例子,c()将更改为:

function createContext(initial) {
    const id = Symbol()

    function provider(value, callback) {
        return new Proxy(callback, {
            apply: (target, thisArg, argumentsList) => {
                const scope = Object.assign({}, thisArg, {[id]: value})
                return Reflect.apply(target, scope, argumentsList)
            }
        })
    }
    
    function consumer(scope = {}) {
        return id in scope ? scope[id] : initial
    }

    return {
        provider,
        consumer,
    }
}

const myContext = createContext('initial value')

function a() {
    console.log(`value in a: ${myContext.consumer(this)}`)
    const bWithContext = myContext.provider('value from a', b)
    bWithContext()
    const cWithContext = myContext.provider('value from a', c)
    cWithContext()
    console.log(`value in a: ${myContext.consumer(this)}`)
}

function b() {
    console.log(`value in b: ${myContext.consumer(this)}`)
}

async function c() {
    await new Promise(resolve => setTimeout(resolve, 200))
    console.log(`value in c: ${myContext.consumer(this)}`) // works in async!
    b() // logs 'initial value', should log 'value from a' (the same as "value in c")
}

a()
Run Code Online (Sandbox Code Playgroud)

其中myContext.forwarda 只是consumer获取值,然后直接 aprovider将其传递:

async function c() {
    await new Promise(resolve => setTimeout(resolve, 200))
    console.log(`value in c: ${myContext.consumer(this)}`)
    const bWithContext = myContext.forward(this, b)
    bWithContext() // logs 'value from a'
}
Run Code Online (Sandbox Code Playgroud)

将其添加到我们之前的解决方案中:

function forward(scope, callback) {
    const value = consumer(scope)
    return provider(value, callback)
}
Run Code Online (Sandbox Code Playgroud)

没有显式转发的异步函数的上下文

现在我陷入困境......我愿意接受想法!


Avi*_*ius 5

你的“在 NodeJS 中复制 React 的 Context”的目标有点模糊。来自 React 文档:

Context 提供了一种通过组件树传递数据的方法,而无需在每个级别手动向下传递 props。

NodeJS 中没有组件树。我能想到的最接近的类比(也基于您的示例)是调用堆栈。此外,如果值发生变化,React 的 Context 还会导致树的重新渲染。我不知道这在 NodeJS 中意味着什么,所以我很乐意忽略这个方面。

因此,我假设您本质上是在寻找一种方法,使值可以在调用堆栈中的任何位置访问,而不必将其作为参数传递到堆栈中在函数之间传递到堆栈中。

我建议你使用所谓的连续本地存储之一来实现这一目标。他们使用的模式与您尝试做的略有不同,但可能没问题。

我最喜欢的是CLS Hooked(无隶属关系)。它利用 NodeJS 的 async_hooks 系统来保留提供的上下文,即使堆栈中存在异步调用也是如此。上次发布是 4 年前,它仍然按预期工作。

我使用 CLS Hooked 重写了您的示例,尽管我认为这不是最好/最直观的使用方式。我还添加了一个额外的函数调用来演示可以覆盖值(即创建某种子上下文)。最后,有一个明显的区别 - 上下文现在必须有一个 ID。如果你想坚持使用这种 React Contexty 模式,你可能不得不接受它。

// context.js

import cls from "cls-hooked";

export const createContext = (contextID, defaultValue) => {
  const ns = cls.createNamespace(contextID);

  return {
    provide(value, callback) {
      return () =>
        ns.run(() => {
          ns.set("value", value);
          callback();
        });
    },
    useContext() {
      return ns.active ? ns.get("value") : defaultValue;
    }
  };
};
Run Code Online (Sandbox Code Playgroud)
// my-context.js

// your example had a circular dependency problem
// the context has to be created in a separate file

import { createContext } from "./context";

export const context = createContext("my-context", 0);
Run Code Online (Sandbox Code Playgroud)
// zz.js

import { context } from "./my-context";

export const zz = function () {
  console.log("zz", context.useContext());
};
Run Code Online (Sandbox Code Playgroud)
// functions.js

import { context } from "./my-context";
import { zz } from "./zz";

export const a = function () {
  const zzz = context.provide("AAA", zz);

  zzz();

  const result = context.useContext();

  console.log("a", result);
};

export const b = function () {
  const zzz = context.provide("BBB", zz);

  zzz();

  const result = context.useContext();

  console.log("b", result);
};
Run Code Online (Sandbox Code Playgroud)
// index.js

import { context } from "./c";
import { a, b } from "./functions";

const foo = context.provide(1, a);
const bar = context.provide(2, b);

console.log("default value", context.useContext());
foo();
bar();
Run Code Online (Sandbox Code Playgroud)

运行node index日志:

default value 0
zz AAA
a 1
zz BBB
b 2
Run Code Online (Sandbox Code Playgroud)

如果堆栈中发生各种异步调用,这也将起作用。

我如何使用它

我的方法有点不同。我并没有尝试复制 React 的 Context,它也有一个限制,因为它总是绑定到单个值。

// cls.ts

import cls from "cls-hooked";

export class CLS {
  constructor(private readonly NS_ID: string) {}

  run<T>(op: () => T): T {
    return (cls.getNamespace(this.NS_ID) || cls.createNamespace(this.NS_ID)).runAndReturn(op);
  }

  set<T>(key: string, value: T): T {
    const ns = cls.getNamespace(this.NS_ID);
    if (ns && ns.active) {
      return ns.set(key, value);
    }
  }

  get(key: string): any {
    const ns = cls.getNamespace(this.NS_ID);
    if (ns && ns.active) {
      return ns.get(key);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
// operations-cls.ts

import { CLS } from "./cls";

export const operationsCLS = new CLS("operations");
Run Code Online (Sandbox Code Playgroud)
// consumer.ts

import { operationsCLS } from "./operations-cls";

export const consumer = () => {
  console.log(operationsCLS.get("some-value")); // logs 123
};
Run Code Online (Sandbox Code Playgroud)
// app.ts

import { operationsCLS } from "./operations-cls";
import { consumer } from "./consumer";

cls.run(async () => {
  cls.set("some-value", 123);
  consumer();
});
Run Code Online (Sandbox Code Playgroud)

CLS 的工作原理

我更喜欢将 CLS 视为魔法,因为在没有我干预的情况下它总是运行良好,所以不能在这里发表太多评论,抱歉:]