JS 誕生的時候,僅僅是爲了實現網頁表單的本地校驗和簡單的 dom 操做處理。因此並無模塊化的規範設計。javascript
項目小的時候,咱們能夠經過命名空間、局部做用域、自執行函數等手段實現變量不衝突。可是到了大一點的項目,各類組件,各類第三方插件和各類 js 腳步融合的時候,就會發現這些技巧遠遠不夠。css
爲何要有 JS 模塊化呢?在瀏覽器中,頂層做用域的變量是全局的,因此項目稍微複雜點,若是引用的 js 很是多的時候,很容易形成命名衝突,而後形成很大意想不到的結果。html
爲了不全局污染,JS 前輩們想了不少辦法,也就是前端的模塊化的演變過程,能夠參考個人視頻:前端模塊化演變前端
模塊化演變過程:java
對象封裝node
var module = { star : 0, f1 : function () //... }, f2 : function (){ //... } }; module.f1(); module.star = 1;
命名空間(對象封裝的變種或者叫作升級)jquery
var Shop = {}; // 頂層命名空間 Shop.User = {}; // 電商的用戶模塊 Shop.User.UserList = {}; //用戶列表頁面模塊。 Shop.User.UserList.length = 19; // 用戶一共有19個。
私有空間webpack
// => 給單個文件裏面定義的局部變量都 變成 局部做用域裏面的變量。 // 第二個嘗試: // a.js (function() { var a = 9; })(); // b.js (function() { var a = 'ssss'; })();
模塊的維護和擴展git
// laoma.core.js (function(laoma, d1, d2) { laoma.Btn = { getVal: function() { console.log('val'); }, setVal: function(str) { console.log('setvale'); } }; })(window.laoma || {}, depend1, depend2); // laoma.animate.js // 動畫組件 (function(laoma, d1, d2) { laoma.animate = {}; })(window.laoma || {}, depend1, depend2); // laoma.form.js // 表單組件 (function(laoma, d1, d2) { laoma.form = {}; })(window.laoma || {}, depend1, depend2);
(function(window, undefined) { var jQuery = function() {} // ... window.jQuery = window.$ = jQuery; })(window);
後續的演變就是,出現了 AMD、CMD、CommonJS 等模塊化標準,而後前端模塊化進入大爆發時代。es6
JS 模塊化就是指 JS 代碼分紅不一樣的模塊,模塊內部定義變量做用域只屬於模塊內部,模塊之間變量命名不會相互衝突。各個模塊相互獨立,並且又能夠經過某種方式相互引用協做。
目前前端流行的幾個模塊化標準:CommonJs標準(node 的方案)、AMD、CMD、ES6 模塊方案。
將來的趨勢確定是 ES6 的標準方案會逐漸統一。可是 AMD、CMD 標準跟 CommonJs 的標準相差不大,須要咱們都研究一下。
requirejs 的使用:
第一步:requirejs 下載
第二步: 把 requirejs 直接引入到 html
<script src="js/require.js"></script>
第三步: 設置當前頁面的 js 入口文件
<script src="js/require.js" data-main="js/main"></script>
data-main 屬性的做用是,指定網頁程序的主模塊。意思是當前整個網頁的入口代碼。那麼其餘須要引用的 JS 文件呢?
第四步: 引用其餘模塊的文件
主模塊依賴於其餘模塊,這時就要使用 AMD 規範定義的的 require()函數。
// main.js require(['moduleA', 'moduleB', 'moduleC'], function(moduleA, moduleB, moduleC) { // some code here });
require()函數接受兩個參數。第一個參數是一個數組,表示所依賴的模塊,上例就是['moduleA', 'moduleB', 'moduleC'],即主模塊依賴這三個模塊;第二個參數是一個回調函數,當前面指定的模塊都加載成功後,它將被調用。加載的模塊會以參數形式傳入該函數,從而在回調函數內部就可使用這些模塊。
require()異步加載 moduleA,moduleB 和 moduleC,瀏覽器不會失去響應;它指定的回調函數,只有前面的模塊都加載成功後,纔會運行,解決了依賴性的問題。
實際應用例子:
require(['jquery', 'underscore', 'backbone'], function($, _, Backbone) { // some code here });
若是依賴的 JS 文件跟咱們的 require.js 不在相同的目錄,那麼須要咱們單獨設置一下路徑映射關係。
require.config({ paths: { underscore: 'lib/underscore.min', backbone: 'lib/backbone.min' } });
第五步:如何自定義 AMD 模塊(可選)
自定義的模塊還依賴其餘模塊,那麼 define()函數的第一個參數,必須是一個數組,指明該模塊的依賴性
define(['myLib'], function(myLib) { function foo() { myLib.doSomething(); } return { foo: foo }; });
[Sea.js]在推廣過程當中逐漸造成了 CMD 的模塊定義標準。具體詳情請參考。
跟 AMD 比較相似,並且兼容 CommonJS 的模塊寫法。
CMD 推崇的是:依賴就近依賴,AMD 則默認約束模塊一開始就聲明相關依賴。其餘定義方式及模塊相關的變量都很類似。
因爲 Sea.js 官方文檔很詳細,在此就再也不贅述。如何使用請參考官網。
Node.js 有一個簡單的模塊加載系統,遵循的是 CommonJS 的規範。 在 Node.js 中,文件和模塊是一一對應的(每一個文件被視爲一個獨立的模塊)。
Node 在加載 JS 文件的時候,自動給 JS 文件包裝上定義模塊的頭部和尾部。
// nodejs 會自動給咱們的js文件添加頭部,見下行 (function(exports, require, module, __filename, __dirname) { // 這裏是你本身寫的js代碼文件 }); // 自定添加上尾部
見 NodeJs 的源碼截圖:
Node會自動給js文件模塊傳遞的5個參數,每一個模塊內的代碼均可以直接用。並且您也看到了,咱們的代碼都會被包裝到一個函數中,因此咱們的代碼的做用域都是在這個包裝的函數內,這點跟瀏覽器的window全局做用域是不一樣的。
模塊內的參數說明:
在模塊內,咱們能夠經過require函數(此函數由nodejs自動傳入,在模塊內能夠直接用)來加載js文件模塊、node內置模塊等。require函數須要傳入要加載的模塊的名字或者是文件名或者目錄。
/* 假設開發目錄下有文件: . ├── circle.js └── main.js */ // circle.js exports.pi = 3.1415926; // 其餘模塊引用當前模塊時,能夠直接經過模塊對象訪問到 pi屬性。 // 主文件main.js: const circle = require('./circle.js'); // 加載circle.js文件的module.export 賦值給circle console.log(circle.pi); // => 3.1415926
解釋:
require加載文件circle.js後,此文件被node拼裝成模塊的代碼,而後執行文件裏面的js代碼,並把模塊內的module.exports作爲模塊的對外接口返回給引用者。
// circle.js 包裝後的代碼就是 // nodejs 會自動給咱們的js文件添加頭部 (function(exports, require, module, __filename, __dirname) { exports.pi = 3.1415926; // exports === modeule.exports }); // 自定添加上尾部 // 主文件main.js: const circle = require('./circle.js'); circle => circle.js中的module.exports
Node.js的模塊分爲兩類,一類爲原生(核心)模塊,一類爲文件模塊。
模塊在第一次加載後會被緩存。 這也意味着若是每次調用 require('foo') 都解析到同一文件,則返回相同的對象。
Node.js提供了一些底層的核心模塊,它們定義在 Node.js 源代碼的 lib/ 目錄下。這些原生模塊在Node.js源代碼編譯的時候編譯進了二進制執行文件,加載的速度最快。開發人員自定義的js文件是動態加載的,加載速度比原生模塊慢,這個只是在第一次加載有區別,模塊加載完後都會被緩存,後續使用就不會被再次加載。
require() 老是會優先加載核心模塊。 例如,require('http') 始終返回內置的 HTTP 模塊,即便有同名文件。
文件模塊中,又分爲3類模塊。這三類文件模塊之後綴來區分,Node.js會根據後綴名來決定加載方法。
參考源碼:
require方法接受如下幾種參數的傳遞:
文件加載的邏輯仍是比較複雜的,並且考慮不少種狀況。 require加載文件模塊,直接找對應完整文件名最快,若是不給文件後綴名,node會自動嘗試添加 js\json\mod
等後綴進行嘗試。當沒有以 '/'、'./' 或 '../' 開頭來表示文件時,這個模塊必須是一個核心模塊或加載自 node_modules 目錄。若是給定的路徑不存在,則 require() 會拋出一個 code 屬性爲 'MODULE_NOT_FOUND' 的 Error。 若是加載目錄,又分三種狀況: 第一種方式是在根目錄下建立一個 package.json 文件,並指定一個 main 模塊。 例子,package.json 文件相似:
{
"name" : "some-library", "main" : "./lib/some-library.js" }
若是這是在 ./some-library 目錄中,則 require('./some-library') 會試圖加載 ./some-library/lib/some-library.js。不存在也會報錯。
若是目錄裏沒有 package.json 文件,則 Node.js 就會試圖加載目錄下的 index.js 或 index.node 文件。 例如,若是上面的例子中沒有 package.json 文件,則 require('./some-library') 會試圖加載:
./some-library/index.js ./some-library/index.node
其餘的狀況,則從 node_modules 目錄加載。 Node.js 會從當前模塊的父目錄開始,嘗試從它的 /node_modules 目錄里加載模塊。 Node.js 不會附加 node_modules 到一個已經以 node_modules 結尾的路徑上。
若是仍是沒有找到,則移動到再上一層父目錄,直到文件系統的根目錄。
例子,若是在 '/home/ry/projects/foo.js' 文件裏調用了 require('bar.js'),則 Node.js 會按如下順序查找:
/home/ry/projects/node_modules/bar.js /home/ry/node_modules/bar.js /home/node_modules/bar.js /node_modules/bar.js
這使得程序本地化它們的依賴,避免它們產生衝突。
能夠經過module.paths打印當前node尋找模塊要搜索的全部路徑。
綜上邏輯,看官網的加載邏輯僞代碼:
從 Y 路徑的模塊 require(X)
1\. 若是 X 是一個核心模塊,
a. 返回核心模塊
b. 結束
2\. 若是 X 是以 '/' 開頭 a. 設 Y 爲文件系統根目錄 3\. 若是 X 是以 './' 或 '/' 或 '../' 開頭 a. 加載文件(Y + X) b. 加載目錄(Y + X) 4\. 加載Node模塊(X, dirname(Y)) 5\. 拋出 "未找到" 加載文件(X) 1\. 若是 X 是一個文件,加載 X 做爲 JavaScript 文本。結束 2\. 若是 X.js 是一個文件,加載 X.js 做爲 JavaScript 文本。結束 3\. 若是 X.json 是一個文件,解析 X.json 成一個 JavaScript 對象。結束 4\. 若是 X.node 是一個文件,加載 X.node 做爲二進制插件。結束 加載索引(X) 1\. 若是 X/index.js 是一個文件,加載 X/index.js 做爲 JavaScript 文本。結束 3\. 若是 X/index.json 是一個文件,解析 X/index.json 成一個 JavaScript 對象。結束 4\. 若是 X/index.node 是一個文件,加載 X/index.node 做爲二進制插件。結束 加載目錄(X) 1\. 若是 X/package.json 是一個文件, a. 解析 X/package.json,查找 "main" 字段 b. let M = X + (json main 字段) c. 加載文件(M) d. 加載索引(M) 2\. 加載索引(X) 加載Node模塊(X, START) 1\. let DIRS=NODE_MODULES_PATHS(START) 2\. for each DIR in DIRS: a. 加載文件(DIR/X) b. 加載目錄(DIR/X) NODE_MODULES_PATHS(START) 1\. let PARTS = path split(START) 2\. let I = count of PARTS - 1 3\. let DIRS = [] 4\. while I >= 0, a. if PARTS[I] = "node_modules" CONTINUE b. DIR = path join(PARTS[0 .. I] + "node_modules") c. DIRS = DIRS + DIR d. let I = I - 1 5\. return DIRS
總結:
咱們本身加載模塊的時候,儘可能的寫全點,儘可能不要讓node去推斷,引用文件模塊直接把文件名寫全,文件
若是想查看當前模塊,能夠直接使用console直接打印一下module對象。
console.dir(module); // 打印結果: Module { id: '.', exports: {}, parent: null, filename: '/Users/flydragon/Desktop/work/gitdata/nodedemos/demos/02console.js', loaded: false, children: [], paths: [ '/Users/flydragon/Desktop/work/gitdata/nodedemos/demos/node_modules', '/Users/flydragon/Desktop/work/gitdata/nodedemos/node_modules', '/Users/flydragon/Desktop/work/gitdata/node_modules', '/Users/flydragon/Desktop/work/node_modules', '/Users/flydragon/Desktop/node_modules', '/Users/flydragon/node_modules', '/Users/node_modules', '/node_modules' ] }
在每一個模塊中,module 的自由變量是一個指向表示當前模塊的對象的引用。 爲了方便,module.exports 也能夠經過全局模塊的 exports 對象訪問。
module.exports 與 exports區別,看Node中的源碼就知道了。
// 模塊的構造函數 function Module(id, parent) { this.id = id; this.exports = {}; // 模塊實例的exports屬性初始化!!!module.exports === exports this.parent = parent; updateChildren(parent, this, false); this.filename = null; this.loaded = false; this.children = []; }
exports
是 module.exports
的一個引用,就比如在每個模塊定義最開始的地方寫了這麼一句代碼:var exports = module.exports
要注意的一點就是: 最終模塊會把module.exports做爲對外的接口。因此,module.exports的引用地址發生了改變,在改變以前經過exports屬性設置的都會被遺棄。
module的其餘屬性: 屬性|類型|屬性說明 ---|--- module.filename|string|模塊的徹底解析後的文件名 module.id|string|模塊的標識符。 一般是徹底解析後的文件名。 module.loaded| boolean |模塊是否已經加載完成,或正在加載中。 module.loaded| boolean |模塊是否已經加載完成,或正在加載中。 module.parent| object | 最早引用該模塊的模塊。 module.paths|string|模塊的搜索路徑。 module.children|object |被該模塊引用的模塊對象。
詳情請參考:中文Node文檔
es6的模塊引入和導出跟以上都有點區別。不過確定是將來的統一的模型。node目前版本位置並無es6的模塊api支持的很好,只是在實驗階段。不過咱們能夠藉助babel來轉換咱們的js代碼,能夠放心的使用。
因爲這塊內容,請直接參考阮一峯老師的es6入門
從客戶端到服務端咱們都搞定了js的模塊化,也就是說讓js走向了工程化,大型應用的基礎被奠基了。固然,目前業界模塊化已經走入深水區,尤爲是webpack已經可讓前端的大部分資源都模塊化使用。
咱們已經搞定了,本身書寫模塊,已經引用核心模塊、本身寫的模塊,那麼怎麼引用第三方模塊,怎麼使用package文件,好吧提早透露一下:npm解密(下一節)
參考: