向 Modern JavaScript 转型

814 阅读5分钟

背景

自 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 的加成下,这个影响可能是指数级的。

如何转型

  1. 浏览器

<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" />
  1. 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.

  1. 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 转型!

相关


我们是阿里巴巴 CCO 技术部团队,希望有更多的同学加入跟我们一起做更多有意义的事情。技术栈限 React,有 Typescript 经验者更佳,Base 南京或杭州。有意者联系:henry.lx@alibaba-inc.com