1. 程式人生 > >你必須要知道的babel二三事

你必須要知道的babel二三事

## 1. 什麼是babel 本文基於的babel版本是7.11.6,本文所有示例[github](https://github.com/Rynxiao/babel-study) > Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. > > Babel是一個工具鏈,主要用於將ECMAScript 2015+程式碼轉換為當前和較老的瀏覽器或環境中的向後相容的JavaScript版本。 ### 1.1 我們能用bebel做什麼? - 針對於新出的ECMAScript標準,部分瀏覽器還不能完全相容,需要將這部分語法轉換為瀏覽器能夠識別的語法。比如有些瀏覽器不能正常解析es6中的箭頭函式,那通過babel轉換後,就能將箭頭函式轉換為瀏覽器能夠“認懂”得語法。 - 針對於一些較老的瀏覽器,比如IE10或者更早之前。對一些最新的內建物件`Promise/Map/Set`,靜態方法`Arrary.from/Object.assign`以及一些例項方法`Array.prototype.includes`,這些新的特性都不存在與這些老版本的瀏覽器中,那麼就需要給這些瀏覽器中的原始方法中新增上這些特性,即所謂的`polyfill`。 - 可以做一些原始碼的轉換,即可以直接使用babel中提供的API對程式碼進行一些分析處理,例如 - ```javascript const filename = 'index.js' const { ast } = babel.transformSync(source, { filename, ast: true, code: false }); const { code, map } = babel.transformFromAstSync(ast, source, { filename, presets: ["minify"], babelrc: false, configFile: false, }); ``` ## 2. 使用babel 下面講到的幾種轉換方式,其實本質上都是一樣的,都是呼叫babel-core中的API來進行直接轉換 ### 2.1 使用babel.transform直接轉換 ```javascript const source = ` const someFun = () => { console.log('hello world'); } `; require("@babel/core").transform(source, { plugins: ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-parameters"], }, result => { console.log(result.code); }); ``` ### 2.1 使用babel-cli babel提供了cli的方式,可以直接讓我們使用命令列的方式來使用babel,具體參照一下做法 ```shell ## install ## 首先需要安裝 @babel/core @babel/cli ## @babel/cli是提供的命令列工具,會內部呼叫@babel/core來進行程式碼轉換 npm install @babel/core @babel/cli --save-dev ## usage npx babel ./cli/index.js ``` 本地安裝完依賴後,就可以使用babel來進行程式碼轉換了,`npx babel [options] files`,babel提供了一些常用的cli命令,可以使用`npx babel --help`來檢視 ```shell > $ npx babel --help ⬡ 12.13.0 [±master ●●●] Usage: babel [options] Options: -f, --filename [filename] The filename to use when reading from stdin. This will be used in source-maps, errors etc. --presets [list] A comma-separated list of preset names. --plugins [list] A comma-separated list of plugin names. --config-file [path] Path to a .babelrc file to use. --env-name [name] The name of the 'env' to use when loading configs and plugins. Defaults to the value of BABEL_ENV, or else NODE_ENV, or else 'development'. ``` 下面是一個簡單的例子,比如有這麼一段原始碼, ```javascript // cli/index.js const arrayFn = (...args) => { return ['babel cli'].concat(args); } arrayFn('I', 'am', 'using'); ``` 執行以下命令:`npx babel ./cli/index.js --out-file ./cli/index.t.js`,結果如下圖: ![babel-cli-1](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914104739024-1827236452.png) 程式碼和原始碼竟然是一模一樣的,為什麼箭頭函式沒有進行轉換呢?這裡就會引入[plugins](https://babeljs.io/docs/en/plugins)以及[preset](https://babeljs.io/docs/en/presets)的概念,這裡暫時不會具體講解,只需要暫時知道,程式碼的轉換需要使用plugin進行。 轉換箭頭函式,我們需要使用到`@babel/plugin-transform-arrow-functions/parameters`,首先安裝完之後,在此執行轉換 ```shell npm install @babel/plugin-transform-arrow-functions @babel/plugin-transform-parameters --save-dev npx babel ./cli/index.js --out-file ./cli/index.t.js --plugins=@babel/plugin-transform-arrow-functions,@babel/plugin-transform-parameters ``` 執行完之後,再看生成的檔案 ![babel-cli-2](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914104803819-1772446174.png) ### 2.3 使用webpack babel-loader來進行轉換 建立webpack.config.js,編寫如下配置 ```javascript // install npm install webpack-cli --save-dev // webpack/webpack.config.js module.exports = { entry: './index.js', output: { filename: 'index.bundle.js' }, module: { rules: [ { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { plugins: ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-parameters"] } } } ] } }; // usage cd webpack npx webpack ``` 可以得到轉換之後的程式碼如下: ![webpack-1](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914104830820-2047813155.png) 可以對比檢視babel-cli的轉換之後的程式碼是一致的。 ### 2.4 使用配置檔案來進行轉換 參看以上三種方式,都必須載入了plugins這個引數選項,尤其是在cli方式中,如果需要載入很多外掛,是非常不便於書寫的,同時,相同的配置也不好移植,比如需要在另外一個專案中同樣使用相同的cli執行,那麼顯然外掛越多,就會越容易出錯。鑑於此,babel提供了config的方式,類似於webpack的cli方式以及config方式。 babel在7.0之後,引入了`babel.config.[extensions]`,在7.0之前,專案都是基於`.babelrc`來進行配置,這裡暫時不會講解它們之間的區別。下面就是一個比較基於上面例子的一個.babelrc檔案。 ```json // .babelrc { "plugins": ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-parameters"] } ``` 我們將這個檔案放置在根目錄下,新建一個`config`的資料夾,從cli目錄中將index.js檔案copy到config目錄下,然後執行`npx babel ./config/index.js --out-file ./config/index.t.js`,完成之後,會發現和cli執行的方式並沒有什麼差別。 ## 3. babel.config.json vs .babelrc babel.config.js是在babel第7版引入的,主要是為了解決babel6中的一些問題,參看[https://babeljs.io/docs/en/config-files#6x-vs-7x-babelrc-loading](https://babeljs.io/docs/en/config-files#6x-vs-7x-babelrc-loading) - .babelrc會在一些情況下,莫名地應用在node_modules中 - .babelrc的配置不能應用在使用符號連結引用進來的檔案 - 在node_modules中的.babelrc會被檢測到,即使它們中的外掛和預設通常沒有安裝,也可能在Babel編譯檔案的版本中無效 ### 3.1 .babelrc在monorepo專案中的一些問題 另外如果只使用.babelrc,在monorepo專案中會遇到一些問題,這得從.babelrc載入的兩條規則有關 - 在向上搜尋配置的過程中,一旦在資料夾中找到了package.json,就會停止搜尋其它配置(babel用package.json檔案來劃定package的範圍) - 這種搜尋行為找到的配置,如`.babelrc`檔案,必須位於babel執行的root目錄下,或者是包含在`babelrcRoots`這個option配置的目錄下,否則找到的配置會直接被忽略 下面我們在之前的例子上進行改造,檔案結構如下: ![babelrc-1](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914104858696-957611226.png) 在mod1資料夾中建立一個package.json檔案,內容為`{}`。現在執行以下程式碼: ```shell npx babel ./config/mod1/index.js -o ./config/mod1/index.t.js ``` 可以發現,index.js沒有編譯,因為在向上查詢的時候,找到了mod1中的package.json,但是在此目錄中並沒有找到`.babelrc`檔案,因此不會編譯。 下面,我們將`.babelrc`檔案移至mod1中,然後再執行上面的命令,這次會編譯成功麼? ![babelrc-2](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914104920267-165380036.png) 答案依舊是不會,因為當前的執行目錄是在src下面,所以在`mod1`目錄中的配置檔案將會被忽略掉。 這裡有兩種方法來解決這個問題: - 進入到mod1目錄中直接執行 `cd ./config/mod1 & npx babel index.js -o index.t.js` - 在執行的root目錄下,新增一個`babel.config.json`檔案,在其中新增`babelrcRoots`將這個目錄新增進去 ![babelrc-3](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914104940154-587978287.png) 然後再執行`npx babel ./config/mod1/index.js -o ./config/mod1/index.t.js`就可以正常編譯了。 正是基於上述的一些問題,babel在7.0.0之後,引入了`babel.config.[json/js/mjs/cjs]`,基於babel.config.json的配置會靈活得多。 ### 3.2 [Project-wide configuration](https://babeljs.io/docs/en/config-files#project-wide-configuration) 一般`babel.config.json`會放置在根目錄下,在執行編譯時,babel會首先去尋找`babel.config.json`檔案,以此來作為整個專案的根配置。 如果在子目錄中不存在.babelrc的配置,那麼在編譯時,會根據根目錄下的配置來進行編譯,比如在config/index.js中新增如下程式碼 ![babel-config-1](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105002417-999426020.png) 執行`npx babel ./config/index -o ./config/index.t.js`後會發現`for..of`這段程式碼會被原樣輸出,因為在config目錄中並沒有針對`for..of`配置外掛。現在在config檔案中新增`.babelrc`,內容如下: ```json { "plugins": [ "@babel/plugin-transform-for-of" ] } ``` 再次執行完,會發現,`for..of`會被babel編譯 ![babel-config-2](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105025558-31604468.png) 說明,如果子資料夾中存在相應的babel配置,那麼編譯項會在根配置上進行擴充套件。 但這點在`monorepo`專案中會有點例外,之前我在mod1檔案家中放置了一個`package.json`檔案: ![babel-config-3](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105047892-700964507.png) 執行下面命令 ```shell npx babel ./config/mod1/index.js -o ./config/mod1/index.t.js ``` 發現`for..of`部分並沒有被babel編譯,這個原因和之前在講bablerc的原因是一樣的,因為執行的根目錄是src,因此在mod1中並不能去載入.babelrc配置,因此只根據根目錄中的配置來執行編譯。想要mod1中的配置也被載入,可以按照相同的方法在`babel.config.json`中配置`babelrcRoots`。 另外如果子檔案家中不存在相應的配置,比如在cli目錄下,在src目錄下執行config/index.js檔案是沒有問題的,但是如果進入cli中,然後直接執行,會發現index.js檔案不會被編譯。由此,你需要告訴babel去找到這個配置,這裡可以使用`rootMode: upward`來使babel向上查詢babel.config.json,並以此作為根目錄。 ```shell cd cli & npx babel ./index.js -o ./index.t.js --root-mode upward ``` ### 3.3 推薦使用場景 - babel.config.json - 你正在使用一個`monorepo`(可以理解為在一個專案中會有多個子工程) - 你希望編譯node_modules以及symobllinked-project中的程式碼 - .babelrc - 你的配置僅適用於專案的單個部分 - 需要在子目錄/檔案中執行一些特定的轉換,比如你可能不希望一些第三方庫被轉碼 - **綜合推薦使用babel.config.json,[Babel itself is using it](https://github.com/babel/babel/blob/master/babel.config.js)** ## 4. [plugins](https://babeljs.io/docs/en/plugins) & [Presets](https://babeljs.io/docs/en/presets) > Babel is a compiler (source code => output code). Like many other compilers it runs in 3 stages: **parsing**, **transforming**, and **printing**. > > Now, out of the box Babel doesn't do anything. It basically acts like `const babel = code => code;` by parsing the code and then generating the same code back out again. You will need to add plugins for Babel to do anything. 沒有plugins,babel將啥事也做不了。 babel提供了豐富的外掛來對不同時期的程式碼進行轉換。例如我們在es6最常使用的箭頭函式,當需要轉化為es5版本時,就用到了[arrow-functions](https://babeljs.io/docs/en/babel-plugin-transform-arrow-functions)這個外掛。 具體的外掛列表,可以檢視[plugins](https://babeljs.io/docs/en/plugins)。 presets的中文翻譯為預設,即為一組外掛列表的集合,我們可以不必再當獨地一個一個地去新增我們需要的外掛。比如我們希望使用es6的所有特性,我們可以使用babel提供的[ES2015](https://babeljs.io/docs/en/plugins#es2015)這個預設。 ### 4.1 基本用法 ```json // 如果plugin已經在釋出到npm中 // npm install @babel/plugin-transform-arrow-functions -D // npm install @babel/preset-react -D { "plugins": ["@babel/plugin-transform-arrow-functions"], "presets": ["@babel/preset-react"] } // 或者按照babel的規範,引入自己編寫的plugin/preset { "plugins": ["path/to/your/plugin"], "presets": ["path/to/your/preset"], } ``` ### 4.2 選項 任何一個外掛都可以擁有自定義的屬性來定義這個外掛的行為。具體的寫法可以為: ```json { "plugins": ["pluginA", ["pluginA"], ["pluginA", {}]], "presets": ["presetA", ["presetA"], ["presetA", {}]] } // example { "plugins": [ [ "@babel/plugin-transform-arrow-functions", { "spec": true } ] ], "presets": [ [ "@babel/preset-react", { "pragma": "dom", // default pragma is React.createElement (only in classic runtime) "pragmaFrag": "DomFrag", // default is React.Fragment (only in classic runtime) "throwIfNamespace": false, // defaults to true "runtime": "classic" // defaults to classic // "importSource": "custom-jsx-library" // defaults to react (only in automatic runtime) } ] ] } ``` ### 4.3 執行順序 - 外掛執行順序在[presets](https://babeljs.io/docs/en/presets)之前 - 外掛會按照宣告的外掛列表順序順序執行(first to last) - preset會按照宣告的列表順序逆序執行(last to first) #### 4.3.1 plugin的執行順序測試 下面我們來做幾個例子測試一下,首先,官方給出的外掛標準寫法如下(**在此之前,強烈建議閱讀[babel-handbook](https://github.com/thejameskyle/babel-handbook)來了解接下來外掛編碼中的一些概念**): ```javascript // 1. babel使用babylon將接受到的程式碼進行解析,得到ast樹,得到一系列的令牌流,例如Identifier就代表一個字 // 符(串)的令牌 // 2. 然後使用babel-traverse對ast樹中的節點進行遍歷,對應於外掛中的vistor,每遍歷一個特定的節點,就會給visitor新增一個標記 // 3. 使用babel-generator對修改過後的ast樹重新生成程式碼 // 下面的這個外掛的主要功能是將字串進行反轉 // plugins/babel-plugin-word-reverse.js module.exports = function() { return { visitor: { Identifier(path) { console.log("word-reverse plugin come in!!!"); const name = path.node.name; path.node.name = name .split("") .reverse() .join(""); }, }, }; } // 然後我們再提供一個外掛,這個外掛主要是修改函式的返回值 // plugins/babel-plugin-replace-return.js module.exports = function({ types: t }) { return { visitor: { ReturnStatement(path) { console.log("replace-return plugin come in!!!"); path.replaceWithMultiple([ t.expressionStatement(t.stringLiteral('Is this the real life?')), t.expressionStatement(t.stringLiteral('Is this just fantasy?')), t.expressionStatement(t.stringLiteral('(Enjoy singing the rest of the song in your head)')), ]); }, }, }; } ``` 首先我們來測試一下原始程式碼是否通過我們自定義的外掛進行轉換了,原始碼如下: ```javascript // plugins/index.js const myPluginTest = (javascript) => { return 'I love Javascript'; } // 然後在plugins目錄下建立一個.babelrc檔案,用於繼承預設的babel.config.json檔案 // plugins/.babelrc { "plugins": ["./babel-plugin-word-reverse", "./babel-plugin-replace-return"] } // usage npx babel ./plugins/index.js -o ./plugins/index.t.js ``` 以下是執行完之後的結果 ![babel-plugin-1](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105109058-247016061.png) 從截圖可以看出,字串被反轉了,以及返回的字串也被替換掉了。 然後我們再來看看執行的順序 ![babel-plugin-2](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105135172-959277465.png) 可以看到,排在外掛列表之前的外掛會在提前執行。 #### 4.3.2 preset的執行順序測試 下面再新建一個外掛,用於自定義的preset編寫 ```javascript // presets/babel-plugin-word-replace.js // 這個外掛主要的功能是給每個節點型別為Identifier的名稱拼接一個_replace的字尾 module.exports = function() { return { visitor: { Identifier(path) { console.log("word-replace plugin come in!!!"); let name = path.node.name; path.node.name = name += '_replace'; }, }, }; } ``` 然後我們藉助之前編寫的`babel-plugin-word-reverse`來編寫兩個新的presets ```javascript // presets/my-preset-1.js module.exports = () => { console.log('preset 1 is executed!!!'); return { plugins: ['../plugins/babel-plugin-word-reverse'] }; }; // presets/my-preset-2.js module.exports = () => { console.log('preset 2 is executed!!!'); return { presets: ["@babel/preset-react"], plugins: ['./babel-plugin-word-replace', '@babel/plugin-transform-modules-commonjs'], }; }; // 建立.babelrc配置 // presets/.babelrc { "presets": [ "./my-preset-1", "./my-preset-2" ] } // 測試程式碼 // presets/index.jsx import React from 'react'; export default () => { const text = 'hello world'; return {text}; } // 執行 npx babel ./presets/index.jsx -o ./presets/index.t.js ``` 可以看到在.babelrc中,將preset-1放在了preset-2的前面,如果按照babel官網給出的解析,那麼preset2會被先執行,執行的順序如下 ![babel-preset-1](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105155853-1161262545.png) 可以看到控制檯列印的順序是preset1 -> preset2,這點與**官網給出的preset執行順序是相反的???** 然後再看編譯之後生成的檔案,發現竟然又是先執行了preset-2中的外掛,然後在執行preset-1中的外掛,如圖: ![babel-preset-2](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105216421-202066421.png) 可以看到顯然是首先經過了新增字尾`_replace`,然後在進行了整體的`reverse`。這裡是不是意味著,在presets列表中後宣告的preset中的外掛會先執行呢??? 懷著這個問題,去啃了下原始碼。**發現babel所說的執行順序,其實是`traverse`訪問外掛中`vistor`的順序**。因為presets其實也是一組外掛的集合,經過程式處理之後,會使得presets末尾的plugins會出現在整個plugins列表的前面。 同時可以看圖中控制檯的列印結果,`word-replace`始終會在`word-reverse`之前,並且是成對出現的。 ```javascript // babel/packages/babel-core/src/transform.js [line 21] const transformRunner = gensync<[string, ?InputOptions], FileResult | null>( function* transform(code, opts) { const config: ResolvedConfig | null = yield* loadConfig(opts); if (config === null) return null; return yield* run(config, code); }, ); ``` `loadConfig(opts)`會被傳遞進來的plugins以及presets進行處理,進去看看發生了什麼? ```javascript // babel/packages/babel-core/src/config/full.js [line 59] export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig( inputOpts: mixed, ): Handler { const result = yield* loadPrivatePartialConfig(inputOpts); // ... const ignored = yield* (function* recurseDescriptors(config, pass) { const plugins: Array = []; for (let i = 0; i < config.plugins.length; i++) { const descriptor = config.plugins[i]; if (descriptor.options !== false) { try { plugins.push(yield* loadPluginDescriptor(descriptor, context)); } catch (e) { // ... } } } const presets: Array<{| preset: ConfigChain | null, pass: Array, |}> = []; for (let i = 0; i < config.presets.length; i++) { const descriptor = config.presets[i]; if (descriptor.options !== false) { try { presets.push({ preset: yield* loadPresetDescriptor(descriptor, context), pass: descriptor.ownPass ? [] : pass, }); } catch (e) { // ... } } } // resolve presets if (presets.length >
0) { // ... for (const { preset, pass } of presets) { if (!preset) return true; const ignored = yield* recurseDescriptors( { plugins: preset.plugins, presets: preset.presets, }, pass, ); // ... } } // resolve plugins if (plugins.length >
0) { pass.unshift(...plugins); } })(//...) } ``` `loadPrivatePartialConfig`中會依次執行我們定義的plugins以及presets,這也是為什麼在上面的例子中preset1會列印在preset2。 ```javascript // babel/packages/babel-core/src/config/config-chain.js [line 629] function mergeChainOpts( target: ConfigChain, { options, plugins, presets }: OptionsAndDescriptors, ): ConfigChain { target.options.push(options); target.plugins.push(...plugins()); target.presets.push(...presets()); return target; } ``` `recurseDescriptors`這裡是一個遞迴函式,是用來在passes中存放解析過後的plugins以及presets的,passes通過unshift的方式解析每次迴圈之後的外掛,因此presets的迴圈越靠後,在passes中的plugins反而會越靠前,這也是為什麼presets列表中的執行順序是逆序的原因。 ```javascript // babel/packages/babel-core/src/config/full.js [line 195] opts.plugins = passes[0]; opts.presets = passes .slice(1) .filter(plugins =>
plugins.length > 0) .map(plugins => ({ plugins })); opts.passPerPreset = opts.presets.length > 0; return { options: opts, passes: passes, }; ``` 設定解析後的`plugins`,然後返回新的config。 ## 5. polyfill >Babel 7.4.0之後,`@babel/polyfill`這個包已經廢棄了,推薦直接是用`core-js/stable`以及`regenerator-runtime/runtime` > >```javascript >import "core-js/stable"; >import "regenerator-runtime/runtime"; >``` `polyfill`的直接翻譯為墊片,是為了新增一些比較老的瀏覽器或者環境中不支援的新特性。比如`Promise/ WeakMap`,又或者一些函式`Array.form/Object.assign`,以及一些例項方法`Array.prototype.includes`等等。 注意:**這些新的特性會直接載入全域性的環境上,在使用時請注意是否會汙染當前的全域性作用域** ### 5.1 基本使用 ```javascript npm install --save @babel/polyfill // commonJs require('@babel/polyfill') // es6 import('@babel/polyfill') ``` 當在webpack中使用時,官方推薦和`@babel/preset-env`一起使用,因為這個preset會根據當前配置的瀏覽器環境自動載入相應的polyfill,而不是全部進行載入,從而達到減小打包體積的目的 ```json // .bablerc { "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", // 'entry/false' "corejs": 3 } ] ] } ``` `useBuiltIns`有三個選項 - **usage** 當使用此選項時,只需要安裝`@babel-polyfill`即可,不需要在webpack中引入,也不需要在入口檔案中引入(require/import) - **entry** 當使用此選項時,安裝完`@babel-polyfill`之後,然後在專案的入口檔案中引入 - **false** 當使用此選項時,需要安裝依賴包,然後加入webpack.config.js的entry中 ```javascript module.exports = { entry: ["@babel/polyfill", "./app/js"], }; ``` 在瀏覽器中使用,可以直接引入`@bable/polyfill`中的`dist/polyfill.js` ```html ``` ### 5.2 示例 通過配合使用`@babel/preset-env`之後,我們可以來看看編譯之後生成了什麼? ```javascript // polyfill/.babelrc { "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", // 其他兩個選項 'entry/false' "corejs": 3 // 如果需要使用includes,需要安裝corejs@3版本 } ] ] } // polyfill/index.js const sym = Symbol(); const promise = Promise.resolve(); const arr = ["arr", "yeah!"]; const check = arr.includes("yeah!"); console.log(arr[Symbol.iterator]()); ``` 編譯之後的結果如下 ![polyfill-1](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105240777-1274003632.png) 可以看到,瀏覽器中缺失的方法、物件都是直接引入的。當你只需要在特定的瀏覽器中做相容時,可以顯式地宣告,使用方式可以參照[browserslist-compatible](https://github.com/ai/browserslist)。 ```json { "targets": "> 0.25%, not dead", // 或者指明特定版本 "targets": { "chrome": "58", "ie": "11" } } ``` ## 6. transform-runtime > A plugin that enables the re-use of Babel's injected helper code to save on codesize. `@babel/plugin-transform-runtime`的主要有三個用處 - 自動引入`@babel/runtime/regenerator`,當你使用了`generator/async`函式(通過`regenerator`選項開啟,預設為true) - 提取一些babel中的工具函式來達到減小打包體積的作用 - 如果開啟了`corejs`選項(預設為false),會自動建立一個沙箱環境,避免和全域性引入的polyfill產生衝突。 這裡說一下第三點,當開發自己的類庫時,建議開啟corejs選項,因為你使用的polyfill可能會和使用者期待的產生衝突。一個簡單的比喻,你開發的類庫是希望相容ie11的,但是使用者的系統是主要基於chorme的,根本就不要去相容ie11的一些功能,如果交給使用者去polyfill,那就的要求使用者也必須要相容ie11,這樣就會引入額外的程式碼來支援程式的執行,這往往是使用者不想看到的。 ### 6.1 基本使用 ```javascript // dev dependence npm install --save-dev @babel/plugin-transform-runtime // production dependence // 因為我們需要在生產環境中使用一些runtime的helpers npm install --save @babel/runtime // .babelrc // 預設配置 { "plugins": [ [ "@babel/plugin-transform-runtime", { "absoluteRuntime": false, "corejs": false, "helpers": true, "regenerator": true, "useESModules": false, "version": "7.0.0-beta.0" } ] ] } ``` ### 6.2 示例 說了這麼多,下面來看一個示例 ```json // transform-runtime/.babelrc { "presets": ["@babel/preset-env"], "plugins": [ [ "@babel/plugin-transform-runtime", { "helpers": false } ] ] } // transform-runtime/index.js const sym = Symbol(); const promise = Promise.resolve(); const arr = ["arr", "yeah!"]; const check = arr.includes("yeah!"); class Person {} new Person(); console.log(arr[Symbol.iterator]()); ``` 這裡暫時關閉了`helpers`,我們來看看編譯之後會是什麼結果 ![transform-runtime-1](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105300211-1770315709.png) 可以看到,編譯之後,將`Person class`生成了一個函式`_classCallCheck`,你可能覺得一個生成這樣的函式也沒什麼特別大的關係,但是如果在多個檔案中都聲明瞭`class`,那就意味著,將會在多個檔案中生成一個這麼一模一樣的工具函式,那麼體積就會變大了。因此,開啟了`helpers`之後,效果又是怎樣的呢? ![transform-runtime-2](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105317801-1927178538.png) 可以看到,需要生成的方法變成了引入的方式,注意引入的庫是`@babel-runtime` 下面來試試開啟了`corejs`選項之後生成的檔案是啥樣的? ![transform-runtime-3](https://img2020.cnblogs.com/blog/681618/202009/681618-20200914105338732-2125032674.png) 可以看到所有的工具方式都來自於`@babel/runtime-corejs2`,因為是獨立於polyfill生成的,所以不會汙染全域性環境。 ## 總結 - 推薦使用babel.config.js來作為整個專案的babel配置,.babelrc更加適用於`monorepo`專案 - babel的編譯基本都是依賴於plugin,preset是一組plugin的集合 - polyfill為一些較老的瀏覽器提供一些新特性的支援 - transform-runtime可以提取一些幫助函式來減小打包的體積,在開發自己的類庫是,建議開啟corejs選項 ## 參考連結 - [babeljs.io](https://babeljs.io/docs/en/) - [babel詳解(七)-配置檔案](https://blog.liuyunzhuge.com/2019/09/09/babel詳解(七)-配置檔案/) - [Babel快速上手使用指南](https://juejin.im/post/6844903858632654856)