JavaScript之無題之讓人煩躁的模組化

2022-10-07 06:01:56

  我怎麼記得我好像寫過相關型別的文章,但是我找遍了我的部落格沒有~那就再寫一遍吧,其實模組化的核心內容也算不上是複雜,只不過需要整理一下,規劃一下罷了。嘻嘻。

  開始寫標題的時候我就在糾結一件事情,就是,先吃喜歡吃的,還是後吃喜歡吃的,翻譯過來就是我應該先寫CommonJS和ES6 Module,還是先寫CMD和AMD。嗯,我決定了,誰先做好了我就先吃誰

  其實模組化的緣由很簡單,就一句話,不對,就一個詞,兩個字,分類。如果一定讓我在加一點,那應該是「隔離」。沒了~~但是這麼少不太好,我舉個可能不那麼恰當的例子吧。

  剛開始這個世界上只有三個人,起名字的時候會「刻意」的避開彼此已經叫過的名字,他們在一起生活,日子欣欣向榮,一片美好,對未來充滿了期待。

  時間飛逝,三個人變成了三十人,他們勉強還是住在一起,起名字的時候雖然費事一點,但是也還能不重複,日子還是欣欣向榮,一片美好,對未來充滿了期待。

  時間又飛逝,三十人變成了三百人,那這不太好管理了,於是三位首領就說,你們有領導能力的幾個人站出來,組成各自的部落,去吧,我相信你們可以的。於是每個部落住在一起,部落與部落之間的人可以重名,叫名字的時候再加上一個部落的名稱唄,嗯~又一片欣欣向榮。

  時間繼續飛逝,三百人變成了三千人,這三千人住在幾個大部落裡也很不方便,你拿我的蘋果,我偷了你得豬,這肯定不行,有礙於社會的穩定發展,於是三個創始者叫上部落的組長說,我們給每個人分一塊地,蓋一個房子,把三五個人分割成一個家庭,家庭之間由部落作為紐帶,關聯彼此,在形式上又相互獨立,不可以隨便拿別家的蘋果。很完美~

  時間飛飛飛飛逝,三千人變成了三百萬人……我們需要法律了。

  OK,上面的小例子,人,就是函數,部落就是名稱空間,房子就是IIFE,法律就是後續發展的模組化規範。那麼我們依照上面的描述,如何轉換成程式碼?

一、社會的起源與法律的雛形

  最開始的時候,瀏覽器只需要簡單的圖文展示就是可以了,沒什麼複雜的互動和邏輯的處理,所以,當我們只有三個人的時候,我們可以很自由,很隨意:

function a(){}

function b(){}

  隨著Web的發展,互動的增多,專案的擴大,很容易有人也宣告了同樣名稱的函數,於是紛爭開始了,那咋解決紛爭呢?嗯,名稱空間也就是拆分部落,就像這樣:

var zaking1 = {
    a:function(){},
    b:function(){}
}
var zaking2 = {
    a:function(){},
    b:function(){}
}

  但是這樣並不能真正的解決問題,因為雖然從形式上區分了部落,但是部落之間沒有任何的隔離,部落內部也是混亂的,所以各個首領就制定了一個方案,IIFE,利用閉包的特性,來實現資料的隔離,暴露出對外的入口:

var module = (function () {
    var name = "zaking";
    function getName() {
    console.log(name);
    }
    return { getName };
})();
module.getName();

  我們蓋好了房子,還給房子建好了可以出入的門,但是我怎麼邀請別人進來呢?

var module = (function (neighbor) {
    var name = "zaking";
    function getName() {
    console.log(name + "和鄰居:" + neighbor);
    }
    return { getName };
})("xiaowangba");
module.getName();

  傳個引數唄,這就是依賴注入。在這個階段,最有代表性的就是jQuery了,它的封閉性的核心實現,跟上面的程式碼幾乎無異,我們可以看下jQuery的模組的實現:

(function (global, factory) {
  factory(global);
})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
  if (typeof noGlobal === "undefined") {
    window.jQuery = window.$ = jQuery;
  }
  return jQuery;
});

  當然我這裡略了很多,你看它,無非就是一個閉包,傳入了window和jQuery本身,然後再繫結到window上,這樣,我們就只能存取到暴露出來的$以及$上的方法和屬性,我們根本無法修改內部的資料。

  OK,到了這個階段,其實算是一個轉折點,我們有了初步的法律,還需要後續針對法律的完善,

二、法律的初現與CommonJs

  隨著社會的發展,出現一種規則已成必然,於是commonJs統領舉起模組化的大旗,讓JavaScript邁向了另一個階段。commonJs最初由 JavaScript 社群中的 Mozilla 的工程師Kevin Dangoor在Google Groups中建立了一個ServerJs小組。該組織的目標是為web伺服器、桌面和命令列應用程式以及瀏覽器構建JavaScript生態系統。嗯,它的野心很大~,後來,他就就把ServerJs改成了commonJs,畢竟ServerJs的範圍有點小,commonJs更符合他們的初衷。

  而後,在同一年的年底,NodeJs出現了,Javascript不僅僅可以用於瀏覽器,在伺服器端也開始攻城略地。NodeJs的初衷是基於commonJs社群的模組化規範,但是NodeJs並沒有完全遵循於社群的一些腐朽過時的約束,它實現了自己的想法。

  commonJs規範的寫法,如果大家寫過NodeJs一定都有所瞭解,大概是這樣的:

// a.js
module.exports = 'zaking'
// b.js
const a = require("./a");
console.log(a); // zaking

  看起來挺簡單的,但是這裡隱藏了一些不那麼容易被理解的特性。

  在NodeJs中,一個檔案就是一個模組,有自己的作用域,在一個檔案裡面定義的函數、物件都是私有的,對其他檔案不可見。並且,當第一次載入某個模組的時候,NodeJ會快取該模組,待再次載入的時候,會直接從模組中取出module.exports屬性返回。比如:

// a.js
var name = "zaking";
exports.name = name;

// b.js
var a = require("./a.js");
console.log(a.name); // zaking
a.name = "xiaoba";
var b = require("./a.js");
console.log(b.name); // xiaoba

  誒?為啥你寫的是「exports.」,不是module.exports?NodeJs在實現CommonJs規範的時候為了方便,給每個模組都提供了一個exports私有變數,指向module.exports。有一點要尤其注意,exports 是模組內的私有區域性變數,它只是指向了 module.exports,所以直接對 exports 賦值是無效的,這樣只是讓 exports 不再指向 module.exports了而已。

  我們回到上面的程式碼,按理來說,我第二次引入的b的name應該是「zaking」啊。但是實際上,在第一次引入之後的引入,並不會再次執行模組的內容,只是返回了快取的結果。

  另外一個核心的點是,我們匯入的是匯出值的拷貝,也就是說一旦引入之後,模組內部關於該值的變化並不會被影響。

// a.js
var name = "zaking";
function changeName() {
  name = "xiaowangba";
}
exports.name = name;
exports.changeName = changeName;

// b.js
var a = require("./a.js");
console.log(a.name); // zaking
a.changeName();
console.log(a.name); // zaking

  嗯,一切看起來都很不錯。

三、爭奇鬥豔,百家爭鳴

  在上一小節,我們簡單介紹了模組化的始祖也就是CommonJs以及實現了該規範的NodeJs的一些核心內容。但是NodeJs的實現的一個關鍵的點是,它在讀取或者說載入模組的時候是同步的,這在伺服器沒什麼問題,但是對於瀏覽器來說,這個問題就很嚴重,因為大量的同步模組載入意味著大量的白屏等待時間。

  基於這樣的問題,從CommonJs中獨立出了AMD規範。

1、AMD規範與RequireJs

  AMD,即Asynchronous Module Definition,翻譯過來就是非同步模組化規範,它的主要目的就是解決CommonJs不能在瀏覽器中使用的問題。但是RequireJs在實現上,希望可以通吃,也就是可以在任何宿主環境下使用。

  我們先來看個例子:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script>
  <body></body>
  <script>
    require(["./a"]);
  </script>
</html>

  然後,我們的a.js是這樣的:

define(function () {
  function fun1() {
    alert("it works");
  }

  fun1();
});

  define用來宣告一個模組,require匯入。我們還可以這樣:

require(["./a"], function () {
  alert("load finished");
});

  匯入前置依賴的模組,在第二個引數也就是回撥中執行。RequireJs會在所有的模組解析完成後執行回撥函數。就算你倒入了一個沒有使用的模組,RequireJs也一樣會載入:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script>
  <body></body>
  <script>
    require(["./a", "./b"], function (a, b) {
      a.fun1();
    });
  </script>
</html>

  然後分別是a.js和b.js:

// a.js
define(function () {
  function fun1() {
    alert("it works fun1");
  }

  return {
    fun1: fun1,
  };
});

// b.js
define(function () {
  function fun2() {
    alert("it works fun2");
  }

  return {
    fun2: fun2,
  };
});

  結果大家可以試一試~

  以上就是RequireJs的簡單用法,我們據此知道了兩個核心內容,RequireJs基於AMD規範,RequireJs會載入你引入的所有模組,哪怕你並不會真的用到它。

2、CMD規範與SeaJs

  由於RequireJs的一些問題,又出現了基於CMD規範的SeaJs,SeaJs和RequireJs有一個最大的不同就是RequireJs希望可以通吃,但是SeaJs則更專注於瀏覽器,哦對了,CMD的全稱叫做:Common Module Definition,即通用模組規範。

  SeaJs的簡單用法如下:

// 所有模組都通過 define 來定義
define(function(require, exports, module) {

  // 通過 require 引入依賴
  var a = require('xxx')
  var b = require('yyy')

  // 通過 exports 對外提供介面
  exports.doSomething = ...

  // 或者通過 module.exports 提供整個介面
  module.exports = ...

})
// a.js
define(function(require, exports, module){
    var name = 'morrain'
    var age = 18

    exports.name = name
    exports.getAge = () => age
})
// b.js
define(function(require, exports, module){
    var name = 'lilei'
    var age = 15
    var a = require('a.js')

    console.log(a.name) // 'morrain'
    console.log(a.getAge()) //18

    exports.name = name
    exports.getAge = () => age
})

  上面的程式碼是從網上抄的,大概說明白了基本的使用方法。我們可以看到,SeaJs的匯入和匯出的方式,跟NodeJs好像~~而SeaJs從書寫形式上,更像是CommonJs和AMD的結合。當然,我只是說書寫形式上。

  而AMD和CMD,RequireJs和SeaJs,都是由社群發起的,並沒有語言層面的規範,包括CommonJs以及NodeJs,所以,這個時代還是一個百花爭豔,沒有統一的時代,不過在現在,這些都不重要了。如果非要我說些什麼,那就是,忘記這兩個東西,去學下面的重點。

四、大一統

  百花爭豔的時代確實有些煩人,這個那個那個這個,又都不被官方認可,還好,官方終於還是出手了,ES6的出現,在語言層面上就提出了對於模組化的規範,也就是ES6 Module。它太重要了,具體語法我就不多說了,文末的連結附上了阮一峰大神的《ES6入門指南》關於ES6 Module的地址。

  所以到了ES6的時候,你要學習的就是ES6 Module,NodeJs也在逐步實現對ES6 Module的支援。最終,秦始皇會一統天下,這是必然的結果。

  這篇文章到這裡就結束了,說實話,模組化的問題和歷史由來已久,從萌芽到統一,至少十幾年的過程,而市面上也已有大量的文章介紹彼此的區別和各自的特點,我寫來寫去,也不過是複製一遍,毫無意義。

  但是我又想學一下模組化,以及模組化的歷史,額……,請原諒我的無知,所以才有了這篇文章,但是寫著寫著,發現我能表達出來的東西並不多,因為都是故事,都是歷史,並且對於未來的開發好像也沒什麼實際的意義和價值。

  所以,在如此糾結的心態下有了這篇文章,原諒我無知又想逼逼的心情吧。

  最後的最後,如果你想學習模組化,在現階段,只需要去深入學習ES6 Module,和學習一下NodeJs的CommonJs,以及瞭解一下各模組化的區別即可,因為現在是即將統一,還未完全統一的時候。

 

參考資料:

  https://wiki.commonjs.org/wiki/CommonJS

  https://github.com/seajs/seajs/issues/242

  https://es6.ruanyifeng.com/#docs/module