Babel从入门到插件开辟

近来的手艺项目里大批用到了须要修正源文件代码的需求,也就天经地义的用到了Babel及其插件开辟。这一系列专题我们引见下Babel相干的学问及运用。

关于刚开始打仗代码编译转换的同砚,纯真的引见Babel相干的观点只是会当时都能看懂,然则到了自身去完成一个需求的时刻就又会变得手足无措,所以我们再引见中交叉一些例子。

或许分为以下几块:

0、Babel基础引见
1、运用npm上好用的Babel插件提拔开辟效力
2、运用Babel做代码转换运用到的模块及实行流程
3、示例:类中插进去要领、类要领中插进去代码
4、Babel插件开辟引见
5、示例:经由过程Babel完成打包构建优化 — 组件模块按需打包

0.Babel基础引见

用到的名词:

  • AST:Abstract Syntax Tree, 笼统语法树

  • DI: Dependency Injection, 依靠注入

我们在现实的开辟过程当中,常常有须要修正js源代码的需求,比方一下几种情况:

  • ES6/7转化为浏览器可支撑的ES5以至ES3代码;

  • JSX代码转化为js代码(本来是Facebook团队支撑在浏览器中实行转换,如今转到在babel插件中保护);

  • 部份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引入并运用。

1、运用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,发起在你的项目中到场这个插件并用起来,不必再困难的誊写三元运算符,会大大提拔你的开辟效力。

2、运用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…

实在悉数大的操纵流程照样比较简朴的,我们直接上例子好了。

3、示例

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树的建立要领,有:
1、运用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一样的原理。

4、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…

下面我们看一个详细的示例。

5、示例:经由过程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上,或许能给你的口试加分也说不定哦。

    原文作者:abell123
    原文地址: https://segmentfault.com/a/1190000010107500
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞