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


    其他章节请看:

    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 对话框
              store.formVisible = false}
                 confirmLoading={loading}
    +            // ok 按钮 props
    +            okButtonProps={{disabled: !canSubmit}}
                 onOk={handleSubmit}>
                 
    - + + {/* shouldUpdate - 自定义字段更新逻辑 */} + {/* 注:需要两个字段都增加 shouldUpdate。如果只有一个,修改该项则不会触发 emptyValid,你可以将 `shouldUpdate={emptyValid}` 放在非必填项中。*/} + + +

    :有两点需要注意

    • 需要两个字段都增加 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:
    
        <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>
    
    
    • 最后启动服务 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 高效高质量搭建后台系统 系列

  • 相关阅读:
    综合管廊安全监测系统,城市‘里子’的守护者
    知识图谱认知智能理论与实战----------第一章 知识图谱概述
    java 数据库 查询 select 2
    深度学习 | TCN时间卷积神经网络模型描述
    vue中ElementUi的el-table表格绑定行点击事件
    LeetCode之二叉树
    分布式--Redis的安装与数据类型的使用
    DevOps到底是什么意思?
    HTML网上书店静态HTML网页作业作品 大学生三联书店网页设计制作成品 简单DIV CSS布局网站
    #边学边记 必修5 高项:对人管理 第1章 项目人力资源管理 之 项目团队组建
  • 原文地址:https://www.cnblogs.com/pengjiali/p/17139099.html