基于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 和低代码开发的更多可能性,推动技术的持续进步。

相关推荐
Triumphlight24 分钟前
【useMergeState】react开源组件常用——组件受控与非受控
前端·react.js
大莲芒11 小时前
react 15-16-17-18各版本的核心区别、底层原理及演进逻辑的深度解析
javascript·react.js·ecmascript
曹天骄13 小时前
如何在 TypeScript + ESLint 中正确处理 React 未定义问题
javascript·react.js·typescript
刺客_Andy16 小时前
React 第二十三节 useTransition Hook 的使用注意事项详解
前端·react.js
刺客_Andy16 小时前
React 第二十四节 useDeferredValue Hook 的用途以及注意事项详解
前端·react.js
市民中心的蟋蟀17 小时前
第三章:使用Context来共享组件状态
前端·javascript·react.js
Yvette-W19 小时前
【更新中】【React】基础版React + Redux实现教程(Vite + React + Redux + TypeScript)
前端·javascript·react.js·typescript·前端框架·vite·redux
IT、木易19 小时前
React 中React.memo的作用,如何利用它进行组件性能优化?
前端·javascript·react.js
Kellen20 小时前
组件只是闪亮的钩子
前端·react.js·面试
梦兮林夕1 天前
17 国际化与 Next.js 中的国际化实现
前端·react.js·next.js