其他章节请看:
尾篇
本篇主要介绍表单查询
、表单验证
、通知
(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 的查询,现在思路得变一下:通过点击搜索按钮
,重新请求数据,附带查询关键字给后端。
核心逻辑
如下:
// myspugsrcpagessystemroleindex.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 中表单的写法:
// myspugsrccomponentsSearchForm.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 中前端写的很少。请看以下一个典型示例:
新建角色
时,为空等校验都是后端
做的。
虽然后端一定要做校验,但前端最好也做一套。
实现
笔者表单的验证思路是:
- 必填项都有值(还可以包括其他逻辑),提交按钮才可点,否则
置灰
- 点击提交后,前端根据需求做进一步验证,例如名字不能有空格
以下是新增和编辑时的效果(重点关注确定
按钮):
- 当必填项都有值时
确定
按钮可点,否则置灰 - 必填项都有值时,点击
确定
按钮做进一步校验(例如名字不能有空格) - 编辑时如果都有值,则
确定
按钮可点击
表单
先实现表单,效果如下:
核心代码如下:
- 首先定义表单模块:
// myspugsrcpagessystemroleForm.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 控制显隐藏表单组件
// myspugsrcpagessystemroleindex.js export default observer(function () { return ( <AuthDiv auth="system.role.view"> <SearchForm> </SearchForm.Item> </SearchForm> <ComTable /> + {/* formVisible 控制表单显示 */} + {store.formVisible && <ComForm />} </AuthDiv> ) })
- 点击
新建
是调用store.showForm()
让表单显示出来
// myspugsrcpagessystemrolestore.js class Store { + @observable formVisible = false; + @observable record = {}; + // 显示新增弹框 + // info 或许是为了编辑 + showForm = (info = {}) => { + this.formVisible = true; + this.record = info + };
表单校验
在表单基础上实现校验。
主要在 Form.js 中修改,思路如下:
- 首先利用
okButtonProps
控制确定按钮是否可点 - 然后通过
shouldUpdate={emptyValid}
自定义字段更新逻辑 - 可提交后,在做进一步判断,例如名字不能为空
// myspugsrcpagessystemroleForm.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 中通知
模块的代码片段:
// spugsrclayoutNotification.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 中有不少的交互点可以提高相关系统的见识。例如这个
抽屉
交互
其他章节请看: