babel各单元简介&如何写一个babel插件
Babel
babel是怎么工作的?
parse->AST->transform->gengerate
babel应用场景
语法糖的polyfill
代码统一hack
相关概念介绍
babel-polyfill
依赖core-js,提供es*->es3的方法,只转化语法,不转换API(类Promise,WeakMap)
babel-helper
babel-register(0.7.0-beta)
babel的基础配置init
利用pirate对require进行劫持,在hook中进行babel 原理见
同时对babel后的code进行缓存,提高下次babel效率
function compile(code, filename) { ... let cacheKey = `${JSON.stringify(opts)}:${babel.version}`; const env = babel.getEnv(false); if (env) cacheKey += `:${env}`; //读取缓存 根据mtime判断是否需要重新babel if (cache) { const cached = cache[cacheKey]; if (cached && cached.mtime === mtime(filename)) { return cached.code; } } const result = babel.transform(code, { ...opts, sourceMaps: opts.sourceMaps === undefined ? "both" : opts.sourceMaps, ast: false, }); if (cache) { cache[cacheKey] = result; result.mtime = mtime(filename); } if (result.map) { if (Object.keys(maps).length === 0) { installSourceMapSupport(); } maps[filename] = result.map; } return result.code; } //hook中传入ext配置 function hookExtensions(exts) { if (piratesRevert) piratesRevert(); piratesRevert = addHook(compile, { exts, ignoreNodeModules: false }); } //入口函数 export default function register(opts?: Object = {}) { // Clone to avoid mutating the arguments object with the 'delete's below. opts = Object.assign({}, opts); if (opts.extensions) hookExtensions(opts.extensions); if (opts.cache === false && cache) { registerCache.clear(); cache = null; } else if (opts.cache !== false && !cache) { registerCache.load(); cache = registerCache.get(); } ... }
babel-core
提供基础的transform方法
如何写一个babel插件
babel-plugin其实是对code转出的ast进行操作,
准备工具
ast的解构可以类比成一个树状或者json嵌套结构,他的每一层结构都可以叫做一个节点,如下图
babel提供一个visitor的方法,允许我们在里面指定我们想要访问的节点,并且可以在命中该节点时做出自定义的的操作
实例分析
现在我们有一个需要移除整个业务bundle包里所有console.log的需求
1.那我们首先要知道console.log实际在ast是怎样的一个节点结构
形如
console.log('a')
实际ast的展现如下
对于各个节点具体含义,这里不做细讲,可以参考文末的babel手册
2.这里直接贴上代码讲吧
module.exports = function (babel) { const { types: t, template } = babel; const visitor = { //需要访问的节点名 //访问器默认会被注入两个参数 path(类比成dom),state ExpressionStatement(path, state) { const node = path.node; //延当前节点向内部访问,判断是否符合console解析出的ast的特征 const expressionNode = keyPathVisitor(node, ['expression']); const isCallExpression = expressionNode.type === 'CallExpression'; if (isCallExpression) { const objectName = keyPathVisitor(expressionNode, ['callee', 'object', 'name']); const prototypeName = keyPathVisitor(expressionNode, ['callee', 'property', 'name']); if (objectName === 'console' && prototypeName === 'log' && !MAC) { //如果符合上述条件,直接移除该节点 path.remove(); } } } }; return { visitor }; };
3.进阶版:如果我们想在babel-plugin中新增代码呢
差不多有三种方法
A:手动添加节点(很恶心~相信你不会想去了解)
B:先生成ast,直接path.insertBefore
C:使用babel-template
例子: 移除autobind装饰器,并在constructor中自动bind this
注意点
1.因为babel判断是否babel是根据modify time,所以babel插件写完想实时生效,需要给当前的env加上 BABEL_DISABLE_CACHE
//babel-register/cache.js function load() { if (process.env.BABEL_DISABLE_CACHE) return; process.on("exit", save); process.nextTick(save); if (!_fs2.default.existsSync(FILENAME)) return; try { data = JSON.parse(_fs2.default.readFileSync(FILENAME)); } catch (err) { return; } }
2.babel插件写完后发布npm时,记得一定要加上babel-plugin-前缀,因为配置在babelrc中的插件名都会被babel在加载时统一加上babel-plugin前缀,然后在模块系统中去查找
题外话
如何实现给require加上hook
传送门