前言
花了一天學習和閱讀原始碼,接下來就是在專案中實踐了。
實踐
Typescript 重構
撇開其他不講,我們先來看下現階段的目錄構造
── src
│ ├── actions
│ ├── components
│ ├── constants
│ ├── containers
│ ├── epics
│ ├── index.tsx
│ ├── logo.svg
│ ├── reducers
│ ├── registerServiceWorker.ts
│ ├── service
│ ├── store
│ ├── stories
│ ├── typing.d.ts
│ └── util
複製程式碼
很明顯我們需要先對 typing.d.ts
進行改造。
改造的思想基於以下幾點:
- 後端採用 Koa + Typescript,所以一些介面的定義是複用的,那麼管理它們就必須統一。
- 需要對命名做統一約定,這樣資料的校驗會很流暢。
- 靈活利用泛型。
首先肯定是對 .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.Component
和 props
都習慣性的用了 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
不是自己寫的外掛,因此修改 props
和 state
介面定義發現很多錯誤,因為原作者對 state
的濫用,導致各種屬性在編譯的時候就報錯。在把 LoadingBarProps
和 LoadingBarState
完善的過程中,其實也是將這個外掛給修正了一遍。
比較糾結的其實還是 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
目錄裡去翻看它 Menu
的 index.d.ts
,往往能找到正確的定義。
最終從 complied with warnings
到 Compiled successfully!
程式碼 重構
解決重複 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 結合點選該資料就能立刻跳到該屬性的定義,這樣對開發人員來講是很方便的一件事情。
如果編譯時期出錯,在下方的問題中都會直接顯示,這樣可以在 熱更新之前就對錯誤進行捕獲。
重構了大概幾十個元件和模組,工作量大是因為之前開發沒注意,導致一批類似的問題,然後需要一個個加 介面定義。
重構的意義更多還是提醒自己在開發之前多思考,多想想,不然到後期各種問題,如果一開始邏輯清晰,程式碼可讀性強,那麼問題的定位將很方便。尤其在複雜的專案中,能不重構還是儘量不要。最後是寫完一個模組就進行檢驗。遇到"髒程式碼"的情況下,能儘快解決就儘快,拖到後期免不得看見程式碼又是懵逼三連。