浅谈一年内的React开发经历

算算时间,从第一次接触 react项目到如今已经一年时间,期间一直想写点react的开发心得与经验,可是因为各类缘由搁置了(其实就是懒hhh),这一年也接触了一些项目,如今按照时间线浅谈一下项目经历,也为以后计划写的React笔记理理思路css


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 来解决。react

2.实现鉴权功能
为了实现token过时就跳转登陆页的功能,改写了框架里的request.js请求函数,在fetch方法后面添加了then回调,经过判断response中的code来跳转登陆而且清除缓存。ios

3.短信验证码组件
由于这个项目多处用到了验证码,因此写成了组件。虽然就几行,可是为了良好的交互体验仍是花了些时间完成的,主要代码以下:css3

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

在邮箱系统顺利交付完成后,就去面试了我目前工做的这家公司,问了js基础和css3的一些经常使用属性,接着主要围绕react问了一些生命周期,组件间的传值的问题,惋惜关于shouldComponentUpdate生命周期的问题没回答上来,不过整体感受仍是比较顺利的,过了一周,就拿到offer入职了。面试

入职后了解到我所在的部门主要研发的是面向政府、国企的党建系统。上岗后接触的第一个项目就是数据可视化的云屏系统,说的简单点就是用Echarts之类的图表或轮播图把后端返回的数据很花哨的渲染到整个屏幕,技术栈为react+antd+dva+umiaxios

当时这个项目的二期刚启动,个人任务是实现大屏的编辑功能,有些须要提早说明一下:大屏的模块虽然各式各样,可是接口返回的数据格式被限定成了三种(基础信息/图表/图文),因此大方向就是针对这三种数据格式写三种编辑组件。下面围绕图文类编辑组件讲讲本身在开发过程当中的收获。后端

云屏图文类编辑组件

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

需求肯定后,首先决定用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'

//建立一个带基础配置的实例
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
   */
  const contentType = headers['content-type']
  const contentDisposition = headers['content-disposition']

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

  if (objRegex && objRegex[1] && (contentType === 'application/vnd.ms-excel') {
    const filename = objRegex[1]
    const blob = new Blob([data], { type: contentType })
    fileDownload(blob, filename)
    return null
  }
  return data || ''
}, error => { ... })

export default instance

2.EditorInput组件

使用场景:当编辑接口能够面向单个字段,而且在编辑时不影响页面视图其余部分。
组件说明:
1.基于AntD Input的受控组件
2.有显示和编辑两个状态,经过点击事件切换
3.编辑完成点击提交请求API,更改为功则更新内容。

组件交互以下:
image

实现过程当中的难点主要在于点击事件,首先须要用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组件,完成后的交互以下:
image

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

4.在表格底部展现每列的总计
当时的需求是在Table下方展现出一行Footer做为每一列的总计,可是Antd的Footer属性返回的是一个Dom,不支持每列对应的场景,如图:
WechatIMG1.png

可是实现起来遇到以下难点:
1.Table不分页,可是能够横纵方向滚动。
2.表格列是动态的。

原本想法是在Footer中写N个div(N表明列数),而后再固定好每列的宽度来作到对齐。可是后来发现固定的宽度只能是百分比(否则显示会出现问题),而表格列是动态的,则须要每次都动态计算每一个div的宽度,再想一想出现x轴滚动条的场景后,我立马pass了这个解决方案。。
最后借鉴了这篇文章,终于豁然开朗。
最终的解决方案:用两个Table来实现,一个渲染原Table, 一个渲染底部footer元素。再配合样式覆盖,隐藏掉Table Footerthead以及原Table滚动区域的滚动条。最后再加入让两个 table 的水平滚动位置对齐的js就完事了。


鲸小云 / 2020-06

这是公司内部使用的系统,目前还在迭代。启动这个项目的时候,Antd4.0刚发布不久,因此愉快地将antd升级到了4.0,并采用流行的react hook进行开发。开心的是,深深的感觉到4.0的更好用了👍,react hook写法上也比class更简单明了,彷佛都在向好的方向发展~,再一次感觉到一线开发人员的伟大,尤为是不分国界的开源精神。

分享一个项目中的SearchBar组件,该组件比较简单,主要目的是为了Team可以统一搜索区域的页面样式,只须要专一于业务开发,以下图:
image
调用方式以下:

<SearchBar
    queryItems={[
        <FormItem {...layout} label="名称" name="name">
            <Input />
        </FormItem>
     ]}
     optionBtns={[
        <Button icon={<PlusOutlined />} onClick={addNewAgent}>新建</Button>,
        <Button type="primary" icon={<VerticalAlignBottomOutlined />} onClick={doExport}>导出</Button>
      ]}
      onFinish={search}
/>

陆陆续续终于写完了,回顾这一年经历的项目,技术栈多为react+antd,再到后来的hook,我也算是踏进了react的大门啦,下一阶段的重心就是准备interview,同时写写学习总结。但愿能够拿到大厂offer,追遇上学长的步伐。

相关文章
相关标签/搜索