前端為什麼需要模組化開發

快意恩仇發表於2019-05-05
寫在開頭

前端的發展總會讓我們眼前一亮,這又有什麼規範出來了,上個規範我還沒理解透徹呢。但不管未來怎麼發展,瞭解歷史還是非常重要的,以史為鏡,可以知得失。知道了規範的發展歷史,才能更好的瞭解目前的規範。

沒有模組化,前端程式碼會怎麼樣?
  • 變數和方法不容易維護,容易汙染全域性作用域
  • 載入資源的方式通過script標籤從上到下。
  • 依賴的環境主觀邏輯偏重,程式碼較多就會比較複雜。
  • 大型專案資源難以維護,特別是多人合作的情況下,資源的引入會讓人奔潰。
當年我們是怎麼引入資源的。

script.png

看著上面的script標籤,是不是有一種很熟悉的感覺。通過script引入你想要的資源,從上到下的順序,這其中順序是非常重要的,資源的載入先後決定你的程式碼是否能夠跑的下去。當然我們還沒有加入defer和async屬性,不然載入的邏輯會更加複雜。這裡引入的資源還是算少的,過多的script標籤會造成過多的請求。同時專案越大,到最後依賴會越來越複雜,並且難以維護,依賴模糊,請求過多。全域性汙染的可能性就會更大。那麼問題來了,如何形成獨立的作用域?

defer和async的區別

defer要等到整個頁面在記憶體中正常渲染結束(DOM 結構完全生成,以及其他指令碼執行完成),才會執行;async一旦下載完,渲染引擎就會中斷渲染,執行這個指令碼以後,再繼續渲染。一句話,defer是“渲染完再執行”,async是“下載完就執行”。另外,如果有多個defer指令碼,會按照它們在頁面出現的順序載入,而多個async指令碼是不能保證載入順序的。

模組化的基石

立即執行函式(immediately-invoked function expression),簡稱IIFE,其實是一個javaScript函式。可以在函式內部定義方法以及私有屬性,相當於一個封閉的作用域。例如下面的程式碼:

let module = (function(){
    let _private = 'myself';
    let fun = () =>{
        console.log(_private)
    }
    return {
        fun:fun
    }
})()
module.fun()//myself
module._private//undefined
複製程式碼

以上程式碼便可以形成一個獨立的作用域,一定程度上可以減少全域性汙染的可能性。這種寫法可是現代模組化的基石。雖然能夠定義方法,但是不能定義屬性,這時候各種前端規範就陸續登場了。

首先登場的是common.js

最先遵守CommonJS規範是node.js。這次變革讓服務端也能用js爽歪歪的寫了,我們的javaScript並不止於瀏覽器,服務端也能分一杯羹,被人稱為模組化的第一座里程碑。想想長征二萬五,第一座里程碑在哪裡?

CommomJS模組的特點
  • 模組內的程式碼只會執行在模組作用域內,不會汙染到全域性作用域
  • 模組的可以多次引入,但只會在第一次載入的時候執行一次,後面的執行都是取快取的值。想要讓模組再次執行,必須清楚快取。
// 刪除指定模組的快取
delete require.cache[moduleName];

// 刪除所有模組的快取
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})
複製程式碼
  • 模組的載入順序,遵循在程式碼中出現的順序。
為什麼要少用exports

exports只是一個變數,指向module.exports,也就是exports只是一個引用而已。所以對外輸出模組的時候,我們就可以通過exports新增方法和和屬性。通過module.exports對外輸出其實也是讀取module.exports的變數。但是使用exports時要非常的小心,因為稍不注意就會切斷和module.exports的聯絡。例如:

exports = function(x) {console.log(x)};
複製程式碼

上面的程式碼執行之後,exports不再指向module.exports。如果你難以區分清楚,一般最好就別用exports,只使用module.exports就行。

怎麼區分模組是直接執行,還是被呼叫執行。

require.mainAPI就有這樣的作用,如果模組是直接執行,那麼這時require.main屬性就指向模組本身。例如下面:

require.main === module
複製程式碼
為什麼客戶端不使用commonjs規範?

我們知道客戶端(瀏覽器)載入資源主要是通過網路獲取,一般本地讀取的比較少,而node.js主要是用於伺服器程式設計,模組檔案一般都存在於本地硬碟上,然後I/O讀取是非常快速的,所以即使是同步載入也是能夠適用的,而瀏覽器載入資源必須通過非同步獲取,比如常見的ajax請求,這時候AMD規範就非常合適了,可以非同步載入模組,允許回撥函式。

客戶端的規範不僅僅只有AMD,還有CMD.

每個規範的興起背後總有一些原因,requirejs的流行是因為commonjs未能滿足我們需要的效果,sea.js被創造的原因也是因為requirejs不能滿足一些場景。

AMD和CMD的區別
- AMD CMD
原理 define(id ?,dependencies ?,factory)定義了一個單獨的函式“define”。id為要定義的模組。依賴通過dependencies傳入factory是一個工廠引數的物件,指定模組的匯出值。 CMD規範與AMD類似,並儘量保持簡單,但是更多的與common.js保持相容性。
優點 特別適用於瀏覽器環境的非同步載入 ,且可以並行載入。依賴前置,提前執行。 定義模組時就能清楚的宣告所要依賴的模組 依賴就近,延遲執行。 按需載入,需要用到時再require
缺點 開發成本較高,模組定義方式的語義交為難理解,不是很符合通過的模組化思維方式。 依賴SPM打包,模組的載入主觀邏輯交重。
體現 require.js sea.js
ES6讓前端模組化觸手可及
概念

ES6的模組不是物件,import語法會被JavaScript引擎靜態分析,請注意,這是一個很重要的功能,我們通常使用commonjs時,程式碼都是在執行時載入的,而es6是在編譯時就引入模組程式碼,當然我們現在的瀏覽器還沒有這麼強大的功能,需要藉助各類的編譯工具(webpack)才能正確的姿勢來使用es6的模組化的功能。也正因為能夠編譯時就引入模組程式碼,所以使得靜態分析就能夠實現了。

ES6模組化有哪些優點
  • 靜態化編譯 如果能夠靜態化,編譯的時候就能確定模組的依賴關係,以及輸出和輸入的變數,然後CommonJS和AMD以及CMD都只能在執行程式碼時才能確定這些關係。

  • 不需要特殊的UMD模組化格式 不再需要UMD模組的格式,將來伺服器和瀏覽器都會支援ES6模組格式。目前各種工具庫(webpack)其實已經做到這一點了。

  • 目前的各類全域性變數都可以模組化 比如navigator現在是全域性變數,以後就可以模組化載入。這樣就不再需要物件作為名稱空間。

需要注意的地方
  • export語句輸出的介面,通過import引入之後,與其對應的值是動態的繫結關係,也就是模組的內的值即使改變了,也是可以取到實時的值的。而commonJS模組輸出的是值的快取,不存在動態更新。
  • 由於es6設計初衷就是要靜態優化,所以export命令不能處於塊級作用域內,如果出現就會報錯,所以一般export統一寫在底部或則頂層。
function fun(){
  export default 'hello' //SyntaxError
}
複製程式碼
  • import命令具有提升效果,會提升到整個模組的頭部首先執行。例如:
fun()
import {fun} from 'myModule';
複製程式碼

上面的程式碼import的執行早於fun呼叫,原因是import命令是編譯階段執行的,也就是在程式碼執行之前。

export default使用

export default就是輸出一個叫default的變數或方法,然後系統允許你為它取任意名字。所以,你可以看到下面的寫法。

//modules.js
function add(x,y){
  return x*y
}
export {add as default};
//等同於
export default add;

//app.js
import {default add foo} from 'modules';
//等同於
import foo from 'modules'
複製程式碼

這是因為export default命令其實只是輸出一個叫做default的變數,所以它後面不能跟變數宣告語句。

特別技巧偵查程式碼是否處於ES6模組中

利用頂層的this等於undefined這個語法點,可以偵測當前程式碼是否在 ES6 模組之中。

const isNotModuleScript = this !== undefined;
複製程式碼

如果大神您想繼續探討或者學習更多知識,歡迎加入QQ或者微信一起探討:854280588

QQ.png
微信

相關文章