你能給前端工程化下個(gè)定義么?
作為前端工程師,前端工程化是經(jīng)常聽(tīng)到的概念,但雖然經(jīng)常聽(tīng)到,很多人對(duì)它的認(rèn)識(shí)依然很模糊。
比如,提到前端工程化,他并不能說(shuō)出什么是前端工程化。給出一門(mén)具體的技術(shù),他也不能確定是不是屬于工程化范疇的技術(shù)。
這是因?yàn)樗麤](méi)有對(duì)前端工程化有一個(gè)概念上的認(rèn)識(shí)。
那么,這篇文章我們就來(lái)給前端工程化下個(gè)定義吧。
什么是前端工程化
提到前端工程化,最容易想到的就是編譯了。很多代碼需要經(jīng)過(guò)編譯才能運(yùn)行在目標(biāo)環(huán)境:
- 高版本的語(yǔ)法需要用 babel 編譯成低版本的。
- less、sass 要經(jīng)過(guò)各自的編譯器轉(zhuǎn)換成 css 代碼。
- TypeScript 代碼需要經(jīng)過(guò) tsc 或者 babel 等編譯器轉(zhuǎn)成 JS 代碼。
- ...
前端工程化首先要做的就是支持各種代碼的編譯。
最早的前端工程化是通過(guò)任務(wù)的形式組織這些編譯過(guò)程的,指定對(duì)什么文件用什么編譯器編譯,然后輸出到哪個(gè)目錄。任務(wù)之間可以規(guī)定先后順序、串行并行。
gulp 就是這一類(lèi)工具,叫做任務(wù)運(yùn)行器(task runner)。
這一類(lèi)工具能夠組織整個(gè)編譯流程,對(duì)不同的文件分別做相應(yīng)的處理,使之能運(yùn)行在目標(biāo)環(huán)境。但因?yàn)槊總€(gè)任務(wù)都比較獨(dú)立,很難做一些全局的優(yōu)化。
后來(lái)出現(xiàn)了另一種思路,不通過(guò)任務(wù)組織了,而是分析模塊之間的依賴(lài)關(guān)系,從入口模塊開(kāi)始構(gòu)建一棵依賴(lài)圖,中間遇到的用到的 js、css、圖片等都會(huì)作為他的依賴(lài)。然后對(duì)依賴(lài)圖的每個(gè)節(jié)點(diǎn)分別用對(duì)應(yīng)的編譯器處理。
有的同學(xué)說(shuō),這個(gè)和 task runner 的方式有啥區(qū)別,不都是對(duì)不同的文件用不同的編譯器處理么?
那肯定有區(qū)別呀,現(xiàn)在有了模塊之間的依賴(lài)圖了,那就可以做一些全局的優(yōu)化:
比如通過(guò)分析依賴(lài)關(guān)系來(lái)去掉一些沒(méi)有用到的代碼,這叫做 tree shaking。
比如把這些模塊拆分到不同的分組(chunk)里,然后生成不同的文件,這樣把變動(dòng)頻繁的模塊和不咋變動(dòng)的模塊分到不同的 chunk,進(jìn)而生成到不同的文件里,就可以更好的利用緩存,這叫做 code splitting。
而且,因?yàn)樯傻拇a是自己控制的,有自己的 runtime 代碼,那就可以配合 runtime 來(lái)實(shí)現(xiàn)一些功能,比如實(shí)現(xiàn)模塊的 lazy load,也就是把 code splitting 分出來(lái)的 chunk,在運(yùn)行時(shí)動(dòng)態(tài)加載。
這叫做打包工具(bundler),典型的是 webpack。
任務(wù)運(yùn)行器和打包工具的區(qū)別還是很明顯的:
任務(wù)運(yùn)行器只是把不同的編譯任務(wù)組織起來(lái),并不參與具體的代碼處理,具體處理啥文件,怎么處理都是開(kāi)發(fā)者指定的。
而打包工具則是分析模塊依賴(lài)關(guān)系,構(gòu)成依賴(lài)圖,通過(guò)這種方式確定處理哪些文件,可以基于這種依賴(lài)關(guān)系實(shí)現(xiàn) tree shking、code splitting 等優(yōu)化,并且生成的代碼會(huì)有自己的 runtime,可以配合 runtime 實(shí)現(xiàn) lazy load 等功能。
因?yàn)榇虬ぞ哌@種明顯的優(yōu)勢(shì),慢慢的就取代了任務(wù)運(yùn)行器,成為了構(gòu)建的主流方式。
但是打包工具也不是完美的,因?yàn)槊看味家獦?gòu)建整個(gè)依賴(lài)圖,對(duì)不同文件分別做處理,之后才能生成代碼,所以當(dāng)項(xiàng)目的模塊多了就會(huì)很慢,大項(xiàng)目打包幾分鐘也是很常見(jiàn)的事情。
有痛點(diǎn)問(wèn)題,大家就會(huì)想辦法去解決,所以出現(xiàn)了 no bundle 的方案,也就是不打包,比如 vite。
不打包也就不會(huì)進(jìn)行依賴(lài)分析,那怎么確定處理哪些文件呢?
no bundle 是基于瀏覽器支持 es module 來(lái)實(shí)現(xiàn)的,瀏覽器會(huì)做 es module 的依賴(lài)分析,然后加載對(duì)應(yīng)的模塊,這樣自然就不用自己做依賴(lài)分析了,只需要實(shí)現(xiàn)模塊的編譯即可。
所以,no bundle 工具會(huì)啟動(dòng)一個(gè)開(kāi)發(fā)服務(wù)器,根據(jù)請(qǐng)求的模塊路徑來(lái)進(jìn)行相應(yīng)的編譯,然后返回編譯后的代碼。
當(dāng)然,生產(chǎn)環(huán)境還是需要打包的,會(huì)用打包工具來(lái)處理。no bundle 方案只是解決了開(kāi)發(fā)環(huán)境下打包工具要構(gòu)建整個(gè)依賴(lài)圖導(dǎo)致比較慢的痛點(diǎn)問(wèn)題。
我們回過(guò)頭來(lái)綜合看一下:
構(gòu)建的核心是對(duì)不同的文件做不同的編譯,最早的任務(wù)運(yùn)行器的方案實(shí)現(xiàn)了編譯流程的組織,但是并沒(méi)有做全局的優(yōu)化,也沒(méi)有自己的 runtime 代碼,所以出現(xiàn)了基于依賴(lài)分析的打包工具,打包工具可以基于依賴(lài)分析實(shí)現(xiàn) treeshking、code splitting 等優(yōu)化,可以配合 runtime 代碼實(shí)現(xiàn) lazy load。但成也依賴(lài)分析,敗也依賴(lài)分析,這個(gè)太慢了,所以出現(xiàn)了 no bundle 的方案,配合瀏覽器對(duì) es module 的支持,只要實(shí)現(xiàn)對(duì)應(yīng)模塊的編譯服務(wù)即可,不過(guò)生產(chǎn)環(huán)境還是要打包的。
那我們馬后炮一下,假如回到 gulp 當(dāng)時(shí)的時(shí)代,能夠?qū)崿F(xiàn)打包工具和 no bundle 服務(wù)么?
還真不一定,因?yàn)榇虬ぞ叩膶?shí)現(xiàn)是基于模塊規(guī)范的,很早的時(shí)候并沒(méi)有,所以只能簡(jiǎn)單的對(duì)編譯流程做下組織。更不用說(shuō) no bundle 還要瀏覽器支持 es module 了,這個(gè)也是近幾年才可以的。
所以,不管是任務(wù)運(yùn)行器、打包工具、no bundle 服務(wù)都是在當(dāng)時(shí)的環(huán)境下的最優(yōu)的解決方案,并不是說(shuō)被淘汰的就是不好的。
上面我們只聊了構(gòu)建,那前端工程化就等于構(gòu)建么?
肯定不是呀,還有很多別的方面,比如代碼的規(guī)范和靜態(tài)分析:
- JS 代碼會(huì)用 ESLint 來(lái)禁止掉一些寫(xiě)法,比如 concole、debugger 的使用,還可以修復(fù)格式問(wèn)題,比如縮進(jìn)方式,還能檢查出一些邏輯錯(cuò)誤,比如 if 中用了賦值。
- CSS 代碼也同樣會(huì)用 StyleLint 來(lái)禁用一些寫(xiě)法,修復(fù)格式問(wèn)題,檢查出一些邏輯錯(cuò)誤。
- ESLint、StyleLint 只是局部的格式修復(fù),我們還可以用 prettier 來(lái)進(jìn)行整體的格式化。
- 如果我們用了 TypeScript,那就可以用 tsc 來(lái)進(jìn)行類(lèi)型檢查,發(fā)現(xiàn)代碼中潛在的類(lèi)型不匹配的錯(cuò)誤。
靜態(tài)分析工具、格式化工具并不影響構(gòu)建,他們一般是單獨(dú)來(lái)跑的,用來(lái)發(fā)現(xiàn)一些代碼潛在的問(wèn)題,規(guī)范代碼格式等。
代碼寫(xiě)完之后,會(huì)上傳到代碼倉(cāng)庫(kù),比如 gitlab,代碼托管也是工程化的一部分。
代碼上線的話(huà),需要進(jìn)行構(gòu)建和部署,我們可以通過(guò) jenkins 來(lái)組織構(gòu)建流程,當(dāng) gitlab 代碼有新的 push 的時(shí)候觸發(fā),進(jìn)行構(gòu)建,然后把產(chǎn)物部署到服務(wù)器,基于 git hook 的構(gòu)建部署流程就叫做持續(xù)集成、持續(xù)部署(CI/CD)。這也是前端工程化的一部分。
好像很多東西都屬于前端工程化,那怎么給前端工程化下個(gè)定義呢?
前面聊了構(gòu)建、靜態(tài)分析、格式化、代碼托管、CI/CD,不知道大家有沒(méi)有發(fā)現(xiàn)這些工具的共同特點(diǎn):
他們的處理對(duì)象都是代碼。
他們只是把代碼當(dāng)作字符串來(lái)處理,并不管你用的是 vue、react 還是 angular,你用的啥狀態(tài)管理庫(kù)、動(dòng)畫(huà)庫(kù)之類(lèi)的。
所以說(shuō),前端工程化就是處理代碼的一系列工具鏈,他們并不會(huì)運(yùn)行代碼,只是把代碼作為字符串來(lái)進(jìn)行一系列處理。編譯構(gòu)建、ci/cd、代碼托管、靜態(tài)分析、格式化等都是。
不知道大家是否理解了。我們來(lái)看兩個(gè)例子:
我們項(xiàng)目用了 react,公共組件比較多,所以封裝了 react 的組件庫(kù)。這屬于前端工程化么?
不屬于。前端框架還有組件都是運(yùn)行時(shí)才有的,工程化并不會(huì)運(yùn)行代碼,只會(huì)處理代碼。所以組件庫(kù)屬于前端基建,但不屬于前端工程化。
我們好幾個(gè)項(xiàng)目之間公共代碼比較多,所以改造成了 monorepo 的形式,也就是一個(gè)工程下保存了多個(gè)項(xiàng)目的代碼,使用了 pnpm workspace 來(lái)作為 monorepo 的管理工具,可以自動(dòng)的進(jìn)行依賴(lài)的關(guān)聯(lián),統(tǒng)一的進(jìn)行依賴(lài)安裝、構(gòu)建、發(fā)版等。這屬于前端工程化么?
屬于。monorepo 是組織代碼的方式,pnpm workspace 是管理 monorepo 的工具,它也是處理代碼的工具,不會(huì)運(yùn)行代碼,所以也屬于前端工程化的范疇。
我們公司自研了 IDE,集成了很多內(nèi)部工具,這屬于前端工程化么?
屬于。IDE 是圍繞代碼編輯的場(chǎng)景來(lái)打造一系列工具鏈,也是處理代碼但不會(huì)運(yùn)行代碼,所以屬于前端工程化的范疇。
經(jīng)過(guò)這些例子,相信大家對(duì)什么是前端工程化,哪些技術(shù)屬于前端工程化就比較清晰了。
我們是前端工程師,所以經(jīng)常談的是前端的工程化,其實(shí)別的語(yǔ)言也有工程化,比如 java 代碼,同樣需要構(gòu)建、格式化、靜態(tài)分析、CI/CD,所以也有工程化的概念。
其實(shí)大公司都會(huì)有一個(gè)工程效能部門(mén),他們做的就是工程化的事情,不過(guò)一般是跨語(yǔ)言的工程化,并不局限于前端工程化、后端工程化等。
總結(jié)
前端工程化是指圍繞代碼處理的一系列工具鏈,他們把代碼當(dāng)作字符串處理,并不運(yùn)行代碼,包括編譯構(gòu)建、靜態(tài)分析、格式化、CI/CD 等等。
我們?cè)敿?xì)了解了編譯構(gòu)建的歷史,從任務(wù)運(yùn)行器、打包工具到 no bundle 服務(wù)的演變歷史,他們都是特定時(shí)代下的產(chǎn)物。
再就是靜態(tài)分析和格式化用的 eslint、stylelint、prettier、tsc 等工具。
前端工程化的范圍可以很大,可以囊括很多工具進(jìn)來(lái),比如 monorepo、IDE 等等,因?yàn)樵诓煌膱?chǎng)景下對(duì)代碼處理,也就是工程化有不同的需求。
當(dāng)你對(duì)前端工程化有了清晰的定義之后,對(duì)于前端工程化要做哪些事情,哪些技術(shù)屬于前端工程化、哪些不屬于,就很容易理清了。