記某次專案的中期重構工作 - 實戰篇

臨水照影發表於2018-02-12

前言

花了一天學習和閱讀原始碼,接下來就是在專案中實踐了。

實踐

Typescript 重構

撇開其他不講,我們先來看下現階段的目錄構造


── src
│   ├── actions
│   ├── components
│   ├── constants
│   ├── containers
│   ├── epics
│   ├── index.tsx
│   ├── logo.svg
│   ├── reducers
│   ├── registerServiceWorker.ts
│   ├── service
│   ├── store
│   ├── stories
│   ├── typing.d.ts
│   └── util

複製程式碼

很明顯我們需要先對 typing.d.ts 進行改造。

改造的思想基於以下幾點:

  1. 後端採用 Koa + Typescript,所以一些介面的定義是複用的,那麼管理它們就必須統一。
  2. 需要對命名做統一約定,這樣資料的校驗會很流暢。
  3. 靈活利用泛型。

首先肯定是對 .d.ts 做歸納劃分。

將外部引用的 module 用 module.d.ts 描述。 然後新建 interfaces 目錄管理介面定義。

然後按照規範將 Number,String,Boolean或Object 改成小寫。

actions 目錄開始排查,看到 dispatch:any 這種用法,立刻改掉.


export function updateDocument(Document: Document) {
  return (dispatch: any) => {
    dispatch(update_document(Document))
  }
}


====>

export function updateDocument(Document: Document) {
  return (dispatch: Function) => {
    dispatch(update_document(Document))
  }
}


複製程式碼

熱更新後測試無報錯,那麼就全域性修改。

接下來是典型的 any 問題


const add_document = (data: any) => ({
  type: ADD_DOCUMENT,
  data: data
})

...

const update_document = (data: any) => 

複製程式碼

data 是開發時候為了圖方便傳入的引數,它可能是Id,可能是一個物件,也可能是字串,這裡不用泛型,只需要將之前定義的介面型別將其替換。增強可讀性。


const add_document = (document: Document) => ({
  type: ADD_DOCUMENT,
  data: document
})

複製程式碼

這樣依次類推,將語義不明確的 data 轉為語義明確的傳參。

接下來是元件這塊,從最基本的 Loadingbar 開始:


class LoadingBar extends React.Component<any, any> {
  constructor (props:any) {
    super(props);
    this.state = {
      className:'',
      show: true,
      // binding class when it end
      full: false,
      // state to animate the width of loading bar
      width: 0,
      // indicate the loading bar is in 100% ( so, wait it till gone )
      wait: false,
      // Error State
      myError: false,
      loadingerror: false,

    }
  }


複製程式碼

對於 React.Componentprops 都習慣性的用了 any。其實只要找到之前傳入的引數,只要列出我們常用的,然後寫進介面即可。 以上程式碼我們可以改為:



interface LoadingBarProps {
  progress: number,
  error: string,
  onErrorDone: Function,
  onProgressDone: Function,
  direction: string,
  className?: string,
  id?: string
}
interface LoadingBarState {
  show: boolean,
  full: boolean,
  wait: boolean,
  width: number,
  myError: boolean,
  className?: string,
  loadingerror?: boolean,
  progress: number
}

class LoadingBar extends React.Component<LoadingBarProps, LoadingBarState> {
  constructor (props:LoadingBarProps) {
    super(props);
    this.state = {
      className:'',
      show: true,
      // binding class when it end
      full: false,
      // state to animate the width of loading bar
      width: 0,
      // indicate the loading bar is in 100% ( so, wait it till gone )
      wait: false,
      // Error State
      myError: false,
      loadingerror: false,

    }
  }

複製程式碼

因為 Loadingbar 不是自己寫的外掛,因此修改 propsstate 介面定義發現很多錯誤,因為原作者對 state 的濫用,導致各種屬性在編譯的時候就報錯。在把 LoadingBarPropsLoadingBarState 完善的過程中,其實也是將這個外掛給修正了一遍。

比較糾結的其實還是 react 中的 event 型別。 在 react 中 經常會用到 e.target.value,但是在 typescript 中各種變化導致後來型別推導的時候各種麻煩。

社群中也有討論 Property 'value' does not exist on type 'EventTarget'

也看了不少寫法 typescript-input-onchange-event-target-value

嘗試了一些寫法發現還是不對,但我又不能容忍 Type declaration of 'any' loses type-safety. Consider replacing it with a more precise type, the empty type ('{}'), or suppress this occurrence. 的報錯。

經過仔細研究,在報錯資訊中推敲最後找到了最終解決方法。

我們只需要引入

import { ChangeEvent } from 'react';

然後型別寫為

ChangeEvent<HTMLInputElement>

然我們就能愉快的這麼寫了


  changeRole = (e: ChangeEvent<HTMLInputElement>) => {
    this.setState({
      role: e.target.value
    })
    const { dispatch } = this.props;
    dispatch(updateUser(
      {
        role: e.target.value
      }
    ))
  }

複製程式碼

型別符合語義。

然後是 antd 一系列型別的問題,簡單的可以通過在官方的 .d.ts裡尋找,有問題的比如


 handleChange = ( info: any) => {
    this.setState({ loading: true });
    if (info.file.status === 'done') {
      this.getBase64(info.file.originFileObj, () => this.setState({
        imgUrl: imgBaseUrl + info.file.response.image,
        loading: false,
      }));
      const { dispatch } = this.props;
      dispatch(updateUser(
        {
          avatar: info.file.response.image
        }
      ))
    }
  }

複製程式碼

其中的 info 用了官方的型別各種報錯,搞到後面還是用類似 any 的自寫介面應付過去了。。。官方問題最為致命。。。

還有比如 Modal 元件的 cancel event 需要用 React.FormEvent<HTMLFormElement> 來匹配。

還有 antd 的 Table 元件在 Typescript中使用現在是需要做一些變動的,並不能直接引用使用。經過多次的迭代,現階段應該是這樣的


class MyTable extends Table<Team>{ }

const columns: ColumnProps<object>[] = [...]

     <MyTable columns={columns}  ...>
            </MyTable>

複製程式碼

Team 的型別是我們傳入的資料型別。

關於一些奇怪的問題比如 JSX attributes must be on a line below the opening tag

這些因為引數裡面需要 render 新的佈局,比如之前這麼寫


  <TreeNode title={
  	<div>
  	...
  	</div>
  } key={project._id} >

複製程式碼

這在編譯的時候就會提醒這麼寫不行。 我們可以把這個佈局給提取出來,然後通過手動渲染的方式來替換


 <TreeNode title={this.renderTreeProjectTitle(project)} key={project._id} >

複製程式碼

至於 Exceeds maximum line length of 120 的錯誤我們可以通過換行將引數下移來優化。

比如原本一行的

<Popconfirm title="確定克隆該介面麼?" onConfirm={() => { this.cloneCurrentInterface(item._id) }} okText="確定克隆" cancelText="取消">

換成


<Popconfirm
title="確定克隆該介面麼?"
onConfirm={() => { this.cloneCurrentInterface(item._id) }}
okText="確定克隆"
cancelText="取消"
>

複製程式碼

這裡也有個小技巧,如果你實在找不到引數的型別,比如在 antd 中,我實在無法對 Menu 的點選事件做正確的判斷,然後我就去 node_modules 目錄裡去翻看它 Menuindex.d.ts,往往能找到正確的定義。

最終從 complied with warningsCompiled successfully!

imgn

程式碼 重構

解決重複 Action 中類似的 Success 與 Error, 在開發前期沒有考慮把中介軟體加進去,因此造成開發的時候 Action 里加了很多"髒程式碼"



export function updateDocumentSuccess(msg: string) {
  notification.success({
    message: '更新成功!',
    description: '更新成功!',
    duration: 1
  })
  return
}

export function updateDocumentError(msg: string) {
  notification.error({
    message: '更新失敗!',
    description: '更新失敗!',
    duration: 1
  })
  return
}
export function removeDocumentSuccess(msg: string) {
  notification.success({
    message: '移除成功!',
    description: '移除成功!',
    duration: 1
  })
  return
}

export function removeDocumentError(msg: string) {
  notification.error({
    message: '移除失敗!',
    description: '移除失敗!',
    duration: 1
  })
  return
}

......

複製程式碼

這裡我們可以把這些非關鍵的極其類似的程式碼統一管理,這裡就用到了 redux middleware.

Redux middleware 被用於解決不同的問題,但其中的概念是類似的。它提供的是位於 action 被髮起之後,到達 reducer 之前的擴充套件點。 你可以利用 Redux middleware 來進行日誌記錄、建立崩潰報告、呼叫非同步介面或者路由等等。

當然,按照接下來的思路就是做個 error & success middleware,然後去捕捉對應動作。

但是當我著手的時候我重新回顧了下我現在的程式碼,發現這樣不妥。因為我以及用 rxjs 來對動作做過一層捕捉,然後我也對相應的結果做了更細緻的處理。那麼我似乎不需要去多此一舉了。


export const EinvitedGroupMember = (action$: EpicAction) =>
  action$.ofType(INVITED_GROUPMEMBER)
    .mergeMap((action: Action) => {
      return fetch.post(invitedGroupMember, action.data)
        .map((response: Response) => {
          if (response.state.code === 1) {
            invitedGroupMemberSuccess(response.state.msg)
            return nothing();
          } else {
            invitedGroupMemberError(response.state.msg)
            return nothing();
          }
        })
        // 只有伺服器崩潰才捕捉錯誤
        .catch((e: Error): Observable<Action> => {
          return Observable.of(({ type: ERROR_TEAM })).startWith(loadingError())
        })

    });

複製程式碼

而且我以前定義的資訊都是服務端傳過來,那麼我只需要做一個簡單抽象然後更改所有類似的呼叫就行了。


import notification from 'antd/lib/notification';

// 簡單的成功和錯誤處理
export function successMsg(msg: string) {
  notification.success({
    message: msg,
    description: msg,
    duration: 1
  })
  return
}

export function errorMsg(msg: string) {
  notification.error({
    message: msg,
    description: msg,
    duration: 1
  })
  return
}

複製程式碼

而在後臺定義的格式如下


// 返回正常資料
export const success = ( data: any, msg: string) => {
  return {
    'state': {
        'code': 1,
        'msg': msg
    },
    'data': {
       data
    }
 }
}
// 返回錯誤提醒
export const error = (msg: string) => {
  return{
    'state': {
        'code': 2,
        'msg':  msg
    }
  }
}

複製程式碼

然後通過約定的介面就可以傳遞顯示資訊了。


export const baseModelList = async (ctx: any) => {
  const result = await BaseModelList()
  return ctx.body = success(result, '獲取成功')
}


複製程式碼

總結

通過重構收穫還是很多的,首先是對 Typescript 理解更加深刻了,而且明白瞭如何處理一些奇怪的問題了。

在對元件的 Props 和 State 進行重構的時候,將之前為了快速開發所定義的資料比如之前會這麼寫


this.state = {
  projectMessagesList: ''
}

複製程式碼

當通過介面定義之後,作為一個陣列其實不應該這樣置空,而且 Typescript 在我定義好介面後立刻提醒不能這樣賦值。改成預設空陣列後就解決了。

而且在之前初始化 state 的時候可能會漏掉某個屬性,而介面定義後就會告訴你你有哪些屬性不存在。


message: '型別“Readonly<InterfaceModeState>”上不存在屬性“mode”。'

複製程式碼

程式碼的可讀性其實就是這麼一點一點增加的。

以及在修改後,查詢某個資料的型別(在 vscode 中)只需要按 ctrl 結合點選該資料就能立刻跳到該屬性的定義,這樣對開發人員來講是很方便的一件事情。

imgn

如果編譯時期出錯,在下方的問題中都會直接顯示,這樣可以在 熱更新之前就對錯誤進行捕獲。

重構了大概幾十個元件和模組,工作量大是因為之前開發沒注意,導致一批類似的問題,然後需要一個個加 介面定義。

重構的意義更多還是提醒自己在開發之前多思考,多想想,不然到後期各種問題,如果一開始邏輯清晰,程式碼可讀性強,那麼問題的定位將很方便。尤其在複雜的專案中,能不重構還是儘量不要。最後是寫完一個模組就進行檢驗。遇到"髒程式碼"的情況下,能儘快解決就儘快,拖到後期免不得看見程式碼又是懵逼三連。

相關文章