算算時間,從第一次接觸 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+umi
。axios
當時這個項目的二期剛啓動,個人任務是實現大屏的編輯功能,有些須要提早說明一下:大屏的模塊雖然各式各樣,可是接口返回的數據格式被限定成了三種(基礎信息/圖表/圖文),因此大方向就是針對這三種數據格式寫三種編輯組件。下面圍繞圖文類編輯組件講講本身在開發過程當中的收穫。後端
上圖就是雲屏的樣子,彈窗就是圖文類編輯組件。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 }) }
當時思考的方向就是屬性之間不要有功能的重疊,避免多餘無用的屬性,再結合雲屏的編輯功能的使用場景以下:
按照如上思路完成了三種編輯組件,雖然以後又添加了幾種數據格式的編輯組件,不過都大同小異。因爲這個項目的重點仍是在頁面的展現效果上,因此也沒遇到其餘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,更改爲功則更新內容。
組件交互以下:
實現過程當中的難點主要在於點擊事件,首先須要用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。
4.在表格底部展現每列的總計
當時的需求是在Table
下方展現出一行Footer做爲每一列的總計,可是Antd的Footer屬性返回的是一個Dom,不支持每列對應的場景,如圖:
可是實現起來遇到以下難點:
1.Table
不分頁,可是能夠橫縱方向滾動。
2.表格列是動態的。
原本想法是在Footer中寫N個div(N表明列數),而後再固定好每列的寬度來作到對齊。可是後來發現固定的寬度只能是百分比(否則顯示會出現問題),而表格列是動態的,則須要每次都動態計算每一個div的寬度,再想一想出現x軸滾動條的場景後,我立馬pass了這個解決方案。。
最後借鑑了這篇文章,終於豁然開朗。
最終的解決方案:用兩個Table
來實現,一個渲染原Table
, 一個渲染底部footer
元素。再配合樣式覆蓋,隱藏掉Table Footer
的thead
以及原Table
滾動區域的滾動條。最後再加入讓兩個 table 的水平滾動位置對齊的js就完事了。
鯨小云 / 2020-06
這是公司內部使用的系統,目前還在迭代。啓動這個項目的時候,Antd4.0剛發佈不久,因此愉快地將antd升級到了4.0,並採用流行的react hook
進行開發。開心的是,深深的感覺到4.0的更好用了👍,react hook
寫法上也比class
更簡單明瞭,彷佛都在向好的方向發展~,再一次感覺到一線開發人員的偉大,尤爲是不分國界的開源精神。
分享一個項目中的SearchBar組件,該組件比較簡單,主要目的是爲了Team可以統一搜索區域的頁面樣式,只須要專一於業務開發,以下圖:
調用方式以下:
<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,追遇上學長的步伐。