前陣子一直忙著找實習,發現已經有一段時間沒寫博客了,面試很多時候會被問到模塊化,今天就讓我們一起來總結下把 一、什麼是模塊化 在js出現的時候,js一般只是用來實現一些簡單的交互,後來js開始得到重視,用來實現越來越複雜的功能,而為了維護的方便,我們也把不同功能的js抽取出來當做一個js文件,但是當 ...
前陣子一直忙著找實習,發現已經有一段時間沒寫博客了,面試很多時候會被問到模塊化,今天就讓我們一起來總結下把
一、什麼是模塊化
在js出現的時候,js一般只是用來實現一些簡單的交互,後來js開始得到重視,用來實現越來越複雜的功能,而為了維護的方便,我們也把不同功能的js抽取出來當做一個js文件,但是當項目變的複雜的時候,一個html頁面可能需要載入好多個js文件,而這個時候就會出現各種命名衝突,如果js也可以像java一樣,把不同功能的文件放在不同的package中,需要引用某個函數或功能的時候,import下相關的包,這樣可以很好的解決命名衝突等各種問題,但是js中沒有模塊的概念,又怎麼實現模塊化呢
模塊化開發是一種管理方式,是一種生產方式,一種解決問題的方案,一個模塊就是實現特定功能的文件,有了模塊,我們就可以更方便地使用別人的代碼,想要什麼功能,就載入什麼模塊,但是模塊開發需要遵循一定的規範,否則就都亂套了,因此,才有了後來大家熟悉的AMD規範,CMD規範
接下來,我們就一起學習下AMD,CMD和es6中的模塊化吧
二、AMD
AMD 即Asynchronous Module Definition,中文名是“非同步模塊定義”的意思,它採用非同步方式載入模塊,模塊的載入不影響它後面語句的運行,所有依賴這個模塊的語句,都定義在一個回調函數中,等到載入完成之後,這個回調函數才會運行
一般來說,AMD是 RequireJS 在推廣過程中對模塊定義的規範化的產出,因為平時在開發中比較常用的是require.js進行模塊的定義和載入,一般是使用define來定義模塊,使用require來載入模塊
1、定義模塊
AMD規範只定義了一個函數define,它是全局變數,我們可以用它來定義一個模塊
define(id?, dependencies?, factory);
其中,id是定義中模塊的名字,這個參數是可選的,如果沒有提供該參數,模塊的名字應該預設為模塊載入器請求的指定腳本的名字,如果提供了該參數,模塊名必須是“頂級”的和絕對的
dependencies是定義的模塊中所依賴模塊的數組,依賴模塊必鬚根據模塊的工廠方法優先順序執行,並且執行的結果應該按照依賴數組中的位置順序以參數的形式傳入(定義中模塊的)工廠方法中
factory是模塊初始化要執行的函數或對象,如果為函數,它應該只被執行一次,如果是對象,此對象應該為模塊的輸出值
下麵來看一個定義模塊的例子
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { exports.verb = function() { return beta.verb(); //Or: return require("beta").verb(); } });
上面的代碼定義了一個alpha的模塊,這個模塊依賴require,exports,beta,因此需要先載入它們,再執行後面的factory
2、載入模塊
require.js中採用require()語句載入模塊,在定義好了模塊後,我們可以使用require進行模塊的載入
require([module], callback);
require要傳入兩個參數,第一個參數[module],是一個數組,裡面的成員就是要載入的模塊,第二個參數callback,則是載入成功之後的回調函數
下麵我們來看一個例子
require([increment'], function (increment) {
increment.add(1);
});
上面的代碼中,比如我們現在已經定義了一個模塊,名字為increment,裡面有一個add方法,我們現在需要用到裡面的方法,只要像上面一樣將模塊載入進來,然後調用方法就可以了
3、requirejs使用例子
在使用require.js時,可以通過define()定義模塊,這時候裡面的模塊的方法和變數外部是無法訪問到的,只有通過return,然後再載入這個模塊,才可以進行訪問
define('math',['jquery'], function ($) {//引入jQuery模塊 return { add: function(x,y){ return x + y; } }; });
上面的代碼定義了一個math模塊,返回了一個add方法,要使用這個模塊的方法,我們需要向下麵這樣進行訪問
require(['jquery','math'], function ($,math) { console.log(math.add(10,100));//110 });
通過require,我們載入了math模塊,這樣就可以使用math模塊裡面的add方法了
三、CMD
CMD 即Common Module Definition通用模塊定義,CMD規範是國內發展出來的,同時,CMD是在SeaaJS推廣的過程中形成的,CMD和AMD要解決的都是同個問題,在使用上也都很像,只不過兩者在模塊定義方式和模塊載入時機上有所不同 1、定義模塊 在 CMD 規範中,一個模塊就是一個文件,通過define()進行定義define(factory);
define接受factory參數,factory可以是一個函數,也可以是一個對象或字元串
factory為對象、字元串時,表示模塊的介面就是該對象、字元串,比如可以如下定義一個 JSON 數據模塊
define({ "foo": "bar" });
也可以通過字元串定義模板模塊
define('I am a template. My name is {{name}}.');
factory為函數時,表示是模塊的構造方法,執行該構造方法,可以得到模塊向外提供的介面,factory方法在執行時,預設會傳入三個參數:require,exports和 module
define(function(require, exports, module) { // 模塊代碼 });
其中,require用來載入其它模塊,而exports可以用來實現向外提供模塊介面
define(function(require, exports) { // 對外提供 foo 屬性 exports.foo = 'bar'; // 對外提供 doSomething 方法 exports.doSomething = function() {}; });
module是一個對象,上面存儲了與當前模塊相關聯的一些屬性和方法,傳給factory構造方法的exports參數是module.exports對象的一個引用,只通過exports參數來提供介面,有時無法滿足開發者的所有需求,比如當模塊的介面是某個類的實例時,需要通過module.exports來實現
define(function(require, exports, module) { // exports 是 module.exports 的一個引用 console.log(module.exports === exports); // true // 重新給 module.exports 賦值 module.exports = new SomeClass(); // exports 不再等於 module.exports console.log(module.exports === exports); // false });
說了這麼多,相信大家可能有點亂,來個簡單的例子,我們看看使用AMD和CMD定義的模塊的寫法
// CMD define(function(require, exports, module) { var a = require('./a') a.doSomething() // 此處略去 100 行 var b = require('./b') // 依賴可以就近書寫 b.doSomething() // ... }) // AMD 預設推薦的是 define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好 a.doSomething() // 此處略去 100 行 b.doSomething() ... })
在上面的代碼中,相信大家很容易可以看出區別吧,AMD和CMD都是通過define()定義模塊,AMD需要把依賴的模塊先寫出來,可以通過return暴露介面,CMD在定義模塊需要傳入require,exports和module這幾個參數,要載入某個模塊時,使用require進行載入,要暴露介面時,可以通過exports,module.exports和return
2、載入模塊
在前面定義模塊時,我們說過,當factory為函數時,require會作為預設參數傳遞進去,而require可以實現模塊的載入
require是一個方法,接受模塊標識作為唯一參數,用來獲取其他模塊提供的介面
define(function(require, exports) { // 獲取模塊 a 的介面 var a = require('./a'); // 調用模塊 a 的方法 a.doSomething(); });從上面定義模塊和載入模塊的方式上,我們也可以看出AMD和CMD主要有下麵幾個不同: (1)AMD是RequireJS在推廣過程中對模塊定義的規範化產出,CMD是SeaJS在推廣過程中對模塊定義的規範化產出 (2)對於依賴的模塊,AMD是提前執行,CMD是延遲執行 (3)對於依賴的模塊,AMD推崇依賴前置,CMD推崇依賴就近 3、seajs使用例子 因為CMD是SeaJS在推廣過程中對模塊定義的規範化產出,因此一般在實際開發中,我們都是通過SeaJS進行模塊的定義和載入 下麵是一個簡單的例子
// 定義模塊 myModule.js define(function(require, exports, module) { var $ = require('jquery.js') $('div').addClass('active'); exports.data = 1; }); // 載入模塊 seajs.use(['myModule.js'], function(my){ var star= my.data; console.log(star); //1 });
上面的代碼中定義了myModule.js模塊,因為該模塊依賴於jquery.js,因此在需要使用該模塊時可以使用require進行模塊的載入,然後通過exports暴露出介面,通過SeaJS的use方法我們可以載入該模塊,並且使用該模塊暴露出的介面
四、es6中的模塊化
在es6沒有出來之前,社區制定了一些模塊載入方案,最主要的有 CommonJS 和 AMD 兩種,前者用於伺服器,後者用於瀏覽器,ES6 在語言標準的層面上,實現了模塊功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規範,成為瀏覽器和伺服器通用的模塊解決方案
es6中的模塊化有一個比較大的特點,就是實現儘量的靜態化,比如說在CommonJS中我們要載入fs中的幾個方法,需要這樣寫
// CommonJS模塊 let { stat, exists, readFile } = require('fs'); // 等同於 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;
上面的代碼其實是載入了fs中的所有方法,生成一個對象,再從這個對象上讀取方法,這種載入其實叫做運行時載入,也就是只有運行時才能得到這個對象,不能實現在編譯時實現靜態優化
ES6 模塊不是對象,而是通過export命令顯式指定輸出的代碼,再通過import命令輸入
// ES6模塊 import { stat, exists, readFile } from 'fs';
上面代碼的實質是從fs模塊載入 3 個方法,其他方法不載入,這種載入稱為“編譯時載入”或者靜態載入,即 ES6 可以在編譯時就完成模塊載入,效率要比 CommonJS 模塊的載入方式高,當然,這也導致了沒法引用 ES6 模塊本身,因為它不是對象
1、export
模塊功能主要由兩個命令構成:export和import,export命令用於規定模塊的對外介面,import命令用於輸入其他模塊提供的功能
一般來說,一個模塊就是一個獨立的文件,該文件內部的所有變數,外部無法獲取,如果你希望外部能夠讀取模塊內部的某個變數,就必須使用export關鍵字輸出該變數
// profile.js export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958;
如果要輸出函數,可以像下麵這樣定義
function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion };
上面的代碼中,我們使用了as對函數的對外介面進行了重命名
2、import
使用export命令定義了模塊的對外介面以後,其他 JS 文件就可以通過import命令載入這個模塊
// main.js import {firstName, lastName, year} from './profile.js'; function setName(element) { element.textContent = firstName + ' ' + lastName; }
import命令接受一對大括弧,裡面指定要從其他模塊導入的變數名。大括弧裡面的變數名,必須與被導入模塊(profile.js)對外介面的名稱相同
我們也可以對載入的模塊進行重命名
import { lastName as surname } from './profile.js';
除了指定載入某個輸出值,還可以使用整體載入,即用星號(*
)指定一個對象,所有輸出值都載入在這個對象上面
下麵是一個circle.js文件,它輸出兩個方法area和circumference
// circle.js export function area(radius) { return Math.PI * radius * radius; } export function circumference(radius) { return 2 * Math.PI * radius; }
整體載入的寫法如下
import * as circle from './circle'; console.log('圓面積:' + circle.area(4)); console.log('圓周長:' + circle.circumference(14));
這裡有一個地方需要註意,模塊整體載入所在的那個對象(上例是circle
),應該是可以靜態分析的,所以不允許運行時改變,下麵的寫法都是不允許的
import * as circle from './circle'; // 下麵兩行都是不允許的 circle.foo = 'hello'; circle.area = function () {};
關於import其實還有很多用法,具體的大家可以查看相關的文檔
今天就先介紹到這裡,其實還有commonjs,還沒有進行介紹,如果大家感興趣,可以查看相關的用法呢