1. 程式人生 > >javascript中的require、import和export

javascript中的require、import和export

為什麼有模組概念

理想情況下,開發者只需要實現核心的業務邏輯,其他都可以載入別人已經寫好的模組。

但是,Javascript不是一種模組化程式語言,在es6以前,它是不支援”類”(class),所以也就沒有”模組”(module)了。

require時代

Javascript社群做了很多努力,在現有的執行環境中,實現”模組”的效果。

原始寫法

模組就是實現特定功能的一組方法。
只要把不同的函式(以及記錄狀態的變數)簡單地放在一起,就算是一個模組。

1 2 3 4 5 6 function m1(){   //... } function m2(){   //...   }

上面的函式m1()和m2(),組成一個模組。使用的時候,直接呼叫就行了。

這種做法的缺點很明顯:”汙染”了全域性變數,無法保證不與其他模組發生變數名衝突,而且模組成員之間看不出直接關係。

物件寫法

為了解決上面的缺點,可以把模組寫成一個物件,所有的模組成員都放到這個物件裡面

1 2 3 4 5 6 7 8 9 var module1 = new Object({ _count : 0,   m1 : function (){    //...  },   m2 : function (){    //...  } });

上面的函式m1()和m2(),都封裝在module1物件裡。使用的時候,就是呼叫這個物件的屬性

1 module 1.m1();

這樣的寫法會暴露所有模組成員,內部狀態可以被外部改寫。比如,外部程式碼可以直接改變內部計數器的值。

1 module._ count = 1;

立即執行函式寫法

使用”立即執行函式”(Immediately-Invoked Function Expression,IIFE),可以達到不暴露私有成員的目的

1 2 3 4 5 6 7 8 9 10 11 12 13 14 var module = (function() { var _count = 0; var m1 = function() { alert(_count) } var m2 = function() { alert(_count +  1) }   return { m1: m1, m2: m2 } })()

使用上面的寫法,外部程式碼無法讀取內部的_count變數。

1    console.info(module._count); //undefined

module就是Javascript模組的基本寫法。

主流模組規範

在es6以前,還沒有提出一套官方的規範,從社群和框架推廣程度而言,目前通行的javascript模組規範有兩種:CommonJS 和 AMD

CommonJS規範

2009年,美國程式設計師Ryan Dahl創造了node.js專案,將javascript語言用於伺服器端程式設計。

這標誌”Javascript模組化程式設計”正式誕生。前端的複雜程度有限,沒有模組也是可以的,但是在伺服器端,一定要有模組,與作業系統和其他應用程式互動,否則根本沒法程式設計。

node程式設計中最重要的思想之一就是模組,而正是這個思想,讓JavaScript的大規模工程成為可能。模組化程式設計在js界流行,也是基於此,隨後在瀏覽器端,requirejs和seajs之類的工具包也出現了,可以說在對應規範下,require統治了ES6之前的所有模組化程式設計,即使現在,在ES6 module被完全實現之前,還是這樣。

在CommonJS中,暴露模組使用module.exports和exports,很多人不明白暴露物件為什麼會有兩個,後面會介紹區別

在CommonJS中,有一個全域性性方法require(),用於載入模組。假定有一個數學模組math.js,就可以像下面這樣載入。

1 var math = require('math');

然後,就可以呼叫模組提供的方法:

1 2   var math = require('math');  math.add( 2,3); // 5

正是由於CommonJS 使用的require方式的推動,才有了後面的AMD、CMD 也採用的require方式來引用模組的風格

AMD規範

有了伺服器端模組以後,很自然地,大家就想要客戶端模組。而且最好兩者能夠相容,一個模組不用修改,在伺服器和瀏覽器都可以執行。

但是,由於一個重大的侷限,使得CommonJS規範不適用於瀏覽器環境。還是上一節的程式碼,如果在瀏覽器中執行,會有一個很大的問題

1 2 var  math = require('math'); math.add(2, 3);

第二行math.add(2, 3),在第一行require(‘math’)之後執行,因此必須等math.js載入完成。也就是說,如果載入時間很長,整個應用就會停在那裡等。

這對伺服器端不是一個問題,因為所有的模組都存放在本地硬碟,可以同步載入完成,等待時間就是硬碟的讀取時間。但是,對於瀏覽器,這卻是一個大問題,因為模組都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於”假死”狀態。
因此,瀏覽器端的模組,不能採用”同步載入”(synchronous),只能採用”非同步載入”(asynchronous)。這就是AMD規範誕生的背景。

AMD是”Asynchronous Module Definition”的縮寫,意思就是”非同步模組定義”。它採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。

模組必須採用特定的define()函式來定義。

1 define(id?, dependencies?, factory)
  • id:字串,模組名稱(可選)
  • dependencies: 是我們要載入的依賴模組(可選),使用相對路徑。,注意是陣列格式
  • factory: 工廠方法,返回一個模組函式

如果一個模組不依賴其他模組,那麼可以直接定義在define()函式之中。

1 2 3 4 5 6 7 8 9 // math.js   define( function (){      var add = function (x,y){        return x+y;     };      return {        add: add     };   });

如果這個模組還依賴其他模組,那麼define()函式的第一個引數,必須是一個數組,指明該模組的依賴性。

1 2 3 4 5 6 7 8 define([ 'Lib'], function(Lib){      function foo(){       Lib.doSomething();     }      return {        foo : foo     };   });

當require()函式載入上面這個模組的時候,就會先載入Lib.js檔案。

AMD也採用require()語句載入模組,但是不同於CommonJS,它要求兩個引數:

1 require([module], callback);

第一個引數[module],是一個數組,裡面的成員就是要載入的模組;第二個引數callback,則是載入成功之後的回撥函式。如果將前面的程式碼改寫成AMD形式,就是下面這樣:

1 2 3 require(['math'], function (math) {   math.add(2, 3); });

math.add()與math模組載入不是同步的,瀏覽器不會發生假死。所以很顯然,AMD比較適合瀏覽器環境。

目前,主要有兩個Javascript庫實現了AMD規範:require.jscurl.js

CMD規範

CMD (Common Module Definition), 是seajs推崇的規範,CMD則是依賴就近,用的時候再require。它寫起來是這樣的:

1 2 3 4 define( function(require, exports, module) { var clock = require('clock') ; clock.start() ; }) ;

CMD與AMD一樣,也是採用特定的define()函式來定義,用require方式來引用模組

1 define(id?, dependencies?, factory)
  • id:字串,模組名稱(可選)
  • dependencies: 是我們要載入的依賴模組(可選),使用相對路徑。,注意是陣列格式
  • factory: 工廠方法,返回一個模組函式
1 2 3 4 5 define( 'hello', ['jquery'], function(require, exports, module) {   // 模組程式碼   });

如果一個模組不依賴其他模組,那麼可以直接定義在define()函式之中。

1 2 3 define( function(require, exports, module) { // 模組程式碼 });

注意:帶 id 和 dependencies 引數的 define 用法不屬於 CMD 規範,而屬於 Modules/Transport 規範。

CMD與AMD區別

AMD和CMD最大的區別是對依賴模組的執行時機處理不同,而不是載入的時機或者方式不同,二者皆為非同步載入模組。

AMD依賴前置,js可以方便知道依賴模組是誰,立即載入;

而CMD就近依賴,需要使用把模組變為字串解析一遍才知道依賴了那些模組,這也是很多人詬病CMD的一點,犧牲效能來帶來開發的便利性,實際上解析模組用的時間短到可以忽略。

現階段的標準

ES6標準釋出後,module成為標準,標準使用是以export指令匯出介面,以import引入模組,但是在我們一貫的node模組中,我們依然採用的是CommonJS規範,使用require引入模組,使用module.exports匯出介面。

export匯出模組

export語法宣告用於匯出函式、物件、指定檔案(或模組)的原始值。

注意:在node中使用的是exports,不要混淆了

export有兩種模組匯出方式:命名式匯出(名稱匯出)預設匯出(定義式匯出),命名式匯出每個模組可以多個,而預設匯出每個模組僅一個。

1 2 3 4 5 6 7 8 9 10 11 12 13 export { name1, name2, …, nameN }; export { variable1 as name1, variable2 as name2, …, nameN }; export let name1, name2, …, nameN; // also var export let name1 = …, name2 = …, …, nameN; // also var, const   export default expression; export default function (…) { … } // also class, function* export default function name1(…) { … } // also class, function* export { name1 as default, … };   export * from …; export { name1, name2, …, nameN } from …; export { import1 as name1, import2 as name2, …, nameN } from …;
  • name1… nameN-匯出的“識別符號”。匯出後,可以通過這個“識別符號”在另一個模組中使用import引用
  • default-設定模組的預設匯出。設定後import不通過“識別符號”而直接引用預設匯入
  • -繼承模組並匯出繼承模組所有的方法和屬性
  • as-重新命名匯出“識別符號”
  • from-從已經存在的模組、指令碼檔案…匯出

命名式匯出

模組可以通過export字首關鍵詞宣告匯出物件,匯出物件可以是多個。這些匯出物件用名稱進行區分,稱之為命名式匯出。

1 2 export { myFunction }; // 匯出一個已定義的函式 export const foo = Math.sqrt(2); // 匯出一個常量

我們可以使用*和from關鍵字來實現的模組的繼承:

1 export * from 'article';

模組匯出時,可以指定模組的匯出成員。匯出成員可以認為是類中的公有物件,而非匯出成員可以認為是類中的私有物件:

1 2 3 4 5 var name = 'IT筆錄'; var domain = 'http://itbilu.com';   export {name, domain}; // 相當於匯出 {name:name,domain:domain}

模組匯出時,我們可以使用as關鍵字對匯出成員進行重新命名:

1 2 3 4 var name = 'IT筆錄'; var domain = 'http://itbilu.com';   export {name as siteName, domain};

注意,下面的語法有嚴重錯誤的情況:

1 2 3 4 5 // 錯誤演示 export 1; // 絕對不可以   var a = 100; export a;

 

export在匯出介面的時候,必須與模組內部的變數具有一一對應的關係。直接匯出1沒有任何意義,也不可能在import的時候有一個變數與之對應

export a雖然看上去成立,但是a的值是一個數字,根本無法完成解構,因此必須寫成export {a}的形式。即使a被賦值為一個function,也是不允許的。而且,大部分風格都建議,模組中最好在末尾用一個export匯出所有的介面,例如:

1 export { fun as default,a,b,c};

預設匯出

預設匯出也被稱做定義式匯出。命名式匯出可以匯出多個值,但在在import引用時,也要使用相同的名稱來引用相應的值。而預設匯出每個匯出只有一個單一值,這個輸出可以是一個函式、類或其它型別的值,這樣在模組import匯入時也會很容易引用。

1 2 export default function() {}; // 可以匯出一個函式 export default class(){}; // 也可以出一個類

命名式匯出與預設匯出

預設匯出可以理解為另一種形式的命名匯出,預設匯出可以認為是使用了default名稱的命名匯出。

下面兩種匯出方式是等價的:

1 2 3 4 const D = 123;   export default D; export { D as default };

export使用示例

使用名稱匯出一個模組時:

1 2 3 4 5 6 // "my-module.js" 模組 export function cube(x) { return x * x * x; } const foo = Math.PI + Math.SQRT2; export { foo };

在另一個模組(指令碼檔案)中,我們可以像下面這樣引用:

1 2 3 import { cube, foo } from 'my-module'; console.log(cube(3)); // 27 console.log(foo); // 4.555806215962888

使用預設匯出一個模組時:

1 2 3 4 // "my-module.js"模組 export default function (x) { return x * x * x; }

在另一個模組(指令碼檔案)中,我們可以像下面這樣引用,相對名稱匯出來說使用更為簡單:

1 2 3 // 引用 "my-module.js"模組 import cube from 'my-module'; console.log(cube(3)); // 27

import引入模組

import語法宣告用於從已匯出的模組、指令碼中匯入函式、物件、指定檔案(或模組)的原始值。

import模組匯入與export模組匯出功能相對應,也存在兩種模組匯入方式:命名式匯入(名稱匯入)和預設匯入(定義式匯入)。

import的語法跟require不同,而且import必須放在檔案的最開始,且前面不允許有其他邏輯程式碼,這和其他所有程式語言風格一致。

1 2 3 4 5 6 7 8 9 import defaultMember from "module-name"; import * as name from "module-name"; import { member } from "module-name"; import { member as alias } from "module-name"; import { member1 , member2 } from "module-name"; import { member1 , member2 as alias2 , [...] } from "module-name"; import defaultMember, { member [ , [...] ] } from "module-name"; import defaultMember, * as name from "module-name"; import "module-name";
  • name-從將要匯入模組中收到的匯出值的名稱
  • member, memberN-從匯出模組,匯入指定名稱的多個成員
  • defaultMember-從匯出模組,匯入預設匯出成員
  • alias, aliasN-別名,對指定匯入成員進行的重新命名
  • module-name-要匯入的模組。是一個檔名
  • as-重新命名匯入成員名稱(“識別符號”)
  • from-從已經存在的模組、指令碼檔案等匯入

命名式匯入

我們可以通過指定名稱,就是將這些成員插入到當作用域中。匯出時,可以匯入單個成員或多個成員:

注意,花括號裡面的變數與export後面的變數一一對應

1 2 import {myMember} from "my-module"; import {foo, bar} from "my-module";

通過*符號,我們可以匯入模組中的全部屬性和方法。當匯入模組全部匯出內容時,就是將匯出模組(’my-module.js’)所有的匯出繫結內容,插入到當前模組(’myModule’)的作用域中:

1 import * as myModule from "my-module";

匯入模組物件時,也可以使用as對匯入成員重新命名,以方便在當前模組內使用:

1 import {reallyReallyLongModuleMemberName as shortName} from "my-module";

匯入多個成員時,同樣可以使用別名:

1 import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";

匯入一個模組,但不進行任何繫結:

1 import "my-module";

預設匯入

在模組匯出時,可能會存在預設匯出。同樣的,在匯入時可以使用import指令匯出這些預設值。

直接匯入預設值:

1 import myDefault from "my-module";

也可以在名稱空間匯入和名稱匯入中,同時使用預設匯入:

1 2 3 4 import myDefault, * as myModule from "my-module"; // myModule 做為名稱空間使用 或   import myDefault, {foo, bar} from "my-module"; // 指定成員匯入

import使用示例

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // --file.js-- function getJSON(url, callback) { let xhr = new XMLHttpRequest(); xhr.onload =  function () { callback( this.responseText) }; xhr.open( "GET", url, true); xhr.send(); }   export function getUsefulContents(url, callback) { getJSON(url,  data => callback(JSON.parse(data))); }   // --main.js-- import { getUsefulContents } from "file"; getUsefulContents( "http://itbilu.com", data => { doSomethingUseful(data); });

default關鍵字

1 2 3 4 5 6 // d.js export default function() {}   // 等效於: function a() {}; export {a as default};

在import的時候,可以這樣用:

1 2 3 4 import a from './d';   // 等效於,或者說就是下面這種寫法的簡寫,是同一個意思 import {default as a} from './d';

這個語法糖的好處就是import的時候,可以省去花括號{}。

簡單的說,如果import的時候,你發現某個變數沒有花括號括起來(沒有*號),那麼你在腦海中應該把它還原成有花括號的as語法。

所以,下面這種寫法你也應該理解了吧:

1 import $,{each,map} from 'jquery';

import後面第一個$是{defalut as $}的替代寫法。

as關鍵字

as簡單的說就是取一個別名,export中可以用,import中其實可以用:

1 2 3 4 5 6 7 // a.js var a = function() {}; export {a  as fun};   // b.js import {fun as a} from './a'; a();

上面這段程式碼,export的時候,對外提供的介面是fun,它是a.js內部a這個函式的別名,但是在模組外面,認不到a,只能認到fun。

import中的as就很簡單,就是你在使用模組裡面的方法的時候,給這個方法取一個別名,好在當前的檔案裡面使用。之所以是這樣,是因為有的時候不同的兩個模組可能通過相同的介面,比如有一個c.js也通過了fun這個介面:

1 2 // c.js export function  fun() {};

如果在b.js中