摘要

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

javascript前端接口验签规则_低代码


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

有两种方式:

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

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

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

1.创建项目

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

>  npx create-react-app app-builder --template typescript

然后再安装antD

npm install antd --save

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

javascript前端接口验签规则_github_02

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

npm install axios --save

2.路由的配置

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

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

npm install react-router-dom --save

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

javascript前端接口验签规则_低代码_03

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

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<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配置,来解决跨域的问题

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

4.构建前端页面

那我们需要的效果就是:

javascript前端接口验签规则_github_04


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

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

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方法。

/**
   * 根据页面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 (
    <div>
      <DesignTop />
      <LeftCom />
      <MainCom />
      <RightCom />
    </div>
  )
}

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

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

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

博主补充

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

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

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

后面还会有一个运行时的项目。。。。。