React: 正式化顶级 ES 导出

创建于 2017-11-09  ·  104评论  ·  资料来源: facebook/react

目前我们只提供所有包的 CommonJS 版本。 但是,我们可能希望将来将它们作为 ESM 发布(https://github.com/facebook/react/issues/10021)。

我们不能很容易地做到这一点,因为我们还没有真正决定每个包的顶级 ES 导出是什么样的。 例如, react是否有一堆命名导出,还有一个名为React的默认导出? 我们是否应该鼓励人们import *以获得更好的摇树效果? 现在导出一个类的react-test-renderer/shallow怎么样(如果它转换为默认导出,那么在 Node 中会开始失败)?

Build Infrastructure React Core Team Breaking Change Discussion

最有用的评论

现在快2020年了,我想知道官方FB团队是否有任何更新? 在 React v17 中会有与此相关的任何更改吗?

所有104条评论

Imho import *是一种方法,我也不反对使用默认导出,但它不应该用于重新导出其他内容,例如本例中:

export const Component = ...
export default React
React.Component = Component

但它不应该用于重新导出本例中的其他内容:

有技术原因吗? (除了有两种方法可以做同样的事情。)

我的印象是导入* (而不使用默认值)的人不会遇到树抖动问题,因为默认值将保持未使用状态。 但也许我高估了 Rollup 等。

@lukastaegert 可能最好回答这些问题。 不确定自https://github.com/facebook/react/issues/10021#issuecomment -335128611 以来是否发生了变化

此外,Rollup 并不是唯一的摇树器,虽然 webpack 的摇树算法比 rollup 中的算法差,但它的使用率可能比 rollup 高得多(这两个工具都做得很好,我不想冒犯任何人,只是陈述事实),如果我们可以(作为社区)同时帮助这两个工具,我们应该尽可能这样做。

考虑到所有东西都被预处理成一个单一的扁平包,在 React 的情况下,摇树会_做_任何事情吗? 我想知道 React 的主要导入样式是什么,我个人倾向于将它视为默认导出,例如React.ComponentReact.Children但偶尔会使用cloneElement来做命名的事情

正如@gaearon已经在别处所说的那样,预计在

  • React.Children 在某些情况下可能会被删除(所以我听说 😉)
  • React 本身可以被支持此功能的模块打包器提升到顶级范围。 这可能会再次删除相当多的字节,也可能会带来非常轻微的性能改进。 主要的改进在于这样一个事实,即不需要为每个模块引用React.Component另一个变量,而只需一个在任何地方共享的变量(这是 rollup 通常的做法)。 另外,虽然这只是我的猜测,但这可能会降低 webpack 的 ModuleConcatenationPlugin 救助的机会
  • react 的静态分析不仅对于模块捆绑器而且对于例如 IDE 和其他工具来说都更容易。 许多这样的工具已经为 CJS 模块在这方面做了合理的工作,但最终,他们有很多猜测。 使用 ES6 模块,分析变得轻而易举。

至于导出的类型,当然只有命名导出才能真正提供轻松摇树的好处(除非您使用 GCC,它可能能够在其激进的移动中做得更多,如果您真的很幸运,也许可以使用最新的汇总) . 是否也提供默认导出的问题更难决定:

  • PRO:现有 ES6 代码库的无痛迁移(例如@jquense描述的内容)
  • 缺点:由于所有东西都附加到一个公共对象上,因此一旦包含该对象,它的所有键都会立即包含在内,这再次阻止了任何摇树尝试。 即使是 GCC 也可能在这里过得很艰难。

作为双版本迁移策略,您可能会出于兼容性目的在下一个版本中添加一个默认导出,该导出被声明为已弃用(它甚至可能通过 getter 等显示警告),然后在更高版本中将其删除。

这也是一个有趣的案例: https :

通过这个 Twitter 对话来到这里。 对我来说,这个问题有一个明确的正确答案:React 和 ReactDOM 应该导出命名导出。 它们不是包含状态的对象,也不是其他库可以改变或附加属性的对象(尽管#11526)——它们存在的唯一原因是作为“放置” ComponentcreateElement等。 换句话说,命名空间,应该这样导入。

(这也让打包者的生活更轻松,但这既不存在也不存在。)

当然,对于目前使用默认导入和转译的人来说,这确实带来了重大变化。 @lukastaegert在这里可能有正确的想法,使用访问器打印弃用警告。 也许这些可以在第 17 版中删除?

不过,我对#11526 没有现成的建议。 也许出于这个原因,发布 ESM 会等待 v17,在这种情况下,无需担心弃用警告。

人们真的开始喜欢上了

import React, { Component } from 'react'

所以说服他们放弃它可能很困难。

我想这还不错,即使有点奇怪:

import * as React from 'react';
import { Component } from 'react';

为了澄清,我们需要React在范围内(在这种情况下,作为命名空间),因为 JSX 转换为React.createElement() 。 我们可以打破 JSX 并说它依赖于全局jsx()函数。 然后导入看起来像:

import {jsx, Component} from 'react';

这也许没问题,但是一个巨大的变化。 这也意味着 React UMD 构建现在也需要设置window.jsx

为什么我建议jsx而不是createElement ? 好吧, createElement已经重载( document.createElement ),虽然React.限定符没问题,但如果没有它在全局上声明它就太多了。 Tbh 我对这些选项中的任何一个都不太兴奋,并认为这可能是最好的中间立场:

import * as React from 'react';
import { Component } from 'react';

并保持 JSX 默认转译为React.createElement

忏悔:我总是觉得有些奇怪,你必须显式导入React才能使用 JSX,即使你实际上并没有在任何地方使用该标识符。 也许将来,如果 JSX 不存在,转译器可以在遇到 JSX 时插入import * as React from 'react' (可配置为 Preact 等)? 这样你只需要这样做......

import { Component } from 'react';

...并且命名空间导入将被自动处理。

在遥远的未来,也许吧。 现在我们需要确保转译器与其他模块系统(CommonJS 或全局变量)一起工作。 使这个可配置也是一个障碍,并进一步分裂社区。

@Rich-Harris 建议的内容(在使用 jsx 时插入特定的导入)很容易通过转译器插件完成。 社区将不得不升级他们的babel-plugin-transform-react-jsx ,仅此而已。 当然,如果只有一个将import * as React from 'react';到文件中,即使现有设置仍然有效。

当然我们需要考虑其他模块系统,但这似乎不是一个很难解决的问题。 是否有任何特定的陷阱?

当然我们需要考虑其他模块系统,但这似乎不是一个很难解决的问题。 是否有任何特定的陷阱?

我不知道,您对如何处理有什么具体建议? Babel JSX 插件的默认设置是什么?

人们真的开始喜欢上了

import React, { Component } from 'react'

什么人? 出来让我嘲笑你。

我经常这样做🙂 很确定我也在其他地方看到过这个。

目前默认值是React.createElement ,它几乎保持不变。 唯一的问题是它现在假设是全局的(或已经在范围内可用)。

我认为因为 es 模块基本上是做模块的标准方式(虽然还没有被所有人采用),所以假设大多数人正在(或应该)使用它是合理的。 绝大多数人已经使用各种构建步骤工具来创建他们的包 - 在本次讨论中更是如此,因为我们正在讨论React.createElement自动插入到作用域中是合理的做法。 babel@7即将推出(-ish),我们正处于进行此更改的最佳时机。 最近添加了babel-helper-module-imports也比以往任何时候都更容易将正确类型的导入 (es/cjs) 插入到文件中。

拥有这个可配置以摆脱今天的行为(假设存在于范围内)似乎真的像是少数用户所需的配置上的微小变化和大多数用户的改进(当然,不是很大 - 但仍然是)。

我们是否应该鼓励人们导入 * 以获得更好的摇树效果?

感谢@alexlamsl uglify-es消除了常见场景中的export default惩罚:

$ cat mod.js 
export default {
    foo: 1,
    bar: 2,
    square: (x) => x * x,
    cube: (x) => x * x * x,
};
$ cat main.js 
import mod from './mod.js'
console.log(mod.foo, mod.cube(mod.bar));



md5-d6d4ede42fc8d7f66e23b62d7795acb9



$ uglifyjs -V
丑化-es 3.2.1

```js
$ cat bundle.js | uglifyjs --toplevel -bc
var mod_foo = 1, mod_bar = 2, mod_cube = x => x * x * x;

console.log(mod_foo, mod_cube(mod_bar));
$ cat bundle.js | uglifyjs --toplevel -mc passes=3
console.log(1,8);

哇,这是一个很棒的新功能 👏 现在uglify-es被认为是稳定的吗? 我记得你几个月前提到它还没有完全到位,但我记错了,所以不确定。

无论如何 - 在汇总世界中这一切都很好,但考虑到React主要捆绑在应用程序中,而那些主要使用webpack默认情况下不进行范围提升,我仍然会说应避免将对象导出为默认值,以帮助除uglisy-es + rollup之外的其他工具生成更小的包大小。 同样对我来说,避免这种情况在语义上更好 - 在这种情况下,libs 实际所做的是提供一个名称空间,并且在使用import * as Namespace from 'namespace'时可以更好地表示

uglify-es 现在被认为是稳定的吗?

与 JS 生态系统中的其他任何东西一样稳定。 每周下载量超过 50 万次。

在汇总世界中这一切都很好,但考虑到 React 主要捆绑在应用程序中,而那些主要使用 webpack 默认情况下不进行范围提升

无论如何,这是一个选择。 无论如何,Webpack 默认值并不理想 - 如您所知,您必须使用ModuleConcatenationPlugin

在这里添加几美分:

  • 我完全同意@Rich-Harris 从语义上讲,命名导出是正确的选择
  • 我真的不喜欢import React from 'react'import * as React from 'react'只是为了能够使用 JSX 语法。 在我看来,这种设计显然违反了接口隔离原则,因为它迫使用户导入所有的 React 只是为了能够使用createElement部分(尽管诚然有命名空间导出,像 Rollup 这样的打包器会再次删除不需要的导出)

因此,如果我们正处于可能做出重大更改决定的时刻,我建议更改这一点,以便 JSX 依赖于单个(全局或导入)函数。 我会称它为createJSXElement() ,在我看来,它比createElement()更好地描述了它,并且不再需要 React 上下文才有意义。 但是在每个字节都很重要的世界中, jsx()也可能没问题。

这也将最终以某种方式将 JSX 与 React 分离,以便其他库可以选择通过使用相同的转换并提供不同的jsx函数来支持 JSX。 当然,你有很多责任在这里指导无数已建立的应用程序通过这样的转换,但从架构的角度来看,这就是我认为 React 和 JSX 应该前进的方向。 使用 Babel 来完成这种转换的繁重工作,对我来说听起来是个好主意!

就我个人而言,我认为迁移到jsx helper 没有太大好处,因为 babel 插件的默认恕我直言应该从react包中导入它,所以实际 helper 的名称并不是真的问题 - 剩下的就是让它可配置的问题。

这可能与主要讨论略有关联,但我很好奇 ES 模块在检查process.env.NODE_ENV以有条件地导出 dev/prod 包时效果如何? 例如,

https://github.com/facebook/react/blob/d9c1dbd61772f8f8ab0cdf389e70463d704c480b/packages/react/npm/index.js#L3 -L7

我可能在这里遗漏了一些明显的东西,但我正在努力了解如何将此模式转换为 ES 模块?

@NMinhNguyen ES 模块无法进行条件导出。

process.env.NODE_ENV检查可以在更细化的(代码)级别进行,准备好由具有适当值的捆绑器替换。

@Andarist @milesj感谢您证实我的怀疑 :)

process.env.NODE_ENV检查可以在更细化的(代码)级别进行,准备好由具有适当值的捆绑器替换。

从 React 16博客文章中,我认为process.env.NODE_ENV检查被故意拉到最顶部(而不是它们更细化,如果我没记错的话,这就是它们在源代码中的内容) ),以帮助提高 Node.js 的性能?

更好的服务端渲染

React 16 包含一个完全重写的服务器渲染器。 它真的很快。 它支持,因此您可以更快地开始向客户端发送字节。 并且由于新的打包策略可以编译掉process.env检查(信不信由你,在 Node 中读取process.env真的很慢!),你不再需要捆绑 React 来获得良好的服务器 -渲染性能。

就像,我不知道一个人如何可以使用module现场package.json和DEV / PROD区分了ESM,同时保持ES捆绑平整,不影响Node.js的PERF

就像,我不确定如何使用 package.json 中的 module 字段并区分 ESM 的 dev/prod,同时保持 ES 包平坦且不影响 Node.js 性能

这肯定是一个缺点,因为目前没有标准方法可以做到这一点。 OTOH 这只是工具的问题,即使在今天,也可以(而且相当容易)在应用程序的构建步骤中编译它。 如果包可以公开 dev/prod 构建并且解析器只知道选择哪个,那会更容易,但也许这只是将这个想法推给工具作者的问题。

上课:

import Component from 'react/Component'

class MyButton extends Component{
  constructor(){
    this.state = {}
  }

  render() {
    return <button> Button <Button>
  }
}

其中transform 将使用super.createElement() 转换为jsx 或使用静态Component.createElement()。

对于无状态组件:

import jsx from 'react/jsx'

const MyButton = () => jsx`<button> Button <Button>`;

也许可以使用标记的模板文字?

Node 希望接受这个 PR https://github.com/nodejs/node/pull/18392

我们同意@Rich-Harris 的观点。

只是在这个线程上发表评论,实际上并没有特别提到。

我处于一种情况,我根本不使用捆绑器,只想通过浏览器( <script type="module" src="..."> )导入 react 和各种组件以供本机使用,即

import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import ReactDOM from “https://unpkg.com/[email protected]/umd/react-dom.development.js”;
ReactDOM.render(
  React.createElement(...),
  document.getElementById('root')
);

据我所知,这在今天是不可能的。 相反,我必须通过 CDN 中的<script>标签包含响应的 UMD 版本,然后假设它存在于我编写的任何<script type="module">模块的窗口中:

// myPage.html
<div id="myComponentRoot"></div>
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script type="module" src="/assets/scripts/components/MyComponent.js"></script>

// MyComponent.js
import AnotherComponent from "/assets/scripts/components/AnotherComponent.js";
window.ReactDOM.render(
  window.React.createElement(AnotherComponent),
  document.getElementById('root')
);

// AnotherComponent.js
export default class AnotherComponent extends window.React.Component {...}

从 CDN 导入 React 会很棒。 这将使浏览器中的原型设计变得非常快速和容易,同时仍然能够保持文件的分离。 在没有打包器的情况下使用 React 时,我一直觉得我要牺牲的一件事是能够按文件分离组件(和其他实用程序功能等)。 但是现在浏览器支持原生 ES 模块,我可以在单独的文件中编写我的 React 组件,并让浏览器在编写时使用它们。 当然,如果我不使用 JSX,但即使我使用 JSX,我也可以通过构建步骤将所有文件传输到位,并且我的所有导入仍然可以在浏览器中工作。

// /assets/scripts/entry.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import React from “https://unpkg.com/[email protected]/umd/react-dom.development.js”;
import RelatedPosts from "/assets/scripts/components/RelatedPosts.js";
ReactDOM.render(
  React.createElement(RelatedPosts),
  document.getElementById('root')
);

// /assets/scripts/components/RelatedPosts.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
import ListItem from "/assets/scripts/components/ListItem.js"
export default class MyComponent extends React.Component {
  componentDidMount() { /* fetch some data */ }
  render() { 
    return React.createElement(
      'ul',
      {},
      this.state.items.map(item => React.createElement(ListItem, { item: item })
    )
  }
}

// /assets/scripts/components/ListItem.js
import React from “https://unpkg.com/[email protected]/umd/react.development.js”;
export default function ListItem(props) {
  return React.createElement('li', null, ...)
}

我敢肯定,有些人会争辩说,一直输入 CDN url 是一个问题(有些人正试图解决这个问题),但对我来说,权衡取舍是值得的。 更改/更新该 url 是一个简单的查找/替换。 对于我的用例,这超过了设置捆绑器的麻烦。

如果 React 支持这样的东西,就不需要工具了。 我只是在使用浏览器。 我可以在一些个人项目中发布这样的代码,这些项目假设现代浏览器并使用 react 作为页面上的渐进增强。 当我在 12 个月后回到代码库时,这太棒了,我不需要更改一堆工具 API,甚至不需要 NPM 作为包管理器。 我只是使用浏览器中的 API,没有别的。

FWIW:如果/当 React 提供这样的支持时,我认为展示如何在文档中像这样使用 React 可能非常有价值,教导您可以通过将每个组件逻辑分开<script type="module"> ,从 CDN(或你自己的本地副本)导入 React,然后你就可以了!”

现在,包括移动版本在内的所有现代浏览器都支持 ESM。 ESM 不再是未来的模块系统,而是当前的事实标准。

请注意,不提供标准化模块是一个关键问题,尤其是对于事实上的标准 Web 库。

import * as React from 'react';
import * as ReactDOM from 'react-dom';

这是应用 React 库的典型代码,事实上实际上并没有可以导入的库,而是由 3rd 方转译器和捆绑器模拟导入过程。

不提供真正的 ESM 有点合理,因为浏览器无论如何都不支持本机 ESM,但很明显,时间到了,现在是时候按照指定的典型示例代码向import提供 ESM。

我开始在这里这里研究这个

@TrySound感谢您的贡献。
有没有地方可以抓取和测试 ESM 构建?

它仅适用于 react-is 包。

@试音
好的,我找到了你的分支https://github.com/TrySound/react/tree/react-is-esm ,并且已经建立,现在我知道你的意思了。 也期待react-dom

我认为 React 社区对这个问题已经讨论了一段时间了。
https://discuss.reactjs.org/t/es6-import-as-react-vs-import-react/360/

请决定官方的 ES6 模块规范,并尽快发布。

@kenokabe我们在路上。 请不要强迫我们。 那并没那么简单。

当前计划是迁移所有仅具有命名导出的包。 此更改不会影响库代码,也不应该引入破坏性更改,因为 docs 也使用命名导出。

对于另一个包,我们需要处理默认和命名导出,它们与各种工具的工作方式不同。

@TrySound我很抱歉。
我不是故意的,因为这个话题的头提是

我们不能很容易地做到这一点,因为我们还没有真正决定每个包的顶级 ES 导出是什么样的。 例如,react 是否有一堆命名导出,还有一个名为 React 的默认导出? 我们是否应该鼓励人们导入 * 以获得更好的摇树效果?

而且,提到的那一天是前一段时间,我只是认为它已经在 React 社区中讨论过,所以我想建议这个决定是明确的。 谢谢!

想要得到一些关于这个的更新......

我正在使用 webpack v4 来捆绑我们的应用程序,而我的 IDE 智能感知 (WebStorm) 建议我使用import * as React from 'react';而我的同事要求我在代码审查中更改import React from 'react'; 。 两者都很好,所以我认为他在说一些废话,但为了让他开心,我还是要改变它。 这也是我找到这个线程的方式。

出于好奇,我比较了它们之间最终构建大小的差异(使用 React 16.8.1):

import * as React from 'react'; :6,618,723 字节
import React from 'react'; :6,619,077 字节

很明显,它确实有一些差异,尽管微不足道。 (注意。我对propTypes做了同样的事情)

如果我对这个线程的理解正确,那么拥有import * as React from 'react';将是有利的,对吧?! 因为(1)是的,它确实节省了一些尺寸; (2) ESM 是一种标准化的方式,所以没有更多的 CJS 剩菜。 如果是这种情况,我想今天更改此设置并与我的 IDE 保持一致。

@leoyli从长远来看是的。 但首先将有命名和默认导出,以免破坏现有代码。

我在这里亲自处理问题,有点像一个实验,因为我不再在我的项目中使用捆绑器,并且想仍然使用 react(直接来自 unpkg.com,就像你可以使用其他库,如 Vue、Hyperapp 等) . 这就是我想出的,没什么特别的,只是一个手工编辑的 umd:

https://github.com/lukejacksonn/es-react

一个暴露了ReactReactDOM最新版本的 ES6 模块

如自述文件中所述,这主要是一个 POC,但对于不能等待此构建登陆的人来说,它是一个16.8.3构建,其中包括钩子、悬念、懒惰等,并通过执行以下操作按预期工作:

import { React, ReactDOM } from 'https://unpkg.com/es-react'

也许你们中的一些人会发现它很有用..我个人一直在使用这种方法来创建一个无构建步骤的反应启动项目。 这也是一项正在进行的工作。

@lukejacksonn我们也一直在生产中使用这样的解决方案,而我们的方法有所不同,因为它更像是您当前项目中的 React 和 ReactDOM 的 UMD 版本的转换器脚本。 并且它单独输出这些文件,因此对于大多数代码来说,它应该是替代品。 如果你有兴趣https://github.com/wearespindle/react-ecmascript你也可以从 unpkg https://unpkg.com/react-ecmascript/加载它

@PM5544哇哦..这是一个比我的更全面的解决方案! 干得好💯

很棒的东西@PM5544。 希望有一天能听到更多关于它的信息。 也许是客串回到 Xebia?
我最近采用了pack来捆绑我的开源包,它支持 UNPKG。
有人知道关于直接从 UNPKG 加载依赖项而不是使用打包器的好文章吗?

我目前正在写一篇文章,并将在 6 月份在 React 挪威发表演讲!

@TrySound自二月以来是否有任何更新? 还剩下什么来推动这个问题,我可以参与通过一些编码工作解决这个问题吗? 我已经签署了 CA,我今天有时间处理它。

@TrySound好的,谢谢我已将我的帮助提议转发给该线程。

当您使用默认的 React 导出时,您可以使用以下方法:

// react/index.js
import * as React from "./react";
export { React as default }
export * from "./react";

// react/react.js
export function createElement() {}
...

这使得它可以静态分析,默认导出是一个命名空间对象,它允许在 webpack 5 和汇总中对这些构造进行摇树:

import React from "react";

React.createElement(); // <- only `createElement` export is used

我在汇总 gitter 聊天中呆了 1.5 年,这种问题每两周左右就会出现一次......

顺便说一句, @lukejacksonnes-react fork 上做了大量工作,强烈推荐它。

顺便说一句, @lukejacksonnes-react fork 上做了大量工作,强烈推荐它。

我想知道为什么官方FB团队对此无所作为。

我也希望这会产生一些推动事情前进的压力,我想@lukejacksonn本人也是

我也确实希望如此。 实际上,在创建包时,我几乎没有得到 React 团队的支持(除了@threepointone 的一些鼓励的话)。 最近 Formidable 的一位同事帮助我以编程方式构建了该包,这比每次发布新版本的 react 时手动进行都有所改进,但确实导致网络选项卡中的输出不那么干净,所以我不确定它是否会保持这种状态。 我们会看到的!

现在快2020年了,我想知道官方FB团队是否有任何更新? 在 React v17 中会有与此相关的任何更改吗?

对于那些需要更新的 ES 模块 React 的人,请尝试@pica/react ,它已经在 v16.13.x 上
https://www.npmjs.com/package/@pika/react
https://www.npmjs.com/package/@pika/react -dom
https://github.com/pikapkg/react

在遥远的未来,也许吧。

好的,已经在未来了。 现在是在不久的将来吗?

@gaearon决定如何构建导出(默认导出与命名导出)的阻滞剂是什么? 社区有什么可以帮助你做出这个决定的吗?

显然,这个决定已经在不久前做出了:#18102。 这个问题现在可以关闭了。

我将对此做一个小更新。

无默认导出

我们的最终计划是完全摆脱默认导出:

import { useState } from 'react';

在那个世界里,这是行不通的:

import React from 'react'; // no

虽然有点吵,但这会起作用:

import * as React from 'react';

但这是踢球者。 实际上根本没有太多理由导入React

JSX 自动导入

人们今天导入React原因主要是由于 JSX。 但@lunaruan正在完成新的 JSX 转换和相关代码模块的工作,这消除了对它的需求。

所以你会从这个出发:

import React from 'react';
import { useState } from 'react';

function Button() {
  const [pressed, setPressed] = useState(false)
  return <button />
}

对此:

import { useState } from 'react';

function Button() {
  const [pressed, setPressed] = useState(false)
  return <button />
}

JSX 会在引擎盖下自动插入正确的导入,因此范围内不需要React
这就是删除默认导出的举动可以容忍的原因。 你只是不需要那么多。

ES 模块

在一小部分爱好者之外部署 ESM 具有挑战性。 广泛的生态系统还没有真正准备好,并且有很多不同的工具组合会出现问题。 CJS 和 ESM 交互的方式非常复杂,互操作(以及它如何失败)是大多数这些问题的根源。

所以我们目前的想法是,当我们去 ESM 时,我们可能想一直尝试去 ESM。 根本没有 CJS——或者在兼容的遗留包中分开。 这不会在 React 17 中发生,在 18 中不太可能发生,但在 React 19 中尝试是合理的。

对于寻找@pika/react的 ESM 构建替代方案的任何人,请查看https://github.com/esm-bundle/reacthttps://github.com/esm-bundle/react-dom。 不同之处在于它们可以在没有导入映射 polyfill 的浏览器中使用 - react-dom 源代码中的import React from 'react';被更改为从完整的 CDN url 导入 React。 另一个区别是,每当发布新的 React 版本时,都会自动发布新版本,而无需任何手动步骤。

代码沙盒演示: https : =/index.html

一些人对生态系统尚未准备就绪的观点提出质疑。 我没有这方面的完整背景信息,但如果您认为这是开始进行更改的好时机,如果您能查看https://github.com/reactjs/rfcs/pull/38并表达任何意见,我将不胜感激对此的担忧。 这是您大致上的想法,还是您更喜欢不同的方法?

但这是踢球者。 实际上根本没有太多理由导入 React。

如果您正在使用 TypeScript,并且在可预见的未来将继续使用。 在 TS 了解 babel 的这种新神奇行为之前,开发人员将不得不继续显式导入 React,并且他们需要知道正确的导入语句是什么。

JSX 会在引擎盖下自动插入正确的导入,因此不需要作用域内的 React。

s/JSX/新的 babel react jsx 转换插件/. JSX 是 JavaScript 的语法扩展,它本身不做任何事情。

如果您正在使用 TypeScript,并且在可预见的未来将继续使用。 在 TS 了解 babel 的这种新神奇行为之前,开发人员将不得不继续显式导入 React,并且他们需要知道正确的导入语句是什么。

TypeScript 团队知道这一变化,并且正在https://github.com/microsoft/TypeScript/issues/34547 中对其进行跟踪

s/JSX/新的 babel react jsx 转换插件/

是的,这就是我的意思!

如果您能查看 reactjs/rfcs#38 并表达对此的任何担忧,我将不胜感激。 这是您大致上的想法,还是您更喜欢不同的方法?

RFC 的一些原始文本已经过时。 现在,当您在 package.json 中指定"type" ,NodeJS 允许 ESM 在.js文件而不是.mjs "type"中运行。 (文档)。

具体来说,RFC 原文说以下是不正确的:

ESM 代码必须.mjs文件中

在与@frehner交谈@gaearon已澄清默认导出最终会消失,但这直到第 4 阶段才在我们的提案中列出。

| | 第一阶段 | 第 2 阶段 | 第 3 阶段 | 第 4 阶段 |
| - | -------- | -------- | -------- | ------- |
| ESM 发布了吗? | ✔️ | ✔️ | ✔️ | ✔️ |
| package.json "module" | ❌ | ✔️ | ✔️ | ✔️ |
| webpack/rollup 使用 esm 1 , 2 | ❌ | ✔️ | ✔️ | ✔️ |
| package.json "exports" | ❌ | ❌ | ✔️ | ✔️ |
| package.json "type" | ❌ | ❌ | ✔️ | ✔️ |
| NodeJS 使用 esm | ❌ | ❌ | ✔️ | ✔️ |
| 突破性变化? | ❌ | ❌ | ❓ | ✔️ |
| 默认导出没了? | ❌ | ❌ | ❌ | ✔️ |
| 导入所需的文件扩展名 | ❌ | ❌ | ❌ | ❌ |
| mjs 文件扩展名 | ❌ | ❌ | ❌ | ❌ |

我认为将第 1 阶段和第 2 阶段结合在一起是有道理的,因为第 1 阶段实际上只针对发烧友。 但是,我将它们分开是因为我认为将阶段分开提供了一个机会,以一种不会立即破坏 CRA 和整个生态系统的方式非常缓慢地推出 ESM,而无需先让早期采用者有机会报告问题并找到任何修复程序.

@joeldenning各阶段的大致时间是什么? 它只是基于时间还是与生态系统中的某些时间检查点有关? require('react')在第 3 阶段仍然有效吗?

这些阶段的大致时间是什么? 它只是基于时间还是与生态系统中的某些时间检查点有关?

所有阶段的时间仅受要完成的工作和 React 的发布时间表的限制。 我的理解是所有提议的东西都受捆绑器和 nodejs 支持,在使用不支持它们的旧版本时有适当的回退。

require('react') 在第 3 阶段仍然有效吗?

我想是的,是的。 请参阅https://nodejs.org/api/esm.html#esm_dual_commonjs_es_module_packageshttps://nodejs.org/api/esm.html#esm_package_entry_points。 当然,我很可能不知道所有情况的所有极端情况,但我的理解是我提出的过渡路径“将适用于任何地方”。 也许那些是著名的遗言😄

额外的说明,这里是我们提议发布的 tarball 对于 React 的样子:

node_modules/react/
  cjs/
    react.development.js
    react.production.min.js
    react.profiling.min.js
  umd/
    react.development.js
    react.production.min.js
    react.profiling.min.js
  esm/
    react.development.js
    react.production.min.js
    react.profiling.min.js
  index.js

这是 package.json 最后的近似值:

{
  "type": "module",
  "main": "index.js",
  "module": "esm/react.development.js",
  "exports": {
    "import": "./esm/react.development.js",
    "require": "./cjs/react.development.js"
  }
}

^ 这还没有经过深思熟虑和完善,但我正在分享以提供提案的具体背景。

所以 - React 遭受双重包危险,因为它是有状态的(钩子),所以必须小心地隔离这个状态。 要么它应该存在于一个可以由 CJS 和 ESM 条目导入的小 CJS 文件中,或者可能有一种方法加载.json并从两个条目中使用该状态对其进行变异(我不是 100% 确定)第二种方法)。

我还认为在表中列出添加命名导出很重要,这在第 1 阶段已经发生。

如果我没有错过一些极端情况,我仍然不能 100% 确定,这个话题非常复杂。 同时,时间表应该与生态系统检查点相关联 - 第 3 阶段只能在exports有足够的捆绑器支持后才能发货,否则我觉得这可能会导致潜在的问题。

React 遭受双重包危险,因为它是有状态的(钩子),所以必须小心地隔离这个状态。

好点,我没有考虑过这个(更多信息)。 在这种情况下,也许您建议在 CJS/ESM 构建之间共享文件会很有用。 或者 ESM 版本可能不是 react 的完整副本,而只是调用 CJS 构建的 ESM 公共接口。

我还认为在表中列出添加命名导出很重要,这在第 1 阶段已经发生。

但是,在源代码中似乎已经是这种情况了? 据我所知,源代码已经将东西导出为命名导出。

https://github.com/facebook/react/blob/ef22aecfc52cdf0d7cedc723c590719c009c2a64/packages/react/index.js#L39

https://github.com/facebook/react/blob/ef22aecfc52cdf0d7cedc723c590719c009c2a64/packages/react/index.js#L39

如果我没有错过一些极端情况,我仍然不能 100% 确定,这个话题非常复杂。 同时,时间表应该与生态系统检查点相关联——第 3 阶段只能在出口在打包器中获得足够支持后才能发货,否则我觉得这可能会导致潜在的问题。

同意第 3 阶段 - 这就是为什么我对它是否是一个重大变化打上一个问号。 我知道添加 package.json 导出对于生态系统中的其他包来说通常是一个突破性的变化。 而且这个话题绝对是复杂的。 需要注意的一件事是,如果需要,可以交换第 3 阶段和第 4 阶段的顺序。 我认为那些实施每个阶段的人必须对许多版本的 webpack、rollup、nodejs 等进行非常非常彻底的测试。我并不是说要做的工作是微不足道的——只是说我确实认为有可能是这里的过渡途径:)

但是,在源代码中似乎已经是这种情况了? 据我所知,源代码已经将东西导出为命名导出。

啊 - 是的,在这种情况下,应该有一个表格行用于添加export default 😂 因为它目前适用于大多数捆绑器并且在野外很流行,但是添加真正的 ESM 条目而不提供default会打破这些习惯。

在这种情况下,应该有一个表格行用于添加导出默认值

是的,好点子。 我会补充的。 我认为这样做的 PR 会“看起来很糟糕”,但我认为它只是接受当前的公共界面,并计划在未来改进。

我们应该注意到,一旦发布了某个版本,就不会再有回头路了,任何对它的语义更改都将被破坏。 鉴于我们不经常发布专业,我们应该考虑如何批量更改任何更改,以便将流失最小化。

还有一个问题是如何处理开发/生产构建拆分。 事实上,React 构建的有状态特性非常重要。

我们应该注意到,一旦发布了某个版本,就不会再有回头路了,任何对它的语义更改都将被破坏。 鉴于我们不经常发布专业,我们应该考虑如何批量更改任何更改,以便将流失最小化。

好点。 我的想法是在 React 16 中发布的 ESM 构建中添加一个明确的export default React 。我认为 PR 可能会引起一些人的注意并且可能会引起争议,因为这不是已经决定的目的地。 但是,我的观点是我们可以在 React 16 中构建 ESM,然后在未来的主要版本中删除export default 。 对我来说,通过import React from 'react';使用 react 非常普遍,以至于在 ESM 构建中导出默认值只是“接受我们所处的位置”。 未来的主要版本将删除它。

还与最小化重大更改有关 - 第 3 阶段和第 4 阶段可能是同一主要版本的一部分。 第 3 阶段有可能以完全向后兼容的方式完成,在这种情况下,它也可以潜入 16 版本。 但那个更棘手,我对它的了解还不够多,因此对此没有信心。

还有一个问题是如何处理开发/生产构建拆分。

这是我忽略的一点。 我不知道如何在捆绑器和 NodeJS 中使用 ESM 做到这一点,但会做一些研究以了解可能的情况。 我发现了这个死的提议,但会研究活着的

事实上,React 构建的有状态特性非常重要。

同意。 需要注意的一件事是,React 构建的有状态性质只需要在第 3 阶段解决,而不需要在第 1 和第 2 阶段解决。 @Andarist和我建议的选项可以解决它。

目前最简单,最向后兼容的方法是添加一个简单的ESM的包装,以允许名为进口。

我很乐意做一个 PR 来将它添加到 React,确保添加“exports”字段不是一个重大更改(我编写了一个工具npx ls-exports ,这使得确定这很容易)。 它不会帮助那些在没有构建过程的情况下尝试使用 ESM 的人,但这是一个无论如何单个包都无法解决的问题。

@gaearon有用吗? 它可以作为 semver-minor 登陆 React 16。

CJS 构建将继续使用环境检测,就像现在一样,因此 ESM 中的(困难的、未解决的)问题还没有被解决。

但是,我的观点是我们可以在 React 16 中构建 ESM,然后在未来的主要版本中删除导出默认值。

另一个问题是,增加配置数量会给生态系统带来压力。 例如,我认为如果我们确实发布了 ESM 版本,它不应该包含我们肯定会在下一个版本中删除的内容,例如默认导出。 换句话说,我认为将即将消失的东西(默认导出)引入刚刚添加的东西(ESM 构建)没有多大意义。

但是,我的观点是我们可以在 React 16 中构建 ESM,然后在未来的主要版本中删除导出默认值。

另一个问题是,增加配置数量会给生态系统带来压力。 例如,我认为如果我们确实发布了 ESM 版本,它不应该包含我们肯定会在下一个版本中删除的内容,例如默认导出。 换句话说,我认为将即将消失的东西(默认导出)引入刚刚添加的东西(ESM 构建)没有多大意义。

我认为这取决于下一个版本的发布时间。 如果我们谈论的是一周的差异,那么这似乎是合理的。 然而,如果我们谈论的是几个月/几年,那么我不明白为什么把它拿出来那么长的时间会很糟糕

在这种情况下,令人担忧的是,它存在的时间越长,人们就越依赖当前的形式。 然后在那里引入一个突破性的变化会让他们更难升级 React。 现在不仅仅是一个 ESM 迁移,而是几个,有些人会被甩在后面,因为他们跳得太早,后来没有资源进行另一次迁移。 在 ESM 的情况下,这将影响整个生态系统。

我认为如果我们确实发布了一个 ESM 版本,它不应该包含我们肯定会在下一个版本中删除的内容,例如默认导出。 换句话说,我认为将即将消失的东西(默认导出)引入刚刚添加的东西(ESM 构建)没有多大意义。

我不认为import React from 'react'是新添加的东西,而是对当前现实的接受。 尽管这可能是无意的并且只是现已过时的 ESM/CJS 互操作的副作用,但仍有数千(数百万?)行代码可以做到这一点。 替代方案(仅导出命名导出)对用户说“我们发布了一个次要版本的 ESM 构建,但您不能在不更改所有代码的情况下使用它”,这对我来说比看到“删除的默认导出”更让用户感到困惑在主要版本的发行说明中。

我很好奇 - 如何以向后兼容的方式添加带有默认导出的 ESM? 这之前已经出现过(例如 https://github.com/facebook/react/pull/18187 也链接到相关问题)。 问题在于 webpack CJS <-> ESM 互操作,如果你有 CJS 代码执行require('react') ,在 ESM react存在的情况下,webpack 将返回带有默认导出的对象,它是一个带有default的对象require('react').default )而不管 CJS react 。 但也许如果你也导出 named 那么它不会有问题吗? 我认为@TrySound之前在其他软件包中也遇到过此类问题。

但也许如果你也导出 named 那么它不会有问题吗?

是的,这就是我正在考虑的方法。 请参阅https://unpkg.com/browse/@esm-bundle/react @16.13.1/esm/react.development.js 的最后 40 行 - 它是 React 的非官方版本,正是这样做的,并且被几个人使用组织作为官方 React 的替代品。 没有重大变化。

但也许如果你也导出 named 那么它不会有问题吗?

是的,这就是我正在考虑的方法。 请参阅https://unpkg.com/browse/@esm-bundle/react @16.13.1/esm/react.development.js 的最后 40 行 - 它是 React 的非官方版本,正是这样做的,并且被几个人使用组织作为官方 React 的替代品。 没有重大变化。

但是您是从 CJS 代码还是 ESM 中使用它? 因为 CJS <-> ESM 互操作问题可能非常令人惊讶。

@gaearon说清楚; 在我提议的 ESM 包装器中没有默认导出是有意义的; 任何做本地 ESM 的人都会做import * as React from 'react'来解决它。 但是,现在这样做的任何人都会将其视为突然没有默认导出的重大更改,因此如果您现在不想添加默认值,则必须等到 v17。

但是您是从 CJS 代码还是 ESM 中使用它? 因为 CJS <-> ESM 互操作问题可能非常令人惊讶。

我已经成功地将它从 CJS 和 ESM 文件导入到 webpack 中。

任何使用本机 ESM 的人都会从“react”导入 * 作为 React 来解决它。 但是,现在这样做的任何人都会将其视为突然没有默认导出的重大更改,因此如果您现在不想添加默认值,则必须等到 v17。

同意。 如果从头开始,则无需使用默认导出。 但是,如果不添加默认导出,则不可能在没有重大更改的情况下实施第 2 阶段。 我个人认为在 React 16 中执行阶段 1 和在 React 17+ 中执行阶段 2-4 没问题,尽管我更喜欢执行阶段 1、2 甚至 3(在@ljharb的导出检查器工具的帮助下)在 React 16 中没有破坏更改。 原因是第 2 阶段是大多数用户开始使用 ESM 捆绑包的重要阶段,而第 1 阶段主要面向早期采用者/爱好者。

这个提议继续。 这似乎是建议切换到具有完整 CJS 和 ESM 源的双包,通过在其他地方隔离状态来处理双包危险(方法 2 )。 与使用像我之前的 RFC(方法 1 )那样的 ESM 包装器相反。

假设,我确实有一些尚未提及的事情的笔记。

  • 你不能有一个 ESM 和 CommonJS 文件都使用.js的 Node.js 包。 如果你设置了"type": "module"那么所有的 CommonJS 文件都需要使用.cjs文件扩展名。
  • 状态和平等双包风险并不是同时拥有 CJS 和 ESM 的唯一问题。 在这些情况下,加载同一包的潜在 2 个版本也会增加 React 的内存占用,而 React 并不是一个小库。 这不是交易破坏者,但值得牢记。
  • 除了 React 的内部状态之外,我确实看到了双包危险的可能性。 例如,如果Component类的实现不是我们共享状态的 CJS 代码的一部分,而是 CJS/ESM 包的一部分,那么在各种库中存在instanceof Component检查的风险打破。

您不能拥有一个 ESM 和 CommonJS 文件都使用 .js 的 Node.js 包。 如果您设置 "type": "module" 则所有 CommonJS 文件都需要使用 .cjs 文件扩展名。

这对于 NodeJS 是正确的(但对于打包器不是),因此cjs目录中的文件必须以.cjs结尾。

在这些情况下,可能加载同一包的 2 个版本也会增加 React 的内存占用

我看到这如何增加发布到 npm 的 tarball 大小,从而增加磁盘上的整体大小。 但我不明白这如何影响记忆。 据我所知,捆绑器和 NodeJS 不会将未通过import / require()加载的代码带入内存。 你能澄清一下内存占用会如何变化吗?

例如,如果 Component 类的实现不是我们共享状态的 CJS 代码的一部分,而是 CJS/ESM 包的一部分,那么存在各种库中的 instanceof Component 检查中断的风险。

一个提议的解决方案 ( 1 , 2 ) 是让 NodeJS esm 实现只是一个调用 CJS 代码的 ESM 接口。 这样一来,Node 中就只有一个Component定义。 但是,阶段 1 和阶段 2 不会改变 NodeJS 中运行的内容,因此这仅适用于阶段 3。

由于我们(webpack)最近还向 webpack 5 添加了exports字段支持,我想为这个主题付出 2 美分:

  • 这个在制品文档有很多关于exports字段的信息,关于 webpack 和 Node.js 以及一般: https ://gist.github.com/sokra/e032a0f17c1721c71cfced6f14516c62
  • 这些是与 Node.js 相比的关键点:

    • webpack 5 还增加了developmentproduction条件,这对 react 非常有用。 ( process.env.NODE_ENV ,虽然仍然受支持,但一般应避免用于前端代码,它是 Node.js 特定的)

    • webpack(和其他打包器)支持require("esm") ,它允许通过始终使用 ESM 来避免双状态问题(即使是require() )。 webpack 为此引入了一个特殊条件: module 。 对于这个 CommonJs 和 ESM 版本必须导出相同的接口。 目前没有比 Node.js 更具有双态问题的东西了。 我不希望我们将来会看到什么,因为这主要是一个向后兼容的问题。

      为了获得最大的兼容性,我会推荐以下内容:

包.json

{
    "type": "commonjs",
    "main": "index.js",
    "module": "esm/wrapper.js",
    "exports": {
        ".": {
            "node": {
                "development": {
                    "module": "./esm/index.development.js",
                    "import": "./esm/wrapper.development.js",
                    "require": "./cjs/index.development.js"
                },
                "production": {
                    "module": "./esm/index.production.min.js",
                    "import": "./esm/wrapper.production.min.js",
                    "require": "./cjs/index.production.min.js"
                },
                "import": "./esm/wrapper.js",
                "default": "./cjs/index.js"
            },
            "development": "./esm/index.development.js",
            "production": "./esm/index.production.min.js",
            "default": "./esm/index.production.min.js"
        },
        "./index": "./index.js",
        "./index.js": "./index.js",
        "./umd/react.development": "./umd/react.development.js",
        "./umd/react.development.js": "./umd/react.development.js",
        "./umd/react.production.min": "./umd/react.production.min.js",
        "./umd/react.production.min.js": "./umd/react.production.min.js",
        "./umd/react.profiling.min": "./umd/react.profiling.min.js",
        "./umd/react.profiling.min.js": "./umd/react.profiling.min.js",
        "./package.json": "./package.json"
    }
}

esm/package.json

允许在这些目录中使用.js作为扩展名。 或者,可以使用.mjs ,但这可能会在工具检查扩展名时产生潜在的副作用。 所以.js是安全的。

{
    "type": "module"
}

esm/wrapper.js

Node.js 需要这个包装器来避免双状态问题。

import React from "../cjs/index.js";
export const {
    Children,
    Component,
    ...,
    useState,
    version
} = React;
export { React as default };

cjs/index.js

developmentproduction条件不受支持时,Node.js 会使用它。

'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

esm/wrapper.development.js (esm/wrapper.production.min.js 类似)

Node.js 需要这些包装器来避免双状态问题。
它们仅在 Node.js 添加developmentproduction条件时使用。

import React from "../cjs/index.development.js";
export const {
    Children,
    Component,
    ...,
    useState,
    version
} = React;
export { React as default };

索引.js

用于向后兼容。

module.exports = require('./cjs/index.js');

esm/index.development.js, esm/index.production.min.js

这被支持exports字段、 module条件和production / development条件的工具使用。

/* React in ESM format */

// compat for the default exports with support for tree-shaking
import * as self from "./esm/index.development.js";
export { self as default }

结果

  • webpack 5: ./esm/index.development.js./esm/index.production.min.js
  • 浏览器: ./cjs/index.js
  • 来自 .mjs 的 webpack 4: ./cjs/index.js
  • 其他打包机: ./esm/wrapper.js
  • Node.js (ESM): ./cjs/index.js (需要)或./esm/wrapper.js (导入)
  • Node.js(旧): ./cjs/index.js
  • Node.js (ESM + dev/prod): ./esm/wrapper.development.js./esm/wrapper.production.min.js用于导入, ./cjs/index.development.js./cjs/index.production.min.js用于需要

笔记

没有esm/index.js因为在没有重大权衡的情况下,在 ESM 中不可能有条件地选择一个版本。
工具只有在支持exports字段、 module条件(由于双态问题)和production / development时才能充分受益于 react ESM

当工具支持module字段或exports字段时,工具可以部分受益于 react ESM。

import { useState } from "react"import * as React from "react"在技​​术上是非法的,因为只要 react 是 CommonJs 模块。
大多数工具仍然支持向后兼容,但有些不支持,例如 Node.js
因此,目前使用在任何地方都有效的 react 的唯一方法是: import React from "react"
这种方式应该保持支持,否则会出现这样的情况(例如 Node.js 14),在现在反应和添加 ESM 后反应中没有有效的语法。

Node.js 目前拒绝为exports添加development / production条件。
这很可悲,我仍然希望他们最终会添加这一点。
这就是为什么在上面的exports字段中准备了对此的支持。

@sokra很棒的故障,非常有帮助,谢谢!

一个小问题:

Node.js 目前拒绝为导出添加开发/生产条件。

我的理解是它仍在开发中? https://github.com/nodejs/node/pull/33171但也许我误解了 PR

[编辑] 我链接的上述 PR 已被https://github.com/nodejs/node/pull/34637取代

[edit2] 现已合并到 nodejs 中

谢谢@sokra ,这些都是非常有用的建议。

这是我看到的选项。 似乎所有这些在技术上都是可行的,并且该决定是战略而非技术实施之一:

选项1

export default React添加到 React 17 ESM 构建中,并在 CJS 对import React from 'react'支持被删除后将其删除(也许在 React 18 中?)。

选项 2

不要添加export default React ,并创建一个仅具有命名导出的 React 17 ESM 构建。

选项 3

不要发布 React 17 ESM 构建。 (😢) 等到import React from 'react';支持被删除,然后再创建 ESM 构建。

比较

| | 选项 1 | 选项 2 | 选项 3 |
| - | -------- | -------- | -------- |
| 未引用的 ESM 构建 | v17 | v17 | v18+ |
| package.json "module" (默认摇树) | v17 | v18+ | v18+ |
| package.json "type" / "exports" (NodeJS 使用 ESM) | v18+ 1 | v18+ | v18+ |

  1. 有可能以完全向后兼容的方式实现 package.json 类型/导出,在这种情况下,如果选择选项 1,它可能成为 React 17 的一部分。

正如我在上面解释的那样,我更喜欢选项 1。 然而,选项 2 对我来说也很令人兴奋。 选项 3 当然不那么令人兴奋。 从我在这个 github 问题中收集的信息来看,我们有技术专长来实现任何这些(甚至可能是劳动力!)。

对这个问题的最初反应是,为什么这个问题在3 年后仍然存在? 通读其中的一部分后,就明白为什么要花这么长时间了。 维护像 React 这样的库是一项艰巨的任务。 所以🙇🏻

鉴于最近关于 React 17 的消息,我更新了我之前的评论以引用 React 17 而不是 16 以用于未来的任何计划。

我很感激人们对以上三个选项中的哪一个更喜欢的反馈。

我认为我们可以在 React 17 中的package.json中添加exports字段,我们也可以将其向后移植到以前的版本:

{
  "exports": {
    ".": {
      "development": "./esm/react.development.mjs",
      "production": "./esm/react.production.mjs",
      "node": {
        "import": "./esm/react.node.mjs",
        "require": "./index.js"
      },
      "default": "./index.js"
    },
    "./jsx-dev-runtime": {
      "development": "./esm/react-jsx-dev-runtime.development.mjs",
      "production": "./esm/react-jsx-dev-runtime.production.mjs",
      "node": {
        "import": "./esm/react-jsx-dev-runtime.node.mjs",
        "require": "./jsx-dev-runtime.js"
      },
      "default": "./jsx-dev-runtime.js"
    },
    "./jsx-runtime": {
      "development": "./esm/react-jsx-runtime.development.mjs",
      "production": "./esm/react-jsx-runtime.production.mjs",
      "node": {
        "import": "./esm/react-jsx-runtime.node.mjs",
        "require": "./jsx-runtime.js"
      },
      "default": "./jsx-runtime.js"
    },
    "./": "./"
  },
}

我们需要新的 esm 包,尽管使用汇总添加应该不会太难。

  • ./esm/react.development.mjs./esm/react.production.mjs捆绑包应该没有process.env.NODE_ENV支票:

    • 该条件在导入/捆绑时通过exports字段解决。

    • process是一个节点 API,它在浏览器环境中没有意义,例如 webpack 5 的 _default_ 不支持。

  • ./esm/react.node.mjs将保留process.env.NODE_ENV支票。
  • AFAIK 目前只有 webpack 5 和 node 支持exports字段。

    • 在汇总方面也有一些兴趣来支持它: https :

我认为这是相当安全的添加,WDYT?

https://webpack.js.org/guides/package-exports/
https://nodejs.org/dist/latest-v15.x/docs/api/packages.html

"./": "./"使它安全,是的,但也可以防止任何封装,因此您希望在获得 semver-major 后立即将其删除。

FWIW babel 将新的 jsx 运行时导入输出为

import { jsxs, jsx, Fragment } from 'react/jsx-runtime';

但是如果你在 Node 中加载这样的模块,它会抱怨缺少文件扩展名:

> node .\node.mjs
node:internal/process/esm_loader:74
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'test\node_modules\react\jsx-runtime' imported from test\node_modules\react-data-grid\lib\bundle.js
Did you mean to import react/jsx-runtime.js?
    at new NodeError (node:internal/errors:259:15)
    at finalizeResolution (node:internal/modules/esm/resolve:307:11)
    at moduleResolve (node:internal/modules/esm/resolve:742:10)
    at Loader.defaultResolve [as _resolve] (node:internal/modules/esm/resolve:853:11)
    at Loader.resolve (node:internal/modules/esm/loader:85:40)
    at Loader.getModuleJob (node:internal/modules/esm/loader:229:28)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:51:40)
    at link (node:internal/modules/esm/module_job:50:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

添加exports应该可以解决它🤔

@nstepien提供完整的exports地图,正如您在之前的帖子中所展示的那样,我认为这不是一个选项。 什么节点实现的关于 cjs 互操作和东西并不能很好地与现有的生态系统配合使用。 双包风险是真实存在的——尤其是对于像 Reac 这样需要单个副本的包。

exports映射仅包含 commonjs 文件可能会在不破坏任何内容的情况下添加,但也必须格外小心并为此进行适当的 e2e 测试(考虑到正确处理的复杂程度)

@Andarist它工作正常,在 react 的情况下也没有什么不同,它总是有这种危险,而生态系统通过使 react 成为无处不在的 peer dep 来解决它。 “导出”映射在这里可以正常工作,只要 ESM 文件和 CJS 文件共享相同的状态——这可以通过编写简单的 ESM 包装器来实现。

如果 React 的所有依赖项,无论是否传递,都在 React 的控制之下(在这种情况下它们是)并且所有这些的 ESM 入口点都只是重新导出 CJS 内容,那么是的 - 也许在这种特殊情况下可以实现。

不过,ESM 条目的实际形状应该是什么(命名、默认、两者),这仍然是一个整体:

  • 仅命名:并不是真正向后兼容,因为那里的很多代码都使用import React from 'react' ,这也是现在使用 ESM 时在节点中实际导入 React 的唯一方法
  • default-only: 不是真正的向后兼容,因为我们那里的很多代码都使用import * as React from 'react' ,这经常被类型检查器和其他工具推广
  • 两者:使其完全向后兼容的唯一方法,因此它可以与所有当前加载样式以及跨依赖关系树混合 ESM 和 CJS 模块时一起使用

我经常忘记 ESM 包装器的可能性,因为它们感觉像是作弊,而且因为这种技术只有在您控制所有依赖项

恐怕同时提供两者的唯一通用策略实际上是将所有实际代码都放在 CJS 中,并围绕它编写 ESM 包装器以提供命名导出。 既无状态也不依赖于身份的代码可以这两种语言本地

jsx-runtime不是有状态的吧? 在没有包装器的情况下运送 esm/cjs 应该是安全的。

我应该react/jsx-runtime在 esm 节点中导入

对于前端包,一个额外的挑战加入了双重状态风险:我们称之为双重捆绑风险。

当导出 ESM 和 CJS 版本并且通过requireimport ,两个版本都将被捆绑并且包的有效包大小加倍。

@sokra在实践中可能吗? 例如,对于汇总,我会假设因为 cjs 模块被转换为 esm 模块,所以它会更喜欢在可用时导入 esm 模块。

对于遵循节点语义的捆绑器来说,它在实践中是可能的——比如 webpack 5。匹配语义很重要,否则你在节点中运行和运行捆绑代码时可能会遇到不同的结果。

然而,webpack 5 处理了一个特殊的"module"条件,用于对 esm/cjs(来自导出映射的那些)进行重复数据删除。

如果我们考虑@ljharb 的提议,这并不是真正相关,因为他提议 esm 文件可能只是几行包装器代码,可以重新导出 cjs 文件。

使用薄 ESM 包装器,有或没有捆绑器的附加风险为零 - 只有对等部门避免图表中的欺骗的正常风险。

此页面是否有帮助?
0 / 5 - 0 等级