對React應用的一些思考

前言

由於筆者對React的了解不深,即便算是學習React的時間,到目前也才剛剛半年,所以錯誤不足之處還望指正。以下都是基於React 15(可能有些是16),webpack1進行探討(註:未學習過Vue,Ng,Ember,Cycle,Immutable,Redux-Saga,Mobx,Observable,Rxjs等等,所以可能有些方面已經被提及或者解決了,希望不要介意)。

正文

本文的排版可能不那麼正經,想到哪寫到哪,見諒見諒。(先在其它地方用md寫的,我悔過。。)

組件

  • JSX
    • 備受關注(吐槽)的if問題: 要討論這個問題,首先要明白jsx是轉換的是什麼,只有這樣,你才明白為什麼不能在jsx裡面寫if語句,switch語句,為什麼只能寫表達式等等。好了,回到正題,我們困撓,那麼其他的程序猿肯定也是這樣滴,那麼正確的姿勢是什麼呢?沒錯,提isssue&討論,我們先來看看已有的提出的一些方案(來自JSX的issue,其他的諸如三目,把邏輯提取出來寫在外面,多return方式,IIFE,do expression等等這裡就不多說了):
  • NO.1

https://github.com/facebook/jsx/issues/65#issuecomment-254056396nn<if {myCondition}>n <div>This part only gets shown if myCondition is true</div>n</if>n

  • NO.2

https://github.com/facebook/jsx/issues/65#issuecomment-255465492(官方人員提出的,所以採用的概率稍微要高一點,基於do expression)nn<div>n Hi!n {if (bool) <MyComponent /> else <div></div>}n</div>n

  • NO.3

https://github.com/facebook/jsx/issues/65#issuecomment-255484351nn<Ifn condition={ condition }n then={ <component /> }n else={ <component /> }n/>nnfunction If(props) {n const condition = props.condition || false;n const positive = props[then] || null;n const negative = props[else] || null;nn return condition ? positive : negative;n}n

  • 當然,新的語法就意味著新的符號(一般來說),所以這種變化不一定是好的,也不一定每個人都能接受。
  • 比如bjrmatos說的

"Its just JavaScript, not a template language" -> no need to replicate JS functionalities with custom syntax. That is the main benefit of JSX IMO, seriously is so easy to do this with js even if it looks "weird" (for me it is not weird, it is just the syntax of the language)

  • 比如jmar777說的:

[if (condition)]n <Foo />n <Bar />n <Baz />n[/if]n

We instead need:

{ condition && <Foo /> }n{ condition && <Bar /> }n{ condition && <Baz /> }n

If the condition itself gets more complex, then the code gets uglier, or alternatively we need to store the result to a temporary variable, which further increases the verbosity.

I will note that this particular pain point would be eased by better support for fragments.

TBH Ive never seen a specific proposal that Im overly fond of, but inasmuch as branching logic is integral to all but the most trivial of rendered outputs, I think theres a compelling case to be made for first-class support in JSX.

  • Atrribute autocomplete

    這也是我們的願望之一,畢竟可以減少很多無用功,代碼也更整潔。

render () {n const {n prop1,n prop2,n prop3n } = this.propsnn return (n <MyComponent prop1 prop2 prop3 />n )n}n

Or

render () {n const {n prop1,n prop2,n prop3n } = this.propsnn return (n <MyComponent {prop1} {prop2} {prop3} />n )n}n

Instead Of

render () {n const {n prop1,n prop2,n prop3n } = this.propsnn return (n <MyComponent prop1={prop1} prop2={prop2} prop3={prop3} />n )n}n

  • 希望支持數字attribute省略花括弧
  • 還有的人希望對於字元串類型的attribute,能夠省略引號。seb最後也解釋了為什麼沒有允許可以對字元串屬性進行簡寫(因為存在兩種不同的語義,如果允許去掉,到底以哪個為準呢)

https://github.com/facebook/jsx/issues/25#issuecomment-137224657nnI think the conclusion is that since these forms are currently semanticallyndifferent in JSX parsers:nn<input value="Foo & Bar" /> // Foo & Barn<input value={"Foo & Bar"} /> // Foo & BarnnWe should keep it consistent which would mean that this would also be different:nn<input value=`Foo & Bar` /> // Foo & Barn<input value={`Foo & Bar`} /> // Foo & Barn

  • Attribute Destructuring

https://github.com/facebook/jsx/issues/76nn<Foo {fizz, buzz}={bar()} />nn// 當然也可以先解構了再賦過去,不過那樣肯定是要麻煩一點。最後seb也說了n如果https://github.com/RReverser/es-borrowed-props這個提案有進展的話就更好了n

  • Attribute Or Children
  • 寫過組件的同學應該有時候會遇到這樣的情況,我們需要調用方傳過來一個組件,那麼問題來了,這個組件到底是以children的形式還是props的形式傳過來呢?有時候我們只需要一個組件,那麼都可以(當然還有考慮語義和大家的慣性思維)。如果需要多個組件就不行了,就需要進行抉擇,有時候明明覺得都該以children的形式傳進來,但是無奈必須做出取捨。所以我們可以看到比如有些同學就提出了:
  • NO.1 attribute2Children模式

https://github.com/facebook/jsx/issues/65#issuecomment-254060186n<SplitContainern left={<div>...</div>}n right={<div>...</div>}n/>nn// vsnn<SplitContainer>n <.left>n <div>...</div>n </.left>n <.right>n <div>...</div>n </.right>n</SplitContainer>n

  • NO.2 style(其實也是attribute2Children模式)

https://github.com/facebook/jsx/issues/65#issuecomment-254100455nn<style>{`n .your-css-here {color: #333;}n`}</style>nn// ornn<style>{component_style}</style>n

  • 關於bind的問題,目前都基本用class里用箭頭函數的方式了。但是依然存在一個問題,在某些情況下我們依然需要使用bind(當然用閉包也可以,不過面臨的問題是一樣的)。比如,我們想給我們的處理函數傳遞參數,我們就不得不使用諸如<Foo bar={this.handleSomething.bind(null, arg)} />的方式。
  • 但是眾所周知,這樣會造成Foo組件的props每次都會變化,對於PureCompoennt可能有性能影響,(雖然如果這裡不是bar而是ref的話不會存在這樣的問題,因為淺比較的時候根本不會考慮key和ref),當然我們可以自己定義shouldComponentUpdate然後自己手動比較下,如果nextProps的arg和現在的arg一樣的話(當然這裡會根據arg是否是對象比較的策略會不一樣),那麼我們就採用之前的函數(假設我們緩存了)。
  • 這樣一看似乎還行,但是仍然存在一個問題(其實我們完全沒有必要考慮這些,因為對性能真的沒啥影響,性能基本不會通過這些得到改善或者降低,但是本著深挖洞,廣積糧的精神,思考思考還是可以的)。什麼問題呢,就是如果這個arg是一個函數或者說是包含函數的對象,那麼兩個函數相等就能夠推導出他們真的相等嗎?或者說兩個函數不相等就能推導出他們真的不相等嗎?顯然不能這樣,因為函數可能「不純」。這就給我們的判斷工作帶來了一定的影響,之前沒有出錯過是因為出錯的幾率本身就很低,因為props和state本身都是「純的」,沒有人會去手動修改它(即直接賦值)。我們不那麼認真的來看一下下面的例子:

class Bar extends Component {n shouldComponentUpdate (nextProps, nextState) {n if (this.props !== nextProps) {n // 問題就出在這裡,假設此後我們需要調用p2n if (nextProps.p2 !== this.props.p2) {n // 那麼這裡到底該返回true還是false呢n // 返回true,那如果p2()的值沒變怎麼辦,算不算是一種浪費n // 返回false,那如果p2()的值變了怎麼辦n } else {n // 這裡p2相等了,我們能在之後調用之前緩存的p2嗎(假設緩存過了,當然緩存的前提n 一般都是pure的才能緩存哈),也不能因為p2可能從一個的變成不純的了n 另外返回false還是true和上面的同理n }n // 所以最後我們會發現,我們根本沒法判斷,因為我們不知道p2到底「純」還是「不純」n }n }n render () { console.log(...this.props); return null; }n}nnlet externalData1 = extenalData1;nsetTimeout(() => externalData1 = externalData2, 1000);nlet util = (...args) => utilFunc(...args, externalData1);nnclass Foo extends Component {n state = { arg1: 1, arg2: util(aa) }nn componentDidMount () {n this.setState({ arg1: 2, arg2: util(bb) });n }nn handleSomething (...args) {n console.log(...args);nn ...somethingElsen }nn render () {n const { arg1, arg2 } = this.state;nn return (n <Bar p1="p1" p2={this.handleSomthing.bind(null, arg1, arg2)} />n );n }n}n

  • 組件
    • 一般來說,我們將組件分為兩大類,即基礎組件與業務組件。在React中,基礎組件基本已由Ant Design(PC端)覆蓋完畢,將來可能會有一些變動/更新,但是都不會太大。基於基礎組件的封裝/改寫,依舊屬於基礎組件。業務組件基於基礎組件,此時就面臨一個非常重要的問題,數據/數據流相關的處理(這個後面再談)。除此之外,主要想提及一下,還應該有一類組件,暫且稱之為邏輯組件,為什麼要分出來呢,因為確實感覺這兩類都不太能準確的描述它。比如,<If>、<Visible mode="null | opacity | visibility">、<Auth>(或者<OnlyShowWhenIsAdmin>之類的)我們的系統基本就是由這三種組件進行拼裝。
    • 表單。
      • 我們從最簡單的說起。一個input,不包含任何聯動。那麼現在存在一個問題,就是每一個input我們都要建立與之對應的onChange函數(假設我們每一個input都採用Controlled Components的形式,因為可控性要好一點。同時都有自己的邏輯,不單單是setState(e.target.value)),太麻煩了。理想的情況是怎麼樣的呢,我們應該可以採取動態建立的方式,只需要提供欄位名,自動將值注入到state中,以及自動在組件的this或者原型鏈上創建對應的onChange函數,然後再賦給input(當然這樣的話基本上要完全依賴提供的組件庫,不能期望自己隨便寫一個input也能自動達到這樣的效果)。
      • 那麼問題來了,這裡需要加糖(使代碼更少)和defineProperty(或者Proxy)(使修改更直接方便)嗎,個人認為,都不可。因為這都會增加debug的難度。我還是更傾向於前面提到的簡單封裝,然後還是用setState去改變欄位。現在流行的主要有antd的form以及redux-form。但是個人認為這並不是最好的方式,因為感覺有點亂,舉個比喻的話,感覺有點像寫傳統的模板一樣,當然,也許就是要專門治治我們這些處女座。
      • 下面說說聯動的情況。在以前(如jQuery),我們要處理聯動,必須手動維護聯動的關係,所以要麼在onChange裡面根據邏輯手動觸發其他組件的更新,要麼是採用after或者callback queue的方式,總之,重心是維護邏輯而不是數據。那麼是否應該存在一種方式,讓我們的重心靠到數據上面?這樣debug,開發,維護都輕鬆一些。對於React應用而言,天然的解決了部分問題。但還存在一些問題,比如,單向數據流導致的有時數據鏈過長過繁瑣(所以才產生了redux),需要在多地保存同一份數據等等。
      • 以上僅僅是一些思考,關於表單的探索還沒有開始(因為目前表單相關的需求還不是很多和複雜,過段時間研究研究之後希望能解決一些問題)。
  • Encapsulated Compose Or Custom Compose

舉例來說。例如響應式,我了解到這個概念應該是從bootstrap的柵格開始,到後面antd也有對應的Grid系統,包含了Row和Col。那麼問題來了,當實現一個響應式列表或者table的時候,是否應該自己去組合,即哪些地方寫個Row,然後下面的Col的sm,md等等依次是多少,挨著填進去。還是說我們先把這些組合邏輯封裝好,比如:

const responsive = responsivelizeComponent({n xs: 1,n sm: 2,n md: 2,n lg: 2,n pc: 3n});n

然後調用傳入數據以及RowCol對應的組件n{ n responsive(n list,n // 這裡如果C本身就是Row,那麼就進行props合併,如果不是Row,n 那麼需要在處理的時候包裹在Row裡面 n () => <C ... />, n // Item同理,只是換成了Coln () => <Item ... />n ) n}n

    • Component Middleware: 我們知道,中間件的概念由來已久了(歷史就沒有去考證了,node也不熟悉就不談了哈哈)。在redux中,我們就可以創建一系列的中間件去處理我們的action。那麼,組件是否也可以具備這樣的能力呢(即組件中間件)?答案是肯定的,decorator就是一個很好的例子,但是decarator與middleware還是存在一定的差距,一是書寫上不直觀/美觀(目前個人能忍受的就是最多1個@),二是控制的力度太小。三是沒有形成一個整體,各個部分只能跟自己的上游打交道。這些方面才剛剛開始探索,就不在大佬們面前介(zhuang)紹(bi)了。

數據流

  • Redux
    • 細粒度的actionType:
      • 目前來說,我們的action的type甚至是action還是設計得太過簡單。我們完全可以進一步進行設計來減少我們的工作量。比如,我們可以看到redux初始化store時的actionType為@@INIT,受此啟發,我們完全可以自定義更複雜的actionType甚至action,配置中間件進行處理。比如@@FOO@@BAR/xx_TODO,或者像官方例子中的那樣const CALL_API = symbol(CALL_API); action = { [CALL_API]: {...} },然後建立與之對應的middleware,從而減少reducer的數量。當然這是一個開頭,複雜應用應該是存在很多個middleware進行職能分工(沒有複雜應用的經驗,這裡就不多說了)。
      • actionType可以與相應的處理的函數的名字形成包含關係,減少switch里的case代碼量。比如,在reducers/foo.js中,聲明了一系列的handlexxxxType,那麼我們可以在foo.js中import * as all from ./foo.js;,然後創建一個類似於mapActionTypeToHanleFunction的處理函數。這裡就不用在每個case都去調用某個函數了。
      • 一次用戶操作導致的多個位置的數據變動是否都可以通過接連觸發不同的dispatch action來實現?答案是否定的,比如dispatch某個action,目的是刪除某個實體,但是同時sotre中其他某個地方存儲了這個id,或者說一個ids數組裡包含了這個id,那麼在這次dispatch完成之後的更新里就會出錯,所以我們不能再dispatch一個action去同步的刪除這些數據。我們只能在不同的reducer裡面監測第一個dispatch的action,然後處理。
      • 還有一個就是群里工業聚大大提到的,action實際上和HTTP請求非常相似,能否用處理HTTP請求的方式去規劃redux?
    • Redux-Orm:顧名思義,這就是一個ORM庫(畢竟這是一個前端的資料庫嘛,之前想過redux配合indexDB,後面發現也不是很方便)。使用後的感受是,確實比純的redux方便一些(廢話,不然人家創建這個幹嘛)。具體的比較等之後用了mobx,immutable和rxjs之後再放在一起比較吧。
    • React要想觸發更新只能採用setState(拋開forceUpdate不談),所以這就限制了我們修改數據 -> 自動觸發所有相關更新這種操作,其他的應用我們可以記錄所以依賴於這個數據的組件,然後修改數據的時候依次觸發它們。但React的話要想實現這個就必須繞一個圈子,如redux方案。同時還存在一些弊端,比如:
      • 一是依賴某個數據的明明只有A,B組件,為什麼我還要挨個通知C,D組件,C,D組件明明是依賴於另外的數據的。即便react-redux內部進行了很多性能優化來避免不必要的更新(實質也就是盡量確保兩點,一是我不需要的數據不應該觸發我的更新,二是即便是我需要的數據,沒變化的情況下也不應該更新)。因為redux和react-redux是隔離的,redux就是一個數據(包裹/快遞)倉庫,當有包裹來的時候,他可以在自己內部對包裹進行分類,把包裹放在指定的某個或某些區域(當然數據天然是可copy很多份的,包裹只有一份,就不要糾結這個啦),然後呢,包裹要出庫該走倉庫的哪個門呢?不清楚。。type只是倉庫內部分類時用於參考的一個東西,對外部並沒有作用。
      • 所以它只能依次打開倉庫的所有門,拿上擴音器大喊,倉庫包裹更新啦,快來看看你們那兒要不要搞些事情~,這裡其實就存在一個問題,倉庫里哪塊區域的包裹更新了其實倉庫本身是可以知道的,但是目前他沒有做記錄,這就導致了收到通知之後的前來的公司又必須親自去倉庫里找自己需要的包裹(selector),與此同時,倉庫也不能保證這批包裹屬於這一次包裹更新中的包裹(即selector出來的並沒有發生變化)。所以這就造成了資源的浪費,有可能一次dispatch最後倉庫里沒有任何包裹更新,倉庫也必須挨個打開出庫門。二就是前面提到的有更新也不應該挨個打開出庫門。
      • 因此,可能地更好的一種方式是。組件應該把自身可能需要的包裹提前告訴倉庫,細粒度一點的,當然既可以是就在type和包裹間建立一種映射關係,如{type: { name: update_todo, from: this, need: [foo, bar] }}(foo和bar是store中的某個key), 粗粒度的話(有些case可能不能這樣做),就不自己去添need參數了(但是還是要寫from),可以直接讓redux本身進行統計,因為一個action導致了哪些reducer發生了變化這個它是能夠統計的(也不難,很早的版本中在combineReducer中就有hasChanged的flag了),最後也能形成一個type到need的映射。然後組件也把need參數傳給connect,同時,redux內部的listeners就不應再是一個簡單的回調數組了,需要按need進行歸類,這樣我們才能保證不是去通知每個subscribe了的組件,而是確實需要這次更新的我們才通知。還有就是,既然有了need,也不再需要selector了,我們在通知的時候就自動在store中把對應的need傳過去了。(這裡需要注意到是,對於實體數據,store存儲的數據肯定還是要normalize後的,不然數據冗餘很嚴重,但是我們通知的時候沒有必要再denomalize解範式化或者說像傳統的挨個把外鍵轉換為實體,我們直接建立一個reducer存儲當前的dispatch對應的網路請求的response)
      • 另外這些方面徐飛叔叔的文章真的是寫得非常非常不錯,自己的想法很多時候就是小巫見大巫了,之後一定還是要抽空多琢磨幾遍。

CSS Modules

  • 全局與局部: 從整個項目來講,可以將第三方非CSS Modules模塊的css統一放在一個目錄下,不做CSS Modules處理。局部的話用global語法就行了。
  • 模塊復用: 比如有兩個css模塊,a.css和b.css,我們知道,composes是給對應的標籤的class加上某個css類,而不是像傳統的使兩個類都具備同樣的規則。對於在業務組件中composed基礎組件這樣還好,但是如果是composes其他的業務組件,就會顯得有點怪怪的,比如<div class="a-foo b-bar">ssss</div>,明明是一個和a組件相關的div,卻不得不打上b的烙印。同時,我們還不能只用b-bar,哪怕我們的a-foo沒有東西,也必須寫成.a { composes: b-bar from b.css }才能形成復用。而我們又不太可能單獨把b-bar提取出來作為一個公共組件。
  • 命名: 模塊化能一定程度上減少長命名,但是無法完全消除長命名。因為缺少需要它。比如一個組件內的一個list,依然需要寫成list,list-item,相比以前,省去了xx-list中的xx。但有些情況下,比如item下面的內容很少,就一兩個,我們不想單獨提取出來,那麼就得寫成list-item-xxx,確實看著有一點不美觀。
  • Css的模塊是否應和js模塊耦合? 通常來說,我們一個css模塊會對應一個js模塊(除去皮膚,改版這些)。但是除此之外,是否應該存在單獨的CSS模塊,它們不屬於具體的某個基礎組件或者業務組件,而是共享給這些組件去使用或者說組合。目前這方面沒有實踐過,就不多說了。
  • Css Modules與React結合
    • 目前已有的方案有react-css-modules以及babel-plugin-react-css-modules,它們的作者是同一個人,後者是前者的改進版本,這位作者也是medium上那篇大家熟悉的stoping css in js的作者。
    • 前者(react-css-modules)存在的問題有,一是性能,因為採用的是HOC的模式,導致在生產環境每次渲染也要走一遍檢索css映射表的過程,造成性能損耗。二是書寫方式麻煩,即便用了decorator,也還是要老是重複同樣的代碼。三是某些情況下要在一個組件內使用多次HOC,整潔性和方便性都不太好。這些在README中基本都有提到。還有就是不能寫空的css規則,內部對此會拋出異常(之前被這個坑過,剛好那個時候的chrome版本吞錯,找了很久才發現是這裡的問題)。後面跟作者交流了一下,提出了空的css規則的適用場景和初衷,所以在babel-plugin-css-modules中對於此採用警告而不再是拋出異常。
    • 後者(babel-plugin-react-css-modules)基本上解決了上述提到的所有問題。但是還有一個地方值得思考一下,那就是import機制的問題,個人認為還需要增加一種特性(可配置對於每個js模塊是否啟用),即import多個css也不需要指定importName,默認後面的會覆蓋前面引入的css中的同名class,否則我們還是要寫成foo.a bar.b的形式,喪失了我們使用這個的部分初衷。
    • 另外採用這樣方式暫時有個缺點就是IDE支持不好(沒有自動補全提示),儘管7月份發布的最新版webstorm對原生的CSS Modules支持度更好了(style.這種方式後會有自動補全的提示)。

其他

  • npm是否應該具備更強的約束?即語義版本號必須滿足語義所賦予的條件,從而讓打包的時候不會1.0.1和1.0.2都打包,應該只打包1.0.2,那個依賴1.0.1的包轉而去依賴1.0.2,從而使得只打包一個。更有想法的是,上傳到npm上的包必須提供類似react-codemod的機制(這個確實很難,不知深度學習能否有幫助),從而可以讓所有使用這個包的應用無痛自動升級,從而實現哪怕是有breaking change的版本更新,比如1.0.2變成1.1.0,最後也只會打包1.1.0。之前包括現在,這部分內容都是通過changelog或者官網更新的方式發放出來。然後讓開發者自己去解決。
  • 一個目錄下有多個js文件,此時一般會建立一個index目錄去導出這個目錄下所有js里的default以及普通export的變數(當然前提是沒有重複的),目的是為了import的時候直接import index文件,避免去記憶某個變數在哪個js文件中。但是這樣有一個麻煩,那就是每次新增js文件的時候都需要記得去index進行export。那麼我們為什麼要南轅北轍的去搞一個index文件呢,我們的目的不就是要import方便嗎,為什麼不讓編譯器(其實webpack的alias也可以,但是畢竟是第三方,包括fb內部也有自己的模塊導入系統,但是弊端他們也說了)去自動尋找包含我們要引入的那個變數都有哪些js文件然後給我們一個清單呢,就像JAVA的import機制那樣,按ctrl+shift+o自動import,如有重複的會讓你選擇到底以哪個文件導出的為準(當然也有些特殊情況,比如我們需要用as取別名)。
  • 越複雜的項目,粒度越小,耦合度越低的項目,組件的數量會成倍的增加,如fb就有3W個組件。那麼,我們如何判斷某個組件我們或者別人或者團隊之前是不是寫過,如果是,我們又如何知道這個組件的名字是什麼,在哪個位置?(fb雖然有fbjs是開源的,但是那個有點像util,而不是component庫,所以也不知道內部到底怎麼搞的,之前也只是問過對於react源碼他們是不是有比較方便的管理工具,能夠快速定位某個功能或者名字或者注釋在哪個地方,得到的答案是,沒有)。

錯誤處理(16beta昨天剛發布了,官方有文檔了,就看官方的啦),構建,測試,體積優化,部署以後再談了(其實是因為沒什麼經驗)。嗯,這次就先這樣吧


推薦閱讀:

ReactEurope 2016 小記 - 下
High Order Component

TAG:前端开发 | 前端框架 | React |