当前位置: 首页 > 科技观察

混沌是进步的阶梯——ESM规范的兴起【上篇】

时间:2023-03-18 18:00:54 科技观察

前言前端发展到现在,社区生态已经非常丰富。在无数开源大神的努力下,很多前端开发痛点(比如“静态类型检查”、“浏览器兼容性”)已经有了事实上的标准解决方案(比如TS、babel)。然而在这种繁荣之下,却有一个日常开发中不易察觉的问题:模块化规范的混乱。你遇到过莫名其妙的bug吗?本包导出为CJS,项目使用ESM。比如这个例子:记得一个包压缩错误[1]如果你觉得这是一个容易发现的问题,那么考虑结合node_modules的层层依赖?这个问题揭示了模块化规范之间的斗争和博弈的冰山一角。作为现代前端工程的基石,模块化规范有太多值得深入研究的内容。我将用几篇文章来解释模块化规范。本文是第一篇,将围绕模块化规范的演进展开。如果问文十年前最头疼的前端是什么?它必须是浏览器兼容性。随着babel等编译工具的出现,兼容性逐渐被工程化解决(ES6+编译成ES5)。不仅仅是“兼容性”问题,DSL(比如JSX、VUE的模板语法)、代码压缩、代码静态检查(TS)等日常开发的刚需都可以在工程方案中找到解决方案。如果我们把今天繁荣的前端工程生态比作一幢大楼,那么大楼的地基一定是“模块化规范”。现代JS代码是基于“模块化规范”组织的。让我们从下往上看一下这个建筑:规范的实现取决于宿主环境。例如,浏览器环境实现了EcmaScriptModule(以下简称ESM)规范。Nodev12之前支持CommonJS(以下简称CJS)规范,Nodev12之后同时支持CJS和ESM。“宿主环境”之上是基于模块化规范的“工具集”,如webpack、vite、VScode生态等。往上走,基于“工具集”提供的API,可以实现各种工程工具。比如:webpackloaderVScodepluginbabelplugin以上,就是开发者自己写的业务代码。开发者只需配置工具集中的工具,即可为业务代码提供服务。比如你在VScode(工具集)中配置了eslint(工具),在开发的时候就可以得到相应的提示。如果在webpack(工具集)中配置babelloader(工具),开发时可以使用ES6+语法。可见,理想情况下,从开发者的角度来看,根本不需要关注底层“模块化规范”的实现。规范之争然而,事物是动态发展的,模块化规范不可能一蹴而就。让我们回到2009年,美国程序员“RyanDahl”创建了node.js项目,使用JS进行服务端开发。node.js使用CJS[2]标准作为模块化规范。有了服务器端模块规范(CJS),JS开发人员自然希望为客户端(主要是浏览器)提供模块化规范。然而,CJS是为服务器端设计的。在服务器端,IO操作通常可以很快完成,所以CJS规范定义:模块加载-->模块解析-->模块执行这个过程整体同步执行。然而,在浏览器环境中,“模块加载”(即数据请求)通常是耗时的。有人曾经打过一个形象的比喻:如果一个CPU周期需要1秒才能完成,那么网络对文件的请求就需要4年。显然,浏览器端需要一个“支持异步”的模块化规范。AMD(AsynchronousModuleDefinition)规范就是在这样的需求背景下产生的。然而,这些社区提出的规范只是为了解决一时的需求。随着历史的发展,新的模块化规范不断涌入又消亡。直到ESM规范被提出。ESM规范是ES标准的模块化规范,他早期的讨论可以追溯到2009年。ESM规范的历史可以看这里es-module-history[3]ESM将模块规范分为三个阶段:模块加载-->模块实例化-->模块执行其中“模块加载”是由宿主环境提供的加载器完成的(例如,在浏览器环境中,加载器的行为由HTML规范定义[4]).“模块实例化”和“模块执行”由ESM规范定义。不同于CJS规范的同步执行,ESM规范将流程拆解为三个独立的阶段。“模块加载”是同步还是异步由宿主环境决定。支持不同主机环境,平滑多端差异,比其他规范能力更强(后面会介绍),血统纯正(ES官方提议),看来ESM规范统一前端“貌似”只是围绕着角落。但是,此时社区已经有大量基于CJS规范的开源包和组件,他们并不能立即切换到ESM规范。因此,JS生态的现状是,在未来很长一段时间内,都会处于CJS-标准库和ESM-标准库并存的状态。但最终ESM规范肯定会占优,毕竟它的优点太多了(再次强调,后面会介绍)。规范碎片化带来的机遇目前模块化规范的混乱,对于开源大佬来说是一个机会。为了让开发者更专注于业务而不是模块规范的适配。很多开源“工具集”试图抹平模块化的差异,例如:在babel中使用babel-plugin-transform-commonjs可以将CJS规范的代码转换为ESM规范,解决目前ESM、CJS、浏览器脚本tagimports一刀切在这三种规范互不兼容的情况下,提出了兼容这三种格式的UMD(UniversalModuleDefinition)规范。一些“工具集”利用模块化规范的差异,与其他竞品形成差异化竞争,例如:browserify这个封装工具的卖点是:使用CJS规范封装,这样一段代码就可以在Node环境和浏览器环境(打包后)都执行。其中,在浏览器环境下,Node的一些核心库(如events、stream、path...)会被打包成浏览器支持的版本。Vite使用ESM规范在DEV环境中构建模块之间的依赖关系。依靠大多数现代浏览器原生支持ESM规范,省略了打包过程,大大提高了编译速度。rollup原生提供了对ESM的更多支持。严格支持ESM规范,提供更好的静态分析,让rollup曾经提供了性能更好的treeShaking能力。成为更多图书馆打包工具的首选。与webpack等大型综合解决方案竞争。规范碎片化带来的痛苦可见一斑。由于底层宿主环境对模块化规范支持的碎片化,需要上层工具集来抹平模块化规范的差异。想象一个同时使用webpack、babel、TS的项目。所有三个工具集都与各种模块规格兼容。例如:单独使用babel时,如下代码:importafrom'lib';console.log(a);将由babel编译为:"usestrict";var_lib=_interopRequireDefault(require("lib"));function_interopRequireDefault(obj){returnobj&&obj.__esModule?obj:{default:obj};}console.log(_lib.default);ESM的“默认导出”将被编译成包含默认属性的对象。你可以打开babelplayground[5]尝试当多个“工具集”在同一个项目中时,为了他们自己的目的做同样的事情(平滑模块化规范的差异),一旦工具链中的插件配置有一个trace线程不符合预期,或者引入了一个不符合预期的包,那么艰难的调试就开始了……黎明纵使现在有很多不便,但历史的进程不能停止。被压垮的模块化规范会逐渐从开发者的视野中消失。赢家注定要通吃。为什么ESM注定是最大的赢家?他有什么优势?我们将在下一篇文章中揭晓。参考文献[1]记录一个包压缩错误:https://cloud.tencent.com/developer/article/1650627[2]CJS:http://wiki.commonjs.org/wiki/Modules/1.1[3]es-模块历史:https://gist.github.com/jkrems/769a8cd8806f7f57903b641c74b5f08a[4]HTML规范:https://html.spec.whatwg.org/#fetch-a-module-script-tree[5]babelplayground:https://babeljs.io/repl