react 高效高质量搭建后台系统 系列 —— 结尾
其他章节请看:
尾篇
本篇主要介绍表单查询
、表单验证
、通知
(WebSocket)、自动构建
。最后附上 myspug 项目源码。
项目最终效果:
表单查询
需求
:给角色管理页面增加表格查询功能,通过输入角色名称
,点击查询
,从后端检索出相应的数据。
效果如下:
spug 中的实现
[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 中有不少的交互点可以提高相关系统的见识。例如这个
抽屉
交互
其他章节请看:
react 高效高质量搭建后台系统 系列 —— 结尾的更多相关文章
- react 高效高质量搭建后台系统 系列 —— 脚手架搭建
其他章节请看: react 高效高质量搭建后台系统 系列 脚手架搭建 本篇主要创建新项目 myspug,以及准备好环境(例如:安装 spug 中用到的包.本地开发和部署.自定义配置 react-app ...
- react 高效高质量搭建后台系统 系列 —— 请求数据
其他章节请看: react 高效高质量搭建后台系统 系列 请求数据 后续要做登录模块(主页),需要先和后端约定JSON数据格式,将 axios 进行封装,实现本地的数据模拟 mockjs. Tip:s ...
- react 高效高质量搭建后台系统 系列 —— antd和样式
其他章节请看: react 高效高质量搭建后台系统 系列 antd 后续要做登录模块(主页),不仅要解决请求数据的问题,还需要完成 antd 配置以及样式的准备. antd 多种主题风格 详情请看 这 ...
- react 高效高质量搭建后台系统 系列 —— 登录
其他章节请看: react 高效高质量搭建后台系统 系列 登录 本篇将完成登录模块.效果和 spug 相同: 需求如下: 登录页的绘制 支持普通登录和LDAP登录 登录成功后跳转到主页,没有登录的情况 ...
- react 高效高质量搭建后台系统 系列 —— 系统布局
其他章节请看: react 高效高质量搭建后台系统 系列 系统布局 前面我们用脚手架搭建了项目,并实现了登录模块,登录模块所依赖的请求数据和antd(ui框架和样式)也已完成. 本篇将完成系统布局.比 ...
- 使用vue1.0+es6+vue-cli+webpack+iview-ui+jQuery 撸一套高质量的后台管理系统
首先按照vue.js官网的指令安装: 1.本地安装好node.js 2.根据官方命令行工具 详情 这样一个官方的脚手架工具就已经搭建好了:但是有一点需要注意的是由于现在按照官方的搭建方法是搭建vue2 ...
- 建站集成软件包 XAMPP搭建后台系统与微信小程序开发
下载安装XAMPP软件,运行Apache和MySQL 查看项目文件放在哪个位置可以正常运行 然后访问localhost即可 下载weiphp官网的weiapp(专为微信小程序开发使用)放在htdocs ...
- 编写高质量的Python代码系列(八)之部署
Python提供了一些工具,使我们可以把软件部署到不同的环境中.它也提供了一些模块,令开发者可以把程序编写的更加健壮.本章讲解如何使用Python调试.优化并测试程序,以提升其质量与性能. 第五十四条 ...
- 编写高质量的Python代码系列(一)之用Pythonic方式来思考
Python开发者用Pythonic这个形容词来描述具有特定风格的代码.这种风格是大家在使用Python语言进行编程并相互协作的过程中逐渐形成的习惯.那么,如何以改风格完成常见的Python编程工作呢 ...
- nodejs 从helloworld到高质量的后台服务server的一点思考
---恢复内容开始--- 新公司用的nodejs作为app和网站的后台服务server,所以最近对nodejs一直在学习,加上之前简单的学习了一点,看了两天后台接口源码,所以就直接上手干活了,下面是我 ...
随机推荐
- 【数据库】E-R图相关知识、手动自动绘制方法及工具推荐
一.知识 1.介绍 E-R图也称实体-联系图(Entity Relationship Diagram),提供了表示实体.属性和联系的方法,用来描述现实世界的概念模型. 2.组成 (1)实体(Entit ...
- 【Shell案例】【取指定列的方式$5 p[6],双括号运算、awk、管道运算】8、统计所有进程占用内存大小的和
假设 nowcoder.txt 内容如下:root 2 0.0 0.0 0 0 ? S 9月25 0:00 [kthreadd]root 4 0.0 0.0 0 0 ? I< 9月25 0:00 ...
- 【每日一题】【归并排序/堆排序&虚拟头结点】148. 排序链表-211220/220217【出栈时不断容易产生环状链表!】
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 . 进阶: 你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗? 方法1:归并排序+使用辅助函数 ...
- bug处理记录:java.util.UnknownFormatConversionException: Conversion = 'Y'
1. 报错: java.util.UnknownFormatConversionException: Conversion = 'Y' at java.util.Formatter$FormatSpe ...
- TiDB上百T数据拆分实践
背景 提高TiDB可用性,需要把多点已有上百T TiDB集群拆分出2套 挑战 1.现有需要拆分的12套TiDB集群的版本多(4.0.9.5.1.1.5.1.2都有),每个版本拆分方法存在不一样 2.其 ...
- VuePress个人博客搭建
vuepress概述 VuePress 由两部分组成:第一部分是一个极简静态网站生成器 (opens new window),它包含由 Vue 驱动的主题系统和插件 API,另一个部分是为书写技术文档 ...
- P8622 [蓝桥杯 2014 国 B] 生物芯片
简要题意 有 \(N\) 个二进制数,编号为 \(1\sim N\),初始时都是 \(0\).博士进行了 \(N-1\) 次操作,在第 \(i\) 次操作时,会将 \(1\sim N\) 中所有编号为 ...
- C#高性能数组拷贝实验
前言 昨天 wc(Wyu_Cnk) 提了个问题 C# 里多维数组拷贝有没有什么比较优雅的写法? 这不是问对人了吗?正好我最近在搞图像处理,要和内存打交道,我一下就想到了在C#里面直接像C/C++一样做 ...
- Java 进阶P-5.3+P-5.4
封装 增加可扩展性 可以运行的代码!=良好的代码 对代码做维护的时候最能看出代码的质量 如果想要增加一个方向,如down或up 用封装来降低耦合 Room类和Game类都有大量的代码和出口相关 尤其是 ...
- Java 进阶P-5.1+P-5.2
城堡游戏 一.城堡游戏介绍:1.这个程序的任务是通过玩家的输入的方向(纯文字)在虚构的城堡内移动(以纯文字作为移动后的返回结果).2.这个程序接受help.bye.go south.go north. ...