基於React搭建一個簡易版豆瓣

nh0007發表於2019-03-03

前言

之前寫過一篇基於vue搭建一個簡易版豆瓣的文章,近來接觸到React,於是將該專案用React重寫了一遍,用於體驗兩者在實際開發中的異同。本文會簡述專案情況及構建專案過程中遇到的一些零散的問題,以作記錄,見識淺薄,若有錯漏處還望指正。


專案概述

專案地址:react-douban

專案簡介:基於React搭建簡易版豆瓣,實現將讀書、電影、音樂、同城活動等內容按不同型別顯示的功能。

技術棧:React + Mobx + react-router + axios + Sass + ES6/7等,同時,為保證程式碼質量和風格統一,專案也使用了ESLintstylelintprettier等優秀的開源工具對專案進行約束調整。

專案背景:本專案介面風格參考自豆瓣,資料獲取來自豆瓣官方api,由於有請求限制,所以頻繁請求的話會導致請求失敗,還需注意。

開發環境:node v8.1.2,npm 6.1.0,yarn 1.7.0;瀏覽器:Chrome 67.0.3396.99,Firefox 61.0.1。


執行專案

執行專案前,請確保已正確安裝node環境。

  • 克隆專案,若沒有安裝git環境,也可直接在react-douban處下載安裝包;

git clone https://github.com/nh0007/react-douban.git
複製程式碼

  • 進入專案根目錄,安裝依賴;

npm install 
or 
yarn install
複製程式碼

  • 安裝完依賴後,執行專案;

npm start
or
yarn start
複製程式碼

  • 等待執行完畢後,瀏覽器會自動彈出訪問頁,即可看到專案執行效果。

專案截圖

本專案主要分為讀書、電影、音樂、同城活動四大模組,以下是各個模組截圖:

基於React搭建一個簡易版豆瓣

基於React搭建一個簡易版豆瓣

基於React搭建一個簡易版豆瓣

基於React搭建一個簡易版豆瓣

基於React搭建一個簡易版豆瓣

基於React搭建一個簡易版豆瓣

基於React搭建一個簡易版豆瓣

基於React搭建一個簡易版豆瓣


開發中需注意的問題

1、專案初始配置

俗話說:好的開始就是成功的一半。專案初期新增檢驗配置,是專案後期發展的基石。因此,在使用create-react-app腳手架構建完專案後,為新增自定義配置,先eject專案,然後新增如下配置:

  • 使用airbnb檢驗規則檢查程式碼質量;
  • 使用prettier工具統一程式碼風格;
  • 使用stylelint檢查樣式程式碼;
  • 使用huskylint-staged對即將提交的程式碼進行校驗。

剛開始接觸這些配置的朋友可能會很彆扭,甚至煩躁,配置完後開發過程總是報錯,影響進度。但這部分成本還是有必要付出的,特別是當你維護過一些“紊亂”的程式碼後就更能體會其中價值~。具體的配置可以檢視提交記錄,裡邊有針對各項配置的提交修改。如果還覺得有點懵,油管上有個不到十分鐘的配置視訊,介紹了初始配置以及與編輯器外掛配套使用過程,感覺挺清晰的,剛接觸的朋友可以看看。


2、使用Mobx的幾點注意事項

  • 新增裝飾器語法配置,個人更為習慣裝飾器語法,相關的配置可以看這裡
  • 所有的observable資料都放到action中修改,無論observable資料是存放於store中還是元件裡;
  • 使用action修改狀態時可使用箭頭函式寫法,保證無論怎麼呼叫函式,this的指向都不會出錯,如下:

@action
setCurrentBookTags = tags => {
  if (tags.tagName !== this.currentBookTags.tagName) {
    this.currentBookTags = tags;
  }
};
複製程式碼

  • 非同步修改狀態官方文件推薦使用flow,這樣可以不用@action、runInAction去包裝非同步程式碼,使程式碼更簡潔。但在使用過程中有兩點要注意:一是需引入flow,不然會報錯,(我能說一開始我還以為是得修改ESLint配置查了好一會嗎);二是繫結this。示例程式碼如下:

// 從mobx引入flow
import { observable, action, flow } from 'mobx';

// 非同步獲取資料時需要繫結this,無論你是this.props.bookStore.setTypeBooks(type)呼叫,
// 還是 const { setTypeBooks } = this.props.bookStore; setTypeBooks(type);呼叫都不會出錯;
setTypeBooks = flow(
  function*(type, start, count, isAfresh = false) {
    const oldData = this.typeBooks.get(type);
    if (oldData && !isAfresh) return;
    try {
      const response = yield getCurrentTypeBooks(type, start, count);
      const data = response.data.books;
      this.typeBooks.set(type, data);
    } catch (error) {
      console.log(error);
    }
  }.bind(this)
);
複製程式碼

  • Mobx4中observable陣列實際上是物件,不能使用PropTypes.array進行型別檢查,Mobx5中使用Proxy追蹤變化,陣列可直接使用PropTypes.array進行型別檢查;
  • Mobx中observable普通物件時新增物件屬性不會觸發更新,可以使用Map,使用Map儘管可以使用PropTypes.object進行型別檢查,但感覺還是mobx-react中的observableMap來檢查更加合適,同時引入prop-types和mobx-react中的PropTypes會有同名問題,可以這樣:

import { observer, inject, PropTypes as mobxPropTypes } from 'mobx-react';
import PropTypes from 'prop-types';
複製程式碼


3、輪播實現

專案中有不少輪播展示內容頁面,在實現過程遇到了些問題,此處記錄一下。

先看看專案中的輪播效果:

基於React搭建一個簡易版豆瓣

雖然有一些優秀的輪播庫,但此處暫不使用專用輪播庫,而是藉助過渡、動畫效果來實現。

可以看看React和Vue在過渡、動畫使用上的一些差異:

  • Vue中自帶過渡的封裝元件,React可引入react-transition-group庫協助;
  • Vue中可結合v-if、v-show控制顯示隱藏的動畫,在react-transition-group中主要藉助它的in屬性;

關於react-transition-group的使用介紹,目前看到最清晰詳細的還是官方文件,網上蠻多的中文資料介紹的是早期v1版本,這個還得結合自身使用的版本對號入座。

來看看官方文件中對於in的介紹:

Show the component; triggers the enter or exit states

type: boolean
default: false

Transition state is toggled via the in prop. When true the component begins the "Enter" stage. During this stage, the component will shift from its current transition state, to 'entering' for the duration of the transition and then to the 'entered' stage once it's complete.

When in is false the same thing happens except the state moves from 'exiting' to 'exited'.

簡單來說,in值的變化會改變過渡元件的狀態:

  • 當in值由false變為true時,元件進入enter狀態;
  • 當in值由true變為false時,元件進入exit狀態。

上面強調了變化二字,主要是因為在實踐過程中會發現,當你一開始掛載CSSTransition元件時它的in值便為true,它是不會觸發onEnter函式的,也不會在相應容器元素上新增對應的狀態class,只有在由false變為true才會觸發。false同理。

來看看專案中遇到的問題,由於輪播本身也算是列表的輪換展示,於是第一反應是使用TransitionGroup,之前在Vue也是使用了相似的<transition-group>元件包裹列表實現滑動效果,但在React中卻不行,樣例程式碼如下:

import React, { PureComponent } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';

export default class Slider extends PureComponent {
  ...
  render() {
    ...
    return (
      <TransitionGroup className="slider-list">
        {itemArray.map((item, index) => (
          <CSSTransition
            key={item.id}
            in={currentPage === index}
            timeout={500}
            classNames={`slider-${slideDirection}`}
            unmountOnExit
          >
            ...
          </CSSTransition>
        ))}
      </TransitionGroup>
    )
  }
}
複製程式碼

一開始以為自己對TransitionGroup和CSSTransition的使用方法不對,或是漏了哪些重要屬性,看著文件折騰了好一會仍沒有效果,且給CSSTransition元件設定的in、unmountOnExit也沒啥作用,之後嘗試著把TransitionGroup標籤換成div標籤,滑動生效。再回頭看看官網給的TransitionGroup的例子,我覺得是我一開始對TransitionGroup理解使我走入一個誤區,TransitionGroup的適用情況應該是同時渲染整個列表,比如增刪列表,像輪播這種每次僅顯示列表中的其中一項顯然是不適用的。

總結一下:在本專案中,主要藉助react-transition-group的CSSTransition元件來實現輪播動畫,通過in與unmountOnExit的搭配使用控制是否顯示,使用classNames屬性結合樣式實現過渡效果。具體的實現可以參見原始碼,此處就不再贅述了。


4、元件拆分

在開始這個專案之前,我在網上看了一些React最佳實踐的文章,普遍提及的一點是拆分元件,以便後期複用與維護。過於龐大的元件毫無疑問是有拆分的必要的,但僅憑元件的大小來拆分未免顯得粗糙,元件拆分的標準是啥,官方文件提及的一點是單一功能原則,寫程式碼的對單一功能原則肯定不陌生,但將這個原則用在元件的拆分上,還是遇到了以下的疑惑:

  • 依據單一功能原則劃分元件,可能導致元件數量增多,元件劃分較為零碎的情況;
  • 業務元件通常各不相同,拆分出來的元件往往僅使用一次,不僅達不到複用的效果,還會平添出更多程式碼;
  • 多元件意味著屬性在更多的元件間傳遞,同時開發時會頻繁的切換元件檔案,往往會比較繞。

那就不拆分了嗎?也不是的,元件拆分還有一個好處,就是優化效能,詳情可以看看這篇文章,寫得很清晰,這裡簡略總結一下:

預設情況下,元件狀態更新,子元件即便狀態不變,依然會執行re-render和vdom-diff操作

理想情況下,如果元件狀態不變,則不需要進行額外的操作

如何達到理想情況?

  1. 使用對shouldComponentUpdate生命週期函式返回值的判斷減少額外操作,可藉助PureRenderMin或PureComponent預設實現;
  2. 單純使用shouldComponentUpdate是不夠的,影響元件的狀態越多,元件re-render等操作的次數就越多,可能性就越大。因此,通過扁平化的狀態去拆分元件時很有必要的。

嗯,總結一下,什麼時候需要拆分元件?

  • 元件較臃腫時,可考慮拆分元件,提高程式碼可讀性,方便後期維護;
  • 元件效能有優化空間時,可以考慮拆分元件,減少效能隱患。

如何拆分?

  • 根據單一功能原則,使得元件職責分明;
  • 根據狀態劃分,使得劃分後的元件能儘量減少不必要的更新操作。

最後,來看一個專案中的例子:

基於React搭建一個簡易版豆瓣

可以看到,上圖有四個輪播頁,當我點選下一頁的時候,上一頁逐漸消失,下一頁逐漸顯示,其餘兩個輪播頁是不變的,當我不拆分元件,把四個輪播頁放在一個元件中時,每次當前顯示的頁數變化時,每個輪播頁都要被更新,執行re-render和vdom-diff等操作,樣例程式碼如下:

{bookList.map((bookArray, index) => (
  <ul
    key={bookArray[0].id}
    style={{display: currentPage === index ? 'block' : 'none'}}
  >
    ...
  </ul>
))}
複製程式碼

這個時候,可以考慮拆分元件,於是我把每個輪播頁拆分成獨立的元件,這樣,當我從第一頁滑動到第二頁時,只有第一二頁因為狀態變化而需要進行更新操作,而第三第四頁則不需要,樣例程式碼如下,具體程式碼可參見原始碼:

{bookList.map((bookArray, index) => (
  <BookTagDisplayItem
    key={bookArray[0].id}
    isShow={currentPage === index}
  />
))}
複製程式碼

以上是個人在元件拆分上的一些主觀的想法與實踐,僅供參考。個人覺得元件的拆分是一門玄學,元件的粒度是一個抽象而又沒有絕對答案的事情。我們很少會遇到一個元件十分臃腫以至於必須拆分的情況,而在介面元素不多的情況下,我們也很少遇到元件不拆分這一下就會對效能產生多大的影響的情況,更多時候我們遇到的是一個可拆可不拆的情況,拆分了吧,過零碎,平白又多了很多檔案;不拆吧,又有點強迫症,如何把握拆分的度也是一門學問。當然了,可能是想的多做的少的原因,希望以後能在過多的實踐中找到平衡點,此處純屬拋磚。


React使用體驗

  • React使用JSX構建頁面元素,初期可能有點不習慣,但有JS基礎的話,JSX上手成本不高,後面熟悉了覺得還是很方便的;
  • 使用React時會下意識思考是否需要拆分元件的問題,以前用其他框架也會思考這個問題,但個人感覺React的粒度會更細,當然了,拆分的度在哪裡,上面也說了,之後仍需要在實踐中去探索;
  • React生態龐大,單就路由管理和狀態管理來說,路由有傳統的react-router,也有新貴reach/router,狀態管理有redux,也有Mobx,如果你選擇了redux,那麼在非同步載入資料時,你還有redux-thunkredux-promiseredux-saga等選擇,在redux最佳實踐方面,你可能還會接觸到dva或者rematch,生態的龐大一方面讓你解決問題的手段多樣化,另一方面,如何在紛雜的資訊中篩選出適合自己的也是一種能力的考驗。


結語

感覺對React的理解尚有許多待加強之處,行文若有不準確的地方,還請大家批評指正。若覺得稍有助益,不煩star本專案一下~


相關文章