?一定要優(yōu)雅,高端前端程序員都應該具備的基本素養(yǎng)
大廠技術 高級前端 Node進階
點擊上方 程序員成長指北,關注公眾號
回復1,加入高級Node交流群
作者:清夜 原文:https://juejin.cn/post/7107119166989336583
近來看到很多公司裁員,忽然驚醒,之前是站在項目角度考慮問題,卻沒站在咱們程序員本身看待問題,險些釀成大錯,如果人人都能做到把項目維護得井井有條,無論什么人都能看明白都能快速接手,那咱們的競爭力在哪里呢?這個時候我再看項目中那些被我天天罵的代碼,頓時心中就無限景仰起來,原來屎山才是真能能夠保護我們的東西,哪有什么歲月靜好,只是有人替你負屎前行罷了
為了能讓更多人認識到這一點,站在前端的角度上,我在仔細拜讀了項目中的那些暗藏玄機的代碼后,決定寫下此文,由于本人功力尚淺,且之前一直走在錯誤的道路上,所以本文在真正的高手看來可能有些班門弄斧,在此獻丑了??
用 TypeScript,但不完全用
TypeScript大行其道,在每個團隊中,總有那么些個宵小之輩想盡一切辦法在項目里引入 ts,這種行為嚴重阻礙了屎山的成長速度,但同是打工人我們也不好阻止,不過就算如此,也無法阻止我們行使正義
眾所周知,TypeScript 別名 AnyScript,很顯然,這就是TypeScript創(chuàng)始人Anders Hejlsberg給我們留下的暗示,我們有理由相信AnyScript 才是他真正的目的
const list: any = []
const obj: any = {}
const a: any = 1
引入了 ts的項目,由于是在原可運行代碼的基礎上額外添加了類型注釋,所以代碼體積毫無疑問會增大,有調查顯示,可能會增加 30%的代碼量,如果充分發(fā)揮 AnyScript 的宗旨,意味著你很輕松地就讓代碼增加了 30% 毫無用處但也挑不出啥毛病的代碼,這些代碼甚至還會增加項目的編譯時間(畢竟增加了ts校驗和移除的成本嘛)
你不僅能讓自己寫的代碼用上 AnyScript,甚至還可以給那些支持 ts 的第三方框架/庫一個大嘴巴子
export default defineComponent({
props: {
// 現(xiàn)在 data 是 any 類型的啦
data: {
type: Number as PropType<any>,
},
},
setup(_, { emit }) {
// 現(xiàn)在 props 是 any 類型的啦
const props: any = _
...
}
})
當然了,全屏 any可能還是有點明顯了,所以你可以適當?shù)亟o部分變量加上具體類型,但是加上類型不意味著必須要正確使用
const obj: number[] = []
// ...
// 雖然 obj 是個 number[],但為了實現(xiàn)業(yè)務,就得塞入一些不是 number 的類型,我也不想的啊是不是
// 至于編輯器會劃紅線報錯?那是小問題,不用管它,別人一打開這個項目就是滿屏的紅線,想想就激動
obj.push('2')
obj.push([3])
命名應該更自由
命名一直是個困擾很多程序員的問題,究其原因,我們總想給變量找個能夠很好表達意思的名稱,這樣一來代碼的可閱讀性就高了,但現(xiàn)在我們知道,這并不是件好事,所以我們應該放縱自我,既擺脫了命名困難癥,又加速了屎山的堆積進度
const a1 = {}
const a2 = {}
const a3 = 2
const p = 1
我必須強調一點,命名不僅是變量命名,還包含文件名、類名、組件名等,這些都是我們可以發(fā)揮的地方,例如類名
<div class="box">
<div class="box1"></div>
<div class="box2"></div>
<div>
<div class="box3"></div>
乍一看似乎沒啥毛病,要說有毛病似乎也不值當單獨挑出來說,沒錯,要的就是這個效果,讓人單看一段代碼不好說什么,但是如果積少成多,整個項目都是 box呢?全局搜索都給你廢了!如果你某些組件再一不小心沒用 scoped 呢?稍不留意就不知道把什么組件的樣式給改了,想想就美得很
關于 css我還想多說一點,鑒于其靈活性,我們還可以做得更多,總有人說什么 BEM 不 BEM的,他們敢用我們就敢寫這樣的代碼
&-card {
&-btn {
&_link {
&--right {
}
}
&-nodata {
&_link {
&--replay {
&--create {}
}
}
}
}
&-desc {}
}好了,現(xiàn)在請在幾百行(關于這一點下一節(jié)會說到)這種格式的代碼里找出類名 .xxx__item_current.mod-xxx__link 對應的樣式吧
代碼一定要長
屎山一定是夠高夠深的,這就要求我們的代碼應該是夠長夠多的
大到一個文件的長度,小到一個類、一個函數(shù),甚至是一個 if 的條件體,都是我們自由發(fā)揮的好地方。
什么單文件最好不超過 400行,什么一個函數(shù)不超過 100行,簡直就是毒瘤,

所以這就要求我們要具備將十行代碼就能解決的事情寫成一百行的能力,最好能給人一種多即是少的感覺
data === 1
? 'img'
: data === 2
? 'video'
: data === 3
? 'text'
: data === 4
? 'picture'
: data === 5
? 'miniApp'
三元表達式可以優(yōu)雅地表達邏輯,像詩一樣,雖然這段代碼看起來比較多,但邏輯就是這么多,我還專門用了三元表達式優(yōu)化,不能怪我是不是?什么map映射枚舉優(yōu)化聽都沒聽過
你也可以選擇其他一些比較容易實現(xiàn)的思路,例如,多寫一些廢話
if (a > 10) {
// 雖然下面幾個 if 中對于 a 的判斷毫無用處,但不仔細看誰能看出來呢?看出來了也不好說什么,畢竟也沒啥錯
// 除此之外,多級 if 嵌套也是堆屎山的一個小技巧,什么提前 return 不是太明白
if (a > 5) {
if (a > 3 && b) {
}
}
if (a > 4) {
}
}
除此之外,你還可以寫一些中規(guī)中矩的方法,但重點在于這些方法根本就沒用到,這種發(fā)揮的地方就更多了,簡直就是擴充代碼體積的利器,畢竟單看這些方法沒啥毛病,但誰能想到根本就用不到呢?就算有人懷疑了,但你猜他敢隨便從運行得好好的業(yè)務項目里刪掉一些沒啥錯的代碼嗎?
一次編寫,到處引用
大家應該都知道,一個公共的方法或變量最好放在公共文件夾中,這樣方便引用,提升聚合度,本來這種做法是會極大阻礙屎山進程的,但俗話說事在人為,哪有什么絕對正確的東西,不過看是誰在當攪屎棍罷了
例如在一個項目中,分為好幾個頁面,首頁和詳情頁,我在寫詳情頁的時候,發(fā)現(xiàn)有一個變量在首頁已經(jīng)存在了,一般做法是把這個變量提升到公共文件中去,這樣將來如果涉及到各自修改的話,能做到心中有數(shù),但我偏不,我TM直接在詳情頁里引用首頁的變量
我不僅在業(yè)務組件里引用,甚至還可以在公共組件里引用,一般做法是業(yè)務組件引用公共組件/方法/變量,嘿,我偏要公共組件引用業(yè)務組件組件/方法/變量
這樣導致的后果就是,一個本該私有的變量/方法,被不知道多少個頁面組件、模塊組件、公共文件所引用,萬一需要修改,處理不好的話直接塌方,任何一個參與改動的人都戰(zhàn)戰(zhàn)兢兢如履薄冰,要么就是復制粘貼重寫一份,要么就是要寫很多沒少意義的兼容邏輯,無論選擇哪種,都將成為屎山的忠實貢獻者
組件、方法多多滴耦合
為了避免其他人復用我的方法或組件,那么在寫方法或組件的時候,一定要盡可能耦合,提升復用的門檻
例如明明可以通過 Props傳參解決的事情,我偏要從全局狀態(tài)里取,例如vuex,獨一份的全局數(shù)據(jù),想傳參就得改 store數(shù)據(jù),但你猜你改的時候會不會影響到其他某個頁面某個組件的正常使用呢?如果你用了,那你就可能導致意料之外的問題,如果你不用你就得自己重寫一個組件
組件不需要傳參?沒關系,我直接把組件的內部變量給掛到全局狀態(tài)上去,雖然這些內部變量確實只有某一個組件在用,但我掛到全局狀態(tài)也沒啥錯啊是不是
嘿,明明一個組件就能解決的事情,現(xiàn)在有了倆,后面還可能有仨,這代碼量不就上來了嗎?
方法也是如此,明明可以抽取參數(shù),遵循函數(shù)式編程理念,我偏要跟外部變量產生關聯(lián)
// 首先這個命名就很契合上面說的自由命名法
function fn1() {
// ...
// fn1 的邏輯比較長,且解決的是通用問題,
// 但 myObj 偏偏是一個外部變量,這下看你怎么復用
window.myObj.name = 'otherName'
window.myObj.children.push({ id: window.myObj.children.length })
// ...
}
翻譯翻譯,什么叫 mutable
翻譯翻譯?
好,我翻譯下
~Immutable Data 就是一旦創(chuàng)建,就不能再被更改的數(shù)據(jù)~
Mutable Data 就是一旦創(chuàng)建,就能隨時被更改的數(shù)據(jù)
得益于 javascript 的靈活性,un-immutable 大大滴有市場,借助這把利器,我們可以讓數(shù)據(jù)的流動如空氣般無處不在,如何使用 Mutable Data相信很多初學者早已無師自通,我就不多說了,只希望你們莫忘初心
難的是,能在使用這把利器的同時,還能駕馭好它
如何做到?我們以 vue為例的話,那么只需要三個單詞:Watch、Watch 還是踏馬的 Watch!
export default {
watch: {
'$route.name': fuction(){
this.handleChange()
},
'userName': fuction(){
this.handleChange()
},
'dateTime': fuction(){
this.handleChange()
},
'color': fuction(){
this.handleChange()
},
'queryKey': fuction(){
this.handleChange()
},
},
methods: {
handleChange() {
// ...
}
}
}
當然,如果你用 vue-ts的話,就更省心了
@Watch('$route.name')
@Watch('userName')
@Watch('dateTime')
@Watch('color')
@Watch('queryKey')
handleChange() {
// ...
}
至于 react,無非是換成useEffect,總之本質上都是踏馬的 watch,能 mutable 的絕不 immutable,能 watch的絕不主動調用!
注意,千萬不要加 debounce,想象一下,watch 的所有字段可能會同時觸發(fā),而 handleChange 恰好又是一個耗性能的方法……
魔術字符串是個好東西
實際上,據(jù)我觀察,排除掉某些居心不軌的人之外,大部分人還是比較喜歡寫魔術字符串的,這讓我很欣慰,看著滿屏的不知道從哪里冒出來也不知道代表著什么的硬編碼字符串,讓人很有安全感
if (a === 'prepare') {
const data = localStorage.getItem('HOME-show_guide')
// ...
} else if (a === 'head' && b === 'repeating-error') {
switch(c) {
case 'pic':
// ...
break
case 'inDrawer':
// ...
break
}
}
基于此,我們還可以做得更多,比如用變量拼接魔術字符串,debug的時候直接廢掉全局搜索
if (a === query.name + '_head') {
}
大家都是中國人,為什么不試試漢字呢?
if (data === '正常') {
} else if (data === '錯誤') {
} else if (data === '通過') {
}
一般人認為魔術字符串就是寫死的字符串,但其實還可以玩得更魔幻一點
enum EventType {
Move,
Skip,
Batch
}
枚舉是個好東西,可以避免直接書寫無意義的字符,但前提是你真的是按照正常人思維來使用的 正常人是怎么用的呢?例如,我們有一段 vue邏輯就是用判斷這個枚舉值的
handleEvent(value: EventType) {
if (value === EventType.Move) {
// ...
} else if (value === EventType.Skip) {
// ...
} else if (value === EventType.Batch) {
// ...
}
}
看著沒啥問題,挺好的,但在模板里,誒,我嫌麻煩,我這樣寫
<template>
<div @click="handleEvent(2)">確定</div>
<template>2是什么?寫這段代碼的人那肯定知道啊,就是 EventType.Batch 的默認值,但是我不跟別人說我也不寫注釋,后面人看到 EventType的定義只會想到這是個枚舉定義,正常人是不可能想到這些枚舉的默認值居然還會以魔術字符串的形式被用到
那么想一下,某一天,一個倒霉蛋想改這段代碼,他想在 EventType 中新增一個枚舉,為了以防萬一改到其他位置,他搜遍了整個項目確認了每個項目中用到 EventType 的地方,發(fā)現(xiàn)自己的改動不會有任何問題,于是他在 EventType的開頭加上了自己的內容
enum EventType {
Clown,
Move,
Skip,
Batch
}
好了兄弟們,<div @click="handleEvent(2)">確定</div> 這段邏輯不聲不響地完蛋了!
輪子就得自己造才舒心
眾所周知,造輪子可以顯著提升我們程序員的技術水平,另外由于輪子我們已經(jīng)自己造了,所以減少了對社區(qū)的依賴,同時又增加了項目體積,有力地推動了屎山的成長進程,可以說是一魚兩吃了
例如我們可能經(jīng)常在項目中使用到時間格式化的方法,一般人都是直接引入 dayjs完事,太膚淺了,我們應該自己實現(xiàn),例如,將字符串格式日期格式化為時間戳
function format(str1: any, str2: any) {
const num1 = new Date(str1).getTime()
const num2 = new Date(str2).getTime()
return (num2 - num1) / 1000
}
多么精簡多么優(yōu)雅,至于你說的什么格式校驗什么 safari下日期字符串的特殊處理,等遇到了再說嘛,就算是dayjs不也是經(jīng)過了多次 fixbug才走到今天的嘛,多一些寬松和耐心好不好啦
如果你覺得僅僅是 dayjs這種小打小鬧難以讓你充分發(fā)揮,你甚至可以造個 vuex,vue官網(wǎng)上寫明了eventBus可以充當全局狀態(tài)管理的,所以我們完全可以自己來嘛,這里就不舉例了,這是自由發(fā)揮的地方,就不局限大家的思路了
借助社區(qū)的力量-輪子還是別人的好
考慮到大家都只是混口飯吃而已,凡事都造輪子未免有些強人所難,所以我們可以嘗試走向另外一個極端——凡事都用輪子解決
判斷某個變量是字符串還是對象,kind-of[1]拿來吧你;獲取某個對象的 key,object-keys[2]拿來吧你;獲取屏幕尺寸,vue-screen-size[3]拿來吧你……等等,就不一一列舉了,需要大家自己去發(fā)現(xiàn)
先甭管實際場景是不是真的需要這些庫,也甭管是不是殺雞用牛刀,要是大家聽都沒聽過的輪子那就更好了,這樣才能彰顯你的見多識廣,總之能解決問題的輪子就是好問題,
在此我得特別提點一下 lodash,這可是解決很多問題的利器,但是別下載錯了,得是 commonjs版本[4]的那個,量大管飽還正宗,es module[5]版本是不行滴,太小家子氣
import _ from 'lodash'
多嘗試不同的方式來解決相同的問題
世界上的路有很多,很多路都能通往同一個目的地,但大多數(shù)人庸庸碌碌,只知道沿著前人的腳步,沒有自己的思想,別人說啥就是啥,這種行為對于我們程序員這種高端的職業(yè)來說,壞處很大,任何一個有遠大理想的程序員都應該避免
落到實際上來,就是嘗試使用不同的技術和方案解決相同的問題
搞個 css模塊化方案,什么BEM、OOCSS、CSS Modules、CSS-in-JS都在項目里引入,緊跟潮流擴展視野vue項目只用template?遜啦你,render渲染搞起來之前看過什么前端依賴注入什么反射的文章,雖然對于絕大多數(shù)業(yè)務項目而言都是水土不服,但問題不大,能跑起來就行,引入引入 還有那什么 rxjs,人家都說好,雖然我也不知道好在哪里,但勝在門檻高一般人搞不清楚所以得試試Pinia是個好東西,什么,我們項目里已經(jīng)有vuex了?out啦,人家官網(wǎng)說了vue2也可以用,我們一定要試試,緊跟社區(qū)潮流嘛,一個項目里有兩套狀態(tài)管理有什么值得大驚小怪的!
做好自己,莫管他人閑事
看過一個小故事,有人問一個年紀很大的老爺爺?shù)拈L壽秘訣是什么,老爺爺說是從來不管閑事
這個故事對我們程序員來說也很有啟發(fā),寫好你自己的代碼,不要去關心別人能不能看得懂,不要去關心別人是不是會掉進你寫的坑里
mounted() {
setTimeout(() => {
const width = this.$refs.box.offsetWidth
const itemWidth = 50
// ...
}, 200)
}
例如對于上述代碼,為什么要在 mounted里寫個 setTimeout呢?為什么這個 setTimeout的時間是 200呢?可能是因為 box 這個元素大概會在 mounted之后的 200ms左右接口返回數(shù)據(jù)就有內容了,就可以測量其寬度進行其他一系列的邏輯了,至于有沒有可能因為網(wǎng)絡等原因超過 200ms還是沒有內容呢?這些不需要關心,你只要保證在你開發(fā)的時候 200ms這個時間是沒問題的就行了;itemWidth代表另外一個元素的寬度,在你寫代碼的時候,這個元素寬度就是 50,所以沒必要即時測量,你直接寫死了,至于后面其他人會不會改變這個元素的寬度導致你這里不準了,這就不是你要考慮的事情了,你開發(fā)的時候確實沒問題,其他人搞出來問題其他人負責就行,管你啥事呢?
代碼自解釋
高端的程序員,往往采用最樸素的編碼方式,高手從來不寫注釋,因為他們寫的代碼都是自解釋的,什么叫自解釋?就是你看代碼就跟看注釋一樣,所以不需要注釋
我覺得很有道理,代碼都在那里擱著了,邏輯寫得清清楚楚,為啥還要寫注釋呢,直接看代碼不就行了嗎?
乍一看,似乎這一條有點阻礙堆屎山的進程,實則不然
一堆注定要被迭代無數(shù)版、被無數(shù)人修改、傳承多年的代碼,其必定是邏輯錯綜復雜,難免存在一些不可名狀的讓人說不清道不明的邏輯,沒有注釋的加成,這些邏輯大概率要永遠成為黑洞了,所有人看到都得繞著走,相當于是圍繞著這些黑洞額外搭起了一套邏輯,這代碼體積和復雜度不就上來了嗎?
如果你實在手癢,倒也可以寫點注釋,我這里透露一個既能讓你寫寫注釋過過癮又能為堆屎山加一把力的方法,那就是:在注釋里撒謊!
沒錯,誰說注釋只能寫對的?我理解不夠,所以注釋寫得不太對有什么奇怪的嗎?我又沒保證注釋一定是對的,也沒逼著你看注釋,所以你看注釋結果被注釋誤導寫了個bug,這憑啥怪我啊
// 計算 data 是否可用
//(實際上,這個方法的作用是計算 data 是否 不可用)
function isDisabledData(data: any) {
// ...
}
上述這個例子只能說是小試牛刀,畢竟多調試一下很容易被發(fā)現(xiàn)的,但就算被發(fā)現(xiàn)了,大家也只會覺得你只是個小粗心鬼罷了,怎么好責怪你呢,這也算是給其他人的一個小驚喜了,況且,萬一真有人不管不顧就信了,那你就賺大了
編譯問題堅決不改
為了阻礙屎山的成長速度,有些陰險的家伙總想在各種層面上加以限制,例如加各種lint,在編譯的時候,命令行中就會告訴你你哪些地方?jīng)]有按照規(guī)則來,但大部分是 waring 級別的,即你不改項目也能正常運行,這就是我們的突破點了。
盡管按照你的想法去寫代碼,lint的事情不要去管,waring報錯就當沒看到,又不是不能用?在這種情況下,如果有人不小心弄了個 error級別的錯誤,他面對的就是從好幾屏的 warning 中找他的那個 error 的場景了,這就相當于是提前跟屎山來了一次面對面的擁抱
根據(jù)破窗理論,這種行為將會影響到越來越多的人,大家都將心照不宣地視 warning于無物(從好幾屏的 warning中找到自己的那個實在是太麻煩了),所謂的 lint就成了笑話
小結
一座歷久彌香的屎山,必定是需要經(jīng)過時間的沉淀和無數(shù)人的操練才能最終成型,這需要我們所有人的努力,多年之后,當你看到你曾經(jīng)參與堆砌的屎山中道崩殂轟然倒塌的時候,你就算是真的領悟了我們程序員所掌控的恐怖實力!
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
如果你覺得這篇內容對你有幫助,我想請你幫我2個小忙:
1. 點個「在看」,讓更多人也能看到這篇文章 2. 訂閱官方博客 www.inode.club 讓我們一起成長 點贊和在看就是最大的支持??
參考資料
https://www.npmjs.com/package/kind-of: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fkind-of
[2]https://www.npmjs.com/package/object-keys: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fobject-keys
[3]https://www.npmjs.com/package/vue-screen-size: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fvue-screen-size
[4]https://www.npmjs.com/package/lodash: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Flodash
[5]https://www.npmjs.com/package/lodash-es: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Flodash-es
