babel各单元简介&如何写一个babel插件

Babel

babel是怎么工作的?

parse->AST->transform->gengerate

如何编译js->AST

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转换工具

ast转换可视化工具

ast的解构可以类比成一个树状或者json嵌套结构,他的每一层结构都可以叫做一个节点,如下图

babel各单元简介&如何写一个babel插件

babel提供一个visitor的方法,允许我们在里面指定我们想要访问的节点,并且可以在命中该节点时做出自定义的的操作

实例分析

现在我们有一个需要移除整个业务bundle包里所有console.log的需求

1.那我们首先要知道console.log实际在ast是怎样的一个节点结构

形如

console.log('a')

实际ast的展现如下
babel各单元简介&如何写一个babel插件

对于各个节点具体含义,这里不做细讲,可以参考文末的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

传送门

参考文献

Babel插件手册

相关推荐