向Modern JavaScript 轉型
背景
自2011年browserify
誕生開始,到我們現在更為廣泛應用的webpack
/ rollup
/ parcel
等構建工具的普及,構建及工程化已經變成了前端開發者的開發生態中不可或缺的部分。日常開發中,我們已經習慣跟隨ESMAScript或TypeScript的新特性,這些特性往往具有更簡單的語法、更高的執行效率,比較典型的例子就是ES6中class
:
classPerson{constructor(name){this.name=name;}greeting(){return`Hi, this is ${this.name}.`;}}classManextendsPerson{}
但如果要用構建函數和原型鏈去實現兩個類的聲明並實現繼承,我相信大多數人會崩潰:
functionPerson(name){this.name=name;}Person.prototype.greeting=function(){return`Hi, this is${this.name}.`;}functionMan(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.
<scripttype="module"src="https://cdn/modern.js"/><scriptnomodulesrc="https://cdn/ledacy.js"/>
2. NPM
{ "exports": "./modern.js" }
在[email protected]
的版本中,引入了package.json
中的exports
字段,來增強原先僅能通過main
聲明的包入口的功能。
{"main":"./index.js","exports":{".":"./index.js","./submodule":{"import":"./submodule/index.js","require":"./submodule/index.cjs"}}}
通過上面的入口聲明我們可以實現對外暴露默認入口以及submodule的入口,並且submodule可以根據是require
還是import
提供不同的實現:
importpkgfrom'pkg';importsubmodulefrom'pkg/submodule';constsubmodule=require('pkg/submodule');
exports除了提供了多入口以及條件入口等強大的功能外,也可以作為提供了Modern JavaScript實現的依據,因為[email protected]已經支持ES2019了,所以在構建時如果檢測到node_modules
中的模塊提供了exports
entry,可以為它們單獨構建我們的Modern JavaScript Bundle.
3. 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:
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