原文:Clean Code vs. Dirty Code: React Best Practicesgit
做者:Donavon West程序員
本文主要介紹了適用於現代 React 軟件開發的整潔代碼實踐,順便談談 ES6/ES2015 帶來的一些好用的「語法糖」。github
整潔代碼表明的是一種一致的編碼風格,目的是讓代碼更易於編寫,閱讀和維護。一般狀況下,開發者在解決問題的時候,一旦問題解決就發起一個 Pull Request(譯註:合併請求,在 Gitlab 上叫 Merge Request)。但我認爲,這時候工做並無真正完成,咱們不能僅僅知足於代碼能夠工做。算法
這時候其實就是整理代碼的最好時機,能夠經過刪除死代碼(殭屍代碼),重構以及刪除註釋掉的代碼,來保持代碼的可維護性。不妨問問本身,「從如今開始再過六個月,其餘人還能理解這些代碼嗎?」簡而言之,對於本身編寫的代碼,你應該保證能很自豪地拿給別人看。express
至於爲何要在意這點?由於咱們常說一個優秀的開發者大都比較」懶「。在遇到須要重複作某些事情的狀況下,他們會去找到一個自動化(或更好的)解決方案來完成這些任務。設計模式
整潔代碼應該能夠經過「味道測試」。什麼意思呢?咱們在看代碼的時候,包括咱們本身寫的或或是別人的,會說:「這裏不太對勁。」若是感受不對,那可能就真的是有問題的。若是你以爲你正在試圖把一個方形釘子裝進一個圓形的洞裏,那麼就暫停一下,而後休息一下。屢次嘗試以後,你會找到一個更好的解決方案。數組
DRY 是一個縮略詞,意思是「不要重複本身」(Don’t Repeat Yourself)。若是發現多個地方在作一樣的事情,那麼這時候就應該合併重複代碼。若是在代碼中看到了模式,那麼代表須要實行 DRY。app
// Dirty const MyComponent = () => ( <div> <OtherComponent type="a" className="colorful" foo={123} bar={456} /> <OtherComponent type="b" className="colorful" foo={123} bar={456} /> </div> );
// Clean const MyOtherComponent = ({ type }) => ( <OtherComponent type={type} className="colorful" foo={123} bar={456} /> ); const MyComponent = () => ( <div> <MyOtherComponent type="a" /> <MyOtherComponent type="b" /> </div> );
有時候,好比在上面的例子中,實行 DRY 原則反而可能會增長代碼量。可是,DRY 一般也可以提升代碼的可維護性。函數
注意,很容易陷入過度使用 DRY 原則的陷阱,應該學會適可而止。單元測試
編寫單元測試不只僅只是一個好想法,並且應該是強制性的。否則,怎麼能確保新功能不會在其餘地方引發 Bug 呢?
許多 React 開發人員選擇 Jest 做爲一個零配置測試運行器,而後生成代碼覆蓋率報告。若是對測試先後對比可視化感興趣,請查看美國運通的 Jest Image snanshot。
之前發生過這種狀況嗎?你寫了一些代碼,而且包含詳細的註釋。後來你發現一個 bug,因而回去修改代碼。可是,你有沒有改變註釋來體現新的邏輯?也許會,也許不會。下一個看你代碼的人可能由於注意到這些註釋而掉進一個陷阱。
註釋只是爲了解釋複雜的想法,也就是說,不要對顯而易見的代碼進行註釋。同時,更少的註釋也減小了視覺上的干擾。
// Dirty const fetchUser = (id) => ( fetch(buildUri`/users/${id}`) // Get User DTO record from REST API .then(convertFormat) // Convert to snakeCase .then(validateUser) // Make sure the the user is valid );
在整潔代碼的版本中,咱們對一些函數進行重命名,以便更好地描述它們的功能,從而消除註釋的必要性,減小視覺干擾。而且避免後續因代碼與註釋不匹配致使的混淆。
// Clean const fetchUser = (id) => ( fetch(buildUri`/users/${id}`) .then(snakeToCamelCase) .then(validateUser) );
在我以前的文章 將函數做爲子組件是一種反模式,強調了命名的重要性。每一個開發者都應該認真考慮變量名,函數名,甚至是文件名。
這裏列舉一下命名原則:
布爾變量或返回布爾值的函數應該以「is」,「has」或「should」開頭。
// Dirty const done = current >= goal;
// Clean const isComplete = current >= goal;
函數命名應該體現作了什麼,而不是是怎樣作的。換言之,不要在命名中體現出實現細節。假若有天出現變化,就不須要所以而重構引用該函數的代碼。好比,今天可能會從 REST API 加載配置,可是可能明天就會將其直接寫入到 JavaScript 中。
// Dirty const loadConfigFromServer = () => { ... };
// Clean const loadConfig = () => { ... };
計算機已經存在很長一段時間了。多年以來,程序員經過解決某些特定問題,發現了一些固有套路,被稱爲設計模式。換言之,有些算法已經被證實是能夠工做的,因此應該站在前人的肩膀上,避免犯一樣的錯誤。
那麼,什麼是最佳實踐,與設計模式相似,可是適用範圍更廣,不只僅針對編碼算法。好比,「應該對代碼進行靜態檢查」或者「當編寫一個庫時,應該將 React 做爲 peerDependency
」,這些均可以稱爲最佳實踐。
構建 React 應用程序時,應該遵循如下最佳實踐:
總會聽到這樣的說法:編寫整潔代碼會下降生產力。簡直是在胡說八道。是的,可能剛開始須要放慢速度,但最終會隨着編寫更少的代碼而節奏加快。
並且,不要小看代碼評審致使的重寫重構,以及修復問題花費的時間。若是把代碼分解成小的模塊,每一個模塊都是單一職責,那麼極可能之後不再用去碰大多數模塊了。時間就省下來了,也就是說 「write it and forget it」。
看看下面的代碼示例。如上所述,從你的顯示器退後一步,發現什麼模式了嗎?注意 Thingie
組件與 ThingieWithTitle
組件除了 Title
組件幾乎徹底相同,這是實行 DRY 原則的最佳情形。
// Dirty import Title from './Title'; export const Thingie = ({ description }) => ( <div class="thingie"> <div class="description-wrapper"> <Description value={description} /> </div> </div> ); export const ThingieWithTitle = ({ title, description }) => ( <div> <Title value={title} /> <div class="description-wrapper"> <Description value={description} /> </div> </div> );
在這裏,咱們將 children
傳遞給 Thingie
。而後建立 ThingieWithTitle
,這個組件包含 Thingie
,並將 Title
做爲其子組件傳給 Thingie
。
// Clean import Title from './Title'; export const Thingie = ({ description, children }) => ( <div class="thingie"> {children} <div class="description-wrapper"> <Description value={description} /> </div> </div> ); export const ThingieWithTitle = ({ title, ...others }) => ( <Thingie {...others}> <Title value={title} /> </Thingie> );
看看下面的代碼。使用邏輯或將 className
的默認值設置成 「icon-large」,看起來像是上個世紀的人才會寫的代碼。
// Dirty const Icon = ({ className, onClick }) => { const additionalClasses = className || 'icon-large'; return ( <span className={`icon-hover ${additionalClasses}`} onClick={onClick}> </span> ); };
這裏咱們使用 ES6 的默認語法來替換 undefined
時的值,並且還能使用 ES6 的箭頭函數表達式寫成單一語句形式,從而去除對 return
的依賴。
// Clean const Icon = ({ className = 'icon-large', onClick }) => ( <span className={`icon-hover ${className}`} onClick={onClick} /> );
在下面這個更整潔的版本中,使用 React 中的 API 來設置默認值。
// Cleaner const Icon = ({ className, onClick }) => ( <span className={`icon-hover ${className}`} onClick={onClick} /> ); Icon.defaultProps = { className: 'icon-large', };
爲何這樣顯得更加整潔?並且它真的會更好嗎?三個版本不是都在作一樣的事情嗎?某種意義上來講,是對的。讓 React 設置 prop 默認值的好處是,能夠產生更高效的代碼,並且在基於 Class
的生命週期組件中容許經過 propTypes
檢查默認值。還有一個優勢是:將默認邏輯從組件自己抽離出來。
例如,你能夠執行如下操做,將全部默認屬性放到一個地方。固然,並非建議你這樣作,只是說具備這樣的靈活性。
import defaultProps from './defaultProps'; // ... Icon.defaultProps = defaultProps.Icon;
將有狀態的數據加載邏輯與渲染邏輯混合可能增長組件複雜性。更好的方式是,寫一個負責完成數據加載的有狀態的容器組件,而後編寫另外一個負責顯示數據的組件。這被稱爲 容器模式。
在下面的示例中,用戶數據加載和顯示功能放在一個組件中。
// Dirty class User extends Component { state = { loading: true }; render() { const { loading, user } = this.state; return loading ? <div>Loading...</div> : <div> <div> First name: {user.firstName} </div> <div> First name: {user.lastName} </div> ... </div>; } componentDidMount() { fetchUser(this.props.id) .then((user) => { this.setState({ loading: false, user })}) } }
在整潔版本中,加載數據和顯示數據已經分離。這不只使代碼更容易理解,並且能減小測試的工做量,由於能夠獨立測試每一個部分。並且因爲 RenderUser
是一個無狀態組件,因此結果是可預測的。
// Clean import RenderUser from './RenderUser'; class User extends Component { state = { loading: true }; render() { const { loading, user } = this.state; return loading ? <Loading /> : <RenderUser user={user} />; } componentDidMount() { fetchUser(this.props.id) .then(user => { this.setState({ loading: false, user })}) } }
React v0.14.0 中引入了無狀態函數組件(SFC),被簡化成純渲染組件,但有些開發者還在使用過去的方式。例如,如下組件就應該轉換爲 SFC。
// Dirty class TableRowWrapper extends Component { render() { return ( <tr> {this.props.children} </tr> ); } }
整潔版本清除了不少可能致使干擾的信息。經過 React 核心的優化,使用無狀態組件將佔用更少的內存,由於沒有建立 Component 實例。
// Clean const TableRowWrapper = ({ children }) => ( <tr> {children} </tr> );
大約在一年前,我還推薦你們多用 Object.assign
。但時代變化很快,在 ES2016/ES7 中引入新特性 rest/spread。
好比這樣一種場景,當傳遞給一些 props 給一個組件,只但願在組件自己使用 className
,可是須要將其餘全部 props 傳遞到子組件。這時,你可能會這樣作:
// Dirty const MyComponent = (props) => { const others = Object.assign({}, props); delete others.className; return ( <div className={props.className}> {React.createElement(MyOtherComponent, others)} </div> ); };
這不是一個很是優雅的解決方案。可是使用 rest/spread,就能垂手可得地實現,
// Clean const MyComponent = ({ className, ...others }) => ( <div className={className}> <MyOtherComponent {...others} /> </div> );
咱們將剩餘屬性展開並做爲新的 props 傳遞給 MyOtherComponent
組件。
ES6 引入 解構(destructuring) 的概念,這是一個很是棒的特性,用相似對象或數組字面量的語法獲取一個對象的屬性或一個數組的元素。
在這個例子中,componentWillReceiveProps
組件接收 newProps
參數,而後將其 active
屬性設置爲新的 state.active
。
// Dirty componentWillReceiveProps(newProps) { this.setState({ active: newProps.active }); }
在整潔版本中,咱們解構 newProps
成 active
。這樣咱們不只不須要引用 newProps.active
,並且也可使用 ES6 的簡短屬性特性來調用 setState
。
// Clean componentWillReceiveProps({ active }) { this.setState({ active }); }
一個常常被忽視的 ES6 特性是數組解構。如下面的代碼爲例,它獲取 locale
的值,好比「en-US」,並將其分紅 language
(en)和 country
(US)。
// Dirty const splitLocale = locale.split('-'); const language = splitLocale[0]; const country = splitLocale[1];
在整潔版本,使用 ES6 的數組解構特性能夠自動完成上述過程:
// Clean const [language, country] = locale.split('-');
但願這篇文章能有助於你看到編寫整潔代碼的好處,甚至能夠直接使用這裏介紹的一些代碼示例。一旦你習慣編寫整潔代碼,將很快就會體會到 「write it and forget it」 的生活方式。