当前市场上主流的低代码解决方案大多基于 Schema 实现渲染功能。开发者能够在低代码平台上通过可视化界面便捷地配置页面与组件。以拖拽组件至画布为例,Schema 会精准记录组件的类型、属性以及层级关系等关键信息。部分低代码平台甚至能够依据当前的 Schema 自动生成对应的代码。
那么,是否可以通过直接修改代码来改变视图的渲染呢?答案是肯定的。我们可以借助 Babel 这一强大的工具,将代码转化为对应的抽象语法树(AST)。随后,依据 AST 中的相关信息,将其处理并转换为我们所需的格式,从而实现对视图的自定义渲染。
话不多说,开干! 首先我们需要一个浏览器端的代码编辑器,此处我使用的是CodeMirrorer, npm地址:www.npmjs.com/package/@ui..., 具体代码如下
js
import { javascript } from '@codemirror/lang-javascript';
import CodeMirrorer from '@uiw/react-codemirror';
import React, { useState } from 'react';
const Demo: React.FC = () => {
const [code, setCode] = useState(`
const Demo=()=>{
return <div style={{color:'red'}}>
<div style={{color:'green'}}> 字体绿色</div>
<div style={{color:'green'}}>字体蓝色</div>
</div>
}
export default Demo`);
return (
<div style={{ display: 'flex' }}>
<CodeMirrorer
value={code}
height="100vh"
width="48vw"
theme="dark"
onChange={(e) => {
setCode(e);
}}
extensions={[javascript({ jsx: true })]}
></CodeMirrorer>
</div>
);
};
export default Demo;
效果如下
接下来进入正题,如何使用babel去解析编辑器中的代码块呢 首先我们需要安装以下几个包 npm i @babel/parser
npm i @babel/traverse
首先我们先将代码转化为ast
js
let ast = parser.parse(code, {
sourceType: 'module',
plugins: [
'jsx',
'flow',
],
});
console.log(ast,'ast')
解析出的ast如图所示
接下来我们需要从当前数据结构中获取我们需要用到的节点数据,这里我使用traverse进行快速查找
js
traverse(ast, {
enter(path) {
if (
path.type === 'JSXElement' &&
path.parent.type == 'ReturnStatement'
) {
console.log(path,'path')
}
},
});
这块便是我们需要的数据结构了 我们需要对拿到的数据进行处理
js
let ast = parser.parse(code, {
sourceType: 'module',
plugins: [
'jsx',
'flow',
],
});
traverse(ast, {
enter(path) {
if (
path.type === 'JSXElement' &&
path.parent.type == 'ReturnStatement'
) {
getJsonItem(path.node)
}
},
});
//处理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') {
res = {
value: item.expression.name,
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 getObjProps = (item: any, obj: Record<string, object | string>) => {
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, {})
}
});
return res;
};
这样我们就可以得到想要的数据
接下来只需要将得到的数据进行渲染就行了,这里我使用的是createElement这个api进行渲染
js
interface itemType{
value:string
type:string
props:Record<string,any>
children?:itemType[]
}
//递归获取子节点
const getChildren = (arr: itemType[]) => {
let res: any[] = []
arr.forEach(item => {
if (item.type === "JSXText") {
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 traverse from '@babel/traverse';
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');
interface itemType {
value: string
type: string
props: Record<string, any>
children?: itemType[]
}
const Demo: React.FC = () => {
const [renderJson, setRenderJson] = useState<any>({ value: null })
const [code, setCode] = useState(`
const Demo=()=>{
return <div style={{color:'red'}}>
<div> demo1</div>
<div>demo2</div>
</div>
}
export default Demo`);
//处理对象属性
const getObjProps = (item: any, obj: Record<string, object | string>) => {
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, {})
}
});
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') {
res = {
value: item.expression.name,
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 === '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 == "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;
在本文中,我们详细介绍了如何基于 Babel 实现一个简易的网页编辑器,并探讨了其在低代码领域的应用潜力,尤其是在实现画布的多形式渲染方面。通过这一实践,我们展示了 Babel 的强大功能以及其在现代 Web 开发中的灵活性和扩展性。
低代码开发正在成为软件开发领域的一个重要趋势,而 Babel 作为 JavaScript 编译器的核心工具,能够为开发者提供强大的支持。我们希望本文的介绍能够为你在低代码开发和 Web 编辑器实现方面提供有价值的参考和启发。
如果你对本文有任何疑问、建议,或者对如何进一步优化和扩展这一编辑器有新的想法,欢迎随时与我们交流。让我们共同探索 Babel 和低代码开发的更多可能性,推动技术的持续进步。