草庐IT

react 高效高质量搭建后台系统 系列 —— 结尾

彭加李 2023-03-28 原文

其他章节请看:

react 高效高质量搭建后台系统 系列

尾篇

本篇主要介绍表单查询表单验证通知(WebSocket)、自动构建。最后附上 myspug 项目源码。

项目最终效果:

表单查询

需求:给角色管理页面增加表格查询功能,通过输入角色名称,点击查询,从后端检索出相应的数据。

效果如下:

spug 中的实现

spug 中的这类查询都是在前端过滤出相应的数据(没有查询按钮),因为 spug 中大多数的 table 都是一次性将数据从后端拿回来。

spug 中角色管理搜索相关代码如下:

  • 随着 input 中输入要搜索的角色名称更改 store 中的 f_name 字段:
<SearchForm>
  <SearchForm.Item span={8} title="角色名称">
    <Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
  </SearchForm.Item>
</SearchForm>

:select 中的值不同于 input(e.target.value),直接就是第一个参数,所以得这么写:onChange={v => store.f_xx = v}

  • 表格的数据源会动态过滤:
@computed get dataSource() {
  // 从 this.records 中过滤出数据
  let records = this.records;
  if (this.f_name) records = records.filter(x => x.name.toLowerCase().includes(this.f_name.toLowerCase()));
  return records
}

实现

相对 spug 的查询,现在思路得变一下:通过点击搜索按钮,重新请求数据,附带查询关键字给后端。

核心逻辑如下:

// myspug\src\pages\system\role\index.js

import ComTable from './Table';
import { AuthDiv, SearchForm, } from '@/components';
import store from './store';

export default function () {
  return (
    <AuthDiv auth="system.role.view">
      <SearchForm>
        <SearchForm.Item span={6} title="角色名称">
          <Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入" />
        </SearchForm.Item>
        <SearchForm.Item span={6}>
          <Button type="primary" onClick={() => {
            // 重置为第一页
            store.setCurrent(1)
            store.fetchRecords();
          }}>查询</Button>
        </SearchForm.Item>
      </SearchForm>
      <ComTable />
    </AuthDiv>
  )
}

Store 中就是在请求表格时将过滤参数带上:

 class Store {
+  @observable f_name;

   @observable records = [];

   _getTableParams = () => ({current: this.current, ...this.tableOptions})

+  @action setCurrent(val){
+    this.current = val
+  }

   fetchRecords = () => {
     const realParams = this._getTableParams()
+    // 过滤参数
+    if(this.f_name){
+      realParams.role_name = this.f_name
+    }
+    console.log('realParams', realParams)
     this.isFetching = true;
     http.get('/api/account/role/', {params: realParams})
       .then(res => {

Tip:剩余部分就没什么了,比如样式直接复制 spug 中(笔者直接拷过来页面有点问题,稍微注释了一段 css 即可);SearchForm 就是对表单简单封装,统一 spug 中表单的写法:

// myspug\src\components\SearchForm.js
import React from 'react';
import { Row, Col, Form } from 'antd';
import styles from './index.module.less';

export default class extends React.Component {
  static Item(props) {
    return (
      <Col span={props.span} offset={props.offset} style={props.style}>
        <Form.Item label={props.title}>
          {props.children}
        </Form.Item>
      </Col>
    )
  }

  render() {
    return (
      <div className={styles.searchForm} style={this.props.style}>
        <Form style={this.props.style}>
          <Row gutter={{md: 8, lg: 24, xl: 48}}>
            {this.props.children}
          </Row>
        </Form>
      </div>
    )
  }
}

效果

实现效果如下:

输入关键字name,点击查询按钮,重新请求表格数据(从第一页开始)

表单验证

spug 中的表单验证

关于表单验证,spug 中前端写的很少。请看以下一个典型示例:

新建角色时,为空等校验都是后端做的。

虽然后端一定要做校验,但前端最好也做一套。

实现

笔者表单的验证思路是:

  • 必填项都有值(还可以包括其他逻辑),提交按钮才可点,否则置灰
  • 点击提交后,前端根据需求做进一步验证,例如名字不能有空格

以下是新增和编辑时的效果(重点关注确定按钮):

  • 当必填项都有值时确定按钮可点,否则置灰
  • 必填项都有值时,点击确定按钮做进一步校验(例如名字不能有空格)
  • 编辑时如果都有值,则确定按钮可点击

表单

先实现表单,效果如下:

核心代码如下:

  • 首先定义表单模块:
// myspug\src\pages\system\role\Form.js
import http from '@/libs/http';
import store from './store';

export default observer(function () {
    // 文档中未找到这种解构使用方法
    const [form] = Form.useForm();
    // useState 函数组件中使用 state
    // loading 默认是 flase
    const [loading, setLoading] = useState(false);

    function handleSubmit() {
        setLoading(true);
        // 取得表单字段的值
        const formData = form.getFieldsValue();
        // 新建时 id 为 undefined
        formData['id'] = store.record.id;
        http.post('/api/account/role/', formData)
            .then(res => {
                message.success('操作成功');
                store.formVisible = false;
                store.fetchRecords()
            }, () => setLoading(false))
    }

    return (
        // Modal 对话框
        <Modal
            visible
            maskClosable={false}
            title={store.record.id ? '编辑角色' : '新建角色'}
            onCancel={() => store.formVisible = false}
            confirmLoading={loading}
            onOk={handleSubmit}>
            <Form form={form} initialValues={store.record} labelCol={{ span: 6 }} wrapperCol={{ span: 14 }}>
                <Form.Item required name="name" label="角色名称">
                    <Input placeholder="请输入角色名称" />
                </Form.Item>
                <Form.Item name="desc" label="备注信息">
                    <Input.TextArea placeholder="请输入角色备注信息" />
                </Form.Item>
            </Form>
        </Modal>
    )
})
  • 然后在入口页中根据 store 中的 formVisible 控制显隐藏表单组件
// myspug\src\pages\system\role\index.js
export default observer(function () {
   return (
     <AuthDiv auth="system.role.view">
       <SearchForm>
         </SearchForm.Item>
       </SearchForm>
       <ComTable />
+      {/* formVisible 控制表单显示 */}
+      {store.formVisible && <ComForm />}
     </AuthDiv>
   )
})
  • 点击新建是调用 store.showForm() 让表单显示出来
 // myspug\src\pages\system\role\store.js

 class Store {
+  @observable formVisible = false;
+  @observable record = {};


+  // 显示新增弹框
+  // info 或许是为了编辑
+  showForm = (info = {}) => {
+    this.formVisible = true;
+    this.record = info
+  };
表单校验

在表单基础上实现校验。

主要在 Form.js 中修改,思路如下:

  • 首先利用 okButtonProps 控制确定按钮是否可点
  • 然后通过 shouldUpdate={emptyValid} 自定义字段更新逻辑
  • 可提交后,在做进一步判断,例如名字不能为空
 // myspug\src\pages\system\role\Form.js
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { observer } from 'mobx-react';
 import { Modal, Form, Input, message } from 'antd';
 import http from '@/libs/http';
     // useState 函数组件中使用 state
     // loading 默认是 flase
     const [loading, setLoading] = useState(false);
+    const [canSubmit, setCanSubmit] = useState(false);
     function handleSubmit() {
         // 取得表单字段的值
         const formData = form.getFieldsValue();
+
+        if(formData.name && (/\s+/g).test(formData.name)){
+            message.error('名字不允许有空格')
+            return
+        }
+        if(formData.tel && (/\s+/g).test(formData.tel)){
+            message.error('电话不允许有空格')
+            return
+        }

         // 新建时 id 为 undefined
         formData['id'] = store.record.id;
         http.post('/api/account/role/', formData).then(...)
         
     }

+    function emptyValid() {
+        const formData = form.getFieldsValue();
+        const { name, tel } = formData;
+        const isNotEmpty = !!(name && tel);
+        setCanSubmit(isNotEmpty)
+    }

+    useEffect(() => {
+        // 主动触发,否则编辑时即使都有数据,`确定`按钮扔不可点
+        emptyValid()
+    }, [])
+
     return (
         // Modal 对话框
         <Modal
             title={store.record.id ? '编辑角色' : '新建角色'}
             onCancel={() => store.formVisible = false}
             confirmLoading={loading}
+            // ok 按钮 props
+            okButtonProps={{disabled: !canSubmit}}
             onOk={handleSubmit}>
             <Form form={form} initialValues={store.record} labelCol={{ span: 6 }} wrapperCol={{ span: 14 }}>
-                <Form.Item required name="name" label="角色名称">
+                <Form.Item required shouldUpdate={emptyValid} name="name" label="角色名称">
                     <Input placeholder="请输入角色名称" />
                 </Form.Item>
+                {/* shouldUpdate - 自定义字段更新逻辑 */}
+                {/* 注:需要两个字段都增加 shouldUpdate。如果只有一个,修改该项则不会触发 emptyValid,你可以将 `shouldUpdate={emptyValid}` 放在非必填项中。*/}
+                <Form.Item required shouldUpdate={emptyValid} name="tel" label="手机号">
+                    <Input placeholder="请输入手机号" />
+                </Form.Item>
                 <Form.Item name="desc" label="备注信息">
                     <Input.TextArea placeholder="请输入角色备注信息" />
                 </Form.Item>

:有两点需要注意

  • 需要两个字段都增加 shouldUpdate。如果只有一个,修改该项则不会触发 emptyValid()
  • 组件加载后主动触发 emptyValid(),否则编辑时即使都有数据,确定按钮扔不可点

效果

以下演示了新建和编辑时的效果:

  • 当必填项都有值时确定按钮可点,否则置灰
  • 必填项都有值时,点击确定按钮做进一步校验(例如名字不能有空格)
  • 编辑时如果都有值,则确定按钮可点击

WebSocket

通知

后端系统通常会有通知功能,用轮询的方式去和后端要数据不是很好,通常是后端有数据后再告诉前端。

spug 中的通知使用的是 webSocket

TipWebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此 API,您可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。

以下是 spug 中通知模块的代码片段:

  // spug\src\layout\Notification.js
  function fetch() {
    setLoading(true);
    http.get('/api/notify/')
      .then(res => {
        setReads(res.filter(x => !x.unread).map(x => x.id))
        setNotifies(res);
      })
      .finally(() => setLoading(false))
  }

  function listen() {
    if (!X_TOKEN) return;
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    // Create WebSocket connection.
    ws = new WebSocket(`${protocol}//${window.location.host}/api/ws/notify/?x-token=${X_TOKEN}`);
    // onopen - 用于指定连接成功后的回调函数。
    // Connection opened
    ws.onopen = () => ws.send('ok');
    // onmessage - 用于指定当从服务器接受到信息时的回调函数。
    // Listen for messages
    ws.onmessage = e => {
      if (e.data !== 'pong') {
        fetch();
        const {title, content} = JSON.parse(e.data);
        const key = `open${Date.now()}`;
        const description = <div style={{whiteSpace: 'pre-wrap'}}>{content}</div>;
        const btn = <Button type="primary" size="small" onClick={() => notification.close(key)}>知道了</Button>;
        notification.warning({message: title, description, btn, key, top: 64, duration: null})
      }
    }
  }

通过 WebSocket 创建 webSocket 连接,然后通过 onmessage 监听服务端的消息。这里好像是后端告诉前端有新消息,前端在通过另一个接口发起 http 请求。

服务端

笔者接下来用 node + ws 实现 WebSocket 服务端。

效果如下(每3秒客户端和服务器都会向对方发送一个消息):

对应的请求字段:

实现如下:

  • 新建项目,安装依赖
$ mkdir websocket-test
$ cd websocket-test
// 初始化项目,生产 package.json
$ npm init -y
// 安装依赖
$ npm i ws express
  • 新建服务器 server.js
const express = require('express')
const app = express()
app.get('/', function (req, res) {
    res.sendfile(__dirname + '/index.html');
});
app.listen(3020);

const WebSocketServer = require('ws');
const wss = new WebSocketServer.Server({ port: 8080 });
wss.on('connection', function connection(ws) {

    // 监听来自客户端的消息
    ws.on('message', function incoming(message) {
        console.log('' + message);
    });

    setInterval(() => {
        ws.send('客户端你好');
    }, 3000)
});
  • 客户端代码 index.html:
<body>
    <script>
        var ws = new WebSocket('ws://localhost:8080');
        ws.onopen = function () {
              ws.send('ok');
        };
        ws.onmessage = function (e) {
            console.log(e.data)
        };

        setInterval(() => {
            ws.send('服务器你好');
        }, 3000)
    </script>
</body>
  • 最后启动服务 node server.js,浏览器访问 http://localhost:3020/

扩展

面包屑

spug 中的面包屑(导航)仅对 antd 面包屑稍作封装,不支持点击。

要实现点击跳转的难点是要有对应的路由,而 spug 这里对应的是 404,所以它干脆就不支持跳转

自动构建

笔者代码提交到 gitlab,使用其中的 CICD 模块可用于构建流水线。以下是 wayland(导入 wayland 官网到内网时发现的,开源精神极高,考虑到网友有这个需求。) 的一个构建截图:

这里不过多展开介绍 gitlab cicd 流水线。总之通过触发流水线,gitlab 就会执行项目下的一个 .yml 脚本,我们则可以通过脚本实现编译部署

需求:通过流水线实现 myspug 的部署。

  • 新建入口文件:.gitlab-ci.yml
// .gitlab-ci.yml

stages:
  - deploy
# 部署到测试环境
deplay_to_test:
  state: deply
  tags:
    # 运行流水线的机器
    - ubuntu2004_27.141-myspug
  rules:
    # 触发流水线时的变量,EFPLOY_TO_TEST 不为空则运行 deploy-to-test.sh 这个脚本
    - if: EFPLOY_TO_TEST != null && $DEPLOY_TO_TEST != ""
  script:
    - chmod + x deploy-to-test.sh && ./deploy-to-test.sh
# 部署到生产环境
deplay_to_product:
  state: deply
  tags:
    - ubuntu2004_27.141-myspug
  rules:
    - if: EFPLOY_TO_product != null && $DEPLOY_TO_product != ""
  script:
    - chmod + x deploy-to-product.sh && ./deploy-to-product.sh 
  • 部署到生产环境的脚本:deploy-to-product.sh
// deploy-to-product.sh 

#!/bin/bash
# 部署到生产环境
# 开启:如果命令以非零状态退出,则立即退出
set -e
DATETIME=$(date +%Y-%m-%d_%H%M%S)
echo DATETIME=$DATETIME

SERVERIP=192.168.27.135

SERVERDIR=/data/docker_data/myspug_web

BACKDIR=/data/backup/myspug

# 将构建的文件传给服务器
zip -r build.zip build
scp ./build.zip root@${SERVERIP}:${BACKDIR}/
rm -rf build.zip

# 登录生产环境服务器
ssh root${SERVERIP}<< reallssh
echo login:${SERVERIP}

# 备份目录
[ ! -d "${BACKDIR}/${DATETIME}" ] && mkdir -p "${BACKDIR}/${DATETIME}"

echo 备份目录已创建或已存在

# 删除30天以前的包
find ${BACKDIR}/ -mtime +30 -exec rm -rf {} \;

# 将包备份一份
cp ${BACKDIR}/build.zip ${BACKDIR}/${DATETIME}

mv ${BACKDIR}/build.zip ${SERVERDIR}/

cd ${SERVERDIR}/

rm -rf ./build

unzip build.zip

rm -rf build.zip

echo 部署完成

exit

reallssh

完整项目

项目已上传至 github(myspug)。

克隆后执行以下两条命令即可在本地启动服务:

$ npm i
$ npm run start

浏览器访问效果如下:

后续

后续有时间还想再写这3部分:

  • 项目文档。一个系统通常得有对应的文档。就像这样:

  • 系统概要设计。用于其他人快速接手这个项目

  • 交互设计。spug 中有不少的交互点可以提高相关系统的见识。例如这个抽屉交互

其他章节请看:

react 高效高质量搭建后台系统 系列

有关react 高效高质量搭建后台系统 系列 —— 结尾的更多相关文章

  1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. ruby-on-rails - 使用一系列等级计算字母等级 - 2

    这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,

  3. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  6. 阿里云RDS——产品系列概述 - 2

    基础版云数据库RDS的产品系列包括基础版、高可用版、集群版、三节点企业版,本文介绍基础版实例的相关信息。RDS基础版实例也称为单机版实例,只有单个数据库节点,计算与存储分离,性价比超高。说明RDS基础版实例只有一个数据库节点,没有备节点作为热备份,因此当该节点意外宕机或者执行重启实例、变更配置、版本升级等任务时,会出现较长时间的不可用。如果业务对数据库的可用性要求较高,不建议使用基础版实例,可选择其他系列(如高可用版),部分基础版实例也支持升级为高可用版。基础版与高可用版的对比拓扑图如下所示。优势 性能由于不提供备节点,主节点不会因为实时的数据库复制而产生额外的性能开销,因此基础版的性能相对于

  7. ruby - 如何在 ruby​​ 中运行后台线程? - 2

    我是ruby​​的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp

  8. ruby-on-rails - 在 Rails 中更高效地查找或创建多条记录 - 2

    我有一个应用需要发送用户事件邀请。当用户邀请friend(用户)参加事件时,如果尚不存在将用户连接到该事件的新记录,则会创建该记录。我的模型由用户、事件和events_user组成。classEventdefinvite(user_id,*args)user_id.eachdo|u|e=EventsUser.find_or_create_by_event_id_and_user_id(self.id,u)e.save!endendend用法Event.first.invite([1,2,3])我不认为以上是完成我的任务的最有效方法。我设想了一种方法,例如Model.find_or_cr

  9. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

    因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

  10. ruby - 以毫秒为单位获取当前系统时间 - 2

    在Ruby中,以毫秒为单位获取自纪元(1970)以来的当前系统时间的正确方法是什么?我试过了Time.now.to_i,好像不是我想要的结果。我需要结果显示毫秒并且使用long类型,而不是float或double。 最佳答案 (Time.now.to_f*1000).to_iTime.now.to_f显示包含十进制数字的时间。要获得毫秒数,只需将时间乘以1000。 关于ruby-以毫秒为单位获取当前系统时间,我们在StackOverflow上找到一个类似的问题:

随机推荐