在上篇文章中,我们成功为编辑器添加了 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
低代码转换
开发者也可以通过与低代码结合实现用户即可以拖拽生成页面布局,也可以在已经生成布局的页面进行微调,从而实现更灵活更快捷的低代码平台。
至此,我们已经完成了编辑器的核心功能搭建。后续,我将开始低代码转换部分的实践,进一步拓展其应用场景与价值,为开发者带来更多便捷与高效。让我们继续探索,开启新的技术篇章!