基于babel做一个简单的react网页编辑器

当前市场上主流的低代码解决方案大多基于 Schema 实现渲染功能。开发者能够在低代码平台上通过可视化界面便捷地配置页面与组件。以拖拽组件至画布为例,Schema 会精准记录组件的类型、属性以及层级关系等关键信息。部分低代码平台甚至能够依据当前的 Schema 自动生成对应的代码。

那么,是否可以通过直接修改代码来改变视图的渲染呢?答案是肯定的。我们可以借助 Babel 这一强大的工具,将代码转化为对应的抽象语法树(AST)。随后,依据 AST 中的相关信息,将其处理并转换为我们所需的格式,从而实现对视图的自定义渲染。

话不多说,开干! 首先我们需要一个浏览器端的代码编辑器,此处我使用的是CodeMirrorer, npm地址:www.npmjs.com/package/@ui..., 具体代码如下

js 复制代码
import { javascript } from '@codemirror/lang-javascript';
import CodeMirrorer from '@uiw/react-codemirror';
import React, { useState } from 'react';


const Demo: React.FC = () => {

  const [code, setCode] = useState(`
const Demo=()=>{
  return <div style={{color:'red'}}>
  <div  style={{color:'green'}}> 字体绿色</div>
  <div style={{color:'green'}}>字体蓝色</div>
  </div>
}

export default Demo`);
  return (
    <div style={{ display: 'flex' }}>
      <CodeMirrorer
        value={code}
        height="100vh"
        width="48vw" 
        theme="dark"
        onChange={(e) => {
          setCode(e);
        }}
        extensions={[javascript({ jsx: true })]}
      ></CodeMirrorer>

    </div>
  );

};

export default Demo;

效果如下

接下来进入正题,如何使用babel去解析编辑器中的代码块呢 首先我们需要安装以下几个包 npm i @babel/parser

npm i @babel/traverse

首先我们先将代码转化为ast

js 复制代码
    let ast = parser.parse(code, {
      sourceType: 'module',
      plugins: [
        'jsx',
        'flow',
      ],
    });

    console.log(ast,'ast')

解析出的ast如图所示

接下来我们需要从当前数据结构中获取我们需要用到的节点数据,这里我使用traverse进行快速查找

js 复制代码
    traverse(ast, {
      enter(path) {
        if (
          path.type === 'JSXElement' &&
          path.parent.type == 'ReturnStatement'
        ) {
          console.log(path,'path')
        }
      },
    });

这块便是我们需要的数据结构了 我们需要对拿到的数据进行处理

js 复制代码
    let ast = parser.parse(code, {
      sourceType: 'module',
      plugins: [
        'jsx',
        'flow',
      ],
    });


    traverse(ast, {
      enter(path) {
        if (
          path.type === 'JSXElement' &&
          path.parent.type == 'ReturnStatement'
        ) {
          getJsonItem(path.node)
        }
      },
    });
    //处理ast相关信息,这里没找到node的类型先用any处理
    const getJsonItem = (item: any) => {
    let res: any;
    if (item.openingElement) {
      res = {
        value: item.openingElement.name.name,
        type: item.type,
        props: getItemProps(item.openingElement.attributes || [], true),
      };
    } else {
      if (item.type == 'JSXText') {
        res = {
          value: item.value,
          type: item.type,
          props: {},
        };
      } else if (item.type == 'JSXExpressionContainer') {
        res = {
          value: item.expression.name,
          type: item.type,
          props: {},
        };
      }
    }

    if (item.children?.length) {
      let resChildren: any = [];
      item.children.forEach((itm: any) => {
        resChildren.push(getJsonItem(itm));
      });
      res.children = resChildren;
    }
    return res;
  };
  
  //处理对象属性
  const getObjProps = (item: any, obj: Record<string, object | string>) => {
    item.properties?.forEach(itm => {
      const keyName = itm.key.name
      if (itm.value.type == 'StringLiteral' || itm.value.type === "NumericLiteral") {
        obj[keyName] = itm.value.value;
      } else if (itm.value.type === "ObjectExpression") {
        obj[keyName] = {}
        getObjProps(itm.value, obj[keyName])
      }

    });
    return obj
  }
  
  //处理dom节点上的属性
  const getItemProps = (attributes: any[], isAttributes: boolean) => {
    let res: any = {};
    attributes.forEach((item: any) => {
      const name = isAttributes ? item.name.name : item.key.value;

      if (item.value.type == 'StringLiteral' || item.value.type === "NumericLiteral") {
        res[name] = item.value.value;
      } else if (item.value.type == 'JSXExpressionContainer') {
        res[name] = getObjProps(item.value.expression, {})
      }
    });
    return res;
  };

这样我们就可以得到想要的数据

接下来只需要将得到的数据进行渲染就行了,这里我使用的是createElement这个api进行渲染

js 复制代码
interface itemType{
  value:string
  type:string
  props:Record<string,any>
  children?:itemType[]
}
  //递归获取子节点
 const getChildren = (arr: itemType[]) => {
    let res: any[] = []
    arr.forEach(item => {
      if (item.type === "JSXText") {
        res.push(item.value)
      }
      else if (
        item.type == "JSXElement"
      ) {
        res.push(React.createElement(item.value, item.props, ...getChildren(item?.children||[])))
      }
    });
    return res
  }

  //渲染当前json到页面
  const jsonToRender = (item:itemType) => {
    return React.createElement(item.value, item.props, ...getChildren(item.children||[]))
  }

这样我们就可以在页面上编辑想要的代码并呈现在页面上了

完整代码如下

js 复制代码
import traverse from '@babel/traverse';
import { javascript } from '@codemirror/lang-javascript';
import CodeMirrorer from '@uiw/react-codemirror';
import { Button } from 'antd';
import React, { useState } from 'react';
const parser = require('@babel/parser');

interface itemType {
  value: string
  type: string
  props: Record<string, any>
  children?: itemType[]
}


const Demo: React.FC = () => {
  const [renderJson, setRenderJson] = useState<any>({ value: null })
  const [code, setCode] = useState(`
    const Demo=()=>{
      return <div style={{color:'red'}}>
      <div> demo1</div>
      <div>demo2</div>
      </div>
    }
    
    export default Demo`);

  //处理对象属性
  const getObjProps = (item: any, obj: Record<string, object | string>) => {
    item.properties?.forEach(itm => {
      const keyName = itm.key.name
      if (itm.value.type == 'StringLiteral' || itm.value.type === "NumericLiteral") {
        obj[keyName] = itm.value.value;
      } else if (itm.value.type === "ObjectExpression") {
        obj[keyName] = {}
        getObjProps(itm.value, obj[keyName])
      }

    });
    return obj
  }
  //处理dom节点上的属性
  const getItemProps = (attributes: any[], isAttributes: boolean) => {
    let res: any = {};
    attributes.forEach((item: any) => {
      const name = isAttributes ? item.name.name : item.key.value;

      if (item.value.type == 'StringLiteral' || item.value.type === "NumericLiteral") {
        res[name] = item.value.value;
      } else if (item.value.type == 'JSXExpressionContainer') {
        res[name] = getObjProps(item.value.expression, {})
      }
    });
    return res;
  };
  //处理ast相关信息,这里没找到node的类型先用any处理
  const getJsonItem = (item: any) => {
    let res: any;
    if (item.openingElement) {
      res = {
        value: item.openingElement.name.name,
        type: item.type,
        props: getItemProps(item.openingElement.attributes || [], true),
      };
    } else {
      if (item.type == 'JSXText') {
        res = {
          value: item.value,
          type: item.type,
          props: {},
        };
      } else if (item.type == 'JSXExpressionContainer') {
        res = {
          value: item.expression.name,
          type: item.type,
          props: {},
        };
      }
    }

    if (item.children?.length) {
      let resChildren: any = [];
      item.children.forEach((itm: any) => {
        resChildren.push(getJsonItem(itm));
      });
      res.children = resChildren;
    }
    return res;
  };


  const codeToJson = (code?: string) => {
    let ast = parser.parse(code, {
      sourceType: 'module',
      plugins: [
        'jsx',
        'flow',
      ],
    });


    traverse(ast, {
      enter(path) {
        if (
          path.type === 'JSXElement' &&
          path.parent.type == 'ReturnStatement'
        ) {
          setRenderJson(getJsonItem(path.node))
        }
      },
    });
  };

  //递归获取子节点
  const getChildren = (arr: itemType[]) => {
    let res: any[] = []
    arr.forEach(item => {
      if (item.type === "JSXText") {
        res.push(item.value)
      }
      else if (
        item.type == "JSXElement"
      ) {
        res.push(React.createElement(item.value, item.props, ...getChildren(item?.children || [])))
      }
    });
    return res
  }

  //渲染当前json到页面
  const jsonToRender = (item: itemType) => {
    return React.createElement(item.value, item.props, ...getChildren(item.children || []))
  }


  return (
    <div style={{ display: 'flex' }}>
      <CodeMirrorer
        value={code}
        height="100vh"
        width="48vw"
        theme="dark"
        onChange={(e) => {
          setCode(e);
        }}
        extensions={[javascript({ jsx: true })]}
      ></CodeMirrorer>
      <Button
        style={{ margin: '0 20px' }}
        onClick={() => {
          codeToJson(code || '');
        }}
      >
        运行
      </Button>
      <div >
        {renderJson.value && jsonToRender(renderJson)}
      </div>
    </div>
  );

};

export default Demo;

在本文中,我们详细介绍了如何基于 Babel 实现一个简易的网页编辑器,并探讨了其在低代码领域的应用潜力,尤其是在实现画布的多形式渲染方面。通过这一实践,我们展示了 Babel 的强大功能以及其在现代 Web 开发中的灵活性和扩展性。

低代码开发正在成为软件开发领域的一个重要趋势,而 Babel 作为 JavaScript 编译器的核心工具,能够为开发者提供强大的支持。我们希望本文的介绍能够为你在低代码开发和 Web 编辑器实现方面提供有价值的参考和启发。

如果你对本文有任何疑问、建议,或者对如何进一步优化和扩展这一编辑器有新的想法,欢迎随时与我们交流。让我们共同探索 Babel 和低代码开发的更多可能性,推动技术的持续进步。

相关推荐
LaughingZhu16 小时前
智能体经典范式构建:ReAct、Plan-and-Solve 与 Reflection
前端·react.js·前端框架
飞天狗17 小时前
长列表滚动卡顿怎么破:虚拟列表选型 + react-virtuoso 实战调优指南
react.js·性能优化
binbin_5220 小时前
React 井字棋教程:用一个小游戏理解组件和状态
前端·react.js·前端框架
张元清21 小时前
React useObjectUrl Hook:预览文件与 Blob,不留内存泄漏(2026)
javascript·react.js
真的想不出名儿1 天前
Md编辑器整合-react
前端·react.js·编辑器
牛仔只喝牛奶2 天前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js
wear工程师2 天前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
wanghowie2 天前
LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展
前端·react.js·前端框架
csj502 天前
前端基础之《React(11)—state声明式》
前端·react.js
张元清2 天前
React useLocalStorage Hook:SSR 安全的持久化状态(2026)
javascript·react.js