企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
&emsp;&emsp;我们组维护的管理后台会接到很多开发需求,每次新开页面,就会到处复制黏贴相关代码。 &emsp;&emsp;并且还会经常性的翻阅文档,先在书签或地址栏输入WIKI地址,然后找到那一份说明文档,再定位到要看的组件位置。 &emsp;&emsp;虽然单人损耗的时间并不是非常多,但还是会打断思路,影响开发的流畅性,当把所有人的时间累加起来,那损耗的时间也很可观。 &emsp;&emsp;为了能提升团队成员的开发效率,就开始构思一套可视化搭建系统。理想状态下,拖动组件,配置交互和样式,页面生成,直接可用。 &emsp;&emsp;但是要完成这套功能,开发成本比较大,现在我想先解决当前的痛点,减少代码复制的频率和快速读取组件文档。 &emsp;&emsp;为此,在构思了好多天后,打算搞一个半吊子的可视化搭建系统。 &emsp;&emsp;所谓半吊子是指搭建完后,点击生成,会在后台创建视图和数据两个脚本文件、自动添加权限、新增菜单栏,不过后续我们还得继续做开发,完善页面功能。 ## 一、界面 &emsp;&emsp;界面分成左右两部分,左边是配置区域,右边空白处是组件的预览区域。 :-: ![](https://img.kancloud.cn/79/77/7977f086bf2c797d2530e8ec402c0315_2681x2089.png =800x) **1)组件区域** &emsp;&emsp;组件区域的第一个下拉框可以选择Ant Design和部分模板组件,选中后,会替换组件地址的链接,点击就能跳转到组件的说明文档。 &emsp;&emsp;第二个下拉框能选择页面中需要的组件,例如图中的提示组件,点击添加后会在右边显示,并且还会提供一个删除图标,目前暂不支持拖动效果。 :-: ![](https://img.kancloud.cn/c7/76/c776d69e94a58daee6c3c9e16c9db8c0_2544x808.png =800x) **2)配置区域** &emsp;&emsp;在配置区域中,可以输入菜单名称、路由、文件目录和权限等信息。 &emsp;&emsp;原先的话,还得手动的在路由和权限两个文件中新增配置项,现在都能自动化了。 &emsp;&emsp;原理就是先用Node分别读取这两份文件,得到一个数组,然后将配置内容塞到此数组中,再将数组序列化写入文件内。 &emsp;&emsp;注意,需求在引入模块(调用require()函数)前删除模块缓存,否则读到的将是之前的文件内容。 ~~~ //权限文件的绝对路径 const absAuthorityPath = pathObj.resolve(__dirname, 'src/utils/authority.ts'); delete require.cache[absAuthorityPath]; //删除模块缓存 const authorities = require(absAuthorityPath); const obj = { id: authority, pid: parent, name: menu, desc: '', routers: currentPath, }; authorities.push(obj); //添加权限 //写入文件 fs.writeFileSync(absAuthorityPath, `module.exports = ${JSON.stringify(authorities, null, 2)}`); ~~~ &emsp;&emsp;fs.writeFileSync()用于同步写入文件。module.exports是Node的模块语法,而export default是ES6语法,Node原生并不支持,好在webpack对于这些模块化语法都支持。 &emsp;&emsp;一旦点击生成文件按钮,在项目重新构建后,左边菜单列表就能出现刚刚配置的菜单名称(例如名称叫菜单测试),并且能够跳转,权限也加好了。 :-: ![](https://img.kancloud.cn/32/13/32136be051cf2db2c869783f3b62a32c_484x476.png =300x) &emsp;&emsp;视图和数据文件也是用Node创建的,在Node项目中写好一份模板字符串(下面是生成视图模板的函数),将可变部分作为参数传入。 ~~~typescript export function setPageTemplate({name, antd, namespace, code='', props, component}) { return `import { connect, Dispatch, ${namespace}ModelState } from "umi"; import { setColumn } from '@/utils/tools'; import { TEMPLATE_MODEL } from '@/utils/constants'; ${antd} // 页面参数类型 interface ${name}Props { dispatch: Dispatch; state: ${namespace}ModelState; } // 全局声明 ${code} const ${name} = ({ dispatch, state }: ${name}Props) => { // dispatch({ type: "xx/xx", payload: {} }); // 状态 // const { } = state; // 通用组件配置 ${props} return <> ${component} </>; }; export default connect((data: {${namespace}: ${namespace}ModelState}) => ({ state: data.${namespace} }))(${name});`; } ~~~ ## 二、配置 &emsp;&emsp;配置是本系统的核心,构思了很久,原先考虑了系统的灵活性,就想直接提供脚本编辑框,自定义逻辑。 &emsp;&emsp;不过出现个问题,那就是我这边目前是用TypeScript语言开发的,那么我在自定义脚本逻辑时,也需要使用TypeScript语法。 &emsp;&emsp;浏览器提供的[eval()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval)函数并不支持TypeScript语法,需要先做转译,网上搜索后,得到了解决方案,下载了TypeScript库后。 &emsp;&emsp;但是却一直报错,在网上也查到了些解决方案([方案一](https://github.com/microsoft/TypeScript/issues/39436),[方案二](https://stackoverflow.com/questions/45153848/evaluate-typescript-from-string)),不过并不适用于我目前的项目环境。 ~~~ ./node_modules/typescript/lib/typescript.js Critical dependency: the request of a dependency is an expression ./node_modules/typescript/lib/typescript.js Critical dependency: the request of a dependency is an expression ./node_modules/typescript/lib/typescript.js Module not found: Can't resolve 'perf_hooks' in 'C:\Users\User\node_modules\typescript\lib' ~~~ &emsp;&emsp;最终决定暂时放弃自定义脚本逻辑,先解决当前痛点,尽快将系统上线。 &emsp;&emsp;期间还遇到个比较隐蔽的bug,如下所示,数组会先调用 toString() 转换成字符串,最终变为 eval("(1, 2)"),所以得到的值是 2。 ~~~ eval(`(${[1,2]})`); //2 ~~~ &emsp;&emsp;还遇到个问题,那就是在用 [JSON.stringify()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) 序列化对象时,若参数是函数,那么就会被过滤掉。 ~~~ JSON.stringify({func:() => {}}); //"{}" ~~~ **1)物料库** &emsp;&emsp;物料库中的组件分为两种,一种是自定义的[后台模板组件](https://github.com/pwstrick/shin-admin/blob/main/docs/template.md),另一种是第三方的[Ant Design 3.X](https://3x.ant.design/docs/react/introduce-cn)组件。 &emsp;&emsp;为了快速搭建页面,选择的组件是前者。这次顺便用TypeScript,再次完善了组件代码的类型声明。 :-: ![](https://img.kancloud.cn/ea/9d/ea9dfe53bfdc66e558a1c2cfa0bcfc57_964x454.png =400x) &emsp;&emsp;后者只是用来文档查询和在模板字符串中拼接引入语句,如下所示。 ~~~typescript `import { ${antds.join(',')} } from 'antd';` ~~~ **2)自定义组件** &emsp;&emsp;自定义组件的声明采用JSON格式,TypeScript声明的类型如下所示。 ~~~typescript interface OptionsType { value: string; label: string; children: Array<{ value: string; label: string; link: string; //链接地址 readonlyProps?: ObjectType; //会影响组件的呈现,并且不能配置的属性 readonlyStrProps?: string; //待拼接的字符串属性 handleProps?: (values:ObjectType) => ObjectType; //在格式化表单数据后,再处理特定的组件属性 handleStrProps?: (values:ObjectType) => string; //拼接无法转换成字符串的属性 props: Array<{ label: string; name: string; params?: ObjectType; control: JSX.Element | ((index: number) => JSX.Element); type?: string; initControl?: (props:any) => JSX.Element; }> }>; } ~~~ &emsp;&emsp;链接地址就是说明文档的地址,在组件的属性中,有一部分是回调函数,而目前已经舍弃了自定义的回调逻辑。 &emsp;&emsp;所以这部分属性要特殊处理(声明在 readonlyProps),不能在界面中输入。 ~~~ readonlyProps: { initPanes: (record: ObjectType): TabPaneType[] => [ { name: "示例", key: "demo", controls: [ { label: '测试组件', control: <>内容</> } ] }, ], }, ~~~ &emsp;&emsp;readonlyStrProps 就是 readonlyProps 对应的字符串格式,该属性还会增加一些其它属性,配上注释,也相当于是份组件文档了。 ~~~typescript readonlyStrProps: `, // 标签栏内容回调函数,参数为 record,当标签栏只有一项时,将不显示菜单 "initPanes": (record: ObjectType): TabPaneType[] => [ { name: "示例", key: "demo", controls: [ { label: '测试组件', control: <>内容</> } ] }, ], // useEffect钩子中的回调函数,参数是 record "effectCallback": (record: ObjectType) => {}`, ~~~ &emsp;&emsp;handleProps() 是一个回调函数,在表单接收到数据后,有些组件需要再做一次特殊的处理。 &emsp;&emsp;例如加些特定属性、数组元素合并成字符串等,从而才能顺利的在预览界面呈现。 ~~~ handleProps: (values:ObjectType) => { //对表单中的值做处理 // 对接口数组做特殊处理,从['api', 'get']转换成api.get values.url && (values.url = values.url.join('.'));// 初始化表单需要的组件 if(values.controls.length === 0) { values.controls = [ { label: "示例", name: "demo", control: <>测试组件</> }, ]; }else { values.originControls = values.controls; //备份组件名称数组 values.controls = values.controls.map((item:string) => getControls(item)); } delete values.controlskeys; //删除冗余属性 return values; }, ~~~ &emsp;&emsp;handleStrProps() 是在输出模板时使用,将那些特殊属性写成字符串形式。 ~~~ handleStrProps: (values:ObjectType):string => { if(values.controls.length === 0) { delete values.originControls; //删除备份数组 delete values.controls; //删除原始属性 return `,"controls": [ { label: "示例", name: "demo", control: <>测试组件</> }, ]`; } // 组件名称数组处理 const newControls = values.originControls.map((item:string) => getStrControls(item)); delete values.originControls; delete values.controls; return `,"controls": [${newControls.join(',')}]`; }, ~~~ &emsp;&emsp;在经过一系列的处理后,将一些字符串代码传递给接口,接口最后拼接成两个文件,输出到指定目录中。 &emsp;&emsp;不过生成的代码,排版有点混乱,每次都还需要手动格式化一下。 ***** > 原文出处: [博客园-Node.js躬行记](https://www.cnblogs.com/strick/category/1688575.html) [知乎专栏-Node.js躬行记](https://zhuanlan.zhihu.com/pwnode) 已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎阅读。 ![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200) 推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。