用了react怎么和后端对接(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 对话框 <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 高效高质量搭建后台系统 系列
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!