前端科普系列(4):Babel —— 把 ES6 送上天的通天塔

本文首發於 vivo互聯網技術 微信公衆號
連接: mp.weixin.qq.com/s/plJewhUd0…
做者:Morrainhtml

1、前言

在上一節 《CommonJS:不是前端卻革命了前端》中,咱們聊到了 ES6 Module,它是 ES6 中對模塊的規範,ES6 是 ECMAScript 6.0 的簡稱,泛指 JavaScript 語言的下一代標準,它的第一個版本 ES2015 已經在 2015 年 6 月正式發佈,本文中提到的 ES6 包括 ES201五、ES201六、ES2017等等。在第一節的《Web:一路前行一路忘川》中也提到過,ES2015 從制定到發佈歷經了十幾年,引入了不少的新特性以及新的機制,瀏覽器對 ES6 的支持進度遠遠趕不上前端開發小哥哥們使用 ES6 的熱情,因而矛盾就日益顯著……前端

2、Babel 是什麼

先來看下它在官網上的定義:webpack

Babel is a JavaScript compilergit

沒錯就一句話,Babel 是 JavaScript 的編譯器。至於什麼是編譯器,能夠參考 the-super-tiny-compiler 這個項目,能夠找到很好的答案。es6

本文是以 Babel 7.9.0 版本進行演示和講解的,另外建議學習者閱讀英文官網,中文官網會比原版網站慢一個版本,而且不少依然是英文的。github

Babel 就是一套解決方案,用來把 ES6 的代碼轉化爲瀏覽器或者其它環境支持的代碼。注意個人用詞哈,我說的不是轉化爲 ES5 ,由於不一樣類型以及不一樣版本的瀏覽器對 ES6 新特性的支持程度都不同,對於瀏覽器已經支持的部分,Babel 能夠不轉化,因此 Babel 會依賴瀏覽器的版本,後面會講到。這裏能夠先參考 browerslist 項目。web

Babel 的歷史

在學習任何一門知識前,我都習慣先了解它的歷史,這樣才能深入理解它存在乎義。chrome

Babel 的做者是 FaceBook 的工程師 Sebastian McKenzie。他在 2014 年發佈了一款 JavaScript 的編譯器 6to5。從名字就能看出來,它主要的做用就是將 ES6 轉化爲 ES5。express

這裏的 ES6 指 ES2015,由於當時尚未正式發佈, ES2015 的名字還未被正式肯定。npm

因而不少人評價,6to5 只是 ES6 獲得支持前的一個過渡方案,它的做者很是不一樣意這個觀點,認爲 6to5 不光會按照標準逐步完善,依然具有很是大的潛力反過來影響並推動標準的制定。正由於如此 6to5 的團隊以爲 '6to5' 這個名字並無準確的傳達這個項目的目標。加上 ES6 正式發佈後,被命名爲 ES2015,對於 6to5 來講更偏離了它的初衷。因而 2015 年 2 月 15 號,6to5 正式改名爲 Babel。

(圖片來源於網絡)

Babel 是巴比倫文化裏的通天塔,用來給 6to5 這個項目命名真得太貼切了!羨慕這些牛逼的人,不光代碼寫得好,還這麼有文化,不像咱們,起個變量名都得憋上半天,吃了沒有文化的虧。這也是爲何我把這篇文章起名爲 《Babel:把 ES6 送上天的通天塔》的緣由。

3、Babel 怎麼用

瞭解了 Babel 是什麼後,很明顯咱們就要開始考慮怎麼使用 Babel 來轉化 ES6 的代碼了,除了 Babel 自己提供的 cli 等工具外,它還支持和其它打包工具配合使用,譬如 webpack、rollup 等等,能夠參考官網對不一樣平臺提供的配置說明

本文爲了感覺 Babel 最原始的用法,不結合其它任何工具,直接使用 Babel 的 cli 來演示。

一、構建 Babel 演示的工程

使用以下命令構建一個 npm 包,並新建 src 目錄 和 一個 index.js 文件。

npm init -y複製代碼

package.json 內容以下:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}複製代碼

二、安裝依賴包

npm install --save-dev @babel/core @babel/cli @babel/preset-env複製代碼

後面會介紹這些包的做用,先看用法

增長 babel 命令來編譯 src 目錄下的文件到 dist 目錄:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "babel": "babel src --out-dir dist",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.9.0",
    "@babel/preset-env": "^7.9.0"
  }
}複製代碼

三、增長 Babel 配置文件

在工程的根目錄添加 babel.config.js 文件,增長 Babel 編譯的配置,沒有配置是不進行編譯的。

const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = []
 
module.exports = { presets, plugins }複製代碼

上例中 debug 配置是爲了打印出 Babel 工做時的日誌,能夠方便的看來,Babel 轉化了哪些語法。

  1. presets 主要是配置用來編譯的預置,plugins 主要是配置完成編譯的插件,具體的含義後面會講
  2. 推薦用 Javascript 文件來寫配置文件,而不是 JSON 文件,這樣能夠根據環境來動態配置須要使用的 presets 和 plugins
const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = []
 
if (process.env["ENV"] === "prod") {
  plugins.push(...)
}
 
module.exports = { presets, plugins }複製代碼

四、編譯的結果

配置好後,咱們運行 npm run babel 命令,能夠看到 dist 文件夾下生成了 index.js 文件,內容以下所示:

// src/index.js
const add = (a, b) => a + b
 
// dist/index.js
"use strict";
 
var add = function add(a, b) {
  return a + b;
};複製代碼

能夠看到,ES6 的 const 被轉化爲 var ,箭頭函數被轉化爲普通函數。同時打印出來以下日誌:

> babel src --out-dir dist
 
@babel/preset-env: `DEBUG` option
 
Using targets:
{}
 
Using modules transform: auto
 
Using plugins:
  proposal-nullish-coalescing-operator {}
  proposal-optional-chaining {}
  proposal-json-strings {}
  proposal-optional-catch-binding {}
  transform-parameters {}
  proposal-async-generator-functions {}
  proposal-object-rest-spread {}
  transform-dotall-regex {}
  proposal-unicode-property-regex {}
  transform-named-capturing-groups-regex {}
  transform-async-to-generator {}
  transform-exponentiation-operator {}
  transform-template-literals {}
  transform-literals {}
  transform-function-name {}
  transform-arrow-functions {}
  transform-block-scoped-functions {}
  transform-classes {}
  transform-object-super {}
  transform-shorthand-properties {}
  transform-duplicate-keys {}
  transform-computed-properties {}
  transform-for-of {}
  transform-sticky-regex {}
  transform-unicode-regex {}
  transform-spread {}
  transform-destructuring {}
  transform-block-scoping {}
  transform-typeof-symbol {}
  transform-new-target {}
  transform-regenerator {}
  transform-member-expression-literals {}
  transform-property-literals {}
  transform-reserved-words {}
  transform-modules-commonjs {}
  proposal-dynamic-import {}
 
Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.
Successfully compiled 1 file with Babel.複製代碼

4、Babel 工做原理

在瞭解瞭如何使用後,咱們一塊兒來探尋一下編譯背後的事情,同時會熟悉 Babel 的組成和進階用法。

一、Babel 工做流程

前面提到 Babel 其實就是一個純粹的 JavaScript 的編譯器,任何一個編譯器工做流程大體均可以分爲以下三步:

  • Parser 解析源文件

  • Transfrom 轉換

  • Generator 生成新文件

Babel 也不例外,以下圖所示:

(圖片來源於網絡)

由於 Babel 使用是 acorn 這個引擎來作解析,這個庫會先將源碼轉化爲抽象語法樹 (AST),再對 AST 做轉換,最後將轉化後的 AST 輸出,便獲得了被 Babel 編譯後的文件。

那 Babel 是如何知道該怎麼轉化的呢?答案是經過插件,Babel 爲每個新的語法提供了一個插件,在 Babel 的配置中配置了哪些插件,就會把插件對應的語法給轉化掉。插件被命名爲 @babel/plugin-xxx 的格式。

二、Babel 組成

(1)@babel/preset-env

上面提到過 @babel/preset-* 實際上是轉換插件的集合,最經常使用的就是 @babel/preset-env,它包含了 大部分 ES6 的語法,具體包括哪些插件,能夠在 Babel 的日誌中看到。若是源碼中使用了不在 @babel/preset-env 中的語法,會報錯,手動在 plugins 中增長便可。

例如 ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。但如今有一個提案提供了類的靜態屬性,寫法是在實例屬性的前面,加上 static 關鍵字。

// src/index.js
const add = (a, b) => a + b
 
class Person {
  static a = 'a';
  static b;
  name = 'morrain';
  age = 18
}複製代碼
編譯時就會報以下錯誤:

根據報錯的提示,添加 @babel/plugin-proposal-class-properties 便可。

npm install --save-dev @babel/plugin-proposal-class-properties複製代碼
// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = ['@babel/plugin-proposal-class-properties']
 
module.exports = { presets, plugins }複製代碼
@babel/preset-env 中還有一個很是重要的參數 targets,最先的時候咱們就提過,Babel 轉譯是按需的,對於環境支持的語法能夠不作轉換的。就是經過配置 targets 屬性,讓 Babel 知道目標環境,從而只轉譯環境不支持的語法。 若是沒有配置會默認轉譯全部 ES6 的語法。
// src/index.js
const add = (a, b) => a + b
 
// dist/index.js  沒有配置targets
"use strict";
 
var add = function add(a, b) {
  return a + b;
};複製代碼
按以下配置 targets
// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      targets: {
        chrome: '58'
      }
    }
  ]
]
const plugins = ['@babel/plugin-proposal-class-properties']
 
module.exports = { presets, plugins }複製代碼
編譯後的結果以下:
// src/index.js
const add = (a, b) => a + b
 
// dist/index.js  配置targets  chrome 58
"use strict";
 
const add = (a, b) => a + b;複製代碼
能夠看到 const 和箭頭函數都沒有被轉譯,由於這個版本的 chrome 已經支持了這些特性。能夠根據需求靈活的配置目標環境。

爲後方便後續的講解,把 targets 的配置去掉,讓 Babel 默認轉譯全部語法。

(2)@babel/polyfill

polyfill 直譯是墊片的意思,又是 Babel 裏一個很是重要的概念。先看下面幾行代碼:

// src/index.js
const add = (a, b) => a + b
 
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()複製代碼
按以前的方法,執行 npm run babel 後,咱們驚奇的發現,Array.prototype.includes 和 Promise 居然沒有被轉譯!
// dist/index.js
"use strict";
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();複製代碼
原來 Babel 把 ES6 的標準分爲 syntax 和 built-in 兩種類型。syntax 就是語法,像 const、=> 這些默認被 Babel 轉譯的就是 syntax 的類型。而對於那些能夠經過改寫覆蓋的語法就認爲是 built-in,像 includes 和 Promise 這些都屬於 built-in。而 Babel 默認只轉譯 syntax 類型的,對於 built-in 類型的就須要經過 @babel/polyfill 來完成轉譯。@babel/polyfill 實現的原理也很是簡單,就是覆蓋那些 ES6 新增的 built-in。示意以下:
Object.defineProperty(Array.prototype, 'includes',function(){
  ...
})複製代碼
因爲 Babel 在 7.4.0 版本中宣佈廢棄 @babel/polyfill ,而是經過 core-js 替代,因此本文直接使用 core-js 來說解 polyfill 的用法。
  • 安裝 core-js

npm install --save core-js複製代碼
  • 注意 core-js 要使用 --save 方式安裝,由於它是須要被注入到源碼中的,在執行代碼前提供執行環境,用來實現 built-in 的注入
  • 配置 useBuiltIns

    在 @babel/preset-env 中經過 useBuiltIns 參數來控制 built-in 的注入。它能夠設置爲 'entry'、'usage' 和 false 。默認值爲 false,不注入墊片。

    設置爲 'entry' 時,只須要在整個項目的入口處,導入 core-js 便可。

// src/index.js
import 'core-js'
 
const add = (a, b) => a + b
 
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()
 
// dist/index.js
"use strict";
 
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.promise");
//
// ……  這裏還有不少
//
require("regenerator-runtime/runtime");
var add = function add(a, b) {
  return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();複製代碼
  • 編譯後,Babel 會把目標環境不支持的全部 built-in 都注入進來,無論是否是用到,這有一個問題,對於只用到比較少的項目來講徹底沒有必要,白白增長代碼,浪費包體大小。

設置爲 'usage' 時,就不用在項目的入口處,導入 core-js了,Babel 會在編譯源碼的過程當中根據 built-in 的使用狀況來選擇注入相應的實現。

// src/index.js
const add = (a, b) => a + b
 
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()
 
// dist/index.js
"use strict";
 
require("core-js/modules/es6.promise");
 
require("core-js/modules/es6.object.to-string");
 
require("core-js/modules/es7.array.includes");
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();複製代碼
  • 配置 corejs 的版本

當 useBuiltIns 設置爲 'usage' 或者 'entry' 時,還須要設置 @babel/preset-env 的 corejs 參數,用來指定注入 built-in 的實現時,使用 corejs 的版本。不然 Babel 日誌輸出會有一個警告。

最終的 Babel 配置以下:

// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      useBuiltIns: 'usage',
      corejs: 3,
      targets: {}
    }
  ]
]
const plugins = ['@babel/plugin-proposal-class-properties']
 
module.exports = { presets, plugins }複製代碼

(3)@babel/plugin-transform-runtime

在介紹 @babel/plugin-transform-runtime 的用途以前,先前一個例子:

// src/index.js
const add = (a, b) => a + b
 
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise(resolve=>resolve(10))
 
class Person {
  static a = 1;
  static b;
  name = 'morrain';
  age = 18
}
 
// dist/index.js
"use strict";
 
require("core-js/modules/es.array.includes");
 
require("core-js/modules/es.object.define-property");
 
require("core-js/modules/es.object.to-string");
 
require("core-js/modules/es.promise");
 
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
 
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
  return resolve(10);
});
 
var Person = function Person() {
  _classCallCheck(this, Person);
 
  _defineProperty(this, "name", 'morrain');
 
  _defineProperty(this, "age", 18);
};
 
_defineProperty(Person, "a", 1);
 
_defineProperty(Person, "b", void 0);複製代碼
在編譯的過程當中,對於 built-in 類型的語法經過 require("core-js/modules/xxxx") polyfill 的方式來兼容,對於 syntax 類型的語法在轉譯的過程會在當前模塊中注入相似 _classCallCheck 和 _defineProperty 的 helper 函數來實現兼容。對於一個模塊而言,可能還好,但對於項目中確定是不少模塊,每一個模塊模塊都注入這些 helper 函數,勢必會形成代碼量變得很大。

而 @babel/plugin-transform-runtime 就是爲了複用這些 helper 函數,縮小代碼體積而生的。固然除此以外,它還能爲編譯後的代碼提供一個沙箱環境,避免全局污染。

使用 @babel/plugin-transform-runtime

  • ①安裝

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime複製代碼
其中 @babel/plugin-transform-runtime 是編譯時使用的,安裝爲開發依賴,而 @babel/runtime 其實就是 helper 函數的集合,須要被引入到編譯後代碼中,因此安裝爲生產依賴
  • ②修改 Babel plugins 配置,增長@babel/plugin-transform-runtime

// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      useBuiltIns: 'usage',
      corejs: 3,
      targets: {}
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties',
  [
    '@babel/plugin-transform-runtime'
  ]
]
 
module.exports = { presets, plugins }複製代碼
  • 以前的例子,再次編譯後,能夠看到,以前的 helper 函數,都變成相似require("@babel/runtime/helpers/classCallCheck") 的實現了。
// dist/index.js
"use strict";
 
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
 
require("core-js/modules/es.array.includes");
 
require("core-js/modules/es.object.to-string");
 
require("core-js/modules/es.promise");
 
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
 
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
  return resolve(10);
});
 
var Person = function Person() {
  (0, _classCallCheck2["default"])(this, Person);
  (0, _defineProperty2["default"])(this, "name", 'morrain');
  (0, _defineProperty2["default"])(this, "age", 18);
};
 
(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);複製代碼
  • 配置 @babel/plugin-transform-runtime

到目前爲止,對於 built-in 類型的語法仍是經過 require("core-js/modules/xxxx") polyfill 的方式來實現的,例如爲了支持 Array.prototype.includes 方法,須要 require

("core-js/modules/es.array.includes") 在 Array.prototype 中添加 includes 方法來實現的,但這會致使一個問題,它是直接修改原型的,會形成全局污染。若是你開發的是獨立的應用問題不大,但若是開發的是工具庫,被其它項目引用,而剛好該項目自身實現了 Array.prototype.includes 方法,這樣就出了大問題!而 @babel/plugin-transform-runtime 能夠解決這個問題,只須要配置 @babel/plugin-transform-runtime 的參數 corejs。該參數默認爲 false,能夠設置爲 2 或者 3,分別對應 @babel/runtime-corejs2 和 @babel/runtime-corejs3。

把 @babel/plugin-transform-runtime 的 corejs 的值設置爲3,把 @babel/runtime 替換爲 @babel/runtime-corejs3。

去掉 @babel/preset-env 的 useBuiltIns 和 corejs 的配置,去掉 core-js。由於使用 @babel/runtime-corejs3 來實現對 built-in 類型語法的兼容,不用再使用 useBuiltIns了。

npm uninstall @babel/runtime
npm install --save @babel/runtime-corejs3
npm uninstall core-js複製代碼
// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      targets: {}
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties',
  [
    '@babel/plugin-transform-runtime',
    {
      corejs: 3
    }
  ]
]
 
module.exports = { presets, plugins }
 
 
// dist/index.js
"use strict";
 
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
 
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
 
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));
 
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
 
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
 
var add = function add(a, b) {
  return a + b;
};
 
var arr = [1, 2];
var hasThreee = (0, _includes["default"])(arr).call(arr, 3);
new _promise["default"](function (resolve) {
  return resolve(10);
});
 
var Person = function Person() {
  (0, _classCallCheck2["default"])(this, Person);
  (0, _defineProperty2["default"])(this, "name", 'morrain');
  (0, _defineProperty2["default"])(this, "age", 18);
};
 
(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);複製代碼
能夠看到 Promise 和 arr.includes 的實現已經變成局部變量,並無修改全局上的實現。

三、Babel polyfill 實現方式的區別

截至目前爲止,對於 built-in 類型的語法的 polyfill,一共有三種方式:

  • 使用 @babel/preset-env ,useBuiltIns 設置爲 'entry'

  • 使用 @babel/preset-env ,useBuiltIns 設置爲 'usage'

  • 使用 @babel/plugin-transform-runtime

前兩種方式支持設置 targets ,能夠根據目標環境來適配。useBuiltIns 設置爲 'entry' 會注入目標環境不支持的全部 built-in 類型語法,useBuiltIns 設置爲 'usage' 會注入目標環境不支持的全部被用到的 built-in 類型語法。注入的 built-in 類型的語法會污染全局。

第三種方式目前不支持設置 targets,因此不會考慮目標環境是否已經支持,它是經過局部變量的方式實現了全部被用到的 built-in 類型語法,不會污染全局。

針對第三種方式不支持設置 targets 的問題,Babel 正在考慮解決,目前意向的方案是經過 Polyfill provider 來統一 polyfill 的實現:

  • 廢棄 @babel/preset-env 中 useBuiltIns 和 corejs 兩個參數,再也不經過 @babel/preset-env 實現 polyfill。

  • 廢棄 @babel/plugin-transform-runtime 中的 corejs 參數,也再也不經過 @babel/plugin-transform-runtime 來實現 polyfill。

  • 增長 polyfills 參數,相似於如今 presets 和 plugins,用來取代如今的 polyfill 方案。

  • 把 @babel/preset-env 中 targets 參數,往上提一層,和 presets、plugins、polyfills 同級別,並由它們共享。

這個方案實現後,Babel 的配置會是下面的樣子:

// babel.config.js
const targets = [
  '>1%'
]
const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties'
]
const polyfills = [
  [
    'corejs3',
    {
      method: 'usage-pure'
    }
  ]
]
 
module.exports = { targets, presets, plugins, polyfills }複製代碼
配置中的 method 值有 'entry-global'、'usage-global'、'usage-pure' 三種。
  • 'entry-global' 等價於 @babel/preset-env 中的 useBuiltIns: 'entry'

  • 'usage-global' 等價於 @babel/preset-env 中的 useBuiltIns: 'usage'

  • 'usage-pure' 等價於 @babel/plugin-transform-runtime 中的 corejs

本文爲了講解方便,都是用 Babel 原生的 @babel/cli 來編譯文件,實際使用中,更多的是結合 webpack、rollup 這樣第三方的工具來使用的。

因此下一節,咱們聊聊打包工具 webpack。

5、參考文獻

  1. 6to5 JavaScript Transpiler Changes Name to Babel

  2. Babel學習系列2-Babel設計,組成

  3. 初學 Babel 工做原理

  4. RFC: Rethink polyfilling story

更多內容敬請關注 vivo 互聯網技術 微信公衆號

注:轉載文章請先與微信號:Labs2020 聯繫。

相關文章
相關標籤/搜索