从零实现一套低代码(保姆级教程)【后端服务】 --- 【3】实现页面接口对应的前端

摘要

本篇是这个系列的第三篇文章,相关参考:

# 从零实现一套代码前端部分专栏

# 从零实现一套低代码(保姆级教程)【后端服务】 --- 【1】初始化后端项目

在上一篇中,我们已经把和页面相关的接口完成的差不多了。从创建页面,更新页面等等:

有了接口之后,我们就可以构建前端内容了。那这部分前端内容我们应该写在哪里呢?

有两种方式:

  1. 直接写在我们的XinBuilder项目里面,然后通过前端路由拆分成两个路由
  2. 在创建一个项目,然后打包到后端服务中,也就是通过后端路由去控制

因为我不确定这个项目后面会有多少代码,虽然我们目前只是想实现页面的管理功能,但是后面我也不知道会增加到多少。

所以我准备使用两个React项目,和页面相关的这些功能我都会写在新的项目里,

1.创建项目

首先就是创建项目了,我们使用create-react-app创建一个项目:

javascript 复制代码
>  npx create-react-app app-builder --template typescript

然后再安装antD

javascript 复制代码
 npm install antd --save

然后把项目里没有用的文件删一删:

最后,因为我们要请求我们写好的接口,在安装一下axios。

javascript 复制代码
npm install axios --save

2.路由的配置

对于这个项目,我们现在只准备完成和pageJson相关的。但是后面可能会有其他的页面,所以我们是需要路由的。

我们就先安装一下react-router-dom,然后使用路由来管理前端的页面。

javascript 复制代码
 npm install react-router-dom --save

对于路由,我们在src下新建一个routes用来管理所有的路由页面。

page文件夹就是代表和pageJson相关的路由。

现在我们回到index.tsx中,对page路由进行引入。

javascript 复制代码
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(
  <Router>
    <Suspense>
    <Routes>
      <Route path={'/'} element={<Page />}></Route>
    </Routes>
    </Suspense>
  </Router>
);

3.服务端的CORS配置

这时候,如果我们在项目里调用服务端的接口,会有跨域的问题。所以在XinBuilderServer中,我们修改一下main.ts文件:

javascript 复制代码
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<NestExpressApplication>(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配置,来解决跨域的问题

XinBuilderServer的代码提交在github上:
github.com/TeacherXin/...
commit: 第二节:修改CORS配置解决跨域问题

4.构建前端页面

那我们需要的效果就是:

至于这部分比较简单,我把代码的注释写一下,读者自己看就行。

不过编辑页面和预览页面,一会再细说。

javascript 复制代码
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<PageJson []>()
  const [isModalOpen,setIsModalOpen] = useState<boolean>(false)
  const [pageName,setPageName] = useState<string>('')
  const [searchValue,setSearchValue] = useState<string>('')

  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 (
    <div className='PageList'>
      {contextHolder}
      <div className='pageLeft'>
        <div className='leftHeader'>XinBuilder</div>
        <div className='leftDiscribe'>轻量级的低代码平台</div>
        <Divider />
      </div>
      <div className='pageRight'>
        <div className='PageHeader'>
          <Search
            style={{ width: 304 }}
            onSearch={onSearch}
          />
          <Button className='pageButton' onClick={addNewPage}>新建页面</Button>
        </div>
        <Divider />
        <div className='PageBody'>
          <Row style={{width:'100%'}} gutter={16}>
            {
              (getSearchList(pageList) || []).map(item => {
                return <Col style={{marginTop:'10px'}} key={item._id} span={6}>
                  <Card
                    title={<div><span>{item.pageName || '匿名'}</span><DeleteOutlined onClick={deletePage(item.pageId)}style={{float:'right',cursor:'pointer'}} /></div>}
                    bordered={false}
                    headStyle={{fontSize:'14px'}}
                  >
                    <div style={{height:'50px'}}>
                      <Button type='text' onClick={toBuilderPage(item.pageId)}>编辑页面</Button>
                      <Button type='text'>预览页面</Button>
                    </div>
                  </Card>
                </Col>
              })
            }
          </Row>
        </div>
      </div>
      <Modal title="创建页面" open={isModalOpen} onOk={handleOk} onCancel={handleCancel} okText='创建' cancelText='取消'>
          <Input addonBefore="页面名称" value={pageName} onChange={changePageName} />
      </Modal>
    </div>
  )
}

5.跳转页面详情

当我点击编辑页面的时候,应该跳转到对应页面的编辑状态。也就是我们之前实现的项目。 那我在我们的设计器项目怎么知道当前的页面ID呢?

所以我们需要再跳转的时候,将pageId带过去,怎么带呢,只能通过URL上面的参数实现,所以我们现在可以实现一下toBuilderPage方法。

javascript 复制代码
  /**
   * 根据页面ID跳转到详情页
   * @param pageId 页面ID
   * @returns 
   */
  const toBuilderPage = (pageId: string) => {
    return () => {
      window.open(`http://localhost:3000?pageId=${pageId}`)
    }
  }

AppBuilder相关的代码提交在github上:
github.com/TeacherXin/...
commit: 第一节:初始化项目,实现页面的创建等操作

6.修改XinBuilder项目

OK,现在我们现在回到我们的低代码项目里,在builder目录下的index.tsx中,我们要根据URL上的pageId,调取接口来获取到页面详情

获取到之后,我们再通过Store去更新redux。

javascript 复制代码
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 (
    <div>
      <DesignTop />
      <LeftCom />
      <MainCom />
      <RightCom />
    </div>
  )
}

OK,现在我们还需要就是给设计器增加保存的功能,我们来到designTop中,给它添加一个保存的按钮。

javascript 复制代码
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 (
    <div className='designTop'>
      <span className='title'>XinBuilder</span>
      <Button onClick={savePage} type='primary' ghost>保存</Button>
    </div>
  )
}

到此为止,在上一篇中实现的所有接口,我们就实现完对它的调用了。

和XinBuilder相关的代码提交在github上:
github.com/TeacherXin/...
commit: 第十七节:实现页面的保存以及加载

博主补充

目前我们已经有三个项目了:

  1. AppBuilder 最外层的壳子,提供创建页面等操作
  2. XinBuilder 设计器项目,负责对页面进行配置
  3. XinBuilderServer 后端服务,负责数据的存储

后面还会有一个运行时的项目。。。。。所以要慢慢来

相关推荐
Dragon Wu2 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
YU大宗师2 小时前
React面试题
前端·javascript·react.js
木兮xg2 小时前
react基础篇
前端·react.js·前端框架
三思而后行,慎承诺4 小时前
Reactnative实现远程热更新的原理是什么
javascript·react native·react.js
知识分享小能手4 小时前
React学习教程,从入门到精通,React 组件生命周期详解(适用于 React 16.3+,推荐函数组件 + Hooks)(17)
前端·javascript·vue.js·学习·react.js·前端框架·vue3
RestCloud6 小时前
低代码、无代码、iPaaS:到底有什么区别?
低代码·api
夏天19957 小时前
React:聊一聊状态管理
前端·javascript·react.js
LFly_ice7 小时前
学习React-11-useDeferredValue
前端·学习·react.js
LFly_ice13 小时前
学习React-10-useTransition
前端·学习·react.js