必知必會的babel基礎

什麼是babel

babel 是一個前端的代碼轉換工具,目的是爲了讓開發者使用ECMA最新的標準甚至一些在stage階段的提案功能,而不用過多考慮運行環境的兼容性。javascript

babel的核心原理

對代碼進行如下步驟:html

  1. 解析 -- 生成tokens
  2. 轉化 -- 生成ast
  3. 生成【也叫打印】 -- 生成對應的輸出代碼

根據開發者的設定,將高版本的ES語法轉化爲瀏覽器或者node環境支持的ES語法前端

固然,babel相關的工具不少,下面咱們以babel7爲例,邊用邊分析。java

構建項目

若是你想掌握babel的基礎配置,請務必跟上一塊兒操做node

若是你實在不想寫代碼,每一步上都會有一個tag,請務必checkout運行一下對應的代碼webpack

git clone github.com/jinggk/babe…git

初始化項目

運行git checkout step-1 && yarn run build查看當前步驟的結果es6

由於涉及到打包,我選擇了webpack,首先咱們不使用任何babel相關的轉碼,嘗試打包一次看下效果:github

測試代碼以下:web

// src/index.js
const fun = async () => {
    const data = await Promise.resolve(123);
    console.log(data);
};

fun(); // 123
複製代碼

打包後的代碼以下:

eval("const fun = async () => {\n const data = await Promise.resolve(123);\n console.log(data);\n};\n\nfun();\n\n\n//# sourceURL=webpack:///./src/index.js?")
複製代碼

能夠看到默認狀況下,咱們的代碼不會通過任何的轉化,下一步,咱們加入babel

加入babel-loader

運行git checkout step-2 && yarn run build查看當前步驟的結果

使用babel以前,咱們須要一個配置文件,以便於告訴babel要按照什麼配置來轉化代碼,有4種配置的方式:

  1. 在根目錄下建立babel.config.js
  2. 在根目錄下建立.babelrc或者.babelrc.js
  3. 在webpack的loader上用options的方式配置
  4. 在packagejson中加入配置

想了解具體內容的能夠看這裏

我採用了官方推薦的方式,建立babel.config.js以下

module.exports = {
    presets: ['@babel/env'] // env 所包含的插件將支持全部最新的 JavaScript (ES201五、ES2016 等)特性
};
複製代碼

添加babel相關的依賴

yarn add babel-loader @babel/core @babel/preset-env -D
複製代碼

@babel/core 包含了轉換api的全部核心模塊 @babel/preset-env 一組默認的預設插件的集合,所包含的插件將支持全部最新的 JavaScript (ES201五、ES2016 等)特性 babel-loader 經過這個loader來開啓babel對js的做用

修改webpack config,增長如下內容

module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    },
複製代碼

ok,如今咱們從新嘗試打包,能夠看到打包後的結果變爲:

eval("function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }\n\nfunction _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"next\", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"throw\", err); } _next(undefined); }); }; }\n\nvar fun =\n/*#__PURE__*/\nfunction () {\n var _ref = _asyncToGenerator(\n /*#__PURE__*/\n regeneratorRuntime.mark(function _callee() {\n var data;\n return regeneratorRuntime.wrap(function _callee$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n _context.next = 2;\n return Promise.resolve(123);\n\n case 2:\n data = _context.sent;\n console.log(data);\n\n case 4:\n case \"end\":\n return _context.stop();\n }\n }\n }, _callee);\n }));\n\n return function fun() {\n return _ref.apply(this, arguments);\n };\n}();\n\nfun();\n\n//# sourceURL=webpack:///./src/index.js?");
複製代碼

能夠看到裏面加入了不少輔助的函數,好比 asyncGeneratorStep、_asyncToGenerator 等等用於處理async和await的,可是若是你直接運行當前被轉化後的代碼,你會看到一些報錯

ReferenceError: regeneratorRuntime is not defined
複製代碼

這是由於默認的預設組件雖然會幫咱們轉化代碼,可是不會把相關的輔助函數引入,咱們須要經過修改配置的方式來開啓一些額外的功能,修改babel.config.js以下:

解決的方式不止一種,咱們先用最簡單的嘗試

module.exports = {
    presets: [
        [
            '@babel/env',
            {
                useBuiltIns: 'usage'
            }
        ]
    ]
};
複製代碼

@babel/env的參數說明:

  1. targets:配置轉化代碼的目標環境,常見配置有:

    1. String, 好比 "targets": "> 0.25%, not dead"或者"targets": { "chrome": "58", "ie": "11" },瞭解更多選項能夠參考browserslist
    2. esmodules: Boolean,啓用這個選項會致使targets browser被忽略,同時要注意,啓用這個選項,轉化後的代碼在瀏覽器中應該和<script type="module"></script>配合使用
    3. node:指定node的信息,好比current || '8.11.0'
  2. spec: 我的理解就是說轉化的過程會更嚴格和規範,可是付出的代價就是轉化時間會比較長

  3. loose:Boolean,是否啓用「鬆散」模式來轉化,默認是false 舉個例子,定義一個class:

class Person{
    say(){
        console.log(123)
    }
}
複製代碼

正常狀況下,類的原型方法須要經過Object.defineProperty去定義,以保證不可枚舉,因此轉化後的代碼會是:

var _createClass = (function () {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor); // (A)
        }
    }
    return function (Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
})();

var Person = (function () {
    function Person() {
        _classCallCheck(this, Person);
    }
    _createClass(Person, [{
        key: "say",
        value: function say() {
            console.log(123)
        }
    }]);
    return Person;
})();
複製代碼

可是若是啓用了loose模式,代碼就會被轉爲:

var Person = (function () {
    function Person() {
        _classCallCheck(this, Person);
    }
    Person.prototype.say = function toString() { 
        console.log(123)
    };
    return Person;
})();
複製代碼

更像是直接用ES5的形式來模擬類,更詳細的內容能夠查看loose-mode

  1. modules: String,常見的有 "amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false默認是auto,這個應該都明白轉化爲不一樣模塊風格的代碼
  2. useBuiltIns:選項有"entry" | "usage" | false,用來決定babel/preset如何處理代碼中的 polyfills
    1. entry:使用這個選項的時候,咱們須要單獨安裝@babel/polyfill,並在文件中引入,這個選項會啓用一個插件,替換咱們全部import或者require @babel/polyfill的地方,更改成只引入對應文件須要的polyfill依賴,以此來減小代碼體積,去除沒必要要的引入
    2. useage: 使用這個選項,不須要引入polyfill,babel在遇到須要加polyfill的地方自動幫咱們引入依賴,可是要注意他只會進行對應代碼的轉化,而不必定保證轉化過程當中引入的polyfill必定存在,後面咱們還會說
    3. false,不作任何優化,在頂部加入@babel/polyfill的狀況下沒有問題

咱們這裏使用了useage這個選項,嘗試打包,依舊報錯了..

Can't resolve 'core-js/modules/es6.object.to-string'
Can't resolve 'core-js/modules/es6.promise
Can't resolve 'regenerator-runtime/runtime' 
複製代碼

babel7.x 以後把一些依賴所有拆分開了,core-js 和 regenerator-runtime相關的lib函數被放在了@babel/polyfill 這個依賴中,所以咱們須要安裝這個依賴,而且爲了全部的js都能正常使用,咱們須要在js文件的入口去主動require一下這個包以獲取一些工具函數的導入~

yarn add @babel/polyfill 
複製代碼

注意這是一個生產環境須要的依賴,不要放到devDependencies中~

而後修改 src/index.js 在最頂部加上require

require('@babel/polyfill');
const fun = async () => {
    const data = await Promise.resolve(123);
    console.log(data);
};

fun();
複製代碼

如今運行從新打包後的代碼,發現已經能夠正常輸出123了~

去掉polyfill

上面咱們介紹了使用usage的時候,是能夠不須要polyfill的,下面咱們就來去掉polyfill

運行git checkout step-3 && yarn run build查看當前步驟的結果

加入polyfill會致使默認打包的內容把全部的polyfill內容都帶上這其實會形成一些額外的問題

  • 代碼體積過大,由於咱們並不必定會須要全部的polyfill
  • 全局污染,polyfill的不少工具函數都是掛在原型上的,若是是開發一個項目還好,若是是開發一個類庫,極可能會對宿主環境的一些對象形成影響

所以咱們須要一個運行時的插件,能夠作到按需加載須要的工具函數,同時還能夠支持只在使用到的運行的地方能夠獲取到而且不污染全局,這個插件就是@babel/plugin-transform-runtime,同時須要安裝@babel/runtime 這個依賴,@babel/plugin-transform-runtime只適用於開發環境,打包以後真正使用的是從babel/runtime中引入的一些runtime tools,而且在開發環境下有一些transform也是須要藉助babel/runtime來實現,回到項目中,執行下面的命令:

yarn remove @babel/polyfill
yarn add @babel/plugin-transform-runtime -D
yarn add @babel/transform
複製代碼

而後運行build,發現,報錯了,報錯信息以下:

Can't resolve 'core-js/modules/es6.object.to-string'
Can't resolve 'core-js/modules/es6.promise'
複製代碼

爲啥,由於@babel/runtime是不包含一些新的語法和對象的,若是須要對新API和語法的polyfill,則還須要安裝@babel/runtime-corejs2這個依賴,並在plugin-transform-runtime中打開對corejs的使用,運行下面命令:

yarn add @babel/runtime-corejs2
複製代碼

修改babel.config.js

module.exports = {
    presets: [
        [
            '@babel/env',
            {
                useBuiltIns: 'usage'
            }
        ]
    ],
    plugins: [
        [
            '@babel/plugin-transform-runtime',
            {
                corejs: 2
            }
        ]
    ]
};
複製代碼

corejs 經常使用2個選項,false或者2,false會污染全局的屬性,而指定爲2則不會污染全局的屬性

設置爲false,打包中對Promise的使用方式:

./node_modules/core-js/modules/_promise-resolve.js
複製代碼

直接去加載依賴會致使全局的屬性被影響,若是設置爲2,打包後的結果是:

var _Promise = __webpack_require__(/*! ../core-js/promise */ \"./node_modules/@babel/runtime-corejs2/core-js/promise.js\ 複製代碼

是會定義一個當前環境下的變量,以後經過_Promise變量來使用promise,這樣就不會影響全局的一些屬性,這對於類庫的開發是很重要的~

當咱們使用了 @babel/runtime-corejs2 後實際上是能夠去掉@babel/runtime了,可是要確保corejs設置爲2,由此也能夠看出來:

@babel/runtime-corejs2 約等於 @babel/runtime + polyfill

babel-plugin-transform-runtime 的全部參數以下:

  1. corejs,默認false,指定爲數字後,表明使用哪一個版本的core-js包
  2. helpers,默認true,開啓後,對一些工具函數會在代碼頂部,從helper包中導入工具而不是在當前代碼中去定義工具函數
  3. regenerator,默認true,generator是否被轉譯成用regenerator runtime包裝,這樣的話每一個包裝函數影響的都是當前的 regenerator,而不是全局的 regenerator。
  4. useESModules,默認false,若是設置爲true,則不會轉化ES6的import和export等語法,對於明確支持的環境能夠開啓,可是考慮兼容的話仍是不要開啓比較好~

更多關於 babel-plugin-transform-runtime,請移步到官網查看

總結

babel的插件有不少,可是 @babel/env 和 @babel/plugin-transform-runtime 是每個前端開發者都應該瞭解和掌握的插件,對於別的插件,用到的時候去了解和配置一下應該就能夠了

到此,咱們瞭解了babel的一些基礎配置,留有一個小問題,我並無說關於babel的最佳實踐,由於在每一個不一樣的場景和項目中,我以爲都應該是不同的,歡迎留言討論,你認爲的最佳實踐是什麼

幾乎每當有新的標準被制定的同時,都會有對應的babel插件的出現,感謝babel社區,讓咱們能開心的使用新語法。

下一篇,咱們一塊兒手擼一個簡單的babel,擁有詞法分析,語法分析,轉化和生成代碼的功能,從底層原理來了解babel,一塊兒期待吧~ 若是當前內容對你有所幫助,請點個贊吧,很是感謝~

相關文章
相關標籤/搜索