淺談一年內的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,追遇上學長的步伐。

相關文章
相關標籤/搜索