隨著業務不斷髮展,產品規模不斷壯大。越來越多應用建立起立,業務邏輯各有不同且日趨複雜,團隊人員越發龐大,程式碼維護成本越來越高。 不斷實踐並不斷思考,前端開發其實就是要推動前端工程化。
-
日常開發中,我們為什麼需要前端工程化?
-
webpack、npm、gulp等它們有什麼關係?
-
有多種構建工具,比如npm、yarn、browserify、gulp、grunt等,你知道他們之間的區別嗎?
-
實際開發中卻經常遇到第三方包報錯,有時候我們可以通過
rm -rf node_modules && npm i
暴力地將問題解決,有時候卻不能。你知道是什麼原因嗎?
本文闡述前端工程化的一些思考和實踐。
什麼是前端工程化
工程化是指
將系統化的、嚴格約束的、可量化的方法應用於軟體的開發、執行和維護,即將工程化應用於軟體。———— 清華大學出版社《軟體工程》
軟體工程與其說是計算機中的一門科學,不如說是偏向於提高系統生產效率的一整套方法論。
前端工程化能夠有效 提高 需求開始,需求開發,釋出部署,測試階段
的生產效率,提高程式碼質量,降低開發成本。
前端工程化是為開發者服務的。簡而言之,就是將日常開發的重複步驟整化為固定流程,把前端開發工作帶入到更加系統和規範體系的一系列過程
為什麼要進行前端工程化
使用者的需求從最開始簡單的頁面在向複雜的應用發展。前端需要做的事情更多,同時也要追求更友好的使用者體驗。
第一階段:石器時代
適合小專案,不分前後端,頁面由jsp、php、tornado template等在服務端生成,瀏覽器負責展現。
第二階段:後端MVC
為了降低複雜度,有了Web Server層的框架升級,比如Structs、Spring MVC等。
以上兩個階段存在的問題
- 前端開發依賴開發環境
- 前後端沒分離,可維護性差
第三階段:SPA
2005年推出Ajax後,前端進入SPA階段。 Single Page Application 單頁面應用。
Ajax需要面臨的問題- 前後端介面約定,開發溝通效率低
- 大量的js來滿足複雜的互動,維護成本高
前端工程化的主要目的是讓開發更加規範化、流程化、自動化,實現敏捷開發。
以上三個階段,開發者想要新增一些邏輯,最簡單的做法是在HTML中插入一個script標籤,然後直接在裡面書寫業務邏輯程式碼。這樣做比較簡單直接了當,但是也存在其他問題:
- 不同script標籤之間命名衝突,容易造成全域性作用域汙染
- 程式碼重用性差
針對這些問題,可以將HTML中內聯的JavaScript提取出來成單獨的JS檔案,和使用立即執行函式表示式IIFE包起來,只把介面暴露到全域性。
(function() {
// 通過立即執行函式表示式將作用域隔離
// 邏輯程式碼...
})();
複製程式碼
但是會隨著頁面邏輯的複雜度增加了其他問題:
-
頁面JS檔案引用順序。
HTML頁面引用和處理js檔案只能是順序的(不考慮async等),因此js之間的依賴關係也是順序的。簡單的順序依賴關係無法滿足需求。一個大型工程內部的模組依賴關係通常是樹狀的。 -
頁面引用js檔案的長度與數量。
檔案越來越長,越來越難維護,一個頁面的單個js檔案可能有幾千行程式碼,如果按功能切割成多個小檔案,就會導致頁面請求過多,每個HTTP請求都需要單獨建立連線,導致頁面渲染速度下降
第四階段:前端MVC/MVP/MVVC
為了降低前端開發複雜度,引入了模組化概念。AngularJS、React、Vue三分天下,還有Ember、Knockout、Polymer、Riot等大量前端框架湧現。
前端工程化的主要目的是讓開發更加規範化、流程化、自動化,實現敏捷開發。
第五階段:Node全棧
Nodejs興起,前端語言可用於後端開發,帶來一種新的開發模式。
後兩個階段的好處是- 前後端分離,開發和維護職責分明
- 有利於重複程式碼模組化,減少迭代成本
- 部署相對獨立,專案合理分層,更好維護
前端工程化的基本概念
工具和語言雖然差異大,但是解決的都是相似的問題,歸納為:
- 擴充套件 javascript 、html、css 本身的語言能力
- 解決重複工作
- 模板化、模組化
- 解決功能複用和變更問題
- 解決開發和產品環境差異問題
- 解決釋出流程問題
開發同學不用擔心不同模組間的框架和依附系統差異,只要知道以上命令就能夠完成所有開發流程。優化傾向於底層,脫離業務,實現更高層級的複用
預編譯語言、模組熱載入等技術可以提升開發效率,而利用自動化測試、lint 工具等可以保證程式碼的功能和質量;本地環境下還要生成 source-map、配置模組熱載入等等便於除錯程式碼;而到了生產環境下則要對資源進行壓縮,生成版本號等。
工程化基本思想:模組化
模組化的系統,當需求改動時,開發者可以更快速地將問題定位到相應模組中,模組邏輯清晰不耦合。因此模組化具備更強的可維護性。
早期設計的js並不具備這一特性,CommonJS 以及 AMD 的出現,為前端定義了模組的標準。也有了實現這些模組化的庫,比如 RequireJS 以及 Browserify。
模組化設計的特點:
- 作用域封裝
- 重用性
- 解除耦合
模組化帶來的正面作用:
- 可以讓開發者將自己工程中的程式碼按模組進行劃分,模組之間也不再僅僅是簡單的順序依賴關係。
- 對於客戶端來說,接受打包後的單一檔案,解決檔案過多導致的HTTP請求耗時長問題
- 並根據模組之間的依賴和實際需求,按需載入。
釋出下載包工具:包管理器
包管理器是一個可以讓開發者便捷地獲取程式碼和釋出程式碼的工具。JavaScript 應用中,最主流的包管理器是 npm 和 Yarn。(釋出自己的npm包教程 )
js沒有強大的標準庫,但是有很多小型的開源框架庫滿足常用的功能,比如日期處理、url處理、非同步流程控制等,不需要人手工編寫。一些具有特定功能的程式碼(框架、庫等)按照特性形式被封裝成包,開發者可以通過包管理器安裝這些包,避免重複造輪子,也可以把自己的程式碼通過包的方式分享給別人使用。
npm 特性
npm
是node包管理器,通過npm
獲取專案需要的依賴,並且通過打包工具和業務程式碼打包在一起。其倉庫是一個遵循 npm 特定包規範的站點,提供 API 來讓使用者上傳和下載包、獲取包資訊、以及管理使用者賬號。
npm init
初始化npm專案,並根據終端輸入的基本資訊,生成配置檔案package.json
npm install
遠端或者從目標路徑獲取npm包。(--save 和--save-dev引數區別)
yarn的衝擊
npm 存在 版本號鎖定問題和效能問題,Yarn 這個競爭對手的出現可以說給 npm 帶來了改進的動力。
Yarn 是 Facebook 公司在 2016 年 10 月 11 日開源的模組管理器,它宣稱比 npm 更快、更安全、更可靠。Yarn 並不重頭建立一個新的 Javascript 模組倉庫,而只是替代 npm 客戶端來管理原有的 node_modules 中的模組,並彌補 npm 的缺陷。 相對於npm的優點是:
-
會幫助開發者自動生成和維護版本號描述檔案。 初次執行 Yarn 時會在專案中自動生成一個名為 yarn.lock 的檔案,它與 npm shrinkwrap 的內容形式很相近,並且會隨著模組的更新自動同步。
-
效能比當時 npm 更優。 Yarn 為了解決當時 npm 安裝模組速度慢問題,在拉取包時採用並行操作,優化了請求佇列,更高效地利用當前的網路資源。同時預設的 yarn.lock 也無形中減少了解析 semver 與獲取模組最新版本的時間。
預編譯語言是什麼?
使用預編譯語言的主要目的是為了實現 HTML、CSS、JavaScript本身語言所不具備的特性。比如最常見的 SASS,它是CSS的預編譯語言,通過它開發者可以使用模組、定義變數、編寫巢狀規則等等來提高開發效率。另外還有 Babel 預編譯 JavaScript 來實現新的 ES 特性,以及使用 TypeScript 去做型別檢查。
Gulp 和 Grunt 有什麼作用?
通過Gulp 和 Grunt 等構建流程管理工具,使得構建變得更加簡單化,通過專案中的一些配置,開發者可以使用簡單的一行命令啟動本地開發環境或者構建和釋出整個工程。
構建流程優化是什麼?可以做什麼?
開發者修改程式碼並儲存,構建工具重新打包重新整理瀏覽器,完整構建一遍需要好幾分鐘,甚至更長,工程越龐大,需要的耗時越長。另外客戶端資源體積過大也是問題,需要針對專案特點進行按需載入、非同步載入、長效快取等。
為什麼使用webpack
- 拆分依賴樹成塊並按需載入
- webpack有著豐富的外掛介面,滿足不同的業務需求
- webpack支援
AMD
和CommonJs
模組樣式 - 它巧妙的在你程式碼的
AST
中進行靜態分析 - 能處理簡單的表示式,允許支援更多的類庫
- 支援
SourceUrls
和SourceMaps
進行簡單的除錯.通過development middleware
來監控檔案和development server
來自動重新整理
如何實現前端工程化
AMD
Asynchronous Module Definition(非同步模組定義)的縮寫。下面的程式碼使用 AMD 規範定義了一個模組:
// 定義一個求和的模組
define('getSum', ['math'], function(math) {
//第一個引數是當前模組的 ID,相當於給這個模組起一個名字
//第二個引數是當前模組的依賴,比如上面我們定義的 getSum 模組需要 math 模組的依賴
//第三個引數可以是函式或者物件。
return function(a, b) {
console.log('sum: ' + math.sum(a, b));
}
});
複製程式碼
通過這種形式定義模組的好處在於,它** 顯式 **地表達出了每個模組所依賴的其它模組。並且模組定義也不再繫結到全域性物件上,不必擔心其在別的地方被篡改。
CommonJS 與 Node.js 模組系統
近兩年來對於開發者來說遵循 CommonJS 標準來編寫和使用模組已經成為了一個基本通識。 CommonJS 是於 2009 年提出的 JavaScript 規範,它最開始是為了定義服務端標準,而非用於瀏覽器環境。
在 CommonJS 中每個檔案是一個模組,並且擁有屬於自己的作用域和上下文。模組的依賴通過 require 函式來引入。
const math = require('./math');
複製程式碼
如果想把模組的介面暴露給外部,則要通過 exports 將其匯出,如:
exports.getSum = function(a, b) {
return a + b;
}
複製程式碼
缺點:
- 阻塞呼叫在網路中呼叫並不是很好,網路請求是非同步的
- 多個模組無平行載入
AMD 和 CommonJS 具有同樣的特性——模組的依賴必須顯式引入,這樣就解決了之前維護複雜模組引入時的順序問題。但是AMD編碼開銷大,閱讀和編寫都更加困難
ES6 Module
之所以在過去我們有各種不同的模組化標準是因為 JavaScript 這門語言本身不具備模組化的特性,而現在 ES6 中已經具備了。ES6 Module 的模組語法和 CommonJS 很像,它通過 import 和 export 來進行模組的匯入和匯出。
import math from './math';
export function sum(a, b) {
return a + b;
}
複製程式碼
在 ES6 Module 中也是每個檔案作為一個模組。和 CommonJS 不同的是,ES6 Module 的模組的依賴是靜態的,或者說是在編譯時確定的,而不是執行時確定的。
舉個例子,我們可以在 CommonJS 中的 if 語句中 require 模組,根據程式碼執行時 if 的判斷條件決定是否要引入該模組。
// 根據執行時條件確定是否引入
if(Date.now() > new Date('2019-01-01')) {
require('./my_module');
}
複製程式碼
而在 ES6 Module 中則不允許這樣做,import 必須在程式碼的頂層作用域,這意味著你不能把它放在 if 等程式碼塊中。ES6 Module 這樣規定的原因在於可以使編譯器在編譯階段就可以獲取到整個依賴樹,從而進行程式碼靜態分析層面的優化,比如檢測出哪些模組是從來沒有被使用過的,然後從打包結果中優化掉等等。但是現在ES6還不算完全普及,不少瀏覽器不相容。
模組打包原理簡述
Webpack 以及其它的一些打包工具最基本的功能就是按照我們定義好的依賴樹將模組合併成單一的檔案,讓瀏覽器能夠按照預想的依賴順序去執行。這個過程我們通常將它叫做模組打包。
Webpack 進行打包:
# Webpack 版本需要大於等於 2,這裡使用的版本是 3.5.5
webpack app.js dist/bundle.js
複製程式碼
app.js 是我們的打包入口檔案,dist/bundle.js 是最終的打包合併結果檔案。Webpack 會在打包的過程中從入口 app.js 開始查詢所有依賴的模組,並最終包裝和合並這些模組放在 bundle.js 中
總結
以上只是鄙人對於前端工程化的一些簡單見解,想要真正熟悉工程化及其相關工具,還是需要多實踐多踩坑。 程式設計是一種修行,應用修行的產物,也是我們與世界交流的方式。未來在哪裡並不重要,重要的是以空杯心態持續學習和實踐,用心寫下每行程式碼。