怎樣設計一個項目的文件和組件的結構、甚至是某個組件的內部結構?這個問題永遠沒有正確答案。
作者 | Mathilde Wrstad
譯者 | 彎月,責編 | 郭芮
以下為譯文:
像許多話題一樣,許多人都持有不同的觀點,而且解決方案也有很多。
可能你自己的意見會受到個人經歷的影響,比如你對於何謂易讀、何謂易解析,甚至何謂漂亮都有自己的看法。當你加入一個團隊時,通常你們都需要在這些結構問題上達成一致,這並不是一件易事。
這類的討論就好像兩個人爭論哪種顏色最好看,是藍色還是紅色。他們都會主張自己的意見,闡明自己的立場,然後固執己見。他們永遠也不會折衷,比如認為紫色最好看。
因此,我並不期望能夠在這個問題上達成任何共識。在本文中,我只會介紹在設計React代碼結構方面的個人喜好,以及其中的原因。希望你能從中借鑑一二,或者至少可以從不同的角度理解這個問題。
拼圖
首先,我想簡單地介紹一下我是哪種程式設計師。在寫代碼時,我會需要構建的東西看成基本的組成部分,就像拼圖一樣。我編寫的每個功能、每個函數、每個組件都是一副拼圖中的一部分。
我很喜歡將我做的東西可視化。我喜歡用畫圖的方式解釋問題時,我希望能夠將用戶真正使用的UI功能反映到代碼中。
對於我而言,還有一件重要的事情,那就是能迅速看到整體樣貌,並儘可能簡單地理解組件的功能。這兩個目的都可以通過選擇正確的命名方式以及可視化結構來實現。
聖誕老人數位化
在這篇文章中我假想了一個項目,名叫聖誕老人的數字願望單。所有人都可以使用這個項目,創建用戶,添加願望,還可以查看個人信息。
下面是組件 EditMyInformationToSanta 的代碼,顯然它需要重構。我們可以通過這個組件添加你希望獲得的聖誕禮物。
import React, { useState } from 'react';import { saveMyInformationToSanta } from '../../api/santa-api';const EditMyInformationToSanta = () => { const [name, setName] = useState(''); const [age, setAge] = useState(''); const [gender, setGender] = useState(null); const [address, setAddress] = useState(''); const [hasFireplace, setHasFireplace] = useState(null); const [naughtyOrNice, setNaughtyOrNice] = useState(null); const [letterToSanta, setLetterToSanta] = useState(''); const [wish, setWish] = useState(''); const [wishList, setWishList] = useState([]); const submitMyInformationToSanta = async event => { event.preventDefault(); await saveMyInformationToSanta({ name, age, gender, address, hasFireplace, naughtyOrNice, letterToSanta, wishList, }); }; return (<div><h1>Hi, Santa! This is me</h1><form><h2>About me:</h2><label><span>My name is:</span><inputtype="text"value={name}placeholder="Write your name"onChange={event => setName(event.target.value)} /></label><label><span>My age is:</span><inputtype="text"value={age}placeholder="Tell Santa your age"onChange={event => setAge(event.target.value)} /></label><fieldset><legend>I am a...</legend><label><inputtype="radio"value="boy"checked={gender === 'boy'}onChange={event => setGender(event.target.value)} /> Boy</label><label><inputtype="radio"value="girl"checked={gender === 'girl'}onChange={event => setGender(event.target.value)} /> Girl</label></fieldset><label><span>My address is:</span><inputtype="text"value={address}placeholder="Where do you live?"onChange={event => setAddress(event.target.value)} /></label><fieldset><legend>I have a fireplace?</legend><label><inputtype="radio"value={true}checked={hasFireplace}onChange={event => setHasFireplace(event.target.value)} /> Yes</label><label><inputtype="radio"value={false}checked={hasFireplace === false}onChange={event => setHasFireplace(event.target.value)} /> No</label></fieldset><fieldset><legend>This year I have been naughty or nice?</legend><label><inputtype="radio"value="naughty"checked={naughtyOrNice === 'naughty'}onChange={event => setNaughtyOrNice(event.target.value)} /> Naughty</label><label><inputtype="radio"value="nice"checked={naughtyOrNice === 'nice'}onChange={event => setNaughtyOrNice(event.target.value)} /> Nice</label></fieldset><div><h2>My wishes this year:</h2><label><span>I want:</span><inputtype="text"value={wish}placeholder="Write a wish"onChange={event => setWish(event.target.value)} /></label><buttontype="button"value="Add wish"onClick={() => { setWishList(wishList.concat(wish)); setWish(''); }} /><h3>My wish list:</h3><ul> {wishList.map(wish => (<li>{wish}</li> ))}</ul></div><div><h2>Santa, I also want to tell you...</h2><textareaplaceholder="Do you want to say something to Santa?"onChange={event => setLetterToSanta(event.target.value)} value={letterToSanta} /></div><buttontype="submit"onClick={submitMyInformationToSanta} /></form></div> );};export default EditMyInformationToSanta;
組件本身並沒有複雜的內容,但由於這個表單十分龐大,包含很多信息,因此文件非常大,或者說非常長。為了改進這一點,我將代碼分割成了多個組件。
有幾個問題很明顯,比如重複的輸入框和單選按鈕的布局,所以我們可以將此作為切入點。如下便是重構後的代碼:
import React, { useState } from'react';import { saveMyInformationToSanta } from'../../api/santa-api';import TextInputWithLabel from'./TextInputWithLabel';import RadioToggle from'./RadioToggle';const EditMyInformationToSanta = () => {const [name, setName] = useState('');const [age, setAge] = useState('');const [gender, setGender] = useState(null);const [address, setAddress] = useState('');const [hasFireplace, setHasFireplace] = useState(null);const [naughtyOrNice, setNaughtyOrNice] = useState(null);const [letterToSanta, setLetterToSanta] = useState('');const [wish, setWish] = useState('');const [wishList, setWishList] = useState([]);const submitMyInformationToSanta = asyncevent => {event.preventDefault();await saveMyInformationToSanta({ name, age, gender, address, hasFireplace, naughtyOrNice, letterToSanta, wishList, }); };return ( <div> <h1>Hi, Santa! This is me</h1> <form> <h2>About me</h2> <TextInputWithLabel label="My name is:" placeholder="Write your name"value={name} onChange={event => setName(event.target.value)} /> <TextInputWithLabel label="My age is:" placeholder="Tell Santa your age"value={age} onChange={event => setAge(event.target.value)} /> <RadioToggle question="I am a..." label1="Boy" toggleValue1="boy" label2="Girl" toggleValue2="girl"value={gender} onChange={event => setGender(event.target.value)} /> <TextInputWithLabel label="My address is:" placeholder="Where do you live?"value={address} onChange={event => setAddress(event.target.value)} /> <RadioToggle question="I have a fireplace?" label1="Yes" toggleValue1={true} label2="No" toggleValue2={false}value={hasFireplace} onChange={event => setHasFireplace(event.target.value)} /> <RadioToggle question="This year I have been naughty or nice?" label1="Naughty" toggleValue1="naughty" label2="Nice" toggleValue2="nice"value={naughtyOrNice} onChange={event => setNaughtyOrNice(event.target.value)} /> <div> <h2>My wishes this year:</h2> <TextInputWithLabel label="I want:" placeholder="Write a wish"value={wish} onChange={event => setWish(event.target.value)} /> <button type="button"value="Add wish" onClick={() => { setWishList(wishList.concat(wish)); setWish(''); }} /> <h3>My wish list:</h3> <ul> {wishList.map(wish => ( <li>{wish}</li> ))} </ul> </div> <div> <h2>Santa, I also want to tell you...</h2> <textarea placeholder="Do you want to say something to Santa?" onChange={event => setLetterToSanta(event.target.value)}value={letterToSanta} /> </div> <button type="submit" onClick={submitMyInformationToSanta} /> </form> </div> );};export default EditMyInformationToSanta;
注意,儘管我不確定輸入框和單選按鈕是否會在應用程式的其他地方使用,但我還是決定將它們移動到單獨的文件中。這些文件應該保存在儘可能靠近原來組件的位置,以方便使用。
保持組件分離!
許多開發人員喜歡將類似上例中的局部組件放到一個文件中,但我傾向於一個文件最多只保存一個組件。對於我來說,在理解代碼全貌的時候,一個文件只包含一個組件的形式比一個文件中包含多個組件更容易。
在本例中,差別也許不是太大,由於我要重構的組件並不是太大,但組件規模增大後問題就會浮現。幾個星期後你就不得不在文件中來回上下滾動才能找到「根」組件,這會非常麻煩。
我喜歡簡單的規則,這樣就可以避免重構時的內部討論,也不需要討論什麼時候應該將組件放到單獨的文件中。
按照域提取
回到我們的例子。儘管我們將重複的代碼提取到了可重用的組件中,我認為依然還有可以重構的地方。我想按照代碼的功能,重構同一功能的代碼。在下面的例子中,我把表單每一部分的組件都提取出來,分別是AboutMe、LetterToSanta 和 MyWishes。
import React, { useState } from'react';import { saveMyInformationToSanta } from'../../api/santa-api';import AboutMe from'./AboutMe';import MyWishes from'./MyWishes';import LetterToSanta from'./LetterToSanta';const EditMyInformationToSanta = () => {const [me, setMeState] = useState({name: '',age: '',address: '',gender: null,hasFireplace: null,naughtyOrNice: null, });const [letterToSanta, setLetterToSanta] = useState('');const [wish, setWish] = useState('');const [wishList, setWishList] = useState([]);const submitMyInformationToSanta = async event => { event.preventDefault();await saveMyInformationToSanta({ ...me, letterToSanta, wishList, }); };return ( <div> <h1>Hi, Santa! This is me</h1> <form> <AboutMe me={me} onMeChange={updatedMeState => setMeState(updatedMeState)} /> <MyWishes wish={wish} wishList={wishList} onWishChange={setWish} onWishListChange={setWishList} /> <LetterToSanta letterToSanta={letterToSanta} onLetterChange={setLetterToSanta} /> <button type="submit" onClick={submitMyInformationToSanta} /> </form> </div> );};export default EditMyInformationToSanta;
項目的完整代碼在此(https://github.com/mathilwa/WishesToSanta)。
現在,這個目錄中除了文件 EditMyInformationToSanta.jsx 之外,還有一堆簡單的組件文件。每個文件都很小,很容易單獨理解。
表單每個部分對應的組件都僅限於 EditMyInformationToSanta.jsx 文件使用,因此我將它們放到了同一個目錄下。重構的目的是為了讓主文件看起來乾淨、容易理解。我們也可以將其他相關的文件放在這個目錄中,比如樣式、圖像、文本、工具函數或其他資源等。
域 vs 組件
應用程式會不斷增長,例如利用 SantaLocation API 來實時跟蹤聖誕老人的位置,或者顯示前十個最期待的禮物,總有一天你會遇到重用表現層組件的情況。我喜歡將代碼分成兩個目錄:/components 和 /domain,目的是將表現層組件分離開來。
/domain 包含應用程式的域邏輯。EditMyInformationToSanta 以及相關的文件就在這個目錄下。
/components 包含整個項目中所有可重用的組件。典型的例子就是 TextInputWithLabel,我們將它從 /domain/edit-my-information-to-santa 目錄移動到 /components/text-input-with-label。與 /domain 目錄一樣,這個目錄也可以包含相關文件,如文本、樣式等。
保持整潔
我們剛才一直在討論怎樣設計React代碼的布局。總結起來就是:
我喜歡將表現層邏輯提取到單獨的組件中,這樣利於重用,也利於簡化根組件。
我將每個組件放到各自的文件中,即使用戶界面變得十分複雜,也應該遵循這一條原則。原因在於,每個文件僅包含一個組件更加方便理解。每個文件都很短,因此可以看到整體情況,而好的組件命名(以及文件命名)可以幫助我們理解每個組件的作用。
將組件分割成一次性的域邏輯組件和可重用的組件,可以讓瀏覽代碼和刪除代碼更加容易。
作為一名諮詢師,你需要花大量時間去閱讀別人的代碼。確保閱讀代碼的過程順利非常重要。大多數時間我都在修改或擴展已有代碼,而簡單、健壯、易於理解的代碼結構可以幫你更快地完成工作。
接受他人的意見
最後我想用本文開頭的觀點來結束本文。團隊中不同的人有不同的選擇和喜好,接受這一點並不容易。但一定要接受嗎?我想說,是的。並不是說你要接受關於什麼是美觀,而是要接受在項目中使用某種代碼風格和結構,即使有些人並不完全同意。
讓整個團隊都在「整潔」或「易閱」問題上達成一致是不現實的。最重要的是,每個人都會表達自己的意見,每個人都會強調為何使用這種方式,並積極討論團隊應該使用這種方式。團隊中的大多數人(即使不是全部)都會發表觀點,但我認為,最好能夠接受統一的代碼風格和結構,而不是為了滿足每個人的喜好而混合多種風格。
不管最後的選擇是什麼,接受統一的解決方案,才能在項目中創建整潔、乾淨、易於理解且標準化的代碼風格。
原文:https://react.christmas/2019/22
本文為 CSDN 翻譯,轉載請註明來源出處。