【React】戲說元件式百度圖表的由來及簡單邏輯

DaWi發表於2019-04-08

溫馨提示:如果您對EchartsReact足夠了解,並且想直接了當的看元件式百度圖表react-component-echarts的解決方案,那麼為了不浪費您寶貴的時間,建議您跳過前面的部分,直接從解決方案部分開始看起。

前言

在眾多資料視覺化產品中,Echarts可謂中流砥柱,其功能的強大、靈活在圖表界的水平以頂尖來形容,個人感覺一點也不過分;我作為一名前端開發人員,也經常用Echarts來解決公司或個人專案中的圖表需求,甚是得心應手;不得不說,在我從事研發工作多年以來,還能保持著一頭烏黑的短髮,Echarts有一部分的功勞,我非常感謝她,幫我解決了不少煩惱。

後來,我遇到了React,她宣告式元件化一次學習,隨處編寫的三大特性一下將我拉向了一個全新的世界,恍惚間我隱約明白,原來世界還能這樣執行;我不斷的瞭解著她,每天與她促膝長談、通力合作,完成了一個又一個專案;是時候了,該向她介紹我的另一位老朋友與她認識了;沒錯,就是Echarts,我覺得大家會很愉快的接納對方;但,事與願違。

我似乎聞到了一絲火藥味;是了,她們都是強者,性格各異,我作為中間人,要磨合她們。

Echarts

Echarts的工作方式十分簡單,你只需要告訴她要什麼圖表、展示什麼資料就可以了,毫秒之間,原本乾枯的資料就會變成繪聲繪色的視覺化圖表,讓人有一種成就感滿滿的錯覺,就像下面這樣:

 const dom = document.getElementById('container')
 const myChart = echarts.init(dom)
 const option = {
            xAxis: {
                type: 'category',
                data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
            },
            yAxis: {
                type: 'value'
            },
            series: [{
                data: [820, 932, 901, 934, 1290, 1330, 1320],
                type: 'line'
            }]
        }
myChart.setOption(option, true)
複製程式碼

如果將這段程式碼放在一個完整的HTML中,開啟瀏覽器,你會看到一幅美麗的圖表,跟下面這張圖片一樣,而且瀏覽器中還是帶動畫的:

Basic Line Chart
單看一堆數字,很難看清楚他在表達什麼,如果將他轉換成圖表,那麼結果就不言而喻了,這就是類Echarts庫的魅力所在。

React

在全新的世界中,我著重說一點:元件化,這是我喜歡React的一大原因;何為元件化?我這裡做個不太恰當的比喻:零件;一個機械化的手錶,裡面由若干個精密的齒輪、發條完美的結合在一起,各自分工,最終都有一個共同的目標:表達時間;我們寫元件,就是在做零件,可以自己做,也可以拿別人做好的;再通過自己的加工打磨,組合在一起,就完成了一個目標,最終構成一個複雜的介面;這就是我所理解的,使用React最基本、最主要的工作模式。

一個簡單的React元件就像這樣:

// 顯示當前時間
class DateNow extends Component {
    constructor(props) {
        super(props)
        this.state = {
            now: Date.now()
        }
    }
    componentDidMount() {
        setInterval(() => this.setState({ now: Date.now() }), 1000)
    }
    render() {
        const { name } = this.props
        const { now } = this.state
        return (
            <div className="date-now">
                <p>當前時間:{Date(now)}</p>
                <p>你好:{name}</p>
            </div>
        )
    }
}
// 當前時間:Sun Apr 07 2019 16:30:10 GMT+0800 (中國標準時間)
// 你好:安妮
<DateNow name="安妮" />
複製程式碼

這樣一個零件就製作好了,哪裡使用安哪裡。一個設計巧妙的元件,總應該儘可能的發揮它自己的作用,這才是寫一個元件的意義,不過現實是,當你前期不遺餘力的編寫好了很多個元件,考慮了足夠多的應用場景,準備在將來一展拳腳,向老闆展示自己真正的實力的時候,回過頭來才發現,專案...亡了,這是一個足夠悲傷又透露著可笑的故事,然而這也是現實中絕大多數專案的一種情況,所以說我們在從頭開始做一個專案的時候,真的要好好考慮它的實際情況,一上來就從開山掘石的起點去蓋樓是極不明智的,它或許沒有那麼多的時間來等你做準備工作,其實拿來主義的背後透露著三個字:更可靠;公司的專案能夠準時交付、業務線能穩定增長,是需要更成熟的輪子來作為鋪墊的,即便你的能力再強、邏輯再縝密、造的輪子零BUG,那也是需要時間的,而往往很多專案來不及等就要黃了,更何況自己趕時間造的輪子漏洞百出;總之,我的看法就是:專案與學習要分清,專案就是要更靠譜,學習才能去造輪子,說難聽點,專案不是你練手的地方。

撤遠了,這不是我們們現在討論的主題,談到元件就由感而發,還請閱者批正;不過話說回來,如果在工作之餘,能夠造一些基礎輪子還是很有必要的,這種輪子不屬於任何一類專案,只是很明確的解決一種需求或完成一個基本功能,這是一勞永逸的事情。

再回到正題,我們們接下來的主題是:介紹EchartsReact成為好朋友,讓她們更默契的為大家工作。

強行組合

為什麼說要讓她們更默契呢,這裡不得不將她們拉出來讓大家看看,為什麼我這麼說:

class Exmaple extends Component{
    componentDidMount(){
        const myChart = echarts.init(this.chartDom)
        const option = {
                    xAxis: {
                        type: 'category',
                        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
                    },
                    yAxis: {
                        type: 'value'
                    },
                    series: [{
                        data: [820, 932, 901, 934, 1290, 1330, 1320],
                        type: 'line'
                    }]
                }
        myChart.setOption(option, true)
    }
    render(){
        return (
            <div ref={chartDom=>this.chartDom=chartDom}/>
        )
    }
}
複製程式碼

這樣的程式碼是能執行,但是EchartReact基本上是自己幹自己的,完全沒有要相互靠攏的意思,這還怎麼更好的玩耍呢?而且大段的option也很難維護,在實際專案中,若干個屬性值還是動態資料,比如這裡的series[].data,萬一圖表複雜,動態資料更多,如果頁面上還有很多其它元素,那最後你就會發現自己寫了一坨程式碼,過後自己都不想再看,更別說讓別人維護了;即便把圖表相關的程式碼拿出來另起檔案,一樣是各玩各的,極不和諧。

有道是:程式碼千萬行,註釋第一行;編碼不規範,同事兩行淚,何解呢?

遇到問題總是要解決的,即便測試的小姐姐們以容嬤嬤的步伐緩緩走來、面帶蒙娜麗莎般的微笑告訴我:上線吧。但那一坨魔一樣存在的程式碼總是時不時的在我心裡冒出來嬉笑自己一番,真真的飯也不香、夜也不眠吶;想辦法解決它吧,誰叫我們是有追求的人呢?

剎那間,妖風四起,天空中滾滾烏雲壓將下來,我一陣翻雲倒海,與那一坨魔一樣的程式碼斗的你死我活,就在我快要支撐不住的時候,忽然感覺右肩一沉,嚇的我一個機靈,然後一聲低沉的聲音入耳:發什麼楞呢?嗚呼~,原來是自己走火入魔了,不過我已經想到了解決方案。

解決方案

直觀來講,Echarts為配置式開發,主要維護一個option就可以了;React為元件式開發,對外可接收props引數,內部通過state控制邏輯,再加上jsx來編寫頁面結構,簡直完美!那麼,何不以元件式的形式來完成Echarts的開發?就像這樣:

配置式與元件式對比

單從行數上來看,已經少了很多,可維護性也更高,將一個龐大的配置物件拆分成一個個屬性,開發人員只需要關心可變化的那一部分即可,或通過state,或通過redux等來儲存資料,邏輯一下子清晰了很多,那種熟悉又親切的感覺找回來了!

想法是好的,如何實現呢?

將配置與jsx仔細一對比會發現,其實它們的結構是相似的,可以說是一至的,Recharts可以作為option物件本身,那麼它的子節點XAxisYAxisSeries就對應著option物件的三個屬性,而這三個屬性的值在jsx上又以props的形式來體現,一一對應起來了,簡單來說,程式碼是這樣子的:

import React, { PureComponent } from 'react'
export default class Recharts extends PureComponent {
    constructor() {
        super()
        this.option = {}
    }
    componentDidMount(){
        this.chart = echarts.init(this.dom, ...)
        this.chart.setOption(this.option)
    }
    render(){
        const {children} = this.props
        return (
            <div ref={dom=>this.dom=dom}>{children}</div>
        )
    }
}
複製程式碼

這裡主要做了兩件事,一是儲存了配置option物件,一是建立並初始化了圖表,根節點的任務算是完成了大半了,但是這個時候option是個空物件,裡面什麼也沒有,圖表自然也出不來,它的資料主要來自子節點,那麼子節點的任務就很明瞭了,將接收到的props原封不動向上傳遞,統統砸向父節點就完了。

問題來了,如何向上傳遞?React的特性是自上而下,父節點想給子節點傳遞資料很簡單,通過props傳就可以了,但是子節點向父節點傳呢?只能通過函式來傳遞了,我也不得不這麼做:

// 根節點
export default class Recharts extends PureComponent {
    ...
    // 接收子節點配置
    // @name 子節點名稱
    // @option 子節點配置
    handleReceiveChildOption = (name, option) => {
        // 進一步處理
        ...
    }
    render(){
        return (
            <div ref={dom => (this.dom = dom)}>
                    {React.Children.map(this.props.children, children => {
                        if (isValidElement(children)) {
                            return React.cloneElement(children, {
                                triggerPushOption: this.handleReceiveChildOption
                            })
                        }
                        return children
                    })}
                </div>
        )
    }
}

// 子節點
export default class BaseComponent extends PureComponent {
    componentDidMount(){
        const { triggerPushOption, children, ...props } = this.props
        if(triggerPushOption) {
            triggerPushOption(this.name, props)
        }
    }
    render(){
        return this.props.children
    }
}
複製程式碼

這個時候,子節點的任務基本就完成了,它只需要簡單的向上傳遞就可以了,無需再做其它處理,其實這裡子節點還需要考慮一種情況就是子節點也有子節點:<Toolbox><Feature/></Toolbox>,要處理這種情況很簡單,像父節點那樣,給children傳遞一個函式,在函式裡接收資料,合併後繼續向上傳遞就可以了,這裡不再給出程式碼。

是不是就大功告成了?是的,主要邏輯就是這麼簡單,Echarts本身就很強大,我做的只不過是按需將jsx生成option並適時建立圖表就可以了;當然,實際程式碼要複雜一些,因為我還考慮了子節點是陣列的時候如何處理、容器改變大小時如何resize圖表、如何合併option以達到減少setOption操作的目的等等,還有很重要的一點就是,option屬性雖多,但做為子節點它們的行為都是一致的,我只需要將屬性羅列下來,通過陣列來擴充套件BaseComponent就可以了,細節邏輯也比較簡單,就不在這裡細說了,感興趣的朋友可以看原始碼

輔助工具

在我喪心病狂的擼完了程式碼,心喜若狂的準備大幹一番的時候才發現,原來我又給自己製造了一個煩惱,這話怎麼說?我相信絕大多數同學在寫Echarts圖表時,一定是從官方示例中找一個跟自己需求類似的圖表,將其option複製貼上到自己專案中,再按需改改,一個圖表的功能就完成了,快速直接,簡直一個字:美!

但是換成jsx的寫法時..., WTF!我需要將一個個的屬性重新寫成jsx,然後再一個個的把屬性值寫成props,當我把一個比較複雜的圖表全部改成元件式後,我的狀態是:我是誰?我在哪?我在做什麼?如果就此結束,那麼react-component-echarts也就沒什麼作用了,原因三個字:太難用。

我沒有放棄!

伴隨著憤怒的鍵盤敲擊與MacBook風扇嗡嗡作響的哀嚎聲中,為react-component-echarts專門服務的輔助工具應運而生了,它長這個醜樣:

輔助工具

輔助工具主要由三部分組成:

  • 左側:可編輯,此處貼上自己的option
  • 右上:不可編輯,根據左側option生成所依賴的元件
  • 右下:不可編輯,根據左側option生成元件式圖表程式碼

還有隱藏的第四部分與第五部分,一個是提示替換變數,一個是option程式碼有錯誤,生成程式碼失敗時會出現,分別是:

提示替換變數

程式碼有錯誤

程式碼有錯誤這個就不用多解釋了,檢查自己的option有什麼問題改過來就好了;提示替換變數需要說明一下,好比你從官網示例中複製了一段配置,配置裡面某一項的值是取的外部變數,那麼這個時候在輔助工具的執行環境是沒有這個變數的,生成時就會出錯,我在這裡做了一個容錯處理,發現有變數未定義這種錯誤時,會自動建立一個同名變數,並且在生成圖表程式碼時,變數以$varName$的形式存在,當你發現有這個提示時,只需要把以$開頭結尾的字串替換為真實變數就可以了。

從此,EchartsReact終於愉快的生活,哦不,愉快的工作在一起了。

後記

本人水平有限,文筆有限,以上文字還請海涵;另外,如果發現react-component-echarts邏輯有什麼問題或者有什麼更好的建議,還請明示,非常感謝!

React Component Echarts 相關連結 (給個 Star 唄):

相關文章