给网页编辑器添加触发事件和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 支持。这种方法不仅能够满足我们的需求,还能保持代码的简洁性和可维护性。在实际应用中,我们还需要注意安全性、性能和调试等问题,以确保编辑器的稳定运行。

相关推荐
阿鲁冶夫8 分钟前
最佳实践初始化项目公用cli
前端
Json_22 分钟前
实例入门 实例属性
前端·深度学习
Json_22 分钟前
JS中的apply和arguments小练习
前端·javascript·深度学习
云只上37 分钟前
前端界面在线excel编辑器 。node编写post接口获取文件流,使用传参替换表格内容展示、前后端一把梭。
前端·javascript·node.js·excel
Json_41 分钟前
Vue Methods Option 方法选项
前端·vue.js·深度学习
刘 怼怼1 小时前
使用 Vue 重构 RAGFlow 实现聊天功能
前端·vue.js·人工智能·重构
Json_1 小时前
Vue v-bind指令
前端·vue.js·深度学习
姑苏洛言1 小时前
《全民国家安全教育知识竞赛》小程序开发全记录
前端·后端
Json_1 小时前
JS中的冒泡简洁理解
前端·javascript·深度学习
欧雷殿1 小时前
再谈愚蠢的「八股文」面试
前端·人工智能·面试