其他章節請看: react 高效高質量搭建後臺系統 系列 尾篇 本篇主要介紹表單查詢、表單驗證、通知(WebSocket)、自動構建。最後附上 myspug 項目源碼。 項目最終效果: 表單查詢 需求:給角色管理頁面增加表格查詢功能,通過輸入角色名稱,點擊查詢,從後端檢索出相應的數據。 效果如下: ...
其他章節請看:
尾篇
本篇主要介紹表單查詢
、表單驗證
、通知
(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
。
Tip:WebSockets 是一種先進的技術。它可以在用戶的瀏覽器和伺服器之間打開互動式通信會話。使用此 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 中有不少的交互點可以提高相關係統的見識。例如這個
抽屜
交互
其他章節請看:
出處:https://www.cnblogs.com/pengjiali/p/17139099.html
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。