背景
我们为Web应用程序有一个适中的(6位数的SLOC)尺寸代码库,该应用程序历史上是在requirejs上构建的。
如果您以前从未使用过requirjs,请知道它实际上是一个非常整洁的解决方案。特别是在2010年初到中间,没有一致的跨文本模块加载器环境。 (还记得YUI库吗?)您要么打包JS源(即使浏览诸如浏览之类的东西),也许是从高度动力和不一致的commonjs/node上下文中收拾好的,要么是直接在<script\>
标签中写作(我的胃感到很恶心。 ..),如果您没有像样的装载机。
与在浏览器上下文中使用节点风格的commonjs模块相比,requirejs确实感觉就像是新鲜空气的呼吸。开发人员只需使用类似commjs的require()
语句,但可以在独立封闭中定义模块,并期望一致的元符号(module
,exports
和require
函数本身)。装载机的扩展是特别独特且有用的,为您提供了一种挂钩各种不同资源的方法。我什至有一个.ZIP
文件系统加载程序用于归档/压缩和暴露静态资产!
requirejs使用了不同的加载器标准,称为AMD(用于“异步模块定义”)。通过异步定义和加载模块,您可以期望在加载页面或应用程序时更好地启动性能(而不是加载一大堆JS,如果没有大量的开发人员工具插件(即使不是不可能),即使不是不可能在生产中进行调试,也很难进行调试。如果它们根本存在)。最好使用具有明确符号的浏览器优先环境(您会惊讶地暗中实现了多少),并且从CommonJs中移植其他资源非常容易,这真是太容易了,因为您总是可以在AMD兼容的关闭中包装定义。即使UMD(通用模块定义)标题在阳光下持续了几年,情况仍然如此。
es6引入了对import
和export
符号,语法和行为的支持。令人惊讶的是(很大程度上我认为,由于强大的买入commonj/node环境),ES6模块花了很长时间才脱颖而出。即使在过去的几年中,主要包裹主要集中在(例如)跨构建到的ES6模块上,但此后,许多项目已成功地将基本的代码库迁移到ES6上,并将其构建为“构建”到其他情况下。
仍然存在多种理由保留其他模块环境,这在很大程度上(根据我的经验),由于其他抽象购买的ins在部署时如何在浏览器上下文中加载和利用代码。 React(通常)(特别是)(尤其是.TSX
)是一个很好的示例,因为无论如何要执行前期构建通行证的强烈组件被映射到文件结构。谁能忘记JQuery?哈哈哈,只是开玩笑。
动机
这使我们进入了正式的移民。告别,requirejs!你为我们服务了。
(旁注:有时您会看到称为“ ESM”的ES6模块。您还将看到使用的文件扩展名.MJS
使用,很大程度上是为了告诉Node,它使用JavaScript模块。从客户端的角度来看。 ,type="module"
足够了,但是使其显而易见可能很不错,并且像VS Code这样的受欢迎的编辑已经知道.MJS
。您可能需要的最大调整是将MIME类型映射添加到静态文件服务器中,这很容易执行在像nginx
之类的东西中。)
这种迁移的主要原因有几个主要原因:
-
requirejs已达到EOL(生命的尽头),并且不再收到重大更新。在像模块装载机一样基本的事情中,稳定性是一件好事,但是在这种情况下,随着ES6模块最终达到临界质量的水平,它正在整体上失去支持。
。 -
requirejs通过设计,在模块加载上有重大的性能问题。 ES6模块在JavaScript解释器规范本身中实现
import
和export
符号和逻辑,这意味着通常,对于模块加载的性能更好。这是不符合require()
语句的每个模块负载的补充。查找,这是真的!加载define
封闭模块时,RequireJS将其变成字符串,并与require()
调用模式匹配以解决内部依赖关系。对于一个应该包括异步性能的装载机系统,这实际上是令人难以置信的,当我了解它时。除其他外,这意味着您不能动态“构造”require()
参数以在运行时解决依赖项。 -
ES6模块在浏览器和命令行(例如节点和NPM/YARN)上下文上都在本地交叉兼容。这意味着它变得更加容易在模块级别上扮演CI集成,以使其受益于其受益的任何依赖性,包括混淆/缩减;测试;文档;部署/发布触发器和公告;还有许多其他伟大的功能 - 即使模块本身是浏览器优先的。
-
我提到整个社区终于达到了其向ES6模块的迁移(远离浏览器本机,requirejs,node/commonjs和任何其他UMD-HEADER上下文)的关键质量。这意味着可重复使用性(对于我们自己的内部开发模块,对于我们想要杠杆的任何其他依赖项)都更好,更容易。没有更多的随机标题!没有更多的任意封闭来强制互相兼容的环境!这真是令人兴奋,您对它的思考越多。大多数开源JS项目都在他们对ES6兼容构建的本机支持的时刻,因此这只是
import symbol from "path";
的问题,您很高兴。如果像我们一样,您使用git子模块作为包装管理的替代方案,尤其是这样,尤其是因为您可以从(例如)dist/
文件夹中构建和从尚未移植的项目中构建ES6模块。 p>https://www.zachgollwitzer.com/posts/scripts-commonjs-umd-amd-es6-modules
-
说到路径,
import
具有多种模式,可以将其用于模块路径的解析方式。 (在主要浏览器中,对于importmap
功能也有一些实验性支持,以定义模块路径如何解决,但我们尚未触摸。主机路径eyways(在浏览器上下文中进行评估),但是在外壳方面(对于CI等),这是一个很好的改进,尤其是远离node_modules/
Hell。
移植
因此,您收集了大量的requirejs模块。您如何移植它们?我发现大多数模块都需要三个重大更改:
- 首先,您需要消除“ define()”闭合以及其暴露的符号上的任何依赖项(模块,导出等)。
之前:
define(function(require, exports, module) {
class MyClass {
...
}
});
之后:
class MyClass {
...
}
- 接下来,您需要用适当的“导入”语句替换“ require()”语句。
之前:
const dependency = require("path/to/dependency");
之后:
import dependency from "path/to/dependency.mjs";
- 然后,您需要更改导出语句(有三种方法可以在requirejs关闭中导出符号:从闭合返回,分配给 exports ,然后分配给 module.exports.exports < /em>)。
之前:
return Object.assign(MyClass, {
"__metadata__": "..."
});
之后:
export default Object.assign(MyClass, {
"__metadata__:" "..."
});
最后,可能有一些使用外部资源的模块。例如,我们的某些模块引用了一个侧载.CSV表,我们为其具有自定义的requirejs模块加载程序。当require()
参数以注册前缀为“ CSV!”开始时,加载程序扩展程序将使用Papaparse将内容转换为模块级对象的模块级数组。使用等待关键字,可以很容易地用fetch()然后()承诺的内联链代替。 (顺便说一句,这使我们摆脱了额外的依赖性,因为我们不再需要“加载程序”模块来定义扩展名,并使我们摆脱了针对前缀的扩展的粘性全球封闭式注册。)
之前:
require.config({ "paths": {
"csv": "mycsvloader"
}});
const data = require("csv!path/to/table.csv");
之后:
const data = await fetch("path/to/table.csv")
.then(response => response.text())
.then(text => papaparse.parse(text.trim(), {
"header": true
}).data);
这最终足以满足我遇到的99%的案件。
结论
我会承认,我仍然对requirejs具有情感价值。多年来,它让我们“短路”模块战争,并明确专注于可重复使用的前端领先模块的强大发展。但是,一旦我们有时间和精力将ES6模块放到ES6模块上,好处就很明显了,我们已经看到了重大改进。特别是,我不得不说,我对自己可以在单个模块上携带多少CI力量感到非常兴奋。
如果您处于类似的位置,我鼓励您考虑投入时间和精力来进行类似的重构。 JavaScript生态系统即使不是令人难以置信的过度载体,但对于像模块加载一样基本的东西,我们似乎最终似乎会融入到未来的稳定性(尤其是在口译员级别,而不是在解释层面上)成为另一个高级框架的一部分)。我认为从长远来看,随着开发人员开始共同利用更大程度的可重复性和交叉兼容性,这实际上将在JavaScript生态系统中实现更多更多的多样性和多元化。 p>