在 Nodejs 中复制 React 上下文

Posted

技术标签:

【中文标题】在 Nodejs 中复制 React 上下文【英文标题】:Replicate React Context in Nodejs 【发布时间】:2021-09-11 12:40:53 【问题描述】:

我想在 Nodejs 中复制 React Context 的行为,但我正在为此苦苦挣扎。

在 React 中,通过只创建一个上下文,我可以在我的组件中提供和使用不同的值,这取决于给 <Provider/>value。所以以下工作:

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")
);
<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>

但是我不知道如何在 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());

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

任何帮助将不胜感激!

【问题讨论】:

用例会是什么?能给个实际用处吗?示例用例对于帮助理解您试图在 nodejs 中解决的问题没有多大意义。 我的用例:我在本地运行两台快速服务器,每台都连接到自己的数据库。我正在使用typeorm,我需要将两个连接名称保持为“默认”,但它不起作用,因为两台服务器似乎共享同一个typeorm 实例。我的想法是,在 Nodejs 中拥有类似 React Context 之类的东西可以用自己的 typeorm 实例包围每个服务器。除此之外,我认为这种模式适用于我们在 nodejs 中需要“上下文”的任何情况。 【参考方案1】:

定义期望的行为

如果您只需要 同步 代码,您可以做一些相对简单的事情。您只需指定边界的位置即可。

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

<Context.Provider value=2>
  <MyComponent />
</Context.Provider>

在此示例中,Context 的值将是 2MyComponent 但在 &lt;Context.Provider&gt; 的范围之外,它将是之前的值。

如果我要在原版 JS 中翻译它,我可能希望它看起来像这样:

const myFunctionWithContext = context.provider(2, myFunction)
myFunctionWithContext('an argument')

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

如何解决问题

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

// 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()

现在我们知道污染全局范围globalThiswindow 绝不是明智之举。所以我们可以改用Symbol,以确保不会有任何命名冲突:

const context = Symbol()

globalThis[context] = 'initial value'

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


a()

但是,即使此解决方案永远不会与全局范围发生冲突,它仍然不理想,并且不能很好地适应多种环境。所以让我们做一个“上下文工厂”模块:

// 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()

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

同步码的解决方案

如果您想像在 react 中那样构建您的代码,下面是几个单独的模块的样子:

// 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()

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

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

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()

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

使用this 传递context

在为同步代码开发解决方案时,我们已经看到,只要我们使用 Symbol 键,我们就可以“负担”向“不属于我们的”对象添加属性(例如我们在第一个示例中使用了globalThis)。我们还知道,函数总是使用隐含的this 参数调用

全局范围 (globalThis), 父作用域(调用obj.func()时,在func内,this将是obj) 任意范围对象(使用.bind.call.apply 时) 在某些情况下,任意原始值(仅在严格模式下才有可能)

此外,javascript 允许我们将Proxy 定义为对象与使用该对象的任何脚本之间的接口。在Proxy 中,我们可以定义一组traps,每个都将处理使用我们对象的特定方式。对我们的问题感兴趣的是apply,它会捕获函数调用,并允许我们访问将调用该函数的this

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


    apply: (target, thisArg = , argumentsList) => 
        const scope = Object.assign(, thisArg, [id]: key) // augment `this`
        return Reflect.apply(target, scope, argumentsList) // call function
    

Reflect 将调用函数target,其中this 设置为scope,参数来自argumentsList

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

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

综上所述,这是针对类似反应的上下文的异步解决方案的初步尝试。但是,与prototype chain 不同,this 在从另一个函数中调用函数时不会自动继承。因此,以下解决方案中的上下文仅在 1 级函数调用中存在:

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()

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

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

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'

其中myContext.forward 只是一个consumer 来获取值,然后直接是一个provider 来传递它:

function forward(scope, callback) 
    const value = consumer(scope)
    return provider(value, callback)

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

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
    

    function forward(scope, callback) 
        const value = consumer(scope)
        return provider(value, callback)
    

    return 
        provider,
        consumer,
        forward,
    


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)`)
    const bWithContext = myContext.forward(this, b)
    bWithContext()


a()

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

现在我被困住了……我对想法持开放态度!

【讨论】:

感谢您的贡献@Sheraff!尽管您的解决方案非常适合同步功能,但我确实需要其他答案提供的异步代码。再次感谢!【参考方案2】:

您“在 NodeJS 中复制 React 的上下文”的目标有点模棱两可。来自 React 文档:

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

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

因此,我假设您实际上是在寻找一种方法来使值可在调用堆栈中的任何位置访问,而不必将其作为参数向下传递到堆栈从一个函数到另一个函数。 p>

我建议您使用 NodeJS 的所谓 continuation-local storage 库之一来实现这一点。他们使用的模式与您尝试做的有点不同,但它可能没问题。

我最喜欢的是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;
    
  ;
;
// 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);
// zz.js

import  context  from "./my-context";

export const zz = function () 
  console.log("zz", context.useContext());
;
// 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);
;
// 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();

运行node index 日志:

default value 0
zz AAA
a 1
zz BBB
b 2

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

我如何使用它

我的方法有点不同。我没有尝试复制 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);
    
  

// operations-cls.ts

import  CLS  from "./cls";

export const operationsCLS = new CLS("operations");
// consumer.ts

import  operationsCLS  from "./operations-cls";

export const consumer = () => 
  console.log(operationsCLS.get("some-value")); // logs 123
;
// app.ts

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

cls.run(async () => 
  cls.set("some-value", 123);
  consumer();
);

CLS 的工作原理

我更喜欢将 CLS 视为魔术,因为无需我的干预它总是可以正常工作,所以在这里不能发表太多评论,抱歉:]

【讨论】:

这似乎是一个不错的解决方案。我只有一个问题:如果您从a() 中调用zz() 而不先将其包装在context.provide 中。它记录哪个值,01 It logs 1 : ] 1 最初是提供的,并且这个值在堆栈中向下传播,除非它在某处被更改,这就是我的示例试图显示的内容。 感谢您的解决方案!实际上我无法实现我想要的(在同一过程中创建两个默认连接),但你的答案很优雅。我认为它可以用于其他目的。

以上是关于在 Nodejs 中复制 React 上下文的主要内容,如果未能解决你的问题,请参考以下文章

上下文感知复制和粘贴到反应应用程序

在 nodejs 上下文中使用 jQuery 时遇到问题

如何从 nodejs 模块中删除全局上下文?

NodeJS模块上下文[重复]

在 React 上下文中设置嵌套数组

使用 react-testing-library 在 Jest 中模拟 React 上下文提供程序