Babel從入門到插件開發

最近的技術項目裏大量用到了須要修改源文件代碼的需求,也就理所固然的用到了Babel及其插件開發。這一系列專題咱們介紹下Babel相關的知識及使用。javascript

對於剛開始接觸代碼編譯轉換的同窗,單純的介紹Babel相關的概念只是會當時都能看懂,可是到了本身去實現一個需求的時候就又會變得不知所措,因此咱們再介紹中穿插一些例子。css

大概分爲如下幾塊:html

0、Babel基礎介紹
一、使用npm上好用的Babel插件提高開發效率
二、使用Babel作代碼轉換使用到的模塊及執行流程
三、示例:類中插入方法、類方法中插入代碼
四、Babel插件開發介紹
五、示例:經過Babel實現打包構建優化 -- 組件模塊按需打包java

0.Babel基礎介紹

用到的名詞:node

  • AST:Abstract Syntax Tree, 抽象語法樹react

  • DI: Dependency Injection, 依賴注入webpack

咱們在實際的開發過程當中,常常有須要修改js源代碼的需求,好比一下幾種情形:git

  • ES6/7轉化爲瀏覽器可支持的ES5甚至ES3代碼;github

  • JSX代碼轉化爲js代碼(原來是Facebook團隊支持在瀏覽器中執行轉換,如今轉到在babel插件中維護);web

  • 部分js新的特性動態注入(用的比較多的就是babel-plugin-transform-runtime);

  • 一些便利性特性支持,好比:React If/Else/For/Switch等標籤支持;

因而,咱們就須要一款支持動態修改js源代碼的模塊,babel則是用的最多的一個。

Babel的解析引擎

Babel使用的引擎是babylon,babylon並不是由babel團隊本身開發的,而是fork的acorn項目,不過acorn引擎只提供基本的解析ast的能力,遍歷還須要配套的acorn-travesal, 替換節點須要使用acorn-,而這些開發,在Babel的插件體系開發下,變得一體化了。

如何使用

使用方式有不少種:

  • webpack中做爲js(x)文件的loader使用;

  • 單獨在Node代碼中引入使用;

  • 命令行中使用:
    package.json中配置:

"scripts": {

"build": "rimraf lib && babel src --out-dir lib"

}

命令中執行:npm run build。

一般,若是咱們在項目根目錄下配置一個.babelrc文件,其配置規則會被babel引入並使用。

一、使用npm上好用的Babel插件提高開發效率

在使用webpack作打包工具的時候,咱們隊js(x)文件使用的loader一般就是babel-loader,babel只是提供了最基礎的代碼編譯能力,主要用到的一些代碼轉換則是經過插件的方式實現的。在loader中配置插件有兩種方式:presets及plugins,這裏要注意presets配置的也是插件,只是優先級比較高,並且他的執行順序是從左到右的,而plugins的優先級順序則是從右到左的。咱們常常用到的插件會包括:ES6/7轉ES5代碼的babel-plugin-es2015,React jsx代碼轉換的babel-plugin-react,對新的js標準特性有不一樣支持程度的babel-plugin-stage-0等(不一樣階段js標準特性的制定是不同的,babel插件支持程度也就不同,0表示徹底支持),將瀏覽器裏export語法轉換爲common規範exports/module.exports的babel-plugin-add-module-exports,根據運行時動態插入polyfill的babel-plugin-transform-runtime(毫不建議使用babel-polyfill,一股腦將全部polyfill插入,打的包會很大),對Generator進行編譯的babel-plugin-transform-regenerator等。想了解更多的配置能夠參見這篇文章:如何寫好.babelrc?Babel的presets和plugins配置解析(https://excaliburhan.com/post...

若是你是基於徹底組件化(標籤式)的開發模式的話,若是能提供經常使用的控制流標籤如:If/ElseIf/Else/For/Switch/Case等給咱們的話,那麼咱們的開發效率則會大大提高。在這裏我要推薦一款實現了這些標籤的babel插件:jsx-control-statement,建議在你的項目中加入這個插件並用起來,不用再艱難的書寫三元運算符,會大大提高你的開發效率。

二、使用Babel作代碼轉換使用到的模塊及執行流程

Babel將源碼轉換AST以後,經過遍歷AST樹(其實就是一個js對象),對樹作一些修改,而後再將AST轉成code,即成源碼。

將js源碼轉換爲AST用到的模塊叫:babylon,對樹進行遍歷並作修改用到的模塊叫:babel-traverse,將修改後的AST再生成js代碼用到的模塊則是:babel-generator。而babel-core模塊則是將三者結合使得對外提供的API作了一個簡化,使用babel-core只須要執行如下的簡單代碼便可:

import { transform } from 'babel-core';
var result = babel.transform("code();", options);
result.code;
result.map;
result.ast;

咱們在Node中使用的時候通常都是使用的三步轉換的方式,方便作更多的配置及操做。因此整個的難點主要就在對AST的操做上,爲了能對AST作一些操做後進而能對js代碼作到修改,babel對js代碼語法提供了各類類型,好比:箭頭函數類型ArrowFunctionExpression,for循環裏的continue語句類型:ContinueStatement等等,咱們主要就是根據這些不一樣的語法類型來對AST作操做(生成/替換/增長/刪除節點),具體有哪些類型所有在:babel-types(https://www.npmjs.com/package...

其實整個大的操做流程仍是比較簡單的,咱們直接上例子好了。

三、示例

Babel使用案例0:往類中插入方法

好比咱們有這樣的需求:咱們有一個jsx代碼模板,該模板中有一個相似與下面的組件類:

class MyComponent extends React.Component {
    constructor(props, context) {
        super(props, context);
    }

    // 其餘代碼
}

咱們會須要根據當前的DSL生成對應的render方法並插入進MyComponent組件類中,該如何實現呢?

上面已經講到,咱們對代碼的操做實際上是經過對代碼生成的AST操做生成一個新的AST來完成的,而對AST的操做則是經過babel-traverse這個庫來實現的。

該庫經過簡單的hooks函數的方式,給咱們提供了在遍歷AST時能夠操做當前被遍歷到的節點的相關操做,要獲取並修改(增刪改查)當前節點,咱們須要知道AST都有哪些節點類型,而全部的節點類型都存放於babel-types這個庫中。咱們先看完整的實現代碼,而後再分析:

// 先引入相關的模塊
const babylon = require('babylon');
const Traverse = require('babel-traverse').default;
const generator = require('babel-generator').default;
const Types = require('babel-types');
const babel = require('babel-core');

// === helpers ===

// 將js代碼編譯成AST
 function parse2AST(code) {
    return babylon.parse(code, {
        sourceType: 'module',
        plugins: [
            'asyncFunctions',
            'classConstructorCall',
            'jsx',
            'flow',
            'trailingFunctionCommas',
            'doExpressions',
            'objectRestSpread',
            'decorators',
            'classProperties',
            'exportExtensions',
            'exponentiationOperator',
            'asyncGenerators',
            'functionBind',
            'functionSent'
        ]
    });
}

// 直接將一小段js經過babel.template生成對應的AST
function getTemplateAst(tpl, opts = {}) {
    let ast = babel.template(tpl, opts)({});

    if (Array.isArray(ast)) {
        return ast;
    } else {
        return [ast];
    }
}

/**
 *  檢測傳入參數是否已在插入代碼中定義
 */
checkParams = function(argv, newAst) {
    let params = [];
    const vals = getAstVals(newAst);
    if (argv && argv.length !== 0) {
        for (let i = 0; i < argv.length; i++) {
            if (vals.indexOf(argv[i]) === -1) {
                params.push(Types.identifier(argv[i]));
            } else {
                throw TypeError('參數名' + argv[i] + '已在插入代碼中定義,請改名');
            }
        }
    }
    return params;
}

const code = `
    class MyComponent extends React.Component {
        constructor(props, context) {
            super(props, context);
        }

        // 其餘代碼
    }
`;

const insert = [
    {
        // name爲方法名
        name: 'render',
        // body爲方法體
        body: `
            return (
                <div>我是render方法的返回內容</div>
            );
        `,
        // 方法參數
        argv: null,
        // 若是原來的Class有同名方法則強制覆蓋
        isCover: true
    }
];

const ast = parse2AST(code);

Traverse(ast, {
    // ClassBody表示當前類自己節點
    ClassBody(path) {
        if (!Array.isArray(insert)) {
            throw TypeError('插入字段類型必須爲數組');
        }

        for (let key in insert) {
            const methodObj = insert[key],
                name = methodObj.name,
                argv = methodObj.argv,
                body = methodObj.body,
                isCover = methodObj.isCover;

            if (typeof name !== 'string') {
                throw TypeError('方法名必須爲字符串');
            }

            const newAst = getTemplateAst(body, {
                sourceType: "script"
            });
            
            const params = checkParams(argv, newAst);
            
            // 經過Types.ClassMethodAPI,生成方法AST
            const property = Types.ClassMethod('method', Types.identifier(name), params, Types.BlockStatement(newAst));

            // 插入進AST
            path.node.body.push(property);
        }
    }
});

console.log(generator(ast).code);

其中,最核心的地方就是下面的這一行代碼:

const property = Types.ClassMethod('method', Types.identifier(name), params, Types.BlockStatement(newAst));

肯定好咱們要進行怎麼樣的操做(好比要往一個類中插入一個方法),休閒要肯定是怎樣的鉤子名(這裏是ClassBody),而後經過要插入的代碼生成對應的AST,生成AST能夠經過Babel.Types的相關方法一點點生成,可是這裏有個比較方便的API:babel.template,而後經過path的相關操做將新生成的AST插入便可。

穿插:AST樹的建立方法

一些AST樹的建立方法,有:
一、使用babel-types定義的建立方法建立
好比建立一個var a = 1;

types.VariableDeclaration(
     'var',
     [
        types.VariableDeclarator(
                types.Identifier('a'), 
                types.NumericLiteral(1)
        )
     ]
)

若是使用這樣建立一個ast節點,確定要累死了,能夠:

  • 使用replaceWithSourceString方法建立替換

  • 使用template方法來建立AST結點

  • template方法其實也是babel體系中的一部分,它容許使用一些模板來建立ast節點

好比上面的var a = 1可使用:

var gen = babel.template(`var NAME = VALUE;`);
 
var ast = gen({
    NAME: t.Identifier('a'), 
    VALUE: t.NumberLiteral(1)
});

也能夠簡單寫:

var gen = babel.template(`var a = 1;`);
 
var ast = gen({});

Babel使用案例1:往類的方法中插入代碼

這個案例會更復雜一點,你們能夠先試着去實現下,明天再講解具體實現。

往方法中要插入代碼,咱們先找下類中方法的babel-types值是什麼,查閱文檔:https://www.npmjs.com/package...能夠發現是叫:ClassMethod。因而就能夠像下面這樣實現:

const injectCode = [{
    name: 'constructor',
    code: insertCodeNext,
}];

const ast = parse2AST(originCode);
Traverse(ast, {
    ClassMethod(path) {
        if (!Array.isArray(injectCode)) {
            throw TypeError('插入字段類型必須爲數組');
        }

        // 獲取當前方法的名字
        const methodName = path.get('body').container.key.name;

        for (let key in injectCode) {
            const inject = injectCode[key],
                name = inject.name,
                code = inject.code,
                pos = inject.pos;

            if (methodName === name) {
                const newAst = getTemplateAst(code, {
                    sourceType: "script"
                });

                if (pos === 'prev') {
                    Array.prototype.unshift.apply(path.node.body.body, newAst);
                } else {
                    Array.prototype.push.apply(path.node.body.body, newAst);
                }
            }
        }
    }
});

console.log(generator(ast).code);

其實跟往Class中插入method同樣的道理。

四、Babel插件開發介紹

Babel的插件就是一個帶有babel參數的函數,該函數返回相似於babel-traverse的配置對象,即下面的格式:

module.exports = function(babel) {
    var t = babel.types;

    return {
        visitor: {
            ImportDeclaration(path, ref) {
                var opts = ref.opts; // 配置的參數
            }
        }
    };
};

在babel插件的時候,配置的參數就會存放在ref參數裏,見上面的代碼所所示。具體能夠參見babel插件手冊:https://github.com/thejamesky...

下面咱們看一個具體的示例。

五、示例:經過Babel實現打包構建優化 -- 組件模塊按需打包

需求

好比,咱們有一個UI組件庫,在入口文件中會把全部的組件放在這裏,並export出對外服務,大概相似於以下的代碼:

export Button from './lib/button/index.js';
export Input from './lib/input/index.js';
// ......

那麼咱們在使用的時候就能夠以下引用:

import {Button} from 'ant'

這樣就有一個問題,就是好比咱們只是用了一個Button組件,這樣引用就會致使會把全部的組件打包進來,致使整個js文件會很是大。咱們能不能把代碼動態實時的編譯成以下的代碼來解決這個問題?

import Button from 'ant/lib/button';

咱們能夠寫個babel插件來實現這樣的需求。

// 入口文件
var extend = require('extend');
var astExec = require('./ast-transform');

// 一些個變量預設
var NEXT_MODULE_NAME = 'ant';
var NEXT_LIB_NAME = 'lib';
var MEXT_LIB_NAME = 'lib';

module.exports = function(babel) {
    var t = babel.types;

    return {
        visitor: {
            ImportDeclaration: function ImportDeclaration(path, _ref) {
                var opts = _ref.opts;
                var next = opts.next || {};

                var nextJsName = next.nextJsName || NEXT_MODULE_NAME;
                var nextCssName = next.nextCssName || NEXT_MODULE_NAME;
                var nextDir = next.dir || NEXT_LIB_NAME;
                var nextHasStyle = next.hasStyle;

                var node = path.node;

                var baseOptions = {
                    node: node,
                    path: path,
                    t: t,
                    jsBase: '',
                    cssBase: '',
                    hasStyle: false
                };

                if (!node) {
                    return;
                }

                var jsBase;
                var cssBase;

                if (node.source.value === nextJsName) {
                    jsBase = nextJsName + '/' + nextDir + '/';
                    cssBase = nextCssName + '/' + nextDir + '/';

                    astExec(extend(baseOptions, {
                        jsBase: jsBase,
                        cssBase: cssBase,
                        hasStyle: nextHasStyle
                    }));
                }
            }
        }
    };
};

這裏將部分的功能單獨放到了一個ast-transform文件中,代碼以下:

function transformName(name) {
    if (!name)
        return '';
    return name.replace(/[A-Z]/g, function(ch, index) {
        if (index === 0)
            return ch.toLowerCase();
        return '-' + ch.toLowerCase();
    });
}

module.exports = function astExec(options) {
    var node = options.node; // 當前節點
    var path = options.path; // path輔助處理變量
    var t = options.t; // babel-types
    var jsBase = options.jsBase;
    var cssBase = options.cssBase;
    var hasStyle = options.hasStyle;

    node.specifiers.forEach(specifier => {
        if (t.isImportSpecifier(specifier)) {
            var comName = specifier.imported.name;
            var lcomName = transformName(comName);
            var libName = jsBase + lcomName;
            var libCssName = cssBase + lcomName + '/index.scss';

            // AST節點操做
            path.insertAfter(t.importDeclaration([t.ImportDefaultSpecifier(t.identifier(comName))], t.stringLiteral(libName)));

            if (hasStyle) {
                path.insertAfter(t.importDeclaration([], t.stringLiteral(libCssName)));
            }
        }
    });

    // 把原來的代碼刪除掉
    path.remove();
};

這樣咱們在用的時候就能夠像下面這樣使用:
.babelrc文件中像下面這樣配置便可:

{
  "presets": [...], // babel-preset-react等
  "plugins" :[
    [
      'armor-fusion',
      {
          next: {
              jsName: 'ant', //js庫名,默認值:ant
              cssName: 'ant', //css庫名,當若是其餘的主題包時,能夠換成別的主題包名,默認值:ant
              dir: 'lib', //目錄名,通常不須要設置,默認值:lib
              hasStyle: true //會編譯出scss引用,不加則默認不會編譯
          }
      }
    ]
  ]
}

你們能夠把上面比較實用的插件功能整理下放到本身的github上,也許能給你的面試加分也說不定哦。

相關文章
相關標籤/搜索