最近公司新开了项目,是后台管理系统,在技术选型上选择的时候,选择了react,然后整体的框架选择的是antd pro@4+这个脚手架搭建出来的单页面应用,要是想要仔细了解antd pro 可以看一下他的官方文档:antd pro,此次开发整体的技术栈包括:[email protected],[email protected],管理数据流采用的是[email protected],另外还应用了[email protected],前端后端的数据交互,用的是umi-request,这个umi-request本质上来说是fetch,而目前特别流行的前后端交互工具axios,本质上来说还是对原生XMLHttpRequest的封装,所以两者的在实际开发上是略有不同,具体可以去查看各自的官方API;话不多说,直接看踩坑点吧:
antd@4与3版本略有不同,少了一个getFieldDecorator,3版本绑定表单项如下:
{getFieldDecorator(`${keyId}`, {
rules: rules,
initialValue
})(children)}
4版本的相对3版本来说极大的简化了,一些初始值和校验规则,可以直接放到FormItems中去,代码如下:
若是要给表单设置初始值,可以在Form中设置initialValues,部分代码如下:
可以单独定义每个节点要显示的内容,具体部分代码如下:
}
onSelect={this.onSelect}
onExpand={this.onExpand}
>
{treeData.length > 0 ? this.genTreeNode(treeData) : this.EmptyNode()}
渲染节点genTreeNode具体的方法:
genTreeNode = (treeData: Array) => {
return treeData.map((item: TreeNodeItem) => {
if (item.children && item.children.length > 0) {
return (
{this.genTreeNode(item.children)}
)
}
return (
)
})
}
注意看一下渲染title对应的方法,实现了自定义节点显示的需求
renderTitle = (node: TreeNodeItem) => {
const { isEdit, inputVal, level = 1 } = node
if (isEdit) {
return (
{e.stopPropagation()}}
onBlur={(e) => {e.stopPropagation()}}
onChange={throttle((e) => this.refreshNode(e, node, 'change'),500, { leading: true, trailing: false })}/>
)
}
return (
{node.title}
{
level < 4 && (
}
onClick={(e) => e.stopPropagation()}/>
)
}
}
onClick={(e) => this.refreshNode(e, node, 'edit')}/>
}
onClick={(e) => this.refreshNode(e, node, 'delete')}/>
)
}
自定义节点的显示效果如下图:
还有一个,就是要实现节点拖拽对应的方法,我们产品这边的需求是,节点最多可以设置4级,超过4级的,就不显示加号,使其无法增加子节点,在做拖拽这里的时候,就是每次拖拽完以后,都要改变一下树形节点的level,改变一下树形节点的层级,这样就达到目的了,具体代码如下:
onDrop = info => {
console.log(info)
const dropKey = info.node.props.eventKey
const dragKey = info.dragNode.props.eventKey
const dropPos = info.node.props.pos.split('-')
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1])
const loop = (data, key, callback) => {
for (let i = 0; i < data.length; i++) {
if (data[i].key === key) {
return callback(data[i], i, data)
}
if (data[i].children) {
loop(data[i].children, key, callback)
}
}
}
const data = [ ...this.state.treeData ]
// Find dragObject
let dragObj
loop(data, dragKey, (item, index, arr) => {
arr.splice(index, 1)
dragObj = item
})
if (!info.dropToGap) {
// Drop on the content
loop(data, dropKey, item => {
item.children = item.children || []
// where to insert 示例添加到尾部,可以是随意位置
item.children.push(dragObj)
})
} else if (
(info.node.props.children || []).length > 0 && // Has children
info.node.props.expanded && // Is expanded
dropPosition === 1 // On the bottom gap
) {
loop(data, dropKey, item => {
item.children = item.children || []
// where to insert 示例添加到头部,可以是随意位置
item.children.unshift(dragObj)
})
} else {
let ar
let i
loop(data, dropKey, (item, index, arr) => {
ar = arr
i = index
})
if (dropPosition === -1) {
ar.splice(i, 0, dragObj)
} else {
ar.splice(i + 1, 0, dragObj)
}
}
this.updateArr(data)
this.setState({
treeData: data,
})
};
注意:componentWillReceiveProps is deprecated since React 16.9.0,注意在使用类组件的时候,部分生命周期方法已经废弃酌情使用
本次用Hook用的比较多,用Hook基本上就能满足所有需求了,经常用的有:useState,useEffect(最基础的方法,再次就不在赘述了)
另外还有umi这个框架封装好的一些hook、比如:useParams,直接把路由上的参数给取下来,详见代码:
import { useParams } from 'umi'
//在hook中就直接这样子来使用,就可以把路由中对应的参数取下来了
const { id } = useParams<{id:string}>()
前后端交互的一个工具,具体可以参见github地址:umi-request,umi-request本质上来说是fetch,而目前特别流行的前后端交互工具axios,本质上来说还是对原生XMLHttpRequest的封装,所以两者的在实际开发上是略有不同,在本次开发的过程中有一个问题一直困扰了我很久,就是对后台返回的响应做统一的处理,一旦返回中有响应错误,就走catch方法,不走then方法,后来想到了一个解决方案,记录一下:
/**
* request 网络请求工具
* 更详细的 api 文档: https://github.com/umijs/umi-request
*/
import { extend } from 'umi-request'
import Cookies from 'js-cookie'
import { notification, message } from 'antd'
import { logout } from '@/models/login'
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
}
/**
* 异常处理程序
*/
export const errorHandler = (error: { response: Response }): Response => {
const { response } = error
message.destroy()
if (!response) {
notification.error({
description: '您的网络发生异常,无法连接服务器',
message: '网络异常',
})
return response
}
if (response && response.status) {
const errorText = codeMessage[response.status] || response.statusText
const { status, url } = response
notification.error({
message: `请求错误 ${status}: ${url}`,
description: errorText,
})
}
return response
}
/**
* 配置request请求时的默认参数
*/
const request = extend({
errorHandler, // 默认错误处理
credentials: 'include', // 默认请求是否带上cookie
})
/**
* 对请求统一处理
*/
request.interceptors.request.use((url: any, options: any) => {
//在请求头设置token,每个接口都统一设置
options.headers.Authorization = Cookies.get('tokenPartner')
return options
})
/**
* 对响应统一处理
*/
request.interceptors.response.use(async response => {
const contentType = response.headers.get('content-type')
//此时返回的数据是二进制流,不是json,因此做特殊处理
if (contentType === 'application/octet-stream') {
return response
}
// 将接口返回的数据格式化成json
// const res = await response.clone().json()
return response
})
function handleResError(err: any) {
message.destroy()
message.error(err.message)
const code = err?.errCode
// token过期,重新登录
if (code === 10110002) {
logout()
}
}
function wrapRequest(url: string, newOptions: any) {
return new Promise((resolve) => {
request(url, { ...newOptions }).then(res => {
if (res?.type === 'application/octet-stream') {
resolve(res)
return
}
if (!res?.success) {
const msg = res?.errmsg || res?.errMsg
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw { ...res, message: msg }
return
}
resolve(res)
}).catch(err => {
//这一步很关键,就是要对响应出错做统一的处理,官方提供的errorHandler是对服务器出错做统一处理,而不是后台返回的响应出错做统一的处理
handleResError(err)
})
})
}
//把封装好的抛出去
export default wrapRequest
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
具体详见dva官方文档:dva介绍