1.初始化项目 #

mkdir zhufeng-jsx-transformer
cd zhufeng-jsx-transformer
yarn add @babel/core @babel/plugin-syntax-jsx @babel/plugin-transform-react-jsx @babel/types  --dev
yarn add react

2.JSX #

<h1 id="title" key="title" ref="title">hello</h1>

3. AST抽象语法树 #

ast.jpg

3.1 babel工作流 #

ast-compiler-flow.jpg

3.2 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() {}`;
const ast = babel.parse(code);
let indent = 0;
const padding = ()=>" ".repeat(indent);
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);

3.2 旧转换 #

3.2.1 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);

3.2.2 转译结果 #

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"
  }
}

3.3 新转换 #

3.3.1 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);

3.3.2 转译结果 #

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"
    }
}

4.实现插件 #

4.1 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);

4.2 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 args = [];  
  return call(path,"jsx", args);
}
function call(path,name, args) {
  const callee = types.identifier('_jsx');
  const node = types.callExpression(callee, args);
  return node;
}
module.exports = pluginTransformReactJsx;

5.支持属性 #

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 callee = types.identifier('_jsx');
  const node = types.callExpression(callee, args);
  return node;
}
module.exports = pluginTransformReactJsx;

6.引入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){
+  const programPath = path.find(p => p.isProgram());
+  const scope = programPath.scope;
+  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;