babel 插件开发尝试

发布于 2022-03-09 15:03:21 字数 3742 浏览 1080 评论 0

在阅读本文之前, 您应该已经知识如何安装及使用过babel,并且大概了解过AST是一个什么东西

背景知识

babel是一个用js写的编译器,用于将一种js编译成另外一种js。和大多数编译器一样,babel会先扫描输入的js代码,进行词法分析,生成原始的AST,在此AST基础上进行变换(transform) ,再由新的AST转化为目标代码。
详细的介绍请参考 babel-handbook

例子

这里主要讲述一个具体的例子,使用编译器把一个React 0.14+的stateless component转化为React 0.13 的component。
即把

  (props)=>{
    return <div/>
  }

转化成

  class ID extends React.Component {
    render(){
      const props = this.props;
      return <div/>
    }
  }

主要是有一个0.13版本的旧系统,然后有一些只适配0.14+的库,要让旧代码去适配新的库,于是写了这样一个适配器。

visitor

所谓的stateless component,就是一个返回JSX element的函数,有一个入参表示 props。所以我们的visitor入口都是函数,而函数在js中有三种表示方式,一种是FunctionExpression,一种是FunctionDeclaration,es6新增ArrowFunctionExpression,所以我们在这三种方式中都要运行我们的插件visitor

转化条件

在ES6的语法下,函数体有两种表示方式,一种是用花括号,例如前面的例子,一种不用,即()=><div/>,在这两种情景之下,我们要取出函数最后返回的表达式(expression),如果这个表达式是一个JSXExpression,我们认为这个函数是一个stateless component

  if(t.isBlockStatement(body)){ // 函数有花括号
    fnBody = body.body;
    fnLastBlock = fnBody.slice(-1)[0];
    ifCondition = t.isReturnStatement(fnLastBlock) && t.isJSXElement(fnLastBlock.argument);
  }else{// 函数无花括号,只有箭头函数有这种情况,例如  ()=><div/>
    fnBody = t.returnStatement(body);
    fnLastBlock = body;
    ifCondition = t.isJSXElement(fnLastBlock);
  }

template

在handbook中一开始是说用babel-types去构建一个目标语法树的,本人曾经试过,对于一些简单的例子还好,做过一次之后对于js语法的构成会有较深刻的理解。对于具体应用,推荐用babel-template更加高效。

const ReactClassTemplate = template(`
  class ID extends React.Component {
    render(){
      TPL_PROPS
      TPL_FNBODY
    }
  }
`);

坑1-树的重复遍历

如果认真阅读过handbook,里面也有提过这个问题。具体说来,你的目标代码中的render也是一个函数,也会进入visitor中,于是会不停地重复遍历。正如我们遍历一课树的时候,要标记好节点的访问情况那样。所以我们要设定一个循环的中止条件。在这里我是用代码行数的

let codePosition = 0;
  if(body.loc){
    codePosition = body.loc.start.line; //有代码行数,表明是从文件中读取出来的
  }else{
    return; //无代码行数,表明是template里面的ast
  }
//......  
if(!fnLastBlock || hasVisit[codePosition]) return; //空函数 || 已经访问过
  hasVisit[codePosition] = true;  

坑2-我原来就是 Component

这个错误比较低级,但也会犯,如果一个component原来就是class的形式,那么里面确实有一个函数,这会导致重复两次的AST transform。在这里只能一些语法特性去检测了。

if(  //原来就是React组件
    (t.isCallExpression(parent) &&
    parent.arguments &&
    parent.arguments[0] &&
    parent.arguments[0].object &&
    parent.arguments[0].object.name === 'React')
    ||
    ( node.id && node.id.name === 'render' )
  ){
    return;
  }

总结与迁移

写过babel插件之后,对于js语法的理解更加深刻了。比如在阅读node文档时,看到类似这样的语句while (null !== (chunk = readable.read())) {,以前会觉得很绕,但现在回过头去理解,任何的表达式都能计算(evalute)出一个值,包含assignmentExpression,所以这个语法也是很好理解的。

本插件的写法主要是参考 babel-plugin-import的,在学习完之后也能给它提PR了(#67 #87)。嗯,取之于社区,反馈于社区

本文提及的代码在 https://github.com/p2227/babel-plugin-react-stateless-component

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

lorenzathorton8

文章 0 评论 0

Zero

文章 0 评论 0

萧瑟寒风

文章 0 评论 0

mylayout

文章 0 评论 0

tkewei

文章 0 评论 0

17818769742

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文