react 高效高質量搭建後臺系統 系列 —— 結尾

来源:https://www.cnblogs.com/pengjiali/archive/2023/02/20/17139099.html
-Advertisement-
Play Games

其他章節請看: react 高效高質量搭建後臺系統 系列 尾篇 本篇主要介紹表單查詢、表單驗證、通知(WebSocket)、自動構建。最後附上 myspug 項目源碼。 項目最終效果: 表單查詢 需求:給角色管理頁面增加表格查詢功能,通過輸入角色名稱,點擊查詢,從後端檢索出相應的數據。 效果如下: ...


其他章節請看:

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 高效高質量搭建後臺系統 系列

作者:彭加李
出處:https://www.cnblogs.com/pengjiali/p/17139099.html
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一:背景 1. 講故事 相信大家在使用 SQLSERVER 的過程中經常會遇到 阻塞 和 死鎖,尤其是 死鎖,比如下麵的輸出: (1 row affected) Msg 1205, Level 13, State 51, Line 5 Transaction (Process ID 62) was ...
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 作者:vatebur 文章來源:GreatSQL社區投稿 UOS二進位安裝資料庫和其他 Linux 基本一樣,網上命令行安裝的教程很多。考慮到 UOS ...
  • 本文介紹了TiDB資料庫特性及在之家的發展歷程,典型業務應用場景,TiDB具有相容MySQL協議,易水平擴展、高可用、強一致,HTAP等特性,在之家多個重要業務得到應用。另外文章還介紹了之家TIDB自動化運維建設情況及應用實踐遇到的問題及解決。 未來之家TiDB計劃繼續進行TiDB運維體系建設,並... ...
  • 本文內容主要翻譯自issue 中國外大佬對防抖與節流的解釋, 後面補充了自己的理解和總結。 什麼是防抖與節流 防抖和節流是處理“過於頻繁”發生的事情的常用技術。想象一下,你和朋友見面,朋友正在給你講一個故事,但他們說話時很難停下來。假設您想在可能的情況下不打斷他們滿足他們講故事的興緻,同時還要回應他 ...
  • 本文首發我的博客,github 地址 大家好,我是徐公,今天為大家帶來的是 RxJava 的一個血案,一行代碼 return null 引發的。 前陣子,組內的同事反饋說 RxJava 在 debug 包 crash 了,捕獲到的異常信息不全。(即我們捕獲到的堆棧沒有包含我們自己代碼,都是一些系統或 ...
  • 常見問題一:如何驗證Analytics是否上報/接入成功?以及關鍵日誌含義是什麼? 在初始化Analytics SDK前添加SDK日誌開關如下: HiAnalyticsTools.enableLog (); 2.初始化SDK代碼如下: HiAnalyticsInstance instance = H ...
  • 項目的奇葩需求,需要彈出Dialog不要顯示狀態欄和導航欄,記錄一下解決方法 原文地址:Android 關於Dialog在全屏彈出會顯示狀態欄和導航欄的問題解決 Stars-one的雜貨小窩 說明 Android的原生的Dialog有個問題,如果你的Activity設置為全屏的,然後顯示Dialog ...
  • 說起國際化,開發過跨區域網頁的小伙伴應該都遇到過。我們的網頁需要配置多套語言,方便用戶進行切換。 本文就以 React 為例,介紹其中一種實現方案,並學習一下其中的知識點。 一種國際化方案 方案是這樣的: 為多套語言創建對應的 object,並 export 出去 通過 js 立即執行函數,載入選定 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...