乐趣区

关于react.js:浅谈一年内的React开发经历

算算工夫,从第一次接触 react 我的项目到当初曾经一年工夫,期间始终想写点 react 的开发心得与教训,然而因为各种起因搁置了(其实就是懒 hhh),这一年也接触了一些我的项目,当初依照工夫线浅谈一下我的项目经验,也为之后打算写的 React 笔记理理思路


Panshi Mail 邮箱零碎 / 2019-07

从第一家公司到职后才正式接触 React 我的项目 (杭州某公司的邮箱零碎),这个我的项目是和学长们一起利用业余时间共同完成的,因为大伙在不同的城市,所以都是线上沟通对需要,交付的那天还一起熬了夜,学长们教会了我很多,当初想起那还是很欢快的一段时光????。
言归正传,该我的项目是仿照 Gmail 设计,供公司内网应用的邮箱零碎,我负责后盾治理模块的开发,过后应用的 Ant Pro 框架,对于我这种没有搭过架子的人来说,Ant Pro 真的是帮了大忙,整合了全局路由 / 数据申请 / 状态治理等一系列实用的性能。记得在我的项目正式开始前,我花了一周工夫认真看了 react/antd/dva/umi 的文档,react 那个官网井字棋也反反复复写了两遍,Antd 的组件也全副相熟了一遍,不得不说,Antd 的 UI 真的很漂亮,只是感觉 Form 组件用起来有点简单,一旦加些简单的交互,就会遇到各种问题。过后对于 dva 和 umi 其实也是只知其一; 不知其二,然而曾经来不及解释,我的项目就这样开始了。
我的项目的开发大略花了 1~2 个月,因为我负责的模块比较简单,具体过程就不一一赘述了,在这里就挑几个印象粗浅的问题简略讲讲。

1.react 的款式抵触
当两个款式文件中起了雷同的类名就会引起款式抵触,能够应用顶级类名或者 css in js 来解决。

2. 实现鉴权性能
为了实现 token 过期就跳转登录页的性能,改写了框架里的 request.js 申请函数,在 fetch 办法前面增加了 then 回调,通过判断 response 中的 code 来跳转登录并且革除缓存。

3. 短信验证码组件
因为这个我的项目多处用到了验证码,所以写成了组件。尽管就几行,然而为了良好的交互体验还是花了些工夫实现的,次要代码如下:

onGetCaptcha = () => {dispatch({})···// 此处省略了申请局部
    let count = 59;
    this.setState({count});
    this.interval = setInterval(() => {
      count -= 1;
      this.setState({count});
      if (count === 0) {clearInterval(this.interval);
      }
    }, 1000);
};
<Button disabled={count} onClick={this.onGetCaptcha}>{count? `${count} s`: '发送验证码'}</Button>

数据可视化云屏 / 2019-10

在邮箱零碎顺利交付实现后,就去面试了我目前工作的这家公司,面试过后的局面还是第一次遇到,办公室坐了近十个人,询问后解释到都是 saas 部门的人,产品和研发都在,最初会一起给出意见。好吧,面试就这样开始了,首先问了 js 根底和 css3 的一些罕用属性,接着次要围绕 react 问了一些生命周期,组件间的传值的问题,印象里对于 shouldComponentUpdate 生命周期的问题没答复上来,而后后端又问了些对于前后拆散的问题。前前后后面试继续了近 20 分钟,不过过程感觉还是比较顺利的,过了一周,就拿到 offer 入职了。

入职后理解到我所在的部门名为党建产品部,次要研发的是面向政府、国企的党建零碎。言归正传,上岗后接触的第一个我的项目就是数据可视化的云屏零碎,说的简略点就是用 Echarts 之类的图表或轮播图把后端返回的数据很花哨的渲染到整个屏幕,技术栈为react+antd+dva+umi

过后这个我的项目的二期刚启动,我的工作是实现大屏的编辑性能,有些须要提前阐明一下:大屏的模块尽管各式各样,然而接口返回的数据格式被限定成了三种(根底信息 / 图表 / 图文),所以大方向就是针对这三种数据格式写三种编辑组件。上面围绕图文类编辑组件讲讲本人在开发过程中的播种。

上图就是云屏的样子,弹窗就是图文类编辑组件。

需要确定后,首先决定用 Antd 的 Modal 实现弹窗,其次就要思考组件须要有哪些 props,在屡次尝试后最初得出如下几个属性:

interface IProps{
    initialVal?, // 初始值
    moduleId: string, // 模块 id
    visible: boolean, // 是否可见
    isShowIcon?:boolean, // 是否显示图标抉择
    onClose: (append?) => void, // 敞开弹窗回调
}

组件调用时如下:

<ImageDialog
    moduleId="5_1"
    isShowIcon
    initialVal={this.state.data_5_1}
    visible={this.state.isShowDialog5_1}
    onClose={this.handleCloseDialog5_1}
/>
handleCloseDialog5_1 = (data) => {const { isShowDialog5_1} = this.state;
    if (isShowDialog5_1 && data) {
        this.setState({data_5_1: data})
    }
    this.setState({isShowDialog5_1: !isShowDialog5_1})
}

过后思考的方向就是属性之间不要有性能的重叠,防止多余无用的属性,再联合云屏的编辑性能的应用场景如下:

  • 页面首次渲染的时候会逐个调用每个模块的详情接口,所以点击各个模块进行编辑的时候,须要把数据传递给编辑组件,防止再次申请。
  • 进行编辑操作时,须要给出反馈来晋升交互体验,能够给 Modal 中的 Spin、Button 等组件增加 loading 状态,同时增加上 message 提醒。
  • 当实现对模块对编辑操作后,更新的数据要体现在页面上,所以在 Modal 的敞开回调中,要更新页面的状态,同时也须要重置组件外部状态。

依照如上思路实现了三种编辑组件,尽管之后又增加了几种数据格式的编辑组件,不过都大同小异。因为这个我的项目的重点还是在页面的展现成果上,所以也没遇到其余 react 相干问题,不过在经验完这个我的项目后,倒是对 Echarts/Bizcharts 的应用更加纯熟了,在格式化数据的过程中也把握了数组的罕用函数,比方能够应用 slice 很简洁的实现如下需要:需要是轮播图每页须要展现三条数据,接口会返回一个蕴含所有数据的一维数组(就叫它 arr),前端须要把 arr 解决成每三个为一组。

const res = [];
for(let i = 0; i < arr.length; i+=3){res.push(arr.slice(i,i+3))
}

Particle Martin CMS / 2020-01

云屏我的项目实现没多久,就被安顿去杭州驻地开发了????。杭州那个我的项目比拟乱,就不写了。不过在业余时间投入到了名叫 Particle Martin 的我的项目中,这是我和一位学长共同完成的我的项目,技术栈react+antd+axios,是一个逻辑比较复杂的 CMS,当学长进入字节后就剩我一人保护了,外面很多性能的实现形式都很棒,上面缓缓梳理梳理。

1. 申请办法的封装
利用 axios.create()封装了申请实例,一并处理了文件下载、权限验证和谬误提醒。尤其是文件下载的判断逻辑让业务层少写了很多代码。申请实例的局部细节和调用办法如下:

import axios from 'axios'
import fileDownload from 'js-file-download'
import {baseURL} from '../constants/apiConfig'
import {loginUrl} from '../constants/config'

// 创立一个带根底配置的实例
const instance = axios.create({
  baseURL,
  withCredentials: true,
})

instance.interceptors.response.use(res => {

  let data = res.data
  const headers = res.headers

  /**
   * 首先判断是文件下载的逻辑
   * 判断条件 response headers
   * Content-Disposition: attachment;filename="export.xlsx"
   * Content-Type: application/vnd.ms-excel || application/octet-stream
   */
  const contentType = headers['content-type']
  const contentDisposition = headers['content-disposition']

  const objRegex = /filename="([^"]+)"/.exec(contentDisposition)

  if (objRegex && objRegex[1] && (contentType === 'application/vnd.ms-excel' || contentType === 'application/octet-stream') {const filename = objRegex[1]
    const blob = new Blob([data], {type: contentType})
    fileDownload(blob, filename)
    return null
  }
  return data || ''
}, error => {if (error.response.status === 401) {
    // 没有登录 间接返回登录页面
    window.location.replace(loginUrl)
    return error.response.data
  }
  ...
})

export default instance
import axios from './instance'

export const exportStatus = data =>
  axios({
    url: '/export/xxx',
    method: 'POST',
    data,
    responseType: 'blob',
  })

2.EditorInput 组件

阐明:可编辑的文本组件,默认文本显示模式,通过点击关上编辑模式。

应用场景:1. 编辑接口是面向单个字段的 API。2. 编辑指标字段时,不影响页面其余视图局部。

设计思路
1. 受控组件,基于 AntD Input
2. 有显示和编辑两个状态,通过点击事件切换
3. 编辑实现点击提交申请 API,更改胜利则更新内容。

组件交互如下:

实现过程中的难点次要在于点击事件,首先须要用 React.createRef() 获取到 DOM,而后通过 DOM.contains(e.target) 判断以后组件的状态及更改状态的触发条件,组件代码如下:

import React from 'react'
import classNames from 'classnames'
import {Input, message, Button} from 'antd'
import './index.scss'

/**
 * 在原有 Input 组件根底上减少的相干 props
 * onSubmit // 提交回调
 * required
 * placeholderClassName
 * placeholderStyle
 * wrapperClassName
 * wrapperStyle
 */
class EditorInput extends React.Component {
  state = {
    isEditing: false,
    value: this.props.value || this.props.defaultValue || '',
  }

  containerRef = React.createRef()
  placeholderRef = React.createRef()

  componentDidMount() {
    document.body.addEventListener('click', this.handleOtherDOMClick, {
      capture: false,
      passive: true,
    })
  }

  componentDidUpdate(preProps) {if (preProps.value !== this.props.value) {
      this.setState({
        isEditing: false,
        value: this.props.value,
      })
    }
  }

  componentWillUnmount() {
    document.body.addEventListener('click', this.handleOtherDOMClick, {
      capture: false,
      passive: true,
    })
  }

  handleOtherDOMClick = e => {
    const containerDOM = this.containerRef.current
    const placeholderDOM = this.placeholderRef.current
    const {isEditing} = this.state
    const {loading} = this.props

    if (placeholderDOM) {if (placeholderDOM.contains(e.target) && !isEditing && !loading) {
        // 进入编辑
        this.setState({isEditing: true,})
      }
    }

    if (containerDOM) {if (!containerDOM.contains(e.target) && isEditing && this.props.autoClose) {
        // 点击外侧不提交批改 间接还原批改
        this.handleCloseEdit()}
    }
  }

  handleCloseEdit = () => {const { value} = this.props
    this.setState({
      value,
      isEditing: false,
    })
  }

  handleValueChange = e => {
    const value = e.target.value
    this.setState({value,})
    this.props.onChange && this.props.onChange(e)
  }

  // 实在的提交数据回调
  handleSubmitValue = e => {const { onSubmit, required, onPressEnter} = this.props
    const {value} = this.state

    if (onPressEnter) {onPressEnter(e)
    }

    if (required && value.trim().length === 0) {message.error('you must input something')
    } else {onSubmit(value)
      this.setState({isEditing: false,})
    }
  }

  render() {const { isEditing, value} = this.state
    const {
      size = 'default',
      containerClassName = '',
      containerStyle = {},
      placeholderClassName = '',
      placeholderStyle = {},
      loading,
      autoClose,
      ...others
    } = this.props

    const mappingPlaceholderHeight = {
      large: '40px',
      default: '32px',
      small: '24px',
    }

    const placeholderHeight = mappingPlaceholderHeight[size]

    return (
      <div
        className={classNames('editor-input-container', { [containerClassName]: true })}
        style={containerStyle}
        ref={this.containerRef}>
        {isEditing ? (<div className="editor-input-wrapper" key={1}>
            <Button
              shape="circle"
              icon="close"
              size="small"
              className="editor-icon-button"
              onClick={this.handleCloseEdit}
            />
            <Button
              shape="circle"
              icon="check"
              type="primary"
              size="small"
              className="editor-icon-button"
              onClick={this.handleSubmitValue}
            />
            <Input
              {...others}
              className="editor-input-element"
              value={value}
              size={size}
              onChange={this.handleValueChange}
              onPressEnter={this.handleSubmitValue}
              disabled={loading}
            />
          </div>
        ) : (
            <div
              key={2}
              className={classNames(
                'ant-input editor-value-placeholder-wrapper',
                {[placeholderClassName]: !!placeholderClassName,
                }
              )}
              style={{
                minHeight: placeholderHeight,
                ...placeholderStyle,
              }}
            >
              <span
                ref={this.placeholderRef}
                className={classNames(
                  'editor-value-placeholder',
                  !value && 'no-value'
                )}
              >
                {value || 'Empty'}
              </span>
            </div>
          )}
      </div>
    )
  }
}

export default EditorInput

3. 拖拽排序功能
列表中的排序是通过拖拽实现的,抉择了 react-dnd 组件,实现后的交互如下:

个人感觉,这个排序功能的交互体验十分好!这也是我第一次接触 react-dnd 这类的拖拽组件,感觉还能够利用拖拽实现删除性能,比方在窗口右下角固定一张垃圾箱的Img,而后将某条记录的 Dom 拖入垃圾箱来触发 Delete API,日后有机会写个 Demo for fun。

因为代码量略多,上面就不粘代码片段了,就说说对 react-dnd 组件的应用心得与感悟。

4. 在表格底部展现每列的总计
过后的需要是在 Table 下方展现出一行 Footer 作为每一列的总计,然而 Antd 的 Footer 属性返回的是一个 Dom, 不反对每列对应的场景, 如图:

然而实现起来遇到如下难点:

1.Table不分页,然而能够横纵方向滚动。
2. 表格列是动静的。

原本想法是在 Footer 中写 N 个 div(N 代表列数),而后再固定好每列的宽度来做到对齐。然而起初发现固定的宽度只能是百分比 (不然显示会呈现问题),而表格列是动静的,则须要每次都动静计算每个 div 的宽度,再想想呈现 x 轴滚动条的场景后,我立马 pass 了这个解决方案。。
最初借鉴了这篇文章,终于恍然大悟。

最初的解决方案:用两个 Table 来实现,一个渲染原 Table, 一个渲染底部footer 元素。再配合款式笼罩,暗藏掉 Table Footerthead以及原 Table 滚动区域的滚动条。最初再退出让两个 table 的程度滚动地位对齐的 js 就完事了。

退出移动版