# 前端工程師習慣討論
Hi, 大家好我是Dylan, 想分享一些前端 coding 小習慣, 將會以分享故事的方式讓大家能更好理解, 會以 React 或原生的 js 為主, 若有錯誤的還請糾正或一起討論, 也希望大家能分享自己覺得不錯的好習慣。
### 1.不要寫inline style
**1.不好維護**
有一天, A工程師10個相同的Loading元件全寫了inline style, 過了三個月後, 設計師突然說要改Layout, 這時A工程師剛好不在, B工程師一查, 發現不得了要改這麼多地方, 該直接用取代改嗎? 會不會有沒搜尋到的地方, 因為擔心 **style 順序不確定有沒有一致**, 若搜尋說不定會少查到, 只好一頁一頁去改了...
例圖:B一查要改這麼多地方, 說不定還有沒查到的!

**2.Html 變更亂了, 且沒有識別性**
B工程師第一眼不知道這**一大串div tag到底是代表什麼東西**, 心想:"這是設計師叫我改的東西嗎? 真希望上面寫一個有意義的class名稱能一眼看出這是什麼
例圖:B心想若A能寫class="loading-wrapper"這樣該有多好

**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 的好寫法
拿我家貓的照片來當範例

**不建議的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 組件套件, 後續要維護版本問題就會散落在各套件
