最近的技术项目里大量用到了须要修改源文件代码的需求,也就理所固然的用到了Babel及其插件开发。这一系列专题咱们介绍下Babel相关的知识及使用。javascript
对于刚开始接触代码编译转换的同窗,单纯的介绍Babel相关的概念只是会当时都能看懂,可是到了本身去实现一个需求的时候就又会变得不知所措,因此咱们再介绍中穿插一些例子。css
大概分为如下几块:html
0、Babel基础介绍
一、使用npm上好用的Babel插件提高开发效率
二、使用Babel作代码转换使用到的模块及执行流程
三、示例:类中插入方法、类方法中插入代码
四、Babel插件开发介绍
五、示例:经过Babel实现打包构建优化 -- 组件模块按需打包java
用到的名词: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使用的引擎是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引入并使用。
在使用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将源码转换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...。
其实整个大的操做流程仍是比较简单的,咱们直接上例子好了。
好比咱们有这样的需求:咱们有一个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树的建立方法,有:
一、使用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-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-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...。
下面咱们看一个具体的示例。
好比,咱们有一个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上,也许能给你的面试加分也说不定哦。