在上篇文章中,我们初步搭建了一个基于 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 支持。这种方法不仅能够满足我们的需求,还能保持代码的简洁性和可维护性。在实际应用中,我们还需要注意安全性、性能和调试等问题,以确保编辑器的稳定运行。