鸽到现在终于想起来了Babel的第三篇,也就是最终章了。这篇主要介绍一下如何开发一个Babel的插件,从头实现一个React的jsx语法转换的插件。
想看前两篇的内容:
- Babel基础使用说明
- Babel进阶使用指南
实现JSX转换的Babel插件
我们知道 React
的 jsx
实际上是 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插件开发的姿势,推荐阅读这篇文章
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!