vivado顶层模块怎么建_【第2040期】Node 模块化之争:为什么 CommonJS 和 ES Modules 无法相互协调...

前言

又到周五了。今日早读文章由Shopee@周雨楠翻译授权分享。

@周雨楠,Shopee金融事业群前端研发,自主学习前端技术3年,喜爱各类数字媒体技术、创意设计,多次参与翻译工作。

3d39d17bbd8f92a20abf688771782321.png

福利:有两张门票,有需要的跟@情封联系,先到先得。

正文从这开始~~

两者可以进行适配,但是会徒增负担。

在 Node 14 版本下,现存两类语法:老式的 CommonJS (CJS) 的语法和新式的 ESM 语法(aka MJS)。CJS 使用 require() 和 module.exports;ESM 使用 import 和 export。

ESM 和 CJS 可以看作是完全不同的动物。表面上看,ESM 和 CJS 很像,但是他们的实现却是大相径庭。如果说一个是蜜蜂,那么另一个就是杀人蜂。

4e7f77fea88b3351e05769b42abaf20d.png

图中是一只黄蜂和一只蜜蜂。其中一个好比 ESM,另一个好比 CJS,但是我永远记不住哪个是哪个。图片来源:wikimedia,wikimedia

无论是在 ESM 中使用 CJS 还是反过来,都是有可能的,但这是徒增负担。

以下是一些规则,我会在后文中详细解释。

  • 在 ESM 代码中无法使用 require();你只能 importESM代码,比如:import{foo}from'foo'

  • CJS 代码无法使用如上所示的静态 import 语句;

  • ESM 代码可以 import CJS 代码,但是只能使用“默认导入(default import)”语法,如 import_from'lodash',而不是“命名导入(named import)”语法,如 import{shuffle}from'lodash',因此如果 CJS 代码使用了命名导出,就会很麻烦;

  • ESM 代码可以 require() CJS 代码,即便是命名导出也可以,但是明显不值得大费周章,因为这样需要更多的框架平台,而且最不好的一点就是诸如 Webpack 和 Rollup 这样的包,不知道,也不会知道怎么处理含有 require() 的 ESM 代码;

  • CJS 是默认允许使用的,而 ESM 模式则需要你选择性加入。通过把代码文件从 .js 重命名为 .mjs 就可以启用 ESM 模式。除此之外,在 package.json 中设置 "type": "module",然后就可以通过把 .js 重命名为 .cjs 选择退出 ESM 模式。(你甚至可以在某一个子目录下添加一个只有一行 {"type": "module"} 的 package.json 文件来调整。)

这些条条框框太痛苦了。对于很多使用者,尤其是 Node 入门者来说是更为痛苦的,这些规则压根不可理喻。(不慌,这篇文章里我都将解释清楚。)

很多 Node 生态的关注者已经发现这些规则是由于先前领导的失败,甚至是对 ESM 的敌意导致的。不过正如下文所说,所有的规则都有其存在的意义,这使得未来想要打破这些规则也很难。

我为开源库的开发者整理了三条指南用于借鉴:

  • 为你的开源库提供一个 CJS 的版本;

  • 为你的 CJS 版本提供一个较浅的 ESM 封装;

  • 在你的 package.json 文件中添加一个 exports 的映射。

一切就会好起来了。

背景介绍:CJS 是什么?ESM 又是什么?

从 Node 初见以来,Node 中的模块就是以 CommonJS 模块来写的。我们使用 require() 来引入它们。当实现了一个模块并且想让他人使用时,我们就会定义 exports 内容,要么通过设置 module.exports.foo='bar' 进行“命名导出”,要么通过设置 module.exports = 'baz' 进行“默认导出”。

这是一个 CJS 使用命名导出的例子,util.cjs 有一个命名为 sum 的导出函数。

// 文件名: util.cjs

module.exports.sum = (x, y) => x + y;

// 文件名: main.cjs

const {sum} = require('./util.cjs');

console.log(sum(2, 4));

这是一个 CJS 在 util.cjs 中使用默认导出的例子。默认导出是不指定名字的,而是由使用 require() 的模块自行定义名称。

// 文件名: util.cjs

module.exports = (x, y) => x + y;

// 文件名: main.cjs

const whateverWeWant = require('./util.cjs');

console.log(whateverWeWant(2, 4));

在 ESM 代码中,import 和 export 是这类语言的一部分。和 CJS 类似,它也有两套不同的语法进行命名导出和默认导出。

这是一个 ESM 使用了命名导出的例子,util.mjs 有一个命名为 sum 的导出函数。

// 文件名: util.mjs

export const sum = (x, y) => x + y;

// 文件名: main.mjs

import {sum} from './util.mjs'

console.log(sum(2, 4));

这是一个 ESM 在 util.mjs 中设置了默认导出的例子。和 CJS 中一样,默认导出是没有名字的,但是使用了 import 的模块会自行定义名称。

// 文件名: util.mjs

export default (x, y) => x + y;

// 文件名: main.mjs

import whateverWeWant from './util.mjs'

console.log(whateverWeWant(2, 4));

ESM 和 CJS 是截然不同的动物

在 CommonJS 中,require() 是同步的。它不会返回一个 promise 或者调用回调函数。require() 从硬盘(或者甚至从网络)中进行读操作,然后立刻执行代码。这样就会使得它自行进行 I/O 或产生其它副作用,然后返回任何设置在 module.exports 上的值。

在 ESM 中,模块加载器是在异步阶段执行的。在第一个阶段,它会做词法分析,在不执行导入代码的情况下检测是否存在 import 和 export 的调用。在词法转换阶段,ESM 加载器能够立刻检测到命名导入中的拼写错误,并且在不执行依赖代码的情况下抛出异常。

ESM 加载器接下来异步地下载并转译任何引入的代码,然后对引入的代码进行编码,根据依赖建立出一个“模块图(module graph)”,直到最后它发现某块代码没有引入任何东西。最后,这一块代码被允许执行,然后所有这一块代码所依赖的代码被允许执行,依次类推。

ES 模块图中所有具有“兄弟”关系的代码都是并行下载的,但是是按照次序执行的。这一次序由加载器指定并确保执行。

CJS 是默认模式,因为 ESM 改变了很多东西

ESM 改变了 JavaScript 中的很多东西。ESM 语法默认使用严格模式(use strict),它们的 this 不指向全局对象,作用域也有差异,等等。

这就是为什么甚至在浏览器中 标签默认也不是 ESM 模式的。要添加一个 type="module" 属性来选择进入 ESM 模式。

从默认的 CJS 切换到 ESM 在向前兼容性方面存在很大断层。(最近炙手可热的 Node 替代品 Deno 将 ESM 作为默认,但是其结果就是 Deno 的生态环境要从零开始搭建。)

CJS 无法 require() ESM,因为有顶层的 await 限制

CJS 无法 require() ESM 的最简单原因就是 ESM 可以进行最外层的 await ,但是 CJS 代码不行。

顶层 await 能够让我们在 async 函数的外层使用 await 关键字,也就是处于“顶层”。

ESM 的多阶段加载器使得 ESM 实现顶层 await 时不会搬起石头砸自己的脚。从V8团队的博客文章中引用一些话:

也许你读过 Rich Harris 写的臭名昭著的 gist,一开始就罗列了一些所担心的关于顶层 await 的问题,并且迫切希望 JavaScript 语言不要实现出来。其中的一些问题是:

  • 顶层 await 会阻塞执行;

  • 顶层 await 会阻塞资源获取;

  • CommonJS 模块没法再做清晰的内嵌了。

第三阶段版本的提议也强调了这些问题:

  • 因为兄弟关系的代码是可以执行的,因此最终没有造成阻塞;

  • 顶层 await 出现在模块图的执行阶段。在此阶段,所有的资源都已经获得并且建立了链接,因此是不存在阻塞资源获取风险的;

  • 顶层 await 仅仅限制在 ESM 模块中使用,CommonJS 的模块或者代码中明确没有对此的支持。

(Rich 现在已经同意了当前顶层 await 的实现。)

因为 CJS 不支持顶层 await,那么从 ESM 的顶层 await 转译为 CJS 就是不可能的。在 CJS 中怎么重写这段代码呢?

export const foo = await fetch('./data.json');

有点打击人,因为绝大多数 ESM 代码不会去使用顶层 await,但是正如这一条 thread 中的一个评论者所说,“我并不认为设计系统的时候,单单假定一些功能不会被使用,是一条可行的路。”

如何在 ESM 中进行 require() 的问题,在这条 thread 上依旧激烈争论着。(请看完整条 thread 和其中关联的讨论后再进行评论。如果你深入研究,你就会发现顶层 await 并不是唯一一个有着问题的情形。你觉得如果你同步 require 一个能够异步 import 一些能够同步 require ESM 的 CJS 的 ESM 会发生什么呢?你就会得到像斑马条纹那样一会同步一会异步的能整死人的东西。顶层 await 就是棺材板上的最后一根钉子,也是最容易解释的一个。)

通过对那些讨论进行评审,似乎我们不再会在 ESM 里做 require() 了。

CJS 能够 import() ESM,但是这并不好

目前为止,如果你在写 CJS,你想 import 一段 ESM 代码,你得使用异步动态的 import()。

(async () => {

const {foo} = await import('./foo.mjs');

})();

看上去……还行,只要别有 exports 就行。如果你需要做 exports,你就得导出一个 Promise,这对于你的用户来说会是一个大大的不便。

ESM 无法引入命名引出的 CJS,除非 CJS 代码脱离执行顺序

你可以这样写:

import _ from './lodash.cjs'

但是你没法这样写:

import {shuffle} from './lodash.cjs'

这是因为 CJS 代码会在执行的时候计算它们的命名导出,而 ESM 的命名导出必须在转译阶段才会被计算。

对我们而言,幸运的是有曲线救国的方式!这个曲线十分恼人,但是还是能做的。我们这样引入 CJS 代码就可以了:

import _ from './lodash.cjs';

const {shuffle} = _;

这样做没什么特别的弊端,而且感知了 ESM 的 CJS 库甚至能够提供它们自己的 ESM 包裹层,为我们封装了这样的写法框架。

完全没问题!要是能更好点就好了。

脱离执行顺序也能工作,但是有更坏的结果

有一部分的人提出,在 ESM 引入之前执行 CJS 的引入是脱离了执行顺序的。这样一来,CJS 的命名导出会和 ESM 的命名导出在同时计算。

但是这样就会产生一个新的问题。

import {liquor} from 'liquor';

import {beer} from 'beer';

如果 liquor 和 beer 最初都是 CJS,把 liquor 从 CJS 换成 ESM 就会使得顺序从 liquor, beer 变成 beer, liquor,那么如果 beer 中依赖 liquor 中先执行的内容,这样就会令人呕吐地有问题。

脱离顺序的执行依然在争论当中, 虽然几周之前这个话题就几乎没啥声音了。

动态模块能拯救,但是它们的星号有毒

有一个替代方案的提议,既不需要脱离执行顺序,也不需要做封装,称作动态模块(Dynamic Modules)。

在 ESM 中,导出的地方会静态定义所有命名导出。在动态模块方案下,引入的地方会在 import 中定义导出的名字。ESM 加载器一上来会信任动态模块(CJS 代码)能够提供所有需要的命名导出,如果之后有地方不满足,则再抛出一个异常。

然而,动态模块需要 JavaScript 语言发生一些变化,这些变化需要 TC39 语言委员会进行同意。而他们不同意。

ESM 代码可以 export*from'./foo.cjs',这意思是重新把 foo 中导出的所有名字进行导出。(称为“星号导出(star export)”。?)

然而,如果我们从动态模块中星号导出,加载器就无法知道导出的是什么。

动态模块的星号导出在规范合格性上也产生了问题。比如, export*from'omg';export*from'bbq'; 应该抛出异常,因为 omg 和 bbq 都导出了相同名字的 wtf。允许这些命名能够被用户/消费者进行定义,意味着这个合法性校验阶段需要被滞后处理或者忽略。

动态模块的提倡者提议在动态模块中禁止星号导出方式,但是 TC39 拒绝了这一提议。一个 TC39 的成员把这个提议比作是“语法毒(syntax poisoning)“,因为星号导入在动态模块中就像是被“下毒”了一样。

9afbe8bfb1285e362d8c542302247457.png

这个带毒的星星对你很生气。图片来源:seekpng

在我看来,我们已经居住在一个语法毒的世界里了。在 Node 14 版中,命名导出就是被下毒的,在动态模块中,星号导入也是被下毒的。因为命名导出极其普遍而星号导出相对罕见,动态模块会在生态中减少语法毒的成分。

这并不意味着动态模块已经穷途末路。案上依然有提议,让所有 Node 模块都成为动态模块,甚至带上纯 ESM 模块,并且在 Node 中弃用 ESM 的多阶段加载器。让人眼前一亮的是,这样并不会产生用户可见的影响,除了一些可能发生的轻微启动性能下降。ESM 多阶段加载器是在网络缓慢的情况下加载代码而设计的。

不过我依然不觉得会这么走运。Github 上关于动态模块的 issue 最近被关闭了,因为去年没有关于动态模块的讨论。

还有一个方案悬而未决,那就是做一次充分努力的尝试,把 CJS 模块进行词法分析,从而检测出导出内容,但是这个方案不可能在 100% 的用例中使用。(最新的 PR 在 npm 前 1000 的模块中只有 62% 正常工作。)因为这种启发式的东西太不可靠,一些 Node 模块工作组成员是反对的。

ESM 可以 require(),但是很可能并不值得

require() 默认并不在 ESM 代码范畴内,不过你可以轻松把它找回。

import { createRequire } from 'module';

const require = createRequire(import.meta.url);

const {foo} = require('./foo.cjs');

这个方法的问题在于它没能帮多大忙。实际上也就比做一个默认导入然后解构多了几行代码。

import cjsModule from './foo.cjs';

const {foo} = cjsModule;

另外,像 Webpack 和 Rollup 这样的打包工具并不知道如何处理 createRequire 这样的模式,所以意义何在呢?

如何创建一个良好的包含了 CJS 和 ESM 的“二重包”

如果你手上至今都维护着一个库,需要支持 CJS 和 ESM,那么就给你的用户做点好事,按照上文的方针建造一个“二重包”,能够在 CJS 和 ESM 下都良好工作。

给库提供一个 CJS 的版本

这是为了方便你的 CJS 用户。同时也确保了你的库能够在 Node 的早期版本中正常工作。

如果你使用的是 TypeScript 或者其它最终转译成 JS 的语言,那么就转译成 CJS 吧。

给 CJS 提供一个浅的 ESM 封装

注意,给 CJS 库写一个 ESM 包裹层是不难的,但是给 ESM 库写一个 CJS 包裹层就不可能了。

import cjsModule from '../index.js';

export const foo = cjsModule.foo;

把 ESM 包裹层 放到一个 esm 的子目录下,同时放入一个一行的 package.json,里面只放 {"type": "module"}。(你可以重命名你的包裹层文件为 .mjs,在 Node 14 下是正常的,但是有的工具和 .mjs 搭配不好,因此我倾向于使用一个子目录。)

避免二次转译。如果你是在从 TypeScript 做转译,你可以转译成 CJS 和 ESM,但是这就会带来一个潜在的危害,用户可能偶然既 import 了你的 ESM 代码,又 require() 了你的 CJS 代码。(比如,假设一个库 omg.mjs 依赖于 index.mjs,另一个库 bbq.cjs 依赖于 index.cjs,然后你还既要依赖 omg.mjs 又要依赖 bbq.cjs。)

Node 自身会给模块做去重,不过 Node 并不知道你的 CJS 和 ESM 其实是”相同的“文件,于是你的代码就会执行两次,并且保留你的库状态的两份拷贝。这就能引发各种奇异的 Bug。

给你的 package.json 添加一个 exports 映射

就像这样:

"exports": {

"require": "./index.js",

"import": "./esm/wrapper.js"

}

注意:添加一个 exports 映射永远要作为“语义化版本控制中的主要层级”的重大变化。默认情况下,你的用户能够进入你的包,然后 require() 任何他们想要的代码,甚至是你想要变成内部层的文件。exports 映射确保了用户只能 require/import 你刻意暴露出来的入点文件。

这就快是一个好的东西了!但是这也是一个重大变化。

如果你跟着你的用户进行 import 或者 require() 你的模块里的其他文件,你也可以分开来设置入点。具体请查阅 ESM 的 Node 文档。

始终要在导出映射目标中包含文件扩展名。写成 "index.js" 而不是 "index" 或者一个类似 "./build" 的目录。

如果你遵循了上述的方针,你的用户就会很安分。一切都会变得很安分。

关于本文 译者:@周雨楠 译文:https://mp.weixin.qq.com/s/CxlolXUpK02wZbRNah_ZQA 作者:@Dan Fabulich 原文链接:https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1

0a123f640dfb39404b179dad7076a22b.png

为你推荐

【第1994期】ES11来了

【第1899期】调研 Federated Modules,应用秒开,应用集方案,微前端加载方案改进等

欢迎自荐投稿,前端早读课等你来

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/551535.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

centos7 转换为lvm_(建议收藏)CentOS7挂载未分配的磁盘空间以及LVM详细介绍

简述本文主要介绍CentOS7下如何挂载未分配磁盘空间的详细操作步骤。LVMLVM,逻辑卷管理,英文全称Logical Volume Manager,是Linux环境下对磁盘分区进行管理的一种机制。是在硬盘分区和文件系统之间添加的一个逻辑层,为文件系统屏蔽…

基于python的图书管理系统测试步骤_Django admin实现图书管理系统菜鸟级教程完整实例...

Django 有着强大而又及其易用的admin后台,在这里,你可以轻松实现复杂代码实现的功能,如搜索,筛选,分页,题目可编辑,多选框. 简单到,一行代码就可以实现一个功能,而且模块之间耦合得相当完美. 不信,一起来看看吧!?用Django实现管理书籍的系统,并能在前台界面对书籍进行增删查改…

c# image转换为bitmap_Python PIL.Image与numpy.array之间的相互转换

前言有时我们使用PIL库读入图像数据后需要查看图像数据的维度,比如shape,或者有时我们需要对图像数据进行numpy类型的处理,所以涉及到相互转化,这里简单记录一下。方法当使用PIL.Image.open()打开图片后,如果要使用img…

计算机网络互联设备功能,计算机网络互联设备简介

一、网卡v 网络适配器,俗称网卡(NIC,Network InterfaceCard 或 Ethernet network card ) 原理:• 工作在OSI/RM中数据链路层的设备– 是局域网接入设备,单机与网络间架设的桥梁 特征:• MACaddress:– uniq…

安全使用计算机事例,计算机安全案例分析.ppt

您所在位置:网站首页 > 海量文档&nbsp>&nbsp资格/认证考试&nbsp>&nbsp安全工程师考试计算机安全案例分析.ppt35页本文档一共被下载:次,您可全文免费在线阅读后下载本文档。下载提示1.本站不保证该用户上传的文档完整性&#…

bootstrap外不引用连接_网络编程Netty IoT百万长连接优化,万字长文精讲

IoT是什么The Internet of things的简称IoT,即是物联网的意思IoT推送系统的设计比如说,像一些智能设备,需要通过APP或者微信中的小程序等,给设备发送一条指令,让这个设备下载或者播放音乐,那么需要做什么才…

计算机基础知识与程序设计二,计算机基础与程序设计.doc

计算机基础与程序设计.doc (17页)本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦!14.9 积分《计算机基础与稈序设计》是高等教冇H学考试工科备专业的基础课。这门课也是大部分学 生学习计算…

为什么python 为什么没有接口_python没有接口吗

接口只是定义了一些方法,而没有去实现,多用于程序设计时,只是设计需要有什么样的功能,但是并没有实现任何功能,这些功能需要被另一个类(B)继承后,由 类B去实现其中的某个功能或全部功…

画直线_在鸡面前画一条直线,为什么它会晕?西瓜视频这知识好冷告诉答案

为什么世界有那么多的未解之谜,我们无从而知,今天我们来探讨一下在鸡面前画条直线为什么会晕?你们知道吗?今天西瓜视频这知识好冷告诉你们答案,帮助你们掌握生活中所不知道的涨知识,增加我们的知识库。优秀…

永洪bi_案例分享!永洪BI助力知名三甲医院数字化转型升级

案例一:“新数据需求立刻看到结果”建院至今已有100余年的历史,现已发展成为集医疗、科研、教学为一体的某家三级甲等综合医院,通过永洪科技大数据平台,基于医院的HIS系统为数据源,分别从运营管理、药品管理、病例管理…

github mac 添加 ssh_计算机专业MAC操作技巧(二)

1、MAC 终端启动jupyter jupyter安装与配置就不赘述了,MAC终端启动jupyter有点独特。尝试了很多次都没有打开浏览器,把踩的坑总结一下:一直出现找不到浏览器的错误,在本地浏览器中一直打不开。(1)、首先先在…

计算机有必要报英语四级吗,我已工作了,现在有必要去考英语四级吗?还是 – 手机爱问...

2010-02-20有哪些是衡量好坏的重要指标呢?眼看就是春节,电视还没买回家,不是偷懒,是不晓得该如何抉择是好?需要网友帮忙。液晶显示不像PDP、CRT那样属于自发光显示。液晶面板也好,背光技术也罢。只是显示屏…

python分支结构说课_Python_3.8平台上的分支结构(模块.类.函数)_11

计算机 python语言_3.8平台上的分支结构(模块.类.函数)11上节说了,python程序有注释、缩进和程序主题。其应用软件由模块--文件*.py分割保存。模块中有变量、函数、类(数据与函数)等。模块是最基础的最小的结构要素单元。并用__main__模块演示了按照较规范的执行顺序…

计算机应用基础自主学习,《计算机应用基础》自主学习指导

本资料可供职业中学、高职及初学计算机基础的学生参考也可以供授课教师参考《计算机应用基础》自主学习指导一、课程内容、要求、目的1、本课程是一门有关计算机知识的入门课程,主要着重计算机的基础知识、基本概念和基本操作技能的学习和培养,并兼顾实用…

java run里面定义变量_Java程序员50多道最热门的多线程和并发面试题(答案解析)...

下面是Java程序员相关的热门面试题,你可以用它来好好准备面试。1) 什么是线程?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多…

qpython获取手机gps_基于Python获取照片的GPS位置信息

这篇文章主要介绍了基于Python获取照片的GPS位置信息,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 昨天听人说,用手机拍照会带着GPS信息,原来没注意过这个,因此查看下并使用…

计算机盐城工学院和常熟理工,【选专业】这6所二本院校的专业,就业不输一本学生!...

原标题:【选专业】这6所二本院校的专业,就业不输一本学生!距离高考只剩下十来天,家长们除了要关心孩子的情况,对于志愿填报也渐渐开始关注起来,这个时候许多家长才发现,志愿填报居然这么麻烦&am…

蒸汽机器人q和锤石q_英雄联盟:圣杯锤石的启发,辅助的作用是否应该更倾向多元化?...

圣杯锤石的套路我个人最早是从主播青蛙那儿了解的,偶然地刷到和锤石有关的剪辑,发现他已经在使用圣杯替代基克的聚合,在多次观察锤石在团战中的表现和圣杯的作用后,我觉得这种打法值得说道说道。当然强不强我无从得知,…

计算机英语的语言特点及教学,计算机英语的语言特点及教学.doc

计算机英语的语言特点及教学.doc (6页)本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦!9.90 积分计算机英语的语言特点及教学  1计算机英语的语言特点  计算机英语具有简明性  较传统…

npz文件转为npy_Numpy_快速操作数组 4.4 数组的文件输入输出

Numpy作者:PureFFFmennory对象类型:ndarry上一节:4.3 使用向量计算代替数组PureFFFmennory:《Python for Data Analysis 2nd》学习笔记Chapter 4-4.3​zhuanlan.zhihu.com4.4 数组的文件输入与输出NumPy能够以文本或二进制格式保存…