从零实现一套低代码(保姆级教程)【后端服务】 --- 【18】实现页面接口对应的前端
摘要
在上一篇中,我们已经把和页面相关的接口完成的差不多了。从创建页面,更新页面等等:
有了接口之后,我们就可以构建前端页面了。那这部分前端内容我们应该写在哪里呢?
有两种方式:
- 直接写在我们的XinBuilder项目里面,然后通过前端路由拆分成两个路由
- 在创建一个项目,然后打包到后端服务中,也就是通过后端路由去控制
因为我不确定这个项目后面会有多少代码,虽然我们目前只是想实现页面的管理功能,但是后面我也不知道会增加到多少。
所以我准备使用两个React项目,和页面相关的这些功能我都会写在新的项目里,
1.创建项目
首先就是创建项目了,我们使用create-react-app创建一个项目:
> npx create-react-app app-builder --template typescript
然后再安装antD
npm install antd --save
然后把项目里没有用的文件删一删:
最后,因为我们要请求我们写好的接口,在安装一下axios。
npm install axios --save
2.路由的配置
对于这个项目,我们现在只准备完成和pageJson相关的。但是后面可能会有其他的页面,所以我们是需要路由的。
我们就先安装一下react-router-dom,然后使用路由来管理前端的页面。
npm install react-router-dom --save
对于路由,我们在src下新建一个routes用来管理所有的路由页面。
page文件夹就是代表和pageJson相关的路由。
现在我们回到index.tsx中,对page路由进行引入。
import React, { Suspense } from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import Page from './routes/page'; import { HashRouter as Router, Routes , Route} from "react-router-dom"; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( '/'} element={ );
3.服务端的CORS配置
这时候,如果我们在项目里调用服务端的接口,会有跨域的问题。所以在XinBuilderServer中,我们修改一下main.ts文件:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' import { NestExpressApplication } from '@nestjs/platform-express'; async function bootstrap() { const app = await NestFactory.create(AppModule,{ cors: true }); const options = new DocumentBuilder() .setTitle('API example') .addBearerAuth() .setVersion('1.0') .build() const document = SwaggerModule.createDocument(app, options) SwaggerModule.setup('api-docs', app, document) await app.listen(4000); } bootstrap();
通过修改CORS配置,来解决跨域的问题
的代码提交在github上:
https://github.com/TeacherXin/XinBuilderServer2
commit: 第二节:修改CORS配置解决跨域问题
4.构建前端页面
那我们需要的效果就是:
至于这部分比较简单,我把代码的注释写一下,读者自己看就行。
不过编辑页面和预览页面,一会再细说。
import React, { useEffect, useState } from 'react' import { Card, Col, Row, Button,Input, message, Modal,Divider, Select } from 'antd'; import {DeleteOutlined,DatabaseOutlined,FormOutlined,InsertRowBelowOutlined,UsergroupDeleteOutlined} from '@ant-design/icons'; import axios from 'axios' import './index.css' const { Search } = Input interface PageJson { pageName: string, pageId: string, pageJson: { [key: string]: any }, _id: string } export default function Page() { const [messageApi, contextHolder] = message.useMessage(); const [pageList, setPageList] = useState() const [isModalOpen,setIsModalOpen] = useState(false) const [pageName,setPageName] = useState('') const [searchValue,setSearchValue] = useState('') useEffect(() => { getPageList() }, []) /** * 获取全部List的接口 */ const getPageList = () => { axios.post(`http://localhost:4000/page-json/findAllPage`) .then(res => { setPageList(res.data.data) }) .catch(err => { messageApi.open({ type: 'error', content: '获取页面列表失败', }); }) } /** * 更改搜索框的内容 * @param value 搜索框的内容 */ const onSearch = (value: string) => { setSearchValue(value) } /** * 新建页面的弹窗 */ const addNewPage = () => { setIsModalOpen(true); setPageName('') } /** * 搜索内容的过滤 * @param list 页面列表 * @returns 过滤后的页面列表 */ const getSearchList = (list: PageJson [] | undefined) => { return (list || []).filter(item => { return item.pageName.indexOf(searchValue) > -1 }) } /** * 根据页面ID进行删除 * @param pageId 页面的ID * @returns */ const deletePage = (pageId: string) => { return () => { axios.post(`http://localhost:4000/page-json/deletePage`,{ pageId }) .then(res => { messageApi.open({ type: 'success', content: '删除成功', }); getPageList() }) .catch(err => { messageApi.open({ type: 'error', content: '删除失败', }); }) } } /** * 新增页面掉的接口 */ const handleOk = () => { const user = JSON.parse(localStorage.getItem('user') || '{}'); axios.post(`http://localhost:4000/page-json/addPage`,{ pageName: pageName, pageId:'pageInfo_' + new Date().getTime(), pageJson: {}, }) .then(res => { messageApi.open({ type: 'success', content: '新建页面成功', }); getPageList() setIsModalOpen(false) }) .catch(err => { messageApi.open({ type: 'error', content: '新建页面失败', }); }) } /** * 新建页面弹窗的取消回调 */ const handleCancel = () => { setIsModalOpen(false) } /** * 更改输入的页面名称 * @param e 页面名称 */ const changePageName = (e: any) => { setPageName(e.target.value) } const toBuilderPage = (pageId: string) => { return () => { } } return ({contextHolder}isModalOpen} onOk={handleOk} onCancel={handleCancel} okText='创建' cancelText='取消' pageName} onChange={changePageName} / ) }XinBuilder轻量级的低代码平台{ width: 304 }} onSearch={onSearch} / addNewPage}新建页面{width:'100%'}} gutter={16} { (getSearchList(pageList) || []).map(item => { return {marginTop:'10px'}} key={item._id} span={6} item.pageName || '匿名'}deletePage(item.pageId)}style={{float:'right',cursor:'pointer'}} /false} headStyle={{fontSize:'14px'}} {height:'50px'}} toBuilderPage(item.pageId)}编辑页面 预览页面}) }
5.跳转页面详情
当我点击编辑页面的时候,应该跳转到对应页面的编辑状态。也就是我们之前实现的项目。
那我在我们的设计器项目怎么知道当前的页面ID呢?
所以我们需要再跳转的时候,将pageId带过去,怎么带呢,只能通过URL上面的参数实现,所以我们现在可以实现一下toBuilderPage方法。
/** * 根据页面ID跳转到详情页 * @param pageId 页面ID * @returns */ const toBuilderPage = (pageId: string) => { return () => { window.open(`http://localhost:3000?pageId=${pageId}`) } }
6.修改XinBuilder项目
OK,现在我们现在回到我们的低代码项目里,在builder目录下的index.tsx中,我们要根据URL上的pageId,调取接口来获取到页面详情
获取到之后,我们再通过Store去更新redux。
import { useEffect } from 'react' import DesignTop from './designTop' import LeftCom from './leftPart' import MainCom from './mainPart' import RightCom from './rightPart' import axios from 'axios' import Store from '../../store' import { message } from 'antd' export default function Builder() { useEffect(() => { const search = window.location.search || ''; const pageId = search.replace('?pageId=', ''); axios.post('http://localhost:4000/page-json/findPageByID', { pageId }) .then(res => { if(res.data.data) { Store.dispatch({type: 'changeComList', value: res.data.data.pageJson || []}) }else{ message.error('获取页面详情失败') } }) }, []) return () }
OK,现在我们还需要就是给设计器增加保存的功能,我们来到designTop中,给它添加一个保存的按钮。
import { Button, message } from 'antd' import './index.css' import Store from '../../../store' import axios from 'axios' export default function DesignTop() { const savePage = () => { const search = window.location.search || ''; const pageId = search.replace('?pageId=', ''); const comList = Store.getState().comList; axios.post('http://localhost:4000/page-json/updatePage', { pageId, pageJson: comList }) .then(res => { if(res.data.code == 200) { message.success('保存成功') } }) } return (XinBuilder savePage} type='primary' ghost保存) }
到此为止,在上一篇中实现的所有接口,我们就实现完对它的调用了。
和XinBuilder相关的代码提交在github上:
https://github.com/TeacherXin/XinBuilder2
commit: 第十七节:实现页面的保存以及加载
博主补充
本篇相关的代码提交在github上:
https://github.com/TeacherXin/AppBuilder
commit: 第一节:初始化项目,实现页面的创建等操作
目前我们已经有三个项目了:
- AppBuilder 最外层的壳子,提供创建页面等操作
- XinBuilder 设计器项目,负责对页面进行配置
- XinBuilderServer 后端服务,负责数据的存储
后面还会有一个运行时的项目。。。。。
还没有评论,来说两句吧...