React 進階設計與控制權問題

LucasHC發表於2018-09-12

控制權——這個概念在程式設計中至關重要。比如,“輪子”封裝層與業務消費層對於控制權的“爭奪”,就是一個很有意思的話題。這在 React 世界裡也不例外。表面上看,我們當然希望“輪子”掌控的事情越多越好:因為抽象層處理的邏輯越多,業務呼叫時關心的事情就越少,使用就越方便。可是有些設計卻“不敢越雷池一步”。“輪子”與業務在控制權上的拉鋸,就非常有意思了。

同時,控制能力與元件設計也息息相關:Atomic components 這樣的原子元件設計被受推崇;在原子元件這個概念之上,還有分子元件:Molecules components。不管是分子還是原子,在解決業務問題上都有存在的理由。

這篇文章將以 React 框架為背景,談談我在開發當中對於控制權的一些想法和總結。如果你並不使用 React,原則上仍不妨礙閱讀。


在文章開始之前,我想先向大家介紹一本書。

從去年起,我和知名技術大佬顏海鏡開始了合著之旅,今年我們共同打磨的書籍**《React 狀態管理與同構實戰》**終於正式出版了!這本書以 React 技術棧為核心,在介紹 React 用法的基礎上,從原始碼層面分析了 Redux 思想,同時著重介紹了服務端渲染和同構應用的架構模式。書中包含許多專案例項,不僅為使用者開啟了 React 技術棧的大門,更能提升讀者對前沿領域的整體認知。

如果各位對圖書內容或接下來的內容感興趣,還望多多支援!文末有詳情,不要走開!


從受控與非受控元件說起

初入 React 大門,關於控制權概念,我們最先接觸到的就是受控元件與非受控元件。這兩個概念往往與表單關聯在一起。在大部分情況下,推薦使用受控元件來實現表單、輸入框等狀態控制。在受控元件中,表單等資料都由 React 元件自己處理。而非受控元件,是指表單的資料由 Dom 自己控制。下面就是一個典型的非受控元件:

<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>
複製程式碼

對於 React 來說,非受控元件的狀態和使用者輸入都無法直接掌控,只能依賴 form 標籤的原生能力進行互動。如果使上例非受控元件變為一個受控元件,程式碼也很簡單:

class NameForm extends React.Component {
  state= {value: ''}

  handleChange = event => {
    this.setState({value: event.target.value});
  }

  handleSubmit = event => {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    )
  }
}
複製程式碼

這時候表單值和行為都由 React 元件控制,使得開發更加便利。

這當然是很基礎的概念,藉此丟擲控制權的話題,請讀者繼續閱讀。

UI “輪子”與 Control Props 模式

前文介紹的樣例,我稱之為“狹義受控和非受控”元件。廣義來說,我認為完全的非受控元件是指:不含有內部 states,只接受 props 的函式式元件或無狀態元件。它的渲染行為完全由外部傳入的 props 控制,沒有自身的“自治權”。這樣的元件在很好地實現了複用性,且具有良好的測試性。

但在 UI “輪子”設計當中,**“半自治”或者“不完全受控”**元件,有時也會是一個更好的選擇。我們將此稱之為 “control props” 模式。簡單來說就是:元件具有自身 state,當沒有相關 porps 傳入時,使用自身狀態 statea 完成渲染和互動邏輯;當該元件被呼叫時,如果有相關 props 傳入,那麼將會交出控制權,由業務消費層面控制其行為。

在研究大量社群 UI “輪子” 之後,我發現由 Kent C. Dodds 編寫的,在 paypal 使用的元件庫 downshift 便廣泛採用了這樣的模式。

簡單用一個 Toogle 元件舉例,這個元件由業務方呼叫時:

class Example extends React.Component {
  state = {on: false, inputValue: 'off'}
  handleToggle = on => {
    this.setState({on, inputValue: on ? 'on' : 'off'})
  }
  handleChange = ({target: {value}}) => {
    if (value === 'on') {
      this.setState({on: true})
    } else if (value === 'off') {
      this.setState({on: false})
    }
    this.setState({inputValue: value})
  }
  render() {
    const {on} = this.state
    return (
      <div>
        <input
          value={this.state.inputValue}
          onChange={this.handleChange}
        />
        <Toggle on={on} onToggle={this.handleToggle} />
      </div>
    )
  }
}
複製程式碼

效果如圖:

Toggle 效果

我們可以通過輸入框來控制 Toggle 元件狀態切換(輸入 “on“ 啟用狀態,輸入 ”off“ 狀態置灰),同時也可以通過滑鼠來點選切換,此時輸入框內容也會相應變化。

請思考:對於 UI 元件 Toggle 來說,它的狀態可以由業務呼叫方來控制其狀態,這就賦予了使用層面上的消費便利。在業務程式碼中,不管是 Input 還是其他任何元件都可以控制其狀態,呼叫時我們具有完全的控制權掌控能力。

同時,如果在呼叫 Toggle 元件時,不去傳 props 值,該元件仍然可以正常發揮。如下:

  <Toggle>
    {({on, getTogglerProps}) => (
      <div>
        <button {...getTogglerProps()}>Toggle me</button>
        <div>{on ? 'Toggled On' : 'Toggled Off'}</div>
      </div>
    )}
  </Toggle>
複製程式碼

Toggle 元件在狀態切換時,自己維護內部狀態,實現切換效果,同時通過 render prop 模式,對外輸出本元件的狀態資訊。

我們看 Toggle 原始碼(部分環節已刪減):

const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))

class Toggle extends Component {
  static defaultProps = {
    defaultOn: false,
    onToggle: () => {},
  }
  state = {
    on: this.getOn({on: this.props.defaultOn}),
  }
  getOn(state = this.state) {
    return this.isOnControlled() ? this.props.on : state.on
  }
  isOnControlled() {
    return this.props.on !== undefined
  }
  getTogglerStateAndHelpers() {
    return {
      on: this.getOn(),
      setOn: this.setOn,
      setOff: this.setOff,
      toggle: this.toggle,
    }
  }
  setOnState = (state = !this.getOn()) => {
    if (this.isOnControlled()) {
      this.props.onToggle(state, this.getTogglerStateAndHelpers())
    } else {
      this.setState({on: state}, () => {
        this.props.onToggle(
          this.getOn(),
          this.getTogglerStateAndHelpers()
        )
      })
    }
  }
  setOn = this.setOnState.bind(this, true)
  setOff = this.setOnState.bind(this, false)
  toggle = this.setOnState.bind(this, undefined)
  render() {
    const renderProp = unwrapArray(this.props.children)
    return renderProp(this.getTogglerStateAndHelpers())
  }
}

function unwrapArray(arg) {
  return Array.isArray(arg) ? arg[0] : arg
}
export default Toggle
複製程式碼

關鍵的地方在於元件內 isOnControlled 方法判斷是否有命名為 on 的屬性傳入:如果有,則使用 this.props.on 作為本元件狀態,反之用自身 this.state.on 來管理狀態。同時在 render 方法中,使用了 render prop 模式,關於這個模式本文不再探討,感興趣的讀者可以在社群中找到很多資料,同時也可以在我新書中找到相關內容。

盤點一下,control props 模式反應了典型的控制權問題。這樣的**“半自治”**能夠完美適應業務需求,在元件設計上也更加靈活有效。

Redux 非同步狀態管理與控制權

提到控制權話題,怎能少得了 Redux 這樣的狀態管理工具。Redux 的設計在方方面面都體現出來良好的控制權處理,這裡我們把注意力集中在非同步狀態上,更多的內容還請讀者關注我的新書。

Redux 處理非同步,最為人熟知的就是 Redux-thunk 這樣的中介軟體,它由 Dan 親自編寫,並在 Redux 官方文件上被安利。它與其他所有中介軟體一樣,將 action 到 reducer 中間的過程進行掌控,使得業務使用時可以直接 dispatch 一個函式型別的 action,實現程式碼也很簡單:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();

export default thunk;
複製程式碼

但是很快就有人認為,這樣的方案因為在中介軟體實現中的控制不足,導致了業務程式碼不夠精簡。我們還是需要遵循傳統的 Redux 步驟:八股文似的編寫 action,action creactor,reducer......於是,控制粒度更大的中介軟體方案應運而生

Redux-promise 中介軟體控制了 action type,它限制業務方在 dispatch 非同步 action 時,action的 payload 屬性需要是一個 Promise 物件時,執行 resolve,該中介軟體觸發一個型別相同的 action,並將 payload 設定為 promise 的 value,並設 action.status 屬性為 "success"。

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action) ? action.then(dispatch) : next(action);
    }

    return isPromise(action.payload)
      ? action.payload
          .then(result => dispatch({ ...action, payload: result }))
          .catch(error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          })
      : next(action);
  };
}
複製程式碼

這樣的設計與 Redux-thunk 完全不同,它將 thunk 過程控制在中介軟體自身中,這樣一來,第三方輪子做的事情更多,因此在業務呼叫時更加簡練方便。我們只需要正常編寫 action 即可:

dispatch({
    type: GET_USER,
    payload: http.getUser(userId) // payload 為 promise 物件
})
複製程式碼

我們對比一下 Redux-thunk,相對於“輪子”控制權較弱,業務方控制權更多的 Redux-thunk,實現上述三行程式碼,就得不得不需要:

dispatch(
	function(dispatch, getState) {
        dispatch({
            type: GET_USERE, 
            payload: userId
        })
        http.getUser(id)
            .then(response => {
                dispatch({
                    type: GET_USER_SUCCESS,
                    payload: response
                })
            })
            .catch(error => {
                dispatch({
                    type: GET_DATA_FAILED,
                    payload: error
                })
            }) 
    }
)
複製程式碼

當然,Redux-promise 控制權越多,一方面帶來了簡練,但是另一方面,業務控制權越弱,也喪失了一定的自主性。比如如果想實現樂觀更新(Optimistic updates),那就很難做了。具體詳見 Issue #7

為了平衡這個矛盾,在 Redux-thunk 和 Redux-promise 這兩個極端控制權理念的中介軟體之間,於是便存在了中間狀態的中介軟體:Redux-promise-middleware,它與 Redux-thunk 類似,掌控粒度也類似,但是在 action 處理上更加溫和和漸進,它會在適當的時機 dispatch XXX_PENDING、XXX_FULFILLED 、XXX_REJECTED 三種型別的 action,也就是說這個中介軟體在掌控更多邏輯的基礎上,增加了和外界第三方的通訊程度,不再是直接高冷地觸發 XXX_FULFILLED 、XXX_REJECTED,請讀者仔細體會其中不同

狀態管理中的控制主義和極簡主義

瞭解了非同步狀態中的控制權問題,我們再從 Redux 全域性角度進行分析。在內部分享時,我將基於 Redux 封裝的狀態管理類庫共同特性總結為這一頁 slide:

slide

以上四點都是相關類庫基於 Redux 所進行的簡化,其中非常有意思的就是後面三點,它們無一例外地與控制權相關。以 Rematch 為代表,它不再是處理 action 到 reducer 的中介軟體,而是完全控制了 action creator,reducer 以及聯通過程。

具體來看

  • 業務方不再需要顯示申明 action type,它由類庫直接函式名直接生成,如果 reducer 命名為 increment,那麼 action.type 就是 increment;

  • 同時控制 reducer 和 action creator 合二為一,態管理從未變得如此簡單、高效。

我把這樣的實踐稱為控制主義或者極簡主義,相比 Redux-actions 這樣的狀態管理類庫,這樣的做法更加徹底、完善。具體思想可參考 Shawn McKay 的文章,介紹的比較充分,這裡我不再贅述。

總結:碼農和控制權

控制權說到底是一種設計思想,是第三方類庫和業務消費的交鋒和碰撞。它與語言和框架無關,本文只是以 React 舉例,實際上在程式設計領域控制權的爭奪隨處可見;他與抽象類別無關,本文已經在 UI 抽象和狀態抽象中分別例舉分析;控制權與碼農息息相關,它直接決定了我們的程式設計體驗和開發效率。

可是在程式設計的初期階段,優秀的控制權設計難以一蹴而就。只有投身到一線開發當中,真正瞭解自身業務需求,進而總結大量最佳實踐,同時參考社群精華,分析優秀開源作品,相信我們都會得到成長。

最後,前端學習永無止境,希望和每一位技術愛好者共同進步,大家可以在知乎找到我!

Happy coding!

Happy coding!


《React 狀態管理與同構實戰》這本書由我和前端知名技術大佬顏海鏡合力打磨,凝結了我們在學習、實踐 React 框架過程中的積累和心得。**除了 React 框架使用介紹以外,著重剖析了狀態管理以及服務端渲染同構應用方面的內容。**同時吸取了社群大量優秀思想,進行歸納比對。

本書受到百度公司副總裁沈抖、百度資深前端工程師董睿,以及知名 JavaScript 語言專家阮一峰、Node.js 佈道者狼叔、Flarum 中文社群創始人 justjavac、新浪移動前端技術專家小爝、百度資深前端工程師顧軼靈等前端圈眾多專家大咖的聯合力薦。

有興趣的讀者可以點選這裡,瞭解詳情。也可以掃描下面的二維碼購買。再次感謝各位的支援與鼓勵!懇請各位批評指正!

React 狀態管理與同構實戰

React 狀態管理與同構實戰

相關文章