背景
自 2011 年 browserify
诞生开始,到我们现在更为广泛应用的 webpack
/ rollup
/ parcel
等构建工具的普及,构建及工程化已经变成了前端开发者的开发生态中不可或缺的部分。日常开发中,我们已经习惯跟随 ESMAScript 或 TypeScript 的新特性,这些特性往往具有更简单的语法、更高的执行效率,比较典型的例子就是 ES6 中 class
:
class Person {
constructor(name) {
this.name = name;
}
greeting() {
return `Hi, this is ${this.name}.`;
}
}
class Man extends Person {}
但如果要用构建函数和原型链去实现两个类的声明并实现继承,我相信大多数人会崩溃:
function Person(name) {
this.name = name;
}
Person.prototype.greeting = function() {
return `Hi, this is ${this.name}.`;
}
function Man(name) {
Persona.call(this, name);
}
Man.prototype = Object.assign(Person.prototype);
Man.prototype.constructor = Man;
问题在于,这些提效的新特性并不能 100% 的运行在所有的浏览器中,比如上个时代的噩梦 - IE8. 在这样的背景之下,就诞生了 6to5
这样的编译器,来帮助我们把开发时的 ES6 的语法编译成 ES5 的语法,从而执行在浏览器中,当然 6to5
也跟随我们一起进化,变成了现在的 babel
.
但 babel
也不是万能膏药,虽然帮助我们解决了语法的问题,但也带来了一些副作用,比如编译后的代码通常都会插入一些内置的函数或者 Pollyfill,这也就造成了代码体积的骤增,上文中我们 class Person
的源码实现总共 155B, 使用 babel 编译成 ES5 的代码后,体积变成了 2.7K!
从我们认为的 JavaScript 下一代的标准
的开始 -- ECMAScript 2015 到现在已经过去 5 年多的时间了,ECMAScript 每年都会不断的推进语言标准的更新,而浏览器厂商除了参与标准的指定,也在不断的跟进着新的语言标准在浏览器中的原生实现的支持。不知道你有没有注意到,在 2021 年的今天,class
的浏览器支持率已经达到了 95%
, 所有的主流浏览器都已支持 class
特性:
类似于 class
这类特性,如果浏览器已经支持了,但我们还是把编译后的体积更大、执行更慢的 ES5 代码交给浏览器,不管是构建、存储、传输还是执行,无疑都是一种浪费。而这就关系到我们这篇文章的主题:Modern JavaScript.
什么是 Modern JavaScript
Modern JavaScript
指的不是哪一个特定版本的 ECMAScript 标准,而是指的一系列已经被现代浏览器所支持的特性的集合。
我们常说的现代浏览器包括:Chrome, Edge, Firefox, Safari, 它们占了浏览器市场份额的 90% 以上,除此之外,还有一些基于跟上述浏览器相同渲染引擎的浏览器实现,比如 UC, QQ 等也占了 5% 左右的份额。这也就意味着,我们广泛使用的一些特性已经得到了 95% 的支持,主要包括:
- 类
class
- 箭头函数
arrow function
- 构造器
generator
- 块级作用域
let / const
- 解构
destructuring
- Rest 参数
rest and spread parameters
- 对象简写
object shorthand
- Async 函数
async / await
Modern JavaScript 不是一个固定的特性集合,它是动态跟随我们所定义的现代浏览器的的支持度的。就目前而言,ES2017 是最接近 Modern JavaScript 的标准。
Legacy JavaScript
对应 Modern JavaScript, 我们编译完的 ES5 的结果就可以称为 Legacy JavaScript
, 它是我们向浏览器兼容器委屈求全的结果。
在现代浏览器中,这种转换是得不偿失:我们通过编译让我们的代码支持度从 95% 上升到了 98%, 然而这却给我们带来了 20% 的代码体积的上升,同时代码执行效率会变得更低。另外,在 node_modules hell
的加成下,这个影响可能是指数级的。
如何转型
- 浏览器
<script type="module" />
现代浏览器支持通过 <script type="module" />
直接在浏览器中 ES6+ 的代码,因此我们可以通过这种方式来为现代浏览器加载 Modern JavaScript, 而小部分的老浏览器则由 fallback 逻辑兜底,加载执行 Legacy JavaScript.
<script type="module" src="https://cdn/modern.js" />
<script nomodule src="https://cdn/ledacy.js" />
- NPM
{ "exports": "./modern.js" }
在 Node.js@12.7.0
的 版本 中,引入了 package.json 中的 exports
字段,来增强原先仅能通过 main
声明的包入口的功能。
{
"main": "./index.js",
"exports": {
".": "./index.js",
"./submodule": {
"import": "./submodule/index.js",
"require": "./submodule/index.cjs"
}
}
}
通过上面的入口声明我们可以实现对外暴露默认入口以及 submodule
的入口,并且 submodule
可以根据是 require
还是 import
提供不同的实现:
import pkg from 'pkg';
import submodule from 'pkg/submodule';
const submodule = require('pkg/submodule');
exports 除了提供了 多入口 以及 条件入口 等强大的功能外,也可以作为提供了 Modern JavaScript 实现的依据,因为 Node.js@12.7.0 已经支持 ES2019 了,所以在构建时如果检测到 node_modules
中的模块提供了 exports
entry, 可以为它们单独构建我们的 Modern JavaScript Bundle.
- Bundle 通过上述浏览器和 NPM 包的对 Modern JavaScript 特性的支持,我们已经解决了出口的问题,并且在不支持的场景下也都可以 fallback 到 Legacy JavaScript 来执行。
接下来只需要资源构建的问题即可,我们需要提供满足 95% 的现代浏览器可执行的 Modern JavaScript Bundle 以及 fallback 剩余的老浏览器的 Legacy JavaScript Bundle.
通过 babel 配合不同的 browserslist targets
就可以简单的达成目标。当然也有一些现成的插件可以直接使用,比如 optimize-plugin, babel-esm-plugin 或者 babel-preset-modern-browsers 等。
而在构建模式上,我们可以选择从源码分别构建出 Modern Bundle 和 Legacy Bundle:
亦或是先从源码构建出 Modern Bundle, 然后从 Modern Bundle 再构建出 Legacy Bundle:
图片来源: web.dev/publish-mod…
Modern JavaScript 其实更像是一种理念,而不是一种技术,通过 Modern JavaScript 可以让开发者、用户享受到技术进步带来的福利,更甚者,Modern JavaScript 所带来的存储、传输、执行上的提升还能为地球 节能减排。
摆脱 ES5 的禁锢,向 Modern JavaScript 转型!
相关
- Transitioning to modern JavaScript
- Publish, ship, and install modern JavaScript for faster applications
- Serve modern code to modern browsers for faster page loads
- Bringing Modern JavaScript to Libraries
我们是阿里巴巴 CCO 技术部团队,希望有更多的同学加入跟我们一起做更多有意义的事情。技术栈限 React
,有 Typescript
经验者更佳,Base 南京或杭州。有意者联系:henry.lx@alibaba-inc.com