给网页编辑器添加触发事件和useState支持

在上篇文章中,我们初步搭建了一个基于 Babel 的简单网页编辑器,它能够实现基本的代码编辑和渲染功能。然而,为了让编辑器更加实用,我们还需要为其添加触发事件的功能,并且让它能够支持 React 的 useState 钩子。接下来,我将详细介绍如何实现这些功能。

一、实现思路

在 JavaScript 中,useState 是 React 提供的一个用于在函数组件中添加状态的钩子。为了让我们的编辑器支持 useState,我们需要对代码进行一些处理。具体来说,我们可以将代码中的 useState 调用放到外层进行初始化处理,然后通过 Babel 对函数代码进行修改,使其能够正确地使用这些状态。最后,我们使用 eval 函数来执行修改后的代码。

这里不使用 new Function 来执行代码的原因是,new Function 创建的函数无法访问外部变量。如果要使用 new Function,我们需要将方法和变量都传递到内部,这会使代码变得复杂且难以维护。相比之下,eval 函数可以直接在当前上下文中执行代码,能够方便地访问外部变量,因此更适合我们的需求。

二、具体实现步骤

首先我们将代码中的state,setState储存起来,并将对应的方法和函数名映射储存,并在外部写上一个大的state存储代码中的状态值

js 复制代码
  var  [state,setState]=useState<Record<string,any>>({})
  let stateNameMap :Record<string,boolean>={}
  let stateMap:Record<string,string>={}
  
  const codeToJson = (code?: string) => {
    let ast = parser.parse(code, {
      sourceType: 'module',
      plugins: [
        'jsx',
        'flow',
      ],
    });


    traverse(ast, {
      enter(path) {
        if(path.type=="VariableDeclarator"){
          if(path.node?.init?.callee?.name=="useState"){
            const callbackName=path?.node?.id?.elements?.[1]?.name
            const stateName=path?.node?.id?.elements?.[0]?.name
            //对应的方法和状态进行映射储存
            stateMap[callbackName]=stateName
            //state中存在的状态名
            stateNameMap[stateName]=true
            //初始化state
            state[stateName]=path?.node?.init?.arguments?.[0]?.value
            setState({...state})
          }
        }
        if (
          path.type === 'JSXElement' &&
          path.parent.type == 'ReturnStatement'
        ) {
          setRenderJson(getJsonItem(path.node))
        }
      },
    });
  };

接着,我们进入getJsonItem这个方法对函数事件进行改写,我们先处理元素中的变量,这里,我们需要用到@babel/generator这个包,它可以将ast直接转换成代码

js 复制代码
npm i @babel/generator
js 复制代码
  //处理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') {
        let code= generate(item)
        console.log(code.code,'修改前')
        let ast = parser.parse(code.code, {
          sourceType: 'module',
          plugins: [
            'js',
            'flow',
          ],
        });
        traverse(ast, {
          enter(path) {
          if(path?.node?.type==="Identifier"&&stateNameMap[path.node.name]){
            //修改元素中的变量
            path.node.name='state.'+path.node.name
            path.skip()
          }}
        });
        console.log(generate(ast).code,'修改前')
        res = {
          value:()=> eval(generate(ast).code),
          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;
  };

效果如下

然后,我们便可以用同样的方法去处理元素上的属性了,我们进入getItemProps这个方法找到对应的类型ArrowFunctionExpression(这里我们先只处理箭头函数),这里我们需要一个多写一个便于修改的方法,去全局的替换掉对应的方法

js 复制代码
  const setActState=(name:string,value:any)=>{
    state[name]=value
    setState({...state})  
  }
    //处理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, {})
      }else{
        
      }
    });
    return res;
  };
  
    //处理对象属性
  const getObjProps = (item: any, obj: Record<string, object | string>) => {
 if(item.type==="ArrowFunctionExpression"){
  let code= generate(item)
  console.log(code.code,'修改前')
  let ast = parser.parse(code.code, {
    sourceType: 'module',
    plugins: [
      'js',
      'flow',
    ],
  });
  traverse(ast, {
    enter(path) {
      //如果是变量且stateNameMap为true则为state中的变量,这里暂时不考虑多层作用域的情况
    if(path?.node?.type==="Identifier"&&stateNameMap[path.node.name]){
      //修改函数中state的值
      path.node.name='state.'+path.node.name
      path.skip()
    }else if(
      //如果是变量且stateNameMap有值则为state中的变量
      path?.node?.type==="Identifier"&&stateMap[path?.node?.name]
    ){
      //创建对应值名称的node节点
      let node={
        "type": "DirectiveLiteral",
        "extra": {
            "rawValue": stateMap[path?.node?.name],
            "raw": `\"${stateMap[path?.node?.name]}\"`,
            "expressionValue":stateMap[path?.node?.name]
        },
        "value":stateMap[path?.node?.name]
    }
      //将对应值名称的node节点插入函数的第一个参数中
      path?.container?.arguments?.unshift(node)
      //修改方法名称
      path.node.name='setActState'
      path.skip()
    }
    }
  });
  console.log(generate(ast).code,'修改后')
  return eval(generate(ast).code)
 }else{
  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
 }

  }
  

效果如下

这样,我们就对代码进行了改写,最后是渲染部分,我们只需要将类型为JSXExpressionContainer作为函数执行即可

js 复制代码
  //递归获取子节点
  const getChildren = (arr: itemType[]) => {
    let res: any[] = []
    arr.forEach(item => {
      if (item.type === "JSXText") {
        res.push(item.value)
      }
      else if (item.type == "JSXExpressionContainer") {
        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 generate from '@babel/generator';
import traverse from '@babel/traverse';
// import types from '@babel/types';
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');
const types = require('@babel/parser');
interface itemType {
  value: string | Function
  type: string
  props: Record<string, any>
  children?: itemType[]
}


const Demo: React.FC = () => {
  const [renderJson, setRenderJson] = useState<any>({ value: null })
  var [state, setState] = useState<Record<string, any>>({})
  let stateNameMap: Record<string, boolean> = {}
  let stateMap: Record<string, string> = {}
  const [code, setCode] = useState(`
      const Demo=()=>{
      const [num,setNum]=useState(0)
      const [num1,setNum1]=useState(0)
 
      return <div style={{color:'red'}}>
      <button onClick={()=>{
      setNum(num+1)}}> {num+1}
      </button>-
      <button onClick={()=>{
      setNum1(num1+1)}}> {num1+1}
      </button>=
      <button> {num-num1}</button>
      </div>
    }
    
    export default Demo`)

  const setActState = (name: string, value: any) => {
    state[name] = value
    setState({ ...state })
  }

  //处理对象属性
  const getObjProps = (item: any, obj: Record<string, object | string>) => {
    if (item.type === "ArrowFunctionExpression") {
      let code = generate(item)
      let ast = parser.parse(code.code, {
        sourceType: 'module',
        plugins: [
          'js',
          'flow',
        ],
      });
      traverse(ast, {
        enter(path) {
          //如果是变量且stateNameMap为true则为state中的变量,这里暂时不考虑多层作用域的情况
          if (path?.node?.type === "Identifier" && stateNameMap[path.node.name]) {
            //修改函数中state的值
            path.node.name = 'state.' + path.node.name
            path.skip()
          } else if (
            //如果是变量且stateNameMap有值则为state中的变量
            path?.node?.type === "Identifier" && stateMap[path?.node?.name]
          ) {
            //创建对应值名称的node节点
            let node = {
              "type": "DirectiveLiteral",
              "extra": {
                "rawValue": stateMap[path?.node?.name],
                "raw": `\"${stateMap[path?.node?.name]}\"`,
                "expressionValue": stateMap[path?.node?.name]
              },
              "value": stateMap[path?.node?.name]
            }
            //将对应值名称的node节点插入函数的第一个参数中
            path?.container?.arguments?.unshift(node)
            //修改方法名称
            path.node.name = 'setActState'
            path.skip()
          }
        }
      });

      return eval(generate(ast).code)
    } else {
      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, {})
      } else {

      }
    });
    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') {
        let code = generate(item)
        let ast = parser.parse(code.code, {
          sourceType: 'module',
          plugins: [
            'js',
            'flow',
          ],
        });
        traverse(ast, {
          enter(path) {
            if (path?.node?.type === "Identifier" && stateNameMap[path.node.name]) {
              //修改元素中的变量
              path.node.name = 'state.' + path.node.name
              path.skip()
            }
          }
        });
        res = {
          value: () => eval(generate(ast).code),
          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 == "VariableDeclarator") {
          if (path.node?.init?.callee?.name == "useState") {
            const callbackName = path?.node?.id?.elements?.[1]?.name
            const stateName = path?.node?.id?.elements?.[0]?.name
            //对应的方法和状态进行映射储存
            stateMap[callbackName] = stateName
            //state中存在的状态名
            stateNameMap[stateName] = true
            //初始化state
            state[stateName] = path?.node?.init?.arguments?.[0]?.value
            setState({ ...state })
          }
        }
        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 == "JSXExpressionContainer") {
        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;

三、总结

通过将 useState 放到外层进行初始化,使用 Babel 对函数代码进行修改,并使用 eval 函数执行代码,我们成功地为简单网页编辑器添加了触发事件和 useState 支持。这种方法不仅能够满足我们的需求,还能保持代码的简洁性和可维护性。在实际应用中,我们还需要注意安全性、性能和调试等问题,以确保编辑器的稳定运行。

相关推荐
Qrun42 分钟前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp43 分钟前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.2 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl4 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫5 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友5 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理7 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻7 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front8 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰8 小时前
纯flex布局来写瀑布流
前端·javascript·css