# 前端工程師習慣討論 Hi, 大家好我是Dylan, 想分享一些前端 coding 小習慣, 將會以分享故事的方式讓大家能更好理解, 會以 React 或原生的 js 為主, 若有錯誤的還請糾正或一起討論, 也希望大家能分享自己覺得不錯的好習慣。 ### 1.不要寫inline style **1.不好維護** 有一天, A工程師10個相同的Loading元件全寫了inline style, 過了三個月後, 設計師突然說要改Layout, 這時A工程師剛好不在, B工程師一查, 發現不得了要改這麼多地方, 該直接用取代改嗎? 會不會有沒搜尋到的地方, 因為擔心 **style 順序不確定有沒有一致**, 若搜尋說不定會少查到, 只好一頁一頁去改了... 例圖:B一查要改這麼多地方, 說不定還有沒查到的! ![](https://i.imgur.com/tExxCw8.png) **2.Html 變更亂了, 且沒有識別性** B工程師第一眼不知道這**一大串div tag到底是代表什麼東西**, 心想:"這是設計師叫我改的東西嗎? 真希望上面寫一個有意義的class名稱能一眼看出這是什麼 例圖:B心想若A能寫class="loading-wrapper"這樣該有多好 ![](https://i.imgur.com/0xhFV7x.png) **3.無法使用特殊元素** B工程師發現設計師的設計是需要寫 hover 跟 RWD, 但**inline style無法寫像 media-query、hover 等特殊元素或偽元素**, 此時可能**要再多寫一個class**, **再把原本的 inline style 搬過去class**, 增加維護成本 B工程師心想:我~~他*的~~改個設計而已, 還要這樣幫前人搬來搬去 4.呈上題, 若B工程師工作太忙或懶得搬, 最後硬著頭皮來個**同時包含 class 跟 style** 的絕技, 這時就很容易發生**層級問題**, B工程師可能**發現哪個style改不動他就又開大絕寫好幾個!important**, 過了6個月後倒楣的C工程師開發者接手這個專案 C工程師心想: ~~今天將會是我在這公司的最後一天~~ css 層級: `!important > inline style > ID > Class/psuedo-class/attribute > Element` ### 2.減少寫重複的code, 能減少錯誤 某天A工程師, ~~對又是可惡的A~~, 要開發給獸醫用的寵物列表網站, 拿到設計稿發現有**3個頁面功能跟設計一模一樣, 但只有少許的字樣跟參數有不同**, 猜猜A工程師心想什麼, 聰明的你應該猜出來了, A工程師心想這簡單, 我只要寫**3個 js 跟3個 html 跟3個 css** 就能輕鬆完成, 當他寫完後功能還算正常, 他覺得很滿意, 事情就這樣過去了。 於是專案新增了: dogsList.js, dogsList.css, dogsList.html catsList.js, catsList.css, catsList.html birdsList.js, birdsList.css, birdsList.html 半年後... B工程師接手專案, 接到下列需求: 請把這3個頁面都多一個功能可以點擊去各自的詳細資料頁面 詳細資料頁面設計及功能都一樣 B工程師心想這3個頁面功能都一樣, 樣式也都一樣應該很好改吧,打開專案不得了, 之前的人居然是分3個文檔寫, 這樣就算了, **寫法居然還能不一樣**, **同樣的樣式居然寫了3份不同的css還各自取不一樣的class name**。 之前~~被荼毒過~~的B工程師心想不管了, 我也加3頁了拉 於是專案新增了: dogDetail.js, dogDetail.css, dogDetail.html catDetail.js, catDetail.css, catDetail.html birdDetail.js, birdDetail.css, birdDetail.html 半年後... 使用者發現怎麼**只有狗狗頁面特別有些Bugs**, C工程師接到維護通知, 打開一看, 功能一樣的頁面居然分9個文檔寫, 還有只有其中幾個能被寫到有bug, 後來發現是**這頁少寫了某個參數**, 可能是A工程師複製貼上時漏了 認真的C工程師決定除了修改問題外, 也用**判斷式跟傳入參數**的方式, 先把文檔合併, 這樣3個頁面也能更一致, 後續維護也不用改9個地方, **減少漏改的風險**, **也減少後續維護浪費的時間** 後來專案剩下 petsList.js, petsList.html, petsList.css petDetail.js, petDetail.html, petDetail.css 改用傳入petType的參數來判斷要區分的文字及functions 結論原因: 1.減少漏改的風險 2.減少後續維護浪費的時間 3.減少不必要的檔案容量 4.減少做重複的事 ### 3.每個支點都要妥善判斷 C工程師在合併時看到蠻多code是這樣寫的 ``` const [dogList,setDogList] = useState([]); const [selected,setSelected] = useState([]); async getData() { let res = await getApi() setDogList(res.data.list) setSelected(res.data.list[0]) // 怎麼確定一定有一個 } useEffect(() => { getData() }, []); ....省略 // 這麼有信心selected一定有值 <img src={selected.image_path}> <input value={selected.description} ``` selected預設是空陣列, 取api時又沒有寫錯誤預防, 直接丟第0筆的值給他, 如果哪一天API給的陣列是空的或甚至是前面的data就沒給了, 網站就會直接crash了 改寫方法 ``` const [dogList,setDogList] = useState([]); const [selected,setSelected] = useState([]); const [noData,setNoData] = useState(true); async getData() { let res = await getApi() // 每個支點都要保護到, 減少漏資料時crash的可能 if(res?.data?.list?.length > 0){ setDogList(res.data.list) setSelected(res.data.list[0]) // 做沒資料時該做的事 // 寫一個參數 noData ,再拿這參數去做頁面的判斷 setNoData(false) } } useEffect(() => { getData() }, []); // 方法三 保護不穩定的參數 {noData ? <NoData/> : <> <img src={selected?.image_path}> <input value={selected?.description} </> } ``` ### 4.重複的東西可以寫成array來map不要讓重複的Tag寫好多次 ``` <Route path='/dogsList' render={(props) => <Main {...props} rootState={rootState} />} /> <Route path='/catsList' render={(props) => <Main {...props} rootState={rootState} />} /> <Route path='/birdsList' render={(props) => <Main {...props} rootState={rootState} />} /> <Route path='/dogDetail' render={(props) => <Main {...props} rootState={rootState} />} /> <Route path='/catDetail' render={(props) => <Main {...props} rootState={rootState} />} /> <Route path='/birdDetail' render={(props) => <Main {...props} rootState={rootState} />} /> ``` 改為 ``` const mainMap = ['/dogsList','/catsList','/birdsList','/dogDetail','/catDetail','/birdDetail'] {mainMap.map(e => (<Route key={e} path={e} render={(props) => <Main {...props} rootState={rootState} />} />))} ``` 不只變少多餘的code了 下次要加一個新的也只要在陣列加上字就好 ``` <div className="table"> <div> <div className='title'>狀態</div> <div className='data'>{petData.status}</div> </div> <div> <div className='title'>名稱</div> <div className='data'>{petData.name}</div> </div> <div> <div className='title'>性別</div> <div className='data'>{petData.gender}</div> </div> <div> <div className='title'>年齡</div> <div className='data'>{petData.age}</div> </div> <div> <div className='title'>顏色</div> <div className='data'>{petData.color}</div> </div> <div> <div className='title'>ID</div> <div className='data'>{petData.id}</div> </div> </div> ``` 改為 ``` const tableMap = () =>{ const {status,name,gender,color,id} = petData return [ {title:'狀態',data:status}, {title:'姓名',data:name}, {title:'性別',data:gender}, {title:'顏色',data:color}, {title:'ID',data:id}, ] } <div className="table"> {tableMap().map(e=>( <div key={e.data}> <div className='title'>{e.title}</div> <div className='data'>{e.data}</div> </div> ))} </div> ``` C工程師改完覺得很舒服, 若未來需要改class或是layout 只要改一個地方就好, 也會減少漏改其中一個的情況 ### 5. 處理物件類別的拷貝要注意 Primitive data 是 call by value 就是深拷貝 意思是將值(value) 真實地複製一份,所以最終結果不會互相影響 Object data 是 call by reference 就是淺拷貝,記憶體會指向同一個位置,複製出的變數與原來的變數間會互相影響 基本型別(Primitive)有七種: 字串(String) 數字(Number) 布林值(Boolean) 空值(Null) 符號(Symbol,ES6 新增的型別) BigInt(ES6 新增的型別 提供了表示大於2^53的整數的功能) 物件型別(Object): Object Array Function 哪些方式可以讓Object data達成深拷貝呢? 一、JSON.stringify/parse (建議少用) const clonedData = JSON.parse(JSON.stringify(data)); 需要特別注意有些值經過處理後,會產生變化,導致非預期的結果發生: undefined : 會連同 key 一起消失。 Infinity :會被轉成 null。 Date : 型別會由 Data 轉成 string。 regExp : 會被轉乘 空 {}。 NaN : 會被轉成 null。 二、Lodash cloneDeep() 不會遇到 JSON.stringify/parse 部分值會非預期改變的問題 三、自己手寫 ### 6. 不要用 var, 除非你很知道自己在幹嘛 原因: 1.Function scope ``` for ( var i = 0 ; i < 3 ; i++ ) { setTimeout( function() { console.log(i) } ,1000 ); } // 3 3 3 console.log(i) // 3 汙染到全域變數 for (let i = 0; i < 3; i++) { setTimeout( function() { console.log(i) } ,1000 ); } // 1 2 3 console.log(i); // ReferenceError: i is not defined ``` 因為 var 是 function scope 不注意會有很多bug 2.Hoisting ``` console.log(a) // 0 var a = 0 console.log(b) // 報錯 let b = 0 ``` 3.重複賦值 ``` var a = 0; var a = 1; console.log(a) // 1 let a = 0; let a = 1; // 報錯 'a' has already been declared. ``` 這也是當我們程式碼越來越多時,可能會造成的困擾,因為有可能會忘了在哪個地方曾經宣告過同名稱的變數,以致前面的函式被覆蓋新值而造成錯誤。所以ES6後,都會希望以let及const宣告變數,以避免上述狀況。 統整: var: 是「可」重複宣告相同變數,區塊語法用var宣告可能會感染全域變數。 let: 是「不可」重複宣告相同變數,其作用域僅在「區塊作用域(Block Scope)」,一旦離開則會無作用 const: 特性同let,但更嚴謹。一旦變數宣告為常數後,則無法再賦新值。 ### 7. 圖片要加 alt 1. 圖片添加 alt 是 Web accessibility 的一項標準。 2. 如果無法加載圖像文件時,將顯示 alt 代替圖像。 3. alt 能幫助失明、視力受損或無法查看頁面上圖像的人, 使屏幕閱讀器能夠閱讀有關頁面上圖片的信息。 4. alt 有助於 SEO 圖像搜索優化。 5. 幫助測試, 有很多測試需要抓取圖片的這時可以用到 alt 讓我們來試著比較看看 alt 的好寫法 拿我家貓的照片來當範例 ![](https://i.imgur.com/1hTtPzO.jpg) **不建議的Alt寫法:** `<img src="moca.png" alt="">` 當你給了空值, 表示這張可愛的貓貓圖片不具任何意義, 無法查看圖像的人也會不知道你放了什麼圖片。 `<img src="moca.png" alt="cat fat fatcat mocha mochacolor mochacolorcat pet cutcat cute cutepet">` alt 中濫用一堆關鍵字填充, 會有被搜尋引擎懲罰的風險。 **勉強過關的寫法** `<img src="moca.png" alt="cat">` 只能勉強過關因為不具描述性, 的確是一隻貓沒錯, 但可以說得更仔細點 **好的寫法** `<img src="moca.png" alt="Fat Cat with mocha color sleeping near an open window">` 此寫法更能傳達更好的圖片描述,並同時保持在125個字符以內 小提醒 1. 若按鈕是圖像做成的,也要寫 Alt 2. 不要在 alt 在寫到 picture image這種強調是圖片的字, 除非這張照片真的顯示的內容真的包含圖片或照片 ### 8. 擺脫../../地域, 引入絕對位置 **故事開始** 工程師B維護專案時發現工程師A寫的components資料夾檔案很亂, 裡面沒有分類包含了 user, shop, cart, layout, login... 一大堆不同的頁面功能的component都放在一起, 於是決定幫忙整理一下, 整理後發現自己陷入一個大坑, 因為專案import都是這樣寫的, 是兩個文件的相對位置 ``` import Dialog from '../components/Dialog'; import Loading from '../../components/Loading'; import Sidebar from '../../../components/Sidebar'; ``` 或許這寫法已經成為標準有很長一段歷史。沒問題, 但管理這一堆 `../` 是一項相當艱鉅的工作。假設像工程師A想更改文件或資料夾的位置。心裡會想:等等, 我應該這樣做嗎?它不會破壞任何東西嗎?這些問題突然襲擊而來。 這時 **絕對位置** 的出現解救了我們, 我們不需關心兩個文件的相對位置。我們只需關心文件相對於專案根目錄的位置。這大大簡化了導入工作。讓我們看看心目中絕對位置的寫法 ``` import Dialog from 'components/Dialog'; import Loading from 'components/Loading'; import Sidebar from 'components/Sidebar'; ``` **絕對位置已在create react app v3中可以使用** 好處: 1. 使代碼編寫更容易, 只關心你的代碼, 不用擔心路徑問題 (最大好處) 2. 使代碼更乾淨 (無庸置疑) 3. 由於是絕對位置,可以輕鬆找到導入的組件, 相對原本看一堆 ../../../ 眼睛很花。 4. 您可以直接從任何位置複製貼上代碼, 不用再更改相對路徑 5. 修改資料夾位置也很輕鬆 **如何使用** 參考 React 官方文件 https://create-react-app.dev/docs/importing-a-component/ 在項目的根目錄創一個 jsconfig.json 並貼上下列代碼 ``` { "compilerOptions": { "baseUrl": "src" }, "include": ["src"] } ``` 若是使用 TypeScript 的專案, 原本就有 tsconfig.json, 只需要加上 ``` { "compilerOptions": { ..., "baseUrl": "src" }, "include": [ "src" ] } ``` 這樣就可以使用了 好消息是 Vscode 無需進行任何更改。它將自動從jsconfig.json / tsconfig.json文件導入配置。 ### 9. 不要用 JavaScript Popup alert(), confirm(), prompt() 彈窗 **故事開始** 工程師A接到新專案由於設計師沒有設計彈窗的樣式, 他決定用alert, confirm, prompt 彈窗解決專案所有的通知, 後續使用者使用開始出現抱怨的聲音: **1.各瀏覽器樣式不統一** 使用者1抱怨: 你們這彈窗設計怎每次看都不一樣, 等等我按錯怎麼辦。 **2.使用者體驗差, 中斷當前操作,趕時間的人會很不開心** 使用者2抱怨: 我只是上來看個東西而已, 一直彈窗有時候還疊了好幾層, 要按好幾次才能關閉, 再繼續彈我就不來了! **3.阻塞線程進行, 無法讓其他 event 在使用者看彈窗時繼續在背景執行** 使用者3抱怨: 這一個頁面我每次都要讀取很久, 我知道資料很大, 沒關係我習慣進來後放著等, 結果你這個彈窗彈出來害我剛剛放著等的時間都白費了, 關閉彈窗才繼續讀取! **4.只能顯示字串, 圖片 icon 都無法使用** 使用者4抱怨: 你們通知窗都沒有圖片, 有時候字很多根本懶得看, 能放張說明圖片嗎? **5.被常見的彈出窗口阻止程序阻止** 使用者5抱怨: 為什麼我一直點這按鈕都沒功能? (原本按鈕會觸發confirm(), 但被使用者瀏覽器的彈窗阻止擋住了) **6.如果系統在正常任務期間一直發出警報,使用者最終傾向於忽略它們,養成習慣性單擊確定。當他意識到自己刪除了錯誤的文檔時,已經為時已晚。** 使用者6抱怨: 好煩每次來都要一直點, 糟糕這個我點錯了! 建議使用 Dialog 或 Notification 的方式來呈現, 改上上面的問題 ### 10 建構專案前一起討論用哪一個 ui framwork 比較好, 才不會裝 framwork 只為了拿其中幾個組件而已 此專案明顯顯示出 bootstrap 無法滿足使專案的 ui 設計, 導致後來又裝了很多其他 ui 組件套件, 後續要維護版本問題就會散落在各套件 ![](https://i.imgur.com/zgEzLNp.png)