react 高效高质量搭建后台系统 系列 —— 结尾

  • react 高效高质量搭建后台系统 系列 —— 结尾已关闭评论
  • 72 次浏览
  • A+
所属分类:Web前端
摘要

其他章节请看:react 高效高质量搭建后台系统 系列本篇主要介绍表单查询、表单验证、通知(WebSocket)、自动构建。最后附上 myspug 项目源码。


其他章节请看:

react 高效高质量搭建后台系统 系列

尾篇

本篇主要介绍表单查询表单验证通知(WebSocket)、自动构建。最后附上 myspug 项目源码。

项目最终效果:

react 高效高质量搭建后台系统 系列 —— 结尾

表单查询

需求:给角色管理页面增加表格查询功能,通过输入角色名称,点击查询,从后端检索出相应的数据。

效果如下:
react 高效高质量搭建后台系统 系列 —— 结尾

spug 中的实现

spug 中的这类查询都是在前端过滤出相应的数据(没有查询按钮),因为 spug 中大多数的 table 都是一次性将数据从后端拿回来。

react 高效高质量搭建后台系统 系列 —— 结尾

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>     )   } } 

效果

实现效果如下:

react 高效高质量搭建后台系统 系列 —— 结尾

输入关键字name,点击查询按钮,重新请求表格数据(从第一页开始)

表单验证

spug 中的表单验证

关于表单验证,spug 中前端写的很少。请看以下一个典型示例:

react 高效高质量搭建后台系统 系列 —— 结尾

新建角色时,为空等校验都是后端做的。

虽然后端一定要做校验,但前端最好也做一套。

实现

笔者表单的验证思路是:

  • 必填项都有值(还可以包括其他逻辑),提交按钮才可点,否则置灰
  • 点击提交后,前端根据需求做进一步验证,例如名字不能有空格

以下是新增和编辑时的效果(重点关注确定按钮):

  • 当必填项都有值时确定按钮可点,否则置灰
  • 必填项都有值时,点击确定按钮做进一步校验(例如名字不能有空格)
  • 编辑时如果都有值,则确定按钮可点击

react 高效高质量搭建后台系统 系列 —— 结尾

表单

先实现表单,效果如下:

react 高效高质量搭建后台系统 系列 —— 结尾

核心代码如下:

  • 首先定义表单模块:
// 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(),否则编辑时即使都有数据,确定按钮扔不可点

效果

以下演示了新建和编辑时的效果:

  • 当必填项都有值时确定按钮可点,否则置灰
  • 必填项都有值时,点击确定按钮做进一步校验(例如名字不能有空格)
  • 编辑时如果都有值,则确定按钮可点击

react 高效高质量搭建后台系统 系列 —— 结尾

WebSocket

通知

后端系统通常会有通知功能,用轮询的方式去和后端要数据不是很好,通常是后端有数据后再告诉前端。

spug 中的通知使用的是 webSocket

TipWebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此 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秒客户端和服务器都会向对方发送一个消息):
react 高效高质量搭建后台系统 系列 —— 结尾

对应的请求字段:
react 高效高质量搭建后台系统 系列 —— 结尾

实现如下:

  • 新建项目,安装依赖
$ 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 官网到内网时发现的,开源精神极高,考虑到网友有这个需求。) 的一个构建截图:

react 高效高质量搭建后台系统 系列 —— 结尾

这里不过多展开介绍 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 

浏览器访问效果如下:

react 高效高质量搭建后台系统 系列 —— 结尾

后续

后续有时间还想再写这3部分:

  • 项目文档。一个系统通常得有对应的文档。就像这样:

react 高效高质量搭建后台系统 系列 —— 结尾

  • 系统概要设计。用于其他人快速接手这个项目

  • 交互设计。spug 中有不少的交互点可以提高相关系统的见识。例如这个抽屉交互

react 高效高质量搭建后台系统 系列 —— 结尾

其他章节请看:

react 高效高质量搭建后台系统 系列