最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【Babel系列 第三篇】从零带你实现一个Babel插件

    正文概述 掘金(梁月)   2021-01-17   598

    鸽到现在终于想起来了Babel的第三篇,也就是最终章了。这篇主要介绍一下如何开发一个Babel的插件,从头实现一个React的jsx语法转换的插件。

    想看前两篇的内容:

    • Babel基础使用说明
    • Babel进阶使用指南

    实现JSX转换的Babel插件

    我们知道 Reactjsx 实际上是 JavaScript 的扩展,jsx 的格式最终会被转换成 React 提供的 createElement 方法(在v17版本之前,v17之后可以使用babel提供的_jsx,不需要import React),举个例子:

    <div>
      <h1>Hello World</h1>
    </div>
    

    会被转换成如下形式:

    var a = React.createElement(
      'div', 
      null, 
      React.createElement("h1", null, "Hello World")
    );
    

    所以我们JSX转换的插件的目标就是识别出 jsx这种 <> 格式的语法,然后将其转换。现在就让我们开始写起来~

    准备工作

    首先先来创建一个webpack项目,方便检验我们的插件的效果。不想动手的同学,可以直接查看这个我已经写好的项目

    mkdir babel_jsx_transform_demo
    cd ./babel_jsx_transform_demo
    npm init
    npm install -D webpack webpack-cli @babel/core babel-loader
    

    创建好项目之后创建一个webpack.config.js添加如下配置信息:

    const path = require('path')
    
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
      },
      mode: 'none',
      module: {
        rules: [{
          test: /\.js$/,
          use: {
            loader: 'babel-loader',
            options: {
              plugins: []
            }
          }
        }]
      }
    }
    

    非常简单的配置,mode 设置为 none 的原因是方便我们观察产物。

    创建 ./src/index.js 作为我们的入口文件,把我们上面用到的JSX内容写进去

    const App = () => {
    
      return (<div><h1>Hello World</h1></div>)
    }
    
    console.log(App())
    

    这里我们就不写 import React from 'react' 了(否则打包产物会带有React 内部的代码,不方便我们查看)

    准备工作到这里就结束了~

    编写插件

    我们可以通过 AST Explorer 这个工具先看一下 上面的那段JSX对应的AST结构是什么:

    {
      type: 'JSXElement',
      openingElement: {
        type: 'JSXOpeningElement',
        name: {
          type: 'JSXIdentifier',
          name: 'div'
        },
        attributes: [],
        selfClosing: false
      },
      closingElement: {
        type: 'JSXClosingElement',
        name: {
          type: 'JSXIdentifier',
          name: 'div'
        }
      },
      children: [{
        type: 'JSXElement',
        openingElement: {
          type: 'JSXOpeningElement',
          name: {
            type: 'JSXIdentifier',
            name: 'h1'
          },
          attributes: [],
          selfClosing: false
        },
        closingElement: {
          type: 'JSXClosingElement',
          name: {
            type: 'JSXIdentifier',
            name: 'h1'
          }
        },
        children: [{
          type: 'JSXText',
          value: 'Hello World'
        }]
      }]
    }
    

    上面的结构是我删除了大部分JSX AST的属性之后得到的一个结构,基本属性有:

    • type:说明这个节点是一个什么类型的节点,比如 JSXText 就是 JSX内部的文案
    • openingElement:JSX的起始标签的节点结构,如 <div>
    • closingElement:JSX的结束标签的节点结构,如 </div>
    • name: 节点名

    我们的Babel插件实际上就是要遍历AST识别到JSX类型的节点,然后对其进行处理转换成新的节点。 现在让我们来开始写我们的插件:

    Babel插件实际上就是导出一个函数,在项目根目录创建一个文件 ./plugin/jsx_transform.js,导出一个函数。Babel插件基于访问者模式,我们的插件就是给访问者提供一个接口:

    module.exports = function({types: t}) {
    
    }
    

    这里我导出了一个函数,函数的参数是一个对象,我们通过解构的方式拿到其中的types,也就是 @babel/types,关于 babel/types 的强大之处这里就不多介绍了,可以去查看 babel系列的第二篇。函数返回一个具有visitor属性的对象,该对象内部是对各种类型的标签(比如 JSXElement)的处理逻辑,是一个个的函数。我们这个插件要处理的是 jsx,那么当然是要写关于 JSXElement 的处理逻辑了,具体逻辑的含义在代码的注释中:

    module.exports = function ({ types: t }) {
      return {
        visitor: {
          // 处理 JSXElement
          JSXElement(path) {
            // 得到当前 JSX的节点结构
            const node = path.node
    
            // JSXOpeningElement
            const { openingElement } = node
            // 获取这个JSX标签的名字
            const tagName = openingElement.name.name
            // 不考虑 JSX上的props,直接传递null
            const attributes = t.nullLiteral()
    
            // React
            const reactIdentifier = t.identifier("React")
            // createElement
            const createElementIdentifier = t.identifier("createElement")
    
            // React.createElement
            const callee = t.memberExpression(
              reactIdentifier,
              createElementIdentifier
            )
            // 调用React.createElement需要传递的参数
            const args = [t.stringLiteral(tagName), attributes]
            
            // 生成React.createElement('xxx', null, children)
            const callRCExpression = t.callExpression(callee, args)
            callRCExpression.arguments = callRCExpression.arguments.concat(
              path.node.children
            )
    
            // 用生成的createElement结构替换之前的jsx结构
            path.replaceWith(callRCExpression, path.node)
          },
          // 处理 JSXText 节点
          JSXText(path) {
            const nodeText = path.node.value
            // 直接用 string 替换 原来的节点
            path.replaceWith(t.stringLiteral(nodeText), path.node)
          },
        },
      }
    }
    

    这里我偷懒没有处理 JSX props相关的逻辑,所以最终生成的会是 React.createElement(TAGNAME, null, children),这里的第二个参数 null 在真实情况下会是一个 props 的数组结构。

    运行验证

    写好了插件之后,我们就要在bable-loader中引入我们的插件:

    rules: [{
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          plugins: ['./plugin/jsx_transform.js']
        }
      }
    }]
    

    在package.json中添加一条 scripts:

    "scripts": {
      "start": "webpack"
    },
    

    执行 npm run start,发现并不是我们想象中的直接运行,而是报错了,报错内容大致是:

    ERROR in ./src/index.js
    Module build failed (from ./node_modules/babel-loader/lib/index.js):
    SyntaxError: xxx/babel_jsx_plugin_demo/src/index.js: Support for the experimental syntax 'jsx' isn't currently enabled (6:11):
    
      4 | const App = () => {
      5 | 
    > 6 |   return (<div><h1>Hello World</h1></div>)
        |           ^
      7 | }
      8 | 
      9 | console.log(App())
    
    Add @babel/preset-react (https://git.io/JfeDR) to the 'presets' section of your Babel config to enable transformation.
    If you want to leave it as-is, add @babel/plugin-syntax-jsx (https://git.io/vb4yA) to the 'plugins' section to enable parsing.
    

    上面的内容大致就是不能解析jsx语法,所以我们还需要安装一个 plugin 让webpack能解析 jsx 语法,运行 npm i -D @babel/plugin-syntax-jsx,安装这个插件,然后更改我们的 webpack.config.js

    rules: [{
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          plugins: ['@babel/plugin-syntax-jsx' ,'./plugin/jsx_transform.js']
        }
      }
    }]
    

    再次运行 npm run start,可以看到 build 成功,内容已经输出到 dist/bundle.js中,文件内容如下:

    /******/ (() => { // webpackBootstrap
    const App = () => {
      return React.createElement("div", null, React.createElement("h1", null, "Hello World"));
    };
    
    console.log(App());
    /******/ })()
    ;
    

    可以看到我们的jsx已经转换成功了~

    总结

    本文简单的从0到1实现了一个 jsx 的转换插件,虽然功能不是很完善(不支持 props处理、不支持自定义Components的处理),真实的jsx转换插件要比这个复杂的多,感兴趣的同学可以查看babel官方的转换插件:@babel/plugin-transform-react-jsx

    本文代码以上传至Github

    还想了解 babel插件开发的姿势,推荐阅读这篇文章


    起源地下载网 » 【Babel系列 第三篇】从零带你实现一个Babel插件

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元