ES6, CommonJS, AMD, CMD,UMD模块化规范介绍及使用
前言
在項(xiàng)目開發(fā)過程中,引入模塊有時(shí)使用require(),有時(shí)使用import…from..,導(dǎo)出模塊有時(shí)用export或export default,一直會這么使用,但是沒有太在意區(qū)別,以及它們分別遵循的是哪個(gè)模塊規(guī)范。接下來就直接介紹幾種模塊的使用。
具體內(nèi)容
一、模塊概念介紹
模塊通常是指編程語言所提供的代碼組織機(jī)制,利用此機(jī)制可將程序拆解為獨(dú)立且通用的代碼單元;
所謂模塊化主要是解決代碼分割、作用域隔離、模塊之間的依賴管理以及發(fā)布到生產(chǎn)環(huán)境的自動(dòng)化打包與處理等多方面;
模塊規(guī)范主要是進(jìn)行模塊加載,一個(gè)單獨(dú)的文件也算是一個(gè)模塊,一個(gè)模塊就是一個(gè)單獨(dú)的作用域,不會污染全局作用域;
一個(gè)模塊就是一個(gè)對其他模塊暴露自己的屬性或者方法的文件。
二、模塊的優(yōu)點(diǎn)
(1)可維護(hù)性。因?yàn)槟K是獨(dú)立的,一個(gè)設(shè)計(jì)良好的模塊會讓外面的代碼對自己的依賴越少越好,這樣自己就可以獨(dú)立去更新和改進(jìn)。
(2)命名空間。在 JavaScript 里面,如果一個(gè)變量在最頂級的函數(shù)之外聲明,它就直接變成全局可用。因此,常常不小心出現(xiàn)命名沖突的情況。使用模塊化開發(fā)來封裝變量,可以避免污染全局環(huán)境。
(3)重用代碼。有時(shí)從之前寫過的項(xiàng)目中拷貝代碼到新的項(xiàng)目,這沒有問題,但是更好的方法是,通過模塊引用的方式,來避免重復(fù)的代碼庫。
三、ES6、CommonJS、AMD、CMD、UMD模塊介紹
(1)ES6模塊(Module)
ES6的模塊自動(dòng)采用嚴(yán)格模式,不管是否在模塊頭部加上"use strict";
嚴(yán)格模式主要有以下這些限制:
變量必須聲明后再使用
函數(shù)的參數(shù)不能有同名屬性,否則報(bào)錯(cuò)
不能使用with語句
不能對只讀屬性賦值,否則報(bào)錯(cuò)
不能使用前綴0表示八進(jìn)制數(shù),否則報(bào)錯(cuò)
不能刪除不可刪除的屬性,否則報(bào)錯(cuò)
不能刪除變量delete prop,會報(bào)錯(cuò),只能刪除屬性delete global[prop]
eval不會在它的外層作用域引入變量
eval和arguments不能被重新賦值
arguments不會自動(dòng)反映函數(shù)參數(shù)的變化
不能使用arguments.callee
不能使用arguments.caller
禁止this指向全局對象
不能使用fn.caller和fn.arguments獲取函數(shù)調(diào)用的堆棧
增加了保留字(比如protected、static和interface)
?導(dǎo)出模塊 export
作為一個(gè)模塊,可以選擇性地給其他模塊暴露自己的屬性和方法,供其他模塊使用
1 //profile.js
2 export var firstName = "wang";
3 export var lastName = "qin";
4 export var born = "1996";
5 //等價(jià)于
6 var firstName = "wang";
7 var lastName = "qin";
8 var born = "1996";
9 export {firstName,lastName,born}
1、通常情況下,export輸出的變量就是本來的名字,但是可以使用as關(guān)鍵字對輸出的變量進(jìn)行重命名
1 function v1(){}
2 function v2(){}
3
4 export{
5 v1 as stream1,
6 v2 as stream2,
7 v2 as streamLatestVersion
8 };
9 //使用as關(guān)鍵字,重命名函數(shù)v1和v2的對外接口;重命名之后,v2可以用不同的名字輸出2次
2、需要特別注意的是,export命令規(guī)定的是對外的接口,必須與模塊內(nèi)部的變量建立一 一對應(yīng)關(guān)系
1 //注意:以下2種寫法報(bào)錯(cuò)是因?yàn)闆]有提供對外的接口;第1種寫法直接輸出1;
2 //第2種寫法通過變量m,還是直接輸出1,1只是一個(gè)值,不是對外接口;
3
4 //報(bào)錯(cuò)
5 export 1;
6
7 //報(bào)錯(cuò)
8 var m = 1;
9 export m;
10
11 //以下3種寫法都是正確的,規(guī)定了對外的接口m;其他腳本通過訪問這個(gè)接口,可以取到1;
12 //實(shí)質(zhì)就是在接口名與模塊內(nèi)部變量之間建立了一一對應(yīng)關(guān)系;
13 //方法一
14 export var m = 1;
15 //方法二
16 var m = 1;
17 export {m};
18 //方法三
19 var n = 1;
20 export {n as m};
3、export命令和import命令可以出現(xiàn)在模塊的任何位置,只要處于模塊頂層就可以。如果處于塊級作用域內(nèi),就會報(bào)錯(cuò)
1 function foo(){
2 export default 'bar' //語法錯(cuò)誤
3 }
4 foo();
?默認(rèn)導(dǎo)出模塊 export default
每個(gè)模塊支持我們導(dǎo)出一個(gè)沒有名字的變量,使用關(guān)鍵語句export default來實(shí)現(xiàn)
1 //module.js
2 export default function(){ //使用export default關(guān)鍵字對外導(dǎo)出一個(gè)匿名函數(shù)
3 console.log('I am default Fn');
4 }
5
6 //導(dǎo)入上面模塊時(shí),可以為該匿名函數(shù)取任意名字
7 import defaultFn from './module.js'
8 defaultFn(); //輸出"I am default Fn"
1、默認(rèn)輸出和正常輸出的比較
1 export default function diff(){} //默認(rèn)導(dǎo)出
2 import diff from 'diff'; //導(dǎo)入
3
4
5 export function diff(){} //正常導(dǎo)出
6 import {diff} from 'diff'; //導(dǎo)入
7
8 //注意:使用export default默認(rèn)導(dǎo)出時(shí),對應(yīng)的import語句不需要使用大括號;
9 //而使用export正常導(dǎo)出時(shí),對應(yīng)的import語句需要使用大括號;
注意:export default命令用于指定模塊的默認(rèn)輸出。顯然,一個(gè)模塊只能有一個(gè)默認(rèn)輸出,因此export default命令只能使用一次。所以,import命令后面才不用加大括號,因?yàn)橹豢赡軐?yīng)一個(gè)方法。
2、export default本質(zhì)是將該命令后面的值,賦給default變量以后再默認(rèn),所以直接將一個(gè)值寫在export default之后
1 //正確;指定對外接口為default 2 export default 66; 3 4 //報(bào)錯(cuò);因?yàn)闆]有指定對外的接口 5 export 66;
3、在一條import語句中,同時(shí)導(dǎo)入默認(rèn)方法和其他變量,可以寫成下面這樣
1 export default function(){} //默認(rèn)導(dǎo)出
2 export function each(obj,iterator,context){} //正常導(dǎo)出
3
4 //同時(shí)導(dǎo)入默認(rèn)方法和其他變量
5 import _,{each} from 'loadsh'
?導(dǎo)入模塊 import
作為一個(gè)模塊,可以根據(jù)需要,引入其他模塊的提供的屬性或者方法,供自己模塊使用
1、import命令接受一對大括號,里面指定要從其他模塊導(dǎo)入的變量名。大括號里面的變量名,必須與被導(dǎo)入模塊(profile.js)對外接口的名稱相同。如果想為輸入的變量重新取一個(gè)名字,import命令要使用as關(guān)鍵字,將輸入的變量重命名
1 import {lastName as realName} from './profile.js'
2、import后面的from指定模塊文件的位置,可以是相對路徑,也可以是絕對路徑,.js路徑可以省略。如果只是模塊名,不帶有路徑,那么必須有配置文件,告訴 JavaScript 引擎該模塊的位置
3、import命令具有提升效果,會提升到整個(gè)模塊的頭部,首先執(zhí)行
1 foo();
2 import { foo } from 'my_module';
3 //先調(diào)用foo()不會報(bào)錯(cuò),原因是import的執(zhí)行早于foo()的調(diào)用;
4 //本質(zhì)是因?yàn)閕mport命令是在編譯階段執(zhí)行,在代碼運(yùn)行之前;
4、由于import是靜態(tài)執(zhí)行,所以不能使用表達(dá)式和變量,這些只有在運(yùn)行時(shí)才能得到結(jié)果的語法結(jié)構(gòu)
1 //報(bào)錯(cuò)
2 import { 'f' + 'oo' } from 'my_module';
3
4 //報(bào)錯(cuò)
5 let module = 'my_module';
6 import { foo } from module;
7
8 //報(bào)錯(cuò)
9 if (x === 1) {
10 import { foo } from 'module1';
11 } else {
12 import { foo } from 'module2';
13 }
5、import語句會執(zhí)行所加載的模塊
1 import 'loadsh' //僅僅執(zhí)行l(wèi)oadsh模塊,但不輸入任何值
(2)CommonJS
CommonJS 最開始是 Mozilla 的工程師于 2009 年開始的一個(gè)項(xiàng)目,它的目的是讓瀏覽器之外的 JavaScript (比如服務(wù)器端或者桌面端)能夠通過模塊化的方式來開發(fā)和協(xié)作。
在 CommonJS 規(guī)范中,每個(gè) JavaScript 文件就是一個(gè)獨(dú)立的模塊上下文(module context),在這個(gè)上下文中默認(rèn)創(chuàng)建的屬性都是私有的。也就是說,在一個(gè)文件定義的變量(還包括函數(shù)和類),都是私有的,對其他文件是不可見的。
需要注意的是,CommonJS規(guī)范的主要適用場景是服務(wù)器端編程,采用同步加載模塊的策略。
如果某個(gè)文件依賴3個(gè)模塊,代碼會一個(gè)一個(gè)依次加載它們。
CommonJS模塊化規(guī)范實(shí)現(xiàn)方案主要包含require與module.exports( exports )這兩個(gè)關(guān)鍵字,允許某個(gè)模塊對外暴露部分接口并且由其他模塊導(dǎo)入使用。
1 //======sayHello.js=======
2 function sayHello(){
3 this.hello = function(){
4 console.log('hello');
5 };
6 this.hi = function(){
7 console.log('hi');
8 }
9 }
10 module.exports = sayHello;
11
12 //=======main.js===========
13 var say = require('./sayHello.js');
14 var sayPlay = new say();
15 sayPlay.hello(); //hello
作為服務(wù)器端的解決方案,CommonJS需要一個(gè)兼容的腳本加載器(即NodeJS)作為前提條件,該腳本加載器必須支持名為require和module.exports的函數(shù),它們可以將模塊相互導(dǎo)入導(dǎo)出。
==================補(bǔ)充說明:Node.js=======================
1、Node是對CommonJS模塊規(guī)范的實(shí)現(xiàn)。
2、Node.js模塊分為兩大類:一類是核心模塊,一類是文件模塊;
核心模塊:就是Node.js標(biāo)準(zhǔn)API中提供的模塊,如fs/http/path等,這些由Node.js官方提供的模塊,編譯成了二進(jìn)制代碼,可以直接通過require獲取核心模塊,例如require('fs'),核心模塊擁有最高的加載優(yōu)先級,如果有模塊與核心模塊命名沖突,Node.js總是會加載核心模塊。
文件模塊:是存儲為單獨(dú)的文件(或文件夾)的模塊,可能是JavaScript代碼、JSON或編譯好的C/C++代碼。在不顯式指定文件模塊擴(kuò)展名的時(shí)候,Node.js會分別試圖加上.js、.json、.node(編譯好的C/C++代碼)。
Node.js會根據(jù)后綴名來決定3類文件模塊的加載方法:
.js通過fs模塊同步讀取js文件并編譯執(zhí)行。
.node通過C/C++進(jìn)行編寫的Addon。通過dlopen方法進(jìn)行加載。
.json讀取文件,調(diào)用JSON.parse解析加載。
1 /* js后綴的編譯過程:【Node.js在編譯js文件的過程中實(shí)際完成的步驟有對js文件內(nèi)容進(jìn)行頭尾包裝 】*/
2 //==========circle.js===========
3 var PI = Math.PI;
4 exports.area = function (r) {
5 return PI * r * r;
6 };
7 exports.circumference = function (r) {
8 return 2 * PI * r;
9 };
10
11 //============app.js============
12 var circle = require('./circle.js');
13 console.log( 'The area of a circle of radius 4 is ' + circle.area(4));
14
15
16 //==============app包裝后=========
17 (function (exports, require, module, __filename, __dirname) {
18 var circle = require('./circle.js');
19 console.log('The area of a circle of radius 4 is ' + circle.area(4));
20 });
21
22 //這段代碼會通過原生模塊vm的runInThisContext方法執(zhí)行
23 //(類似eval,只是具有明確上下文,不污染全局),返回為一個(gè)具體的function對象。
24 //最后傳入module對象的exports,require方法,module,文件名,目錄名作為實(shí)參并執(zhí)行
3、加載方式
按路徑加載模塊
如果require參數(shù)以" / "開頭,那么就以絕對路徑的方式查找模塊名稱,如果參數(shù)以" ./ "、" ../ "開頭,那么則是以相對路徑的方式來查找模塊。
查找node_modules目錄加載模塊
如果require參數(shù)不以" / "、" ./ "、" ../ "開頭,而該模塊又不是核心模塊,那么就要通過查找node_modules加載模塊了。使用npm工具獲取的模塊/包通常就是以這種方式加載的。
4、加載緩存
Node.js模塊不會被重復(fù)加載,因?yàn)镹ode.js通過文件名緩存所有加載過的文件模塊,之后再訪問相同文件模塊時(shí)就不會重新加載了。
注意:Node.js是根據(jù)實(shí)際文件名緩存的,而不是require()提供的參數(shù)緩存的,也就是說即使分別通過require('express')和require('./node_modules/express')加載兩次,也不會重復(fù)加載,因?yàn)楸M管兩次參數(shù)不同,解析到的文件卻是同一個(gè)。
Node.js 中的模塊在加載之后是以單例化運(yùn)行,并且遵循值傳遞原則:如果是一個(gè)對象,就相當(dāng)于這個(gè)對象的引用。
5、模塊載入過程(需進(jìn)一步理解)
加載文件模塊的工作,主要由原生模塊module來實(shí)現(xiàn)和完成,該原生模塊在啟動(dòng)時(shí)已經(jīng)被加載,進(jìn)程直接調(diào)用到runMain靜態(tài)方法。
1 //例如運(yùn)行: node app.js
2
3 Module.runMain = function () {
4 // Load the main module--the command line argument.
5 Module._load(process.argv[1], null, true);
6 };
7
8 //_load靜態(tài)方法在分析文件名之后執(zhí)行
9 var module = new Module(id, parent);
10
11 //根據(jù)文件路徑緩存當(dāng)前模塊對象,該模塊實(shí)例對象則根據(jù)文件名加載。
12 module.load(filename);
6、require 函數(shù)
require 引入的對象主要是函數(shù)。當(dāng) Node 調(diào)用 require() 函數(shù),并且傳遞一個(gè)文件路徑給它的時(shí)候,Node 會經(jīng)歷如下幾個(gè)步驟:
Resolving:找到文件的絕對路徑;
Loading:判斷文件類型;
Wrapping:打包,給文件賦予一個(gè)私有作用范圍。這是使 require 和 module 模塊在本地引用的一種方法;
Evaluating:VM 對加載的代碼進(jìn)行處理;
Caching:當(dāng)再次需要用這個(gè)文件時(shí),不需要重復(fù)一遍上面步驟。
7、require.extensions 來查看對三種文件的支持情況
Node 對每種擴(kuò)展名所使用的函數(shù)及其操作:
對 .js 文件使用 module._compile;
對 .json 文件使用 JSON.parse;
對 .node 文件使用 process.dlopen。
8、文件查找策略
從文件模塊緩存中加載
盡管原生模塊與文件模塊的優(yōu)先級不同,但是優(yōu)先級最高的是從文件模塊的緩存中加載已經(jīng)存在的模塊。
從原生模塊加載
原生模塊的優(yōu)先級僅次于文件模塊緩存的優(yōu)先級。require方法在解析文件名之后,優(yōu)先檢查模塊是否在原生模塊列表中。以http模塊為例,盡管在目錄下存在一個(gè)http、http.js、http.node、http.json文件,require(“http”)都不會從這些文件中加載,而是從原生
模塊中加載。原生模塊也有一個(gè)緩存區(qū),同樣也是優(yōu)先從緩存區(qū)加載。如果緩存區(qū)沒有被加載過,則調(diào)用原生模塊的加載方式進(jìn)行加載和執(zhí)行。
從文件加載
當(dāng)文件模塊緩存中不存在,而且不是原生模塊的時(shí)候,Node.js會解析require方法傳入的參數(shù),并從文件系統(tǒng)中加載實(shí)際的文件,加載過程中的包裝和編譯細(xì)節(jié)在前面說過是調(diào)用load方法。
1 ======當(dāng) Node 遇到 require(X) 時(shí),按下面的順序查找并加載===========
2
3 (1)如果 X 是內(nèi)置模塊(比如 require('http'))
4 a. 返回該模塊。
5 b. 不再繼續(xù)執(zhí)行。
6
7 (2)如果 X 以 "./" 或者 "/" 或者 "../" 開頭
8 a. 根據(jù) X 所在的父模塊,確定 X 的絕對路徑。
9 b. 將 X 當(dāng)成文件,依次查找下面文件,只要其中有一個(gè)存在,就返回該文件,不再繼續(xù)執(zhí)行。
10 X
11 X.js
12 X.json
13 X.node
14
15 c. 將 X 當(dāng)成目錄,依次查找下面文件,只要其中有一個(gè)存在,就返回該文件,不再繼續(xù)執(zhí)行。
16 X/package.json(main字段)
17 X/index.js
18 X/index.json
19 X/index.node
20
21 (3)如果 X 不帶路徑
22 a. 根據(jù) X 所在的父模塊,確定 X 可能的安裝目錄。
23 b. 依次在每個(gè)目錄中,將 X 當(dāng)成文件名或目錄名加載。
24
25 (4) 拋出 "not found"
9、模塊循環(huán)依賴
1 //創(chuàng)建兩個(gè)文件,module1.js 和 module2.js,并且讓它們相互引用
2 //========module1.js==========
3 exports.a = 1;
4 require('./module2');
5 exports.b = 2;
6 exports.c = 3;
7
8 //========module2.js=============
9 const Module1 = require('./module1');
10 console.log('Module1 is partially loaded here', Module1);
在 module1 完全加載之前需要先加載 module2,而 module2 的加載又需要 module1。這種狀態(tài)下,我們從 exports 對象中能得到的就是在發(fā)生循環(huán)依賴之前的這部分。上面代碼中,只有 a 屬性被引入,因?yàn)?b 和 c 都需要在引入 module2 之后才能加載進(jìn)來。
Node 使這個(gè)問題簡單化,在一個(gè)模塊加載期間開始創(chuàng)建 exports 對象。如果它需要引入其他模塊,并且有循環(huán)依賴,那么只能部分引入,也就是只能引入發(fā)生循環(huán)依賴之前所定義的這部分。
(3)AMD
AMD 是 Asynchronous Module Definition 的簡稱,即“異步模塊定義”,是從 CommonJS 討論中誕生的。AMD 優(yōu)先照顧瀏覽器的模塊加載場景,使用了異步加載和回調(diào)的方式。
RequireJS 是一個(gè)前端的模塊化管理的工具庫,遵循AMD規(guī)范。
(4)CMD
CMD(Common Module Definition),在CMD中,一個(gè)模塊就是一個(gè)文件。
Sea.js遵循CMD規(guī)范,與NodeJS般的書寫模塊代碼。
(5)UMD
統(tǒng)一模塊定義(UMD:Universal Module Definition )就是將 AMD 和 CommonJS 合在一起的一種嘗試,常見的做法是將CommonJS 語法包裹在兼容 AMD 的代碼中。
結(jié)束語
在當(dāng)前項(xiàng)目開發(fā)過程中,主要使用ES6模塊化規(guī)范:即import和export/exportdefault;有時(shí)也使用CommonJS模塊化規(guī)范,Nodejs遵循該模塊化規(guī)范:即require和exports。其他模塊化規(guī)范目前只作為了解,后續(xù)項(xiàng)目開發(fā)中進(jìn)行更深入學(xué)習(xí)和總結(jié)。
總結(jié)
以上是生活随笔為你收集整理的ES6, CommonJS, AMD, CMD,UMD模块化规范介绍及使用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C语言 · 前10名
- 下一篇: 解决Warning: Permanent