JSX转换器
初始化项目
mkdir hs-jsx-transformer
cd hs-jsx-transformer
yarn add @babel/core @babel/plugin-syntax-jsx @babel/plugin-transform-react-jsx @babel/types --dev
yarn add react
JSX
- React使用JSX来描述用户界面
- JSX是一种JavaScript的语法扩展
- repl可以在线转换代码
- astexplorer可以把代码转换成AST树
<h1 id="title" key="title" ref="title">hello</h1>
AST抽象语法树
- 抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构
babel工作流
- Parse(解析) 将源代码转换成抽象语法树,树上有很多的节点
- Transform(转换) 对抽象语法树进行转换
- Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码
babel处理语法树
- babeljs是一个JavaScript编译器
- @babel/core是Babel编译器的核心
- babylon是Babel使用的JavaScript解析器
- @babel/types 用于 AST 节点的 Lodash 式工具库
- @babel/traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
- @babel/generator把AST转成代码
let babel = require('@babel/core');
let types = require('@babel/types'); // 类型相关的库,用来生成新的节点
let traverse = require("@babel/traverse").default; // 遍历语法树
let generate = require("@babel/generator").default; // 从语法树生成代码
const code = `function ast() {}`;
// 通过parse方法把老代码转成老的语法树
const ast = babel.parse(code);
let indent = 0;
const padding = ()=>" ".repeat(indent);
// 遍历到每个节点的时候,会把这个节点对象的对象传给enter方法,离开的时候传给exit方法
traverse(ast, {
enter(path){
console.log(padding()+path.node.type+'进入');
indent+=2;
if(types.isFunctionDeclaration(path.node)){
path.node.id.name = 'newAst';
}
},
exit(path){
indent-=2;
console.log(padding()+path.node.type+'离开');
}
});
const output = generate(ast,{},code);
console.log(output.code);
旧转换
jsx.js
const babel = require("@babel/core");
const sourceCode = `<h1 id="title" key="title" ref="title">hello</h1>`;
const result = babel.transform(sourceCode, {
plugins: [['@babel/plugin-transform-react-jsx',{runtime:'classic'}]]
});
console.log(result.code);
转译结果
let React = require('react');
React.createElement("h1", {
id: "title",
key: "title",
ref: "title"
}, "hello");
console.log(JSON.stringify(element,replacer,2));
function replacer(key,value){
if(!['_owner','_store'].includes(key))
return value;
}
{
"type": "h1",
"key": "title",
"ref": "title",
"props": {
"id": "title",
"children": "hello"
}
}
新转换
React17 起用的是新的转换器
jsx.js
const babel = require("@babel/core");
const sourceCode = `<h1 id="title" key="title" ref="title">hello</h1>`;
const result = babel.transform(sourceCode, {
+ plugins: [['@babel/plugin-transform-react-jsx',{runtime:'automatic'}]]
});
console.log(result.code);
转译结果
let {jsx:_jsx} = require("react/jsx-runtime");
//import { jsx as _jsx } from "react/jsx-runtime";
let element = _jsx("h1", {id: "title",key:"title",ref:"title",children: "hello"}, "title");
console.log(JSON.stringify(element,replacer,2));
function replacer(key,value){
if(!['_owner','_store'].includes(key))
return value;
}
{
"type": "h1",
"key": "title",
"ref": "title",
"props": {
"id": "title",
"children": "hello"
}
}
实现插件
- babel-types文档
- babel插件开发指南
- @babel/plugin-syntax-jsx允许解析JSX
- babel-traverse可用于遍历语法树
- Visitors(访问者)是一个用于 AST 遍历的模式,用于定义某个节点的操作方法
- Paths(路径) 是一个对象,它表示两个节点之间的连接
replaceWith
可以用于替换节点get
用于查找特定类型的子路径find
用于向上查找一个指定条件的路径unshiftContainer
用于把AST节点插入到类似于body这样的数组中
- Scope(作用域)
generateUidIdentifier
会生成一个不会和任何本地定义的变量冲突的标识符
jsx.js
const babel = require("@babel/core");
const pluginTransformReactJsx = require('./plugin-transform-react-jsx');
const sourceCode = `<h1 id="title" key="title" ref="title">hello</h1>`;
const result = babel.transform(sourceCode, {
plugins: [pluginTransformReactJsx]
});
console.log(result.code);
plugin-transform-react-jsx.js
const types = require('@babel/types');
const pluginSyntaxJsx = require('@babel/plugin-syntax-jsx').default; // 让babel支持jsx
const pluginTransformReactJsx = {
inherits:pluginSyntaxJsx,
visitor: {
// AST语法树节点类型名
JSXElement(path) {
// 用新的节点替换当前路径上的老节点
let callExpression = buildJSXElementCall(path);
path.replaceWith(callExpression);
}
}
}
function buildJSXElementCall(path) {
const args = [];
return call(path,"jsx", args);
}
function call(path,name, args) {
// 创建一个_jsx标识符节点
const callee = types.identifier('_jsx');
// 创建一个节点 callee是方法名
const node = types.callExpression(callee, args);
return node;
}
module.exports = pluginTransformReactJsx;
支持属性
plugin-transform-react-jsx.js
const types = require('@babel/types');
const pluginSyntaxJsx = require('@babel/plugin-syntax-jsx').default;
const pluginTransformReactJsx = {
inherits:pluginSyntaxJsx,
visitor: {
JSXElement(path) {
let callExpression = buildJSXElementCall(path);
path.replaceWith(callExpression);
}
}
}
function buildJSXElementCall(path) {
+ const openingPath = path.get("openingElement");
+ const {name} = openingPath.node.name; // h1
+ const tag = types.stringLiteral(name);
+ const args = [tag];
+ let attributes = [];
// 把老的属性节点放到attributes数组里
+ for (const attrPath of openingPath.get("attributes")) {
+ attributes.push(attrPath.node);
+ }
+ const children = buildChildren(path.node);
+ const props = attributes.map(convertAttribute);
+ if (children.length > 0) {
+ props.push(buildChildrenProperty(children));
+ }
+ const attributesObject = types.objectExpression(props);
+ args.push(attributesObject);
return call(path,"jsx", args);
}
+function buildChildren(node) {
+ const elements = [];
+ for (let i = 0; i < node.children.length; i++) {
+ let child = node.children[i];
+ if (types.isJSXText(child)) {
+ elements.push(types.stringLiteral(child.value));
+ }
+ }
+ return elements;
+}
+function buildChildrenProperty(children) {
+ let childrenNode;
+ if (children.length === 1) {
+ childrenNode = children[0];
+ } else if (children.length > 1) {
+ childrenNode = types.arrayExpression(children);
+ } else {
+ return undefined;
+ }
+ return types.objectProperty(types.identifier("children"), childrenNode);
+}
// 把jsx对象映射成objectProperty对象
+function convertAttribute(node) {
+ const value = node.value;
+ node.name.type = "Identifier";
+ return types.objectProperty(node.name, value);
+}
function call(path,name, args) {
const callee = types.identifier('_jsx');
const node = types.callExpression(callee, args);
return node;
}
module.exports = pluginTransformReactJsx;
引入runtime模块
plugin-transform-react-jsx.js
const types = require('@babel/types');
const pluginSyntaxJsx = require('@babel/plugin-syntax-jsx').default;
const pluginTransformReactJsx = {
inherits:pluginSyntaxJsx,
visitor: {
JSXElement(path) {
let callExpression = buildJSXElementCall(path);
path.replaceWith(callExpression);
}
}
}
function buildJSXElementCall(path) {
const openingPath = path.get("openingElement");
const {name} = openingPath.node.name;
const tag = types.stringLiteral(name);
const args = [tag];
let attributes = [];
for (const attrPath of openingPath.get("attributes")) {
attributes.push(attrPath.node);
}
const children = buildChildren(path.node);
const props = attributes.map(convertAttribute);
if (children.length > 0) {
props.push(buildChildrenProperty(children));
}
const attributesObject = types.objectExpression(props);
args.push(attributesObject);
return call(path,"jsx", args);
}
function buildChildren(node) {
const elements = [];
for (let i = 0; i < node.children.length; i++) {
let child = node.children[i];
if (types.isJSXText(child)) {
elements.push(types.stringLiteral(child.value));
}
}
return elements;
}
function buildChildrenProperty(children) {
let childrenNode;
if (children.length === 1) {
childrenNode = children[0];
} else if (children.length > 1) {
childrenNode = types.arrayExpression(children);
} else {
return undefined;
}
return types.objectProperty(types.identifier("children"), childrenNode);
}
function convertAttribute(node) {
const value = node.value;
node.name.type = "Identifier";
return types.objectProperty(node.name, value);
}
function call(path,name, args) {
+ const importedSource = 'react/jsx-runtime';
+ const callee = addImport(path,name,importedSource);
const node = types.callExpression(callee, args);
return node;
}
+function addImport(path,importName,importedSource){
// 从当前路径向上查找,找到Program路径
+ const programPath = path.find(p => p.isProgram());
// 获取作用域
+ const scope = programPath.scope;
// 在当前的program作用域内生成不会与作用域内变量冲突的变量名
+ const localName = scope.generateUidIdentifier(importName);
+ const specifiers = [types.importSpecifier(localName, types.identifier(importName))];
+ let statement = types.importDeclaration(specifiers, types.stringLiteral(importedSource));
+ programPath.unshiftContainer("body", [statement]);
+ return localName;
+}
module.exports = pluginTransformReactJsx;