JSX转换器

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)是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构

ast.jpg

babel工作流

  • Parse(解析) 将源代码转换成抽象语法树,树上有很多的节点
  • Transform(转换) 对抽象语法树进行转换
  • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

image-20240911213437195

babel处理语法树

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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值