给网页编辑器添加useEffect支持和antd组件库的引入

在上篇文章中,我们成功为编辑器添加了 useState 的支持,这使得我们能够方便地管理组件的状态。然而,仅仅有状态管理是不够的,我们还需要一种机制来处理副作用(side effects),例如 DOM 操作、订阅事件、定时器等。这就是 useEffect 钩子的作用。

实现 useEffect 钩子:让编辑器更具响应性

首先,我们需要通过 Babel 将代码中的 useEffect 钩子识别出来,并建立一个映射(map),将依赖和对应的方法进行映射。这样做的目的是为了在依赖项发生变化时,能够准确地执行对应的副作用操作。

js 复制代码
      const [effectFnMap,setEffectFnMap]=useState<Record<string,Function>>({})
      if(path.node.type==="Identifier"&&path.node.name==="useEffect"){
          //useEffect方法
          const effectFunctionNode=path.container.arguments?.[0]
          //将方法中的useState提到外部
          const effectFunction:Function=eval(changeCode(effectFunctionNode))
          effectFunction?.()
          //useEffect依赖项
          const depencyNode=path.container.arguments?.[1]
          depencyNode?.elements?.forEach(item=>{
            effectFnMap[item.name]=effectFunction
          })
         }
      setEffectFnMap({...effectFnMap})

接着,我们修改下setSate方法,并创建一个状态值

js 复制代码
  const [stateKey,setStateKey]=useState<string>('')
  const setActState = (name: string, value: any) => {
    state[name] = value
    setStateKey(name)
    setState({ ...state })
  }

最后,我们只需要在页面监听state的变化,根据stateKey去触发effectFnMap中对应的方法即可

js 复制代码
  useEffect(()=>{
    effectFnMap[stateKey]?.()
  },[state])

效果如下

到此,我们就基本实现了编辑器的大致功能,现在我们需要在之前的基础上引入外部组件,来使我们编辑器更加灵活

引入antd组件库,让我们的编辑器具备更强的扩展性和灵活性。

首先,我们需要通过 Babel 将代码中的import识别出来,并建立一个映射(map),将引入的组件和引入的文件进行映射。这样做的目的是为了渲染的时候可以快速找到对应组件

js 复制代码
  const [importMap,setImportMap]= useState<Record<string, string>>({})
  if(path.node.type==="ImportDeclaration"){
          const source=path.node.source
          const specifiers=path.node.specifiers||[]
          specifiers.forEach(item=>{
            importMap[item?.imported?.name]=source
          }) 
        }
   setImportMap({...importMap})

这时,我们只需要将渲染时对应的组件进行替换即可

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

  const getItemValue = (value: string, item) => {
    if (importMap[value]) {
      return eval(importMap[value] + '.' + value)
    } else {
      return value
    }
  }

效果如下

完整代码如下

js 复制代码
import generate from '@babel/generator';
import traverse from '@babel/traverse';

import { javascript } from '@codemirror/lang-javascript';
import CodeMirrorer from '@uiw/react-codemirror';
import { Button } from 'antd';
import React, { useEffect, useState } from 'react';
const parser = require('@babel/parser');
const antd = await import('antd')
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 })
  const [state, setState] = useState<Record<string, any>>({})
  const [stateKey, setStateKey] = useState<string>('')
  const [effectFnMap, setEffectFnMap] = useState<Record<string, Function>>({})
  const [importMap, setImportMap] = useState<Record<string, string>>({})

  let stateNameMap: Record<string, boolean> = {}
  let stateMap: Record<string, string> = {}


  const [code, setCode] = useState(`
import React, { useState } from 'react';
import { Button, Modal, Space } from 'antd';
const App = () => {
  const [open, setOpen] = useState(false);
    useEffect(()=>{
     console.log(open,'open')
  },[open])
  return (
    <div>
      <Space>
        <Button type="primary" onClick={()=>setOpen(true)}>
          Open Modal
        </Button>
      </Space>
      <Modal
        open={open}
        title="Title"
        onOk={()=>{
        setOpen(false)
        }}
        onCancel={()=>setOpen(false)}
    
      >
        <p>Some contents...</p>
        <p>Some contents...</p>
        <p>Some contents...</p>
        <p>Some contents...</p>
        <p>Some contents...</p>
      </Modal>
    </div>
  );
};
export default App;`)

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

  useEffect(() => {
    effectFnMap[stateKey]?.()
  }, [state])

  const changeCode = (item: any) => {
    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 generate(ast).code
  }

  //处理对象属性
  const getObjProps = (item: any, obj: Record<string, object | string>) => {
    if (item.type === "ArrowFunctionExpression") {
      return eval(changeCode(item))
    } else {
      item.properties?.forEach(itm => {
        const keyName = itm.key.name
        if (itm.value.type == 'StringLiteral' || itm.value.type === "NumericLiteral" || item.value.type == "BooleanLiteral") {
          obj[keyName] = {
            value: itm.value.value,
            type: itm.value.type
          }
        } 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" || item.value.type == "BooleanLiteral") {
        res[name] = {
          value: item.value.value,
          type: item.value.type
        }
      } else if (item.value.type == 'JSXExpressionContainer' && item.value.expression.type == "ObjectExpression") {

        res[name] = getObjProps(item.value.expression, {})
      } else {
        //处理代码中的表达式
        if (item.value.expression.type == 'StringLiteral' || item.value.expression.type === "NumericLiteral" || item.value.expression.type == "BooleanLiteral") {
          res[name] = {
            value: item.value.expression.value,
            type: item.value.expression.type
          }
        } else {
          res[name] = {
            value: () => {
              return eval(changeCode(item.value.expression))
            },
            type: 'ExecutionFn'
          }
        }
      }
    });
    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.node.type === "ImportDeclaration") {
          const source = path.node.source
          const specifiers = path.node.specifiers || []
          specifiers.forEach(item => {
            importMap[item?.imported?.name] = source.value
          })
        }

        //处理useEffect触发事件
        if (path.node.type === "Identifier" && path.node.name === "useEffect") {
          //useEffect方法
          const effectFunctionNode = path.container.arguments?.[0]
          //将方法中的useState提到外部
          const effectFunction: Function = eval(changeCode(effectFunctionNode))
          effectFunction?.()
          //useEffect依赖项
          const depencyNode = path.container.arguments?.[1]
          depencyNode?.elements?.forEach(item => {
            effectFnMap[item.name] = effectFunction
          })
        }
        //处理useState
        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 })
          }
        }
        //处理JSX元素
        if (
          path.type === 'JSXElement' &&
          path.parent.type == 'ReturnStatement'
        ) {
          setRenderJson(getJsonItem(path.node))
        }
      },
    });
    setImportMap({ ...importMap })
    setEffectFnMap({ ...effectFnMap })
  };

  //递归获取子节点
  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(getItemValue(item.value, item), getRenderItemProps(item.props), ...getChildren(item?.children || [])))
      }
    });
    return res
  }

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

  const getItemValue = (value: string, item) => {
    if (importMap[value]) {
      return eval(importMap[value] + '.' + value)
    } else {
      return value
    }
  }

  const getRenderItemProps = (props: any) => {
    let propsObj: any = {}
    for (const key in props) {
      if (Object.prototype.hasOwnProperty.call(props, key)) {
        const element = props[key];
        if (element.type == "ExecutionFn") {
          console.log(element, 'element')
          propsObj[key] = element.value()
        } else {
          propsObj[key] = element.value
        }
      }
    }
    console.log(propsObj, 'propsObj')
    return propsObj
  }

  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;

总结

通过前面几章的处理,我们已经基本实现了一个支持 React 的网页编辑器(尽管目前可能还有一些功能尚未完全支持)。那么,这样一个编辑器可以应用在哪些场景呢?

公共组件库的文档网站

我们可以通过它去实现一个公共组件库的文档网站。在这个网站上,开发者可以更快速地看到组件在不同属性配置下的表现,从而更直观地了解每个组件的特性和使用方法。这种可视化的方式使得开发者能够更加快速有效地上手,减少学习成本和开发时间。如antd

低代码转换

开发者也可以通过与低代码结合实现用户即可以拖拽生成页面布局,也可以在已经生成布局的页面进行微调,从而实现更灵活更快捷的低代码平台。

至此,我们已经完成了编辑器的核心功能搭建。后续,我将开始低代码转换部分的实践,进一步拓展其应用场景与价值,为开发者带来更多便捷与高效。让我们继续探索,开启新的技术篇章!

相关推荐
拉不动的猪10 小时前
刷刷题48 (setState常规问答)
前端·react.js·面试
Ada_疯丫头10 小时前
「React」React Router v7 framework qiankun window is not defined
react.js
CreatorRay11 小时前
受控组件和非受控组件的区别
前端·javascript·react.js
来碗螺狮粉13 小时前
CSR mode下基于react+i18next实践国际化多语言解决方案
react.js
Alang13 小时前
记一次错误使用 useEffect 导致电脑差点“报废”
前端·react.js
关山月14 小时前
🌟 正确管理深层嵌套的 React 组件
react.js
lisw0515 小时前
排序算法可视化工具——基于React的交互式应用
算法·react.js·排序算法
__不想说话__15 小时前
面试官问我React Router原理,我掏出了平底锅…
前端·javascript·react.js
DevinJohw15 小时前
为什么我选择[email protected]
react.js·vite
尽-欢19 小时前
以太坊DApp开发脚手架:Scaffold-ETH 2 详细介绍与搭建教程
react.js·typescript·web3·区块链