大綱:
一瞎访、模塊化概述
二西乖、CommonJS規(guī)范
三、ES6 Module
四、CommonJS與ES6模塊的混編
五燥狰、Node.js中的模塊化
六揣云、循環(huán)加載
七捕儒、了解:AMD-Require.js和CMD-SeaJS
八、參考鏈接
因?yàn)閮?nèi)容太多邓夕,沒(méi)有大綱不方便閱讀刘莹,所以也可以跳轉(zhuǎn) 前端各種模塊化方案總結(jié) 附帶大綱 閱讀。
文中七成左右篇幅內(nèi)容都來(lái)自于Module的語(yǔ)法和加載實(shí)現(xiàn) — 阮一峰焚刚、徹底掌握前端模塊化 — codewhy幾篇文章点弯,結(jié)合自己之前掌握的知識(shí),按自己的記憶習(xí)慣重新進(jìn)行了梳理矿咕。
一抢肛、模塊化
1.1 什么是模塊化
那么,到底什么是模塊化開(kāi)發(fā)呢痴腌?
模塊:1雌团、在通信、計(jì)算機(jī)士聪、數(shù)據(jù)處理控制系統(tǒng)的電路中锦援,可以組合和更換的硬件單元。2剥悟、大型軟件系統(tǒng)中的一個(gè)具有獨(dú)立功能的部分灵寺。
- 現(xiàn)實(shí)生活中模塊化的例子:模塊化計(jì)算機(jī)(cpu、內(nèi)存区岗、顯卡略板、風(fēng)扇、硬盤(pán)慈缔、光驅(qū)等等模塊)叮称、谷歌模塊化手機(jī)、模塊化房屋
- 代碼模塊化例子:日期模塊、數(shù)學(xué)計(jì)算模塊瓤檐、日志模塊等赂韵,所有這些模塊共同組成了程序軟件系統(tǒng)
模塊化:
- 模塊化開(kāi)發(fā)就是將程序劃分成一個(gè)個(gè)(互相依賴的)小文件/模塊來(lái)開(kāi)發(fā),然后將小模塊組合起來(lái)挠蛉;
- 這個(gè)模塊中編寫(xiě)屬于自己的邏輯代碼祭示,有自己的作用域,不會(huì)影響到其他的結(jié)構(gòu)谴古;
- 這個(gè)模塊可以將自己希望暴露的變量鸠删、函數(shù)经磅、對(duì)象等導(dǎo)出給其結(jié)構(gòu)使用煤杀;
- 也可以通過(guò)某種方式垦垂,導(dǎo)入另外模塊中的變量、函數(shù)恩敌、對(duì)象等瞬测;
模塊化的好處:
- 防止命名沖突
- 代碼復(fù)用(非模塊化開(kāi)發(fā)時(shí),代碼重用時(shí)纠炮,引入 js 文件的數(shù)目可能少了或者引入的順序不對(duì),會(huì)導(dǎo)致一些問(wèn)題)
- 高維護(hù)性(模塊之間有高耦合低內(nèi)聚的特點(diǎn))
1.2 JavaScript設(shè)計(jì)缺陷
無(wú)論你多么喜歡JavaScript灯蝴,以及它現(xiàn)在發(fā)展的有多好恢口,我們都需要承認(rèn)在Brendan Eich用了10天寫(xiě)出JavaScript的時(shí)候,它都有很多的缺陷:
- 比如var定義的變量作用域問(wèn)題穷躁;
- 比如JavaScript的面向?qū)ο蟛⒉荒芟癯R?guī)面向?qū)ο笳Z(yǔ)言一樣使用class耕肩;
- 比如JavaScript沒(méi)有模塊化的問(wèn)題;
Brendan Eich本人也多次承認(rèn)過(guò)JavaScript設(shè)計(jì)之初的缺陷问潭,但是隨著JavaScript的發(fā)展以及標(biāo)準(zhǔn)化猿诸,存在的缺陷問(wèn)題基本都得到了完善。
- JavaScript目前已經(jīng)得到了快速的發(fā)展狡忙,無(wú)論是web梳虽、移動(dòng)端、小程序端灾茁、服務(wù)器端窜觉、桌面應(yīng)用都被廣泛的使用;
在網(wǎng)頁(yè)開(kāi)發(fā)的早期北专,Brendan Eich開(kāi)發(fā)JavaScript僅僅作為一種腳本語(yǔ)言禀挫,做一些簡(jiǎn)單的表單驗(yàn)證或動(dòng)畫(huà)實(shí)現(xiàn)等,那個(gè)時(shí)候代碼還是很少的:
- 這個(gè)時(shí)候我們只需要講JavaScript代碼寫(xiě)到
<script>
標(biāo)簽中即可拓颓; - 并沒(méi)有必要放到多個(gè)文件中來(lái)編寫(xiě)语婴;
<button id="btn">按鈕</button>
<script>
document.getElementById("btn").onclick = function() {
console.log("按鈕被點(diǎn)擊了");
}
</script>
但是隨著前端和JavaScript的快速發(fā)展,JavaScript代碼變得越來(lái)越復(fù)雜了:
- ajax的出現(xiàn),前后端開(kāi)發(fā)分離砰左,意味著后端返回?cái)?shù)據(jù)后画拾,我們需要通過(guò)JavaScript進(jìn)行前端頁(yè)面的渲染;
- SPA的出現(xiàn)菜职,前端頁(yè)面變得更加復(fù)雜:包括前端路由青抛、狀態(tài)管理等等一系列復(fù)雜的需求需要通過(guò)JavaScript來(lái)實(shí)現(xiàn);
- 包括Node的實(shí)現(xiàn)酬核,JavaScript編寫(xiě)復(fù)雜的后端程序蜜另,沒(méi)有模塊化是致命的硬傷;
所以嫡意,模塊化已經(jīng)是JavaScript一個(gè)非常迫切的需求举瑰。
1.3 沒(méi)有模塊化的JavaScript
1.3.1 技術(shù)方案
演變過(guò)程:
-
全局函數(shù)
- ”污染”了全局變量,無(wú)法保證不與其它模塊發(fā)生變量名沖突
- 沒(méi)有模塊的劃分蔬螟,只能人為的認(rèn)為它們屬于一個(gè)模塊此迅,但是程序并不能區(qū)分哪些函數(shù)是同一個(gè)模塊
-
將函數(shù)封裝到對(duì)象命名空間下
- 從代碼級(jí)別可以明顯的區(qū)分出哪些函數(shù)屬于同一個(gè)模塊
- 從某種程度上解決了變量命名沖突的問(wèn)題,但是并不能從根本上解決命名沖突
- 會(huì)暴露所有的模塊成員旧巾,內(nèi)部狀態(tài)可以被外部改寫(xiě)耸序,不安全
- 命名空間越來(lái)越長(zhǎng)
-
立即函數(shù)調(diào)用表達(dá)式(IIFE,Immediately Invoked Function Expression)
將模塊封裝為立即執(zhí)行函數(shù)形式鲁猩,將公有方法坎怪,通過(guò)在函數(shù)內(nèi)部返回值的形式向外暴露
-
會(huì)有人強(qiáng)調(diào)職責(zé)單一性,不要與程序的其它部分直接交互廓握。比如當(dāng)使用到第三方依賴時(shí)搅窿,通過(guò)向匿名函數(shù)注入依賴項(xiàng)的形式,來(lái)保證模塊的獨(dú)立性隙券,還使模塊之間的依賴關(guān)系變得明顯
var calculator=(function(){ var add=function(v1,v2){ return v1+v2; } return { add:add } })() var calculator=(function(cal,$){ cal.add2=function(){ var v1=$('#v1').val(); var v2= $('#v2').val(); return (v1-0)+(v2-0); } return cal; })(window.calculator||{},window.$) //在這告訴我要jquery //依賴注入 //很牽強(qiáng)的解決文件依賴問(wèn)題的方法
IIFE也是有很大缺陷的男应,見(jiàn)下方代碼舉例
1.3.2 問(wèn)題舉例
我們先來(lái)簡(jiǎn)單體會(huì)一下沒(méi)有模塊化代碼的問(wèn)題。
我們知道娱仔,對(duì)于一個(gè)大型的前端項(xiàng)目沐飘,通常是多人開(kāi)發(fā)的(即使一個(gè)人開(kāi)發(fā),也會(huì)將代碼劃分到多個(gè)文件夾中):
- 我們假設(shè)有兩個(gè)人:小明和小麗同時(shí)在開(kāi)發(fā)一個(gè)項(xiàng)目拟枚,并且會(huì)將自己的JavaScript代碼放在一個(gè)單獨(dú)的js文件中薪铜。
// 小明開(kāi)發(fā)了aaa.js文件,代碼如下(當(dāng)然真實(shí)代碼會(huì)復(fù)雜的多):
var flag = true;
if (flag) {
console.log("aaa的flag為true")
}
// 小麗開(kāi)發(fā)了bbb.js文件恩溅,代碼如下:
var flag = false;
if (!flag) {
console.log("bbb使用了flag為false");
}
很明顯出現(xiàn)了一個(gè)問(wèn)題:
- 大家都喜歡使用flag來(lái)存儲(chǔ)一個(gè)boolean類型的值隔箍;
- 但是一個(gè)人賦值了true,一個(gè)人賦值了false脚乡;
- 如果之后都不再使用蜒滩,那么也沒(méi)有關(guān)系滨达;
但是,小明又開(kāi)發(fā)了ccc.js文件:
if (flag) {
console.log("使用了aaa的flag");
}
問(wèn)題來(lái)了:小明發(fā)現(xiàn)ccc中的flag值不對(duì)
- 對(duì)于聰明的你俯艰,當(dāng)然一眼就看出來(lái)捡遍,是小麗將flag賦值為了false;
- 但是如果每個(gè)文件都有上千甚至更多的代碼竹握,而且有上百個(gè)文件画株,你可以一眼看出來(lái)flag在哪個(gè)地方被修改了嗎?
備注:引用路徑如下:
<script src="./aaa.js"></script>
<script src="./bbb.js"></script>
<script src="./ccc.js"></script>
所以啦辐,沒(méi)有模塊化對(duì)于一個(gè)大型項(xiàng)目來(lái)說(shuō)是災(zāi)難性的谓传。
1.3.3 IIFE的缺陷
使用IIFE解決上面的問(wèn)題:
// aaa.js
const moduleA = (function () {
var flag = true;
if (flag) {
console.log("aaa的flag為true")
}
return { flag: flag }
})();
// bbb.js
const moduleB = (function () {
var flag = false;
if (!flag) {
console.log("bbb使用了flag為false");
}
})();
// ccc.js
const moduleC = (function() {
const flag = moduleA.flag;
if (flag) {
console.log("使用了aaa的flag");
}
})();
命名沖突的問(wèn)題,有沒(méi)有解決呢芹关?解決了续挟。
但是,我們其實(shí)帶來(lái)了新的問(wèn)題:
- 第一侥衬,我必須記得每一個(gè)模塊中返回對(duì)象的命名诗祸,才能在其他模塊使用過(guò)程中正確的使用;
- 第二轴总,代碼寫(xiě)起來(lái)混亂不堪直颅,每個(gè)文件中的代碼都需要包裹在一個(gè)匿名函數(shù)中來(lái)編寫(xiě);
- 第三肘习,在沒(méi)有合適的規(guī)范情況下际乘,每個(gè)人、每個(gè)公司都可能會(huì)任意命名漂佩、甚至出現(xiàn)模塊名稱相同的情況;
所以罪塔,我們會(huì)發(fā)現(xiàn)投蝉,雖然實(shí)現(xiàn)了模塊化,但是我們的實(shí)現(xiàn)過(guò)于簡(jiǎn)單征堪,并且是沒(méi)有規(guī)范的瘩缆。
- 我們需要制定一定的規(guī)范來(lái)約束每個(gè)人都按照這個(gè)規(guī)范去編寫(xiě)模塊化的代碼;
- 這個(gè)規(guī)范中應(yīng)該包括核心功能:模塊本身可以導(dǎo)出暴露的屬性佃蚜,模塊又可以導(dǎo)入自己需要的屬性庸娱;
1.4 JavaScript中模塊化方案
歷史上,JavaScript 一直沒(méi)有模塊(module)體系谐算,無(wú)法將一個(gè)大程序拆分成互相依賴的小文件熟尉,再用簡(jiǎn)單的方法拼裝起來(lái)。其他語(yǔ)言都有這項(xiàng)功能洲脂,比如 Ruby 的require
斤儿、Python 的import
,甚至就連 CSS 都有@import
。直到ES6(2015)才推出了自己的模塊化方案往果,在此之前疆液,社區(qū)制定了一些模塊加載方案,最主要的有:
先有規(guī)范陕贮,后有實(shí)現(xiàn):
- 服務(wù)器端規(guī)范 CommonJS => NodeJS堕油、 Browserify
- 瀏覽器端規(guī)范 AMD => RequireJS
- 瀏覽器端規(guī)范 CMD => SeaJS
二、CommonJS規(guī)范
2.1 CommonJS和Node
我們需要知道CommonJS是一個(gè)規(guī)范肮之,最初提出來(lái)是在瀏覽器意外的地方使用掉缺,并且當(dāng)時(shí)被命名為ServerJS,后來(lái)為了體現(xiàn)它的廣泛性局骤,修改為CommonJS攀圈,平時(shí)我們也會(huì)簡(jiǎn)稱為CJS。
- Node是CommonJS在服務(wù)器端一個(gè)具有代表性的實(shí)現(xiàn)峦甩;
- Browserify是CommonJS在瀏覽器中的一種實(shí)現(xiàn)赘来;
- webpack打包工具具備對(duì)CommonJS的支持和轉(zhuǎn)換(后面會(huì)講到);
所以凯傲,Node中對(duì)CommonJS進(jìn)行了支持和實(shí)現(xiàn)犬辰,讓我們?cè)陂_(kāi)發(fā)node的過(guò)程中可以方便的進(jìn)行模塊化開(kāi)發(fā):
2.2 Node模塊化語(yǔ)法
2.2.1 模塊
// bar.js
const name = 'coderwhy';
const age = 18;
function sayHello(name) { console.log("Hello " + name); }
// main.js
console.log(name, age);
sayHello('kobe');
/*
上面的代碼會(huì)報(bào)錯(cuò):
- 那么,就意味著別的模塊main中不能隨便訪問(wèn)另外一個(gè)模塊bar中的內(nèi)容冰单;
- bar需要 導(dǎo)出 自己想要暴露的變量幌缝、函數(shù)、對(duì)象等诫欠;main從bar中 導(dǎo)入 自己想要使用的變量涵卵、函數(shù)、對(duì)象等數(shù)據(jù)之后荒叼,才能使用轿偎;
*/
在node中每一個(gè)文件都是一個(gè)獨(dú)立的模塊,有自己的作用域被廓。在一個(gè)模塊內(nèi)變量坏晦、函數(shù)、對(duì)象都屬于這個(gè)模塊嫁乘,對(duì)外是封閉的昆婿。
為了實(shí)現(xiàn)模塊的導(dǎo)出,Node中使用的是Module的類(提供了一個(gè)Module構(gòu)造函數(shù))蜓斧,每一個(gè)模塊都是Module的一個(gè)實(shí)例仓蛆,也就是module;
每個(gè)模塊(文件)中都包括CommonJS規(guī)范的核心變量:exports法精、module多律、require痴突;
-
module:是一個(gè)全局對(duì)象,代表當(dāng)前模塊狼荞。里面保存了模塊的信息路徑辽装、父子結(jié)構(gòu)信息、曝露出的對(duì)象信息相味。
module.id //帶有絕對(duì)路徑的模塊文件名 module.filename //模塊的文件名拾积,帶有絕對(duì)路徑 module.loaded //表示模塊是否已經(jīng)完成加載 module.parent //返回一個(gè)對(duì)象,表示調(diào)用該模塊的模塊丰涉。 module.children //返回一個(gè)數(shù)組拓巧,表示該模塊要用到的其他模塊瞧预。 module.exports //模塊對(duì)外輸出的值倔叼。需要打破模塊封裝性曝露的方法和屬性,都要掛載到module.exports上谷饿。其它文件加載該模塊,實(shí)際上就是讀取module.exports屬性 // 在 /Users/computer/Desktop/ccc/lib.js 文件中 console.log(module); Module { id: '.', path: '/Users/computer/Desktop/ccc', exports: { name: 'test' }, parent: null, filename: '/Users/computer/Desktop/ccc/main.js', loaded: false, children: [ Module {...} ], paths: [ //查找路徑 '/Users/computer/Desktop/ccc/node_modules', '/Users/computer/Desktop/node_modules', '/Users/computer/node_modules', '/Users/node_modules', '/node_modules' ] }
exports是module.exports的引用投慈。一起負(fù)責(zé)對(duì)模塊中的內(nèi)容進(jìn)行導(dǎo)出承耿;
require函數(shù)可以幫助我們導(dǎo)入其他模塊(自定義模塊、系統(tǒng)模塊伪煤、第三方庫(kù)模塊)中的內(nèi)容加袋;
在Node.js中,模塊分為兩類:
-
第一類抱既,系統(tǒng)核心模塊(原生模塊)职烧,node自帶。用名稱直接可以加載防泵。
- fs(file system):與文件系統(tǒng)交互
- http:提供http服務(wù)器功能
- os:提供了與操作系統(tǒng)相關(guān)的實(shí)用方法和屬性
- path:處理文件路徑
- querystring:解析url查詢字符串
- url:解析url
- util:提供一系列實(shí)用小工具
- Buffer
- 等等很多蚀之,見(jiàn)官方文檔
- 核心模塊的源碼都在Node的lib子目錄中。為了提高運(yùn)行速度捷泞,它們安裝的時(shí)候都會(huì)被編譯成二進(jìn)制文件
-
第二類恬总,文件模塊,也稱自定義模塊肚邢。用路徑加載。
有一種特殊的文件模塊 — 包拭卿,被管理在
node_modules
文件夾中的包骡湖,也可以直接用名字加載。
2.2.2 exports導(dǎo)出
強(qiáng)調(diào):exports是一個(gè)對(duì)象峻厚,我們可以在這個(gè)對(duì)象中添加很多個(gè)屬性响蕴,添加的屬性會(huì)導(dǎo)出
// bar.js 導(dǎo)出內(nèi)容
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
// main.js 導(dǎo)入內(nèi)容
const bar = require('./bar');
上面這行代碼意味著什么呢?
- 意味著main中的bar變量等于exports對(duì)象惠桃;
main中的bar = bar中的exports
所以浦夷,我可以編寫(xiě)下面的代碼:
const bar = require('./bar');
const name = bar.name;
const age = bar.age;
const sayHello = bar.sayHello;
console.log(name);
console.log(age);
sayHello('kobe');
為了進(jìn)一步論證辖试,bar和exports是同一個(gè)對(duì)象:
- 所以,bar對(duì)象是exports對(duì)象的淺拷貝劈狐;
- 淺拷貝的本質(zhì)就是一種引用的賦值而已罐孝;
2.2.3 module.exports
但是Node中我們經(jīng)常導(dǎo)出東西的時(shí)候,又是通過(guò)module.exports導(dǎo)出的:
- module.exports和exports有什么關(guān)系或者區(qū)別呢肥缔?
我們追根溯源莲兢,通過(guò)維基百科中對(duì)CommonJS規(guī)范的解析:
- CommonJS中是沒(méi)有module.exports的概念的;
- 但是為了實(shí)現(xiàn)模塊的導(dǎo)出续膳,Node中使用的是Module的類(提供了一個(gè)Module構(gòu)造函數(shù))改艇,每一個(gè)模塊都是Module的一個(gè)實(shí)例,也就是module坟岔;
- module才是導(dǎo)出的真正實(shí)現(xiàn)者谒兄;
- 所以在Node中真正用于導(dǎo)出的其實(shí)根本不是exports,而是module.exports社付。只是為了實(shí)現(xiàn)CommonJS的規(guī)范承疲,也為了使用方便,Node為每個(gè)模塊提供了一個(gè)exports對(duì)象瘦穆,讓其對(duì)module.exports有一個(gè)引用而已纪隙。
- 相當(dāng)于在每個(gè)模塊頭部,有這樣一行命令:
var exports = module.exports;
不能直接給exports扛或、module.exports賦值绵咱,這樣等于切斷了exports和module.exports的聯(lián)系。最終輸出的結(jié)果只會(huì)是module.exports的值熙兔。比如代碼這樣修改了:
2.2.4 require
1. require的加載原理
前面已經(jīng)說(shuō)過(guò)悲伶,CommonJS 的一個(gè)模塊,就是一個(gè)腳本文件住涉。
CommonJS是同步加載麸锉。模塊加載的順序,按照其在代碼中出現(xiàn)的順序
require
命令第一次加載模塊時(shí)舆声,會(huì)執(zhí)行整個(gè)模塊(腳本文件)中的js代碼花沉,返回該模塊的module.exports接口數(shù)據(jù)。會(huì)在內(nèi)存生成一個(gè)該模塊對(duì)應(yīng)的module對(duì)象媳握。
// aaa.js
const name = 'coderwhy';
console.log("Hello aaa");
setTimeout(() => {
console.log("setTimeout");
}, 1000);
// main.js
const aaa = require('./aaa'); // aaa.js中的代碼在引入時(shí)會(huì)被運(yùn)行一次
生成的對(duì)象:
{
id: '...', // 模塊名
exports: { ... }, // 模塊輸出的各個(gè)接口
loaded: true, // 是一個(gè)布爾值碱屁,為false表示還沒(méi)有加載,為true表示已經(jīng)加載完畢蛾找。這是保證每個(gè)模塊只加載娩脾、運(yùn)行一次的關(guān)鍵。
...
}
- 以后需要用到這個(gè)模塊的時(shí)候打毛,就會(huì)到
exports
屬性上面取值柿赊。 - 模塊被多次引入時(shí)(多次執(zhí)行
require
命令)俩功,CommonJS 模塊只會(huì)在第一次加載時(shí)運(yùn)行一次,以后再加載碰声,會(huì)去緩存中取出第一次加載時(shí)生成的module對(duì)象并返回module.exports诡蜓。除非手動(dòng)清除系統(tǒng)緩存。
// main.js
const aaa = require('./aaa');
const bbb = require('./bbb');
// aaa.js
const ccc = require("./ccc");
// bbb.js
const ccc = require("./ccc");
// ccc.js
console.log('ccc被加載'); // ccc中的代碼只會(huì)運(yùn)行一次奥邮。
2. require的查找規(guī)則
我們現(xiàn)在已經(jīng)知道万牺,require是一個(gè)函數(shù),可以幫助我們引入一個(gè)文件(模塊)中導(dǎo)入的對(duì)象洽腺。
那么脚粟,require的查找規(guī)則是怎么樣的呢?官方文檔
這里我總結(jié)比較常見(jiàn)的查找規(guī)則: 導(dǎo)入格式如下:require(X)
-
情況一:X是一個(gè)核心模塊蘸朋,比如path核无、http。直接返回核心模塊藕坯,并且停止查找
- 加載核心模塊团南。傳入名字,不需要傳入路徑炼彪。因?yàn)镹ode.js已經(jīng)將核心模塊的文件代碼編譯到了二進(jìn)制的可執(zhí)行文件中了吐根。在加載的過(guò)程中,原生的核心模塊的優(yōu)先級(jí)是是最高的辐马。
-
情況二:X是以
./
或../
或/
(根目錄)開(kāi)頭的- 在Linux或者M(jìn)Ac的操作系統(tǒng)中拷橘,/表示系統(tǒng)的根路徑。在Windows中喜爷,/表示當(dāng)前文件模塊所屬的根磁盤(pán)路徑
- 第一步:將X當(dāng)做一個(gè)文件在對(duì)應(yīng)的目錄下查找冗疮;
- 如果有后綴名,按照后綴名的格式查找對(duì)應(yīng)的文件
- 如果沒(méi)有后綴名檩帐,會(huì)按照如下順序:
- 直接查找文件X
- 查找X.js文件:當(dāng)做JavaScript腳本文件解析
- 查找X.json文件:以JSON格式解析术幔。
- 如果是加載json文件模塊,最好加上后綴.json湃密,能稍微的提高一點(diǎn)加載的速度诅挑。
- json文件Node.js也是通過(guò)fs讀文件的形式讀取出來(lái)的,然后通過(guò)JSON.parse()轉(zhuǎn)換成一個(gè)對(duì)象
- 查找X.node文件:以編譯后的二進(jìn)制文件解析泛源。.node文件通常是c/c++寫(xiě)的一些擴(kuò)展模塊
- 第二步:沒(méi)有找到對(duì)應(yīng)的文件揍障,將X作為一個(gè)目錄。查找目錄下面的index文件
- 查找X/index.js文件
- 查找X/index.json文件
- 查找X/index.node文件
- 如果沒(méi)有找到俩由,那么報(bào)錯(cuò):
not found
-
情況三:直接是一個(gè)X(沒(méi)有路徑),并且X不是一個(gè)核心模塊
比如在
/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/05_javascript-module/02_commonjs/main.js
中編寫(xiě)require('why')
-
查找順序:從當(dāng)前 package 的 node_modules 里面找癌蚁,找不到就到當(dāng)前 package 目錄上層 node_modules 里面取... 一直找到全局 node_modules 目錄幻梯。
-
這樣找到的往往是文件夾兜畸,所以接下來(lái)就是處理一個(gè)文件目錄作為 Node 模塊的情況。如果文件目錄下有 package.json碘梢,就根據(jù)它的 main 字段找到 js 文件咬摇。如果沒(méi)有 package.json,那就默認(rèn)取文件夾下的 index.js煞躬。
由于 webpack browsersify 等模塊打包工具是兼容 node 的模塊系統(tǒng)的肛鹏,自然也會(huì)進(jìn)行同樣的處理流程。不同的是恩沛,它們支持更靈活的配置在扰。比如在 webpack 里面,可以通過(guò) alias 和 external 字段配置雷客,實(shí)現(xiàn)對(duì)默認(rèn) import 邏輯的自定義芒珠。
如果上面的路徑中都沒(méi)有找到,那么報(bào)錯(cuò):
not found
流程圖:
Node.js會(huì)通過(guò)同步阻塞的方式看這個(gè)路徑是否存在搅裙。依次嘗試皱卓,直到找到為止,如果找不到部逮,報(bào)錯(cuò)
優(yōu)先從緩存加載:common.js規(guī)范:載后娜汁,再次加載時(shí),去緩存中取module.exports 參考文獻(xiàn)
3. require的加載順序
如果有多個(gè)模塊的引入兄朋,那么加載順序是什么掐禁?
如果出現(xiàn)下面模塊的引用關(guān)系,那么加載順序是什么呢蜈漓?
- 這個(gè)其實(shí)是一種數(shù)據(jù)結(jié)構(gòu):圖結(jié)構(gòu)穆桂;
- 圖結(jié)構(gòu)在遍歷的過(guò)程中,有深度優(yōu)先搜索(DFS, depth first search)和廣度優(yōu)先搜索(BFS, breadth first search)融虽;
- Node采用的是深度優(yōu)先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
多個(gè)模塊的引入關(guān)系:
2.3 Node的源碼解析
Module類
Module.prototype.require函數(shù)
Module._load函數(shù)
三享完、ES6 Module
4.1 認(rèn)識(shí)ES6 Module
4.1.1 ES6 Module的優(yōu)勢(shì)
ES6 在語(yǔ)言標(biāo)準(zhǔn)的層面上,實(shí)現(xiàn)了模塊功能有额,而且實(shí)現(xiàn)得相當(dāng)簡(jiǎn)單般又,完全可以取代 CommonJS 和 AMD 規(guī)范,成為瀏覽器和服務(wù)器通用的模塊解決方案巍佑。
ES6 模塊的設(shè)計(jì)思想是盡量的靜態(tài)化茴迁,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量萤衰。CommonJS 和 AMD 模塊堕义,都只能在運(yùn)行時(shí)確定這些東西,導(dǎo)致完全沒(méi)辦法在編譯時(shí)做“靜態(tài)優(yōu)化”脆栋。
由于 ES6 模塊是編譯時(shí)加載:
可以在編譯時(shí)就完成模塊加載倦卖,效率要比 CommonJS 模塊的加載方式高
使得靜態(tài)分析成為可能洒擦。有了它,就能進(jìn)一步拓寬 JavaScript 的語(yǔ)法怕膛,比如引入宏(macro)和類型檢驗(yàn)(type system)這些只能靠靜態(tài)分析實(shí)現(xiàn)的功能熟嫩。
除了靜態(tài)加載帶來(lái)的各種好處,ES6 模塊還有以下好處褐捻。
- 不再需要
UMD
模塊格式了掸茅,將來(lái)服務(wù)器和瀏覽器都會(huì)支持 ES6 模塊格式。目前柠逞,通過(guò)各種工具庫(kù)昧狮,其實(shí)已經(jīng)做到了這一點(diǎn)。 - 將來(lái)瀏覽器的新 API 就能用模塊格式提供边苹,不再必須做成全局變量或者
navigator
對(duì)象的屬性陵且。 - 不再需要對(duì)象作為命名空間(比如
Math
對(duì)象),未來(lái)這些功能可以通過(guò)模塊提供个束。
4.1.2 自動(dòng)啟動(dòng)嚴(yán)格模式
ES6 的模塊自動(dòng)采用嚴(yán)格模式慕购,不管你有沒(méi)有在模塊頭部加上"use strict";
。
- 其中茬底,尤其需要注意
this
的限制沪悲。<font color=red>ES6 模塊之中影所,頂層的this
指向undefined
西壮,即不應(yīng)該在頂層代碼使用this
</font>吉捶。 - 參考鏈接:
4.1.3 瀏覽器中加載ES6 Module
1. 加載普通js文件
HTML 網(wǎng)頁(yè)中移斩,瀏覽器通過(guò)<script>
標(biāo)簽加載 JavaScript 腳本。
<!-- 頁(yè)面內(nèi)嵌的腳本 -->
<script type="application/javascript"> // code </script>
<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js"> //code... </script>
上面代碼中藻糖,由于瀏覽器腳本的默認(rèn)語(yǔ)言是 JavaScript江咳,因此
type="application/javascript"
可以省略峭火。-
默認(rèn)情況下爱致,瀏覽器是同步加載 JavaScript 腳本烤送,即渲染引擎遇到
<script>
標(biāo)簽就會(huì)停下來(lái),等到執(zhí)行完腳本糠悯,再繼續(xù)向下渲染帮坚。如果是外部腳本,還必須加入腳本下載的時(shí)間互艾。如果腳本體積很大试和,下載和執(zhí)行的時(shí)間就會(huì)很長(zhǎng),因此造成瀏覽器堵塞纫普,用戶會(huì)感覺(jué)到瀏覽器“卡死”了阅悍,沒(méi)有任何響應(yīng)。這顯然是很不好的體驗(yàn),所以瀏覽器允許腳本異步加載溉箕。
下面就是兩種異步加載的語(yǔ)法晦墙。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代碼中,<script>
標(biāo)簽打開(kāi)defer
或async
屬性肴茄,腳本就會(huì)異步加載。渲染引擎遇到這一行命令但指,就會(huì)開(kāi)始下載外部腳本寡痰,但不會(huì)等它下載和執(zhí)行,而是直接執(zhí)行后面的命令棋凳。
defer
與async
的區(qū)別是:
-
defer
要等到整個(gè)頁(yè)面在內(nèi)存中正常渲染結(jié)束(DOM 結(jié)構(gòu)完全生成拦坠,以及其他腳本執(zhí)行完成),才會(huì)執(zhí)行剩岳; -
async
一旦下載完贞滨,渲染引擎就會(huì)中斷渲染,執(zhí)行這個(gè)腳本以后拍棕,再繼續(xù)渲染晓铆。 - 一句話,
defer
是“渲染完再執(zhí)行”绰播,async
是“下載完就執(zhí)行”骄噪。 - 另外,如果有多個(gè)
defer
腳本蠢箩,會(huì)按照它們?cè)陧?yè)面出現(xiàn)的順序加載链蕊,而多個(gè)async
腳本是不能保證加載順序的。
2. 加載ES6 Module
瀏覽器內(nèi)嵌谬泌、外鏈 ES6 模塊代碼滔韵,也使用<script>
標(biāo)簽,但是都要加入type="module"
屬性掌实。
type
屬性設(shè)為module
陪蜻,所以瀏覽器知道這是一個(gè) ES6 模塊。瀏覽器對(duì)于帶有type="module"
的<script>
潮峦,都是異步加載囱皿,不會(huì)造成堵塞瀏覽器,即等到整個(gè)頁(yè)面渲染完忱嘹,再執(zhí)行模塊腳本嘱腥,等同于打開(kāi)了<script>
標(biāo)簽的defer
屬性。
<script type="module" src="./foo.js"></script>
<!-- 等同于下面代碼拘悦。如果網(wǎng)頁(yè)有多個(gè) <script type="module">齿兔,它們會(huì)按照在頁(yè)面出現(xiàn)的順序依次執(zhí)行。 -->
<script type="module" src="./foo.js" defer></script>
<!--
<script>標(biāo)簽的async屬性也可以打開(kāi):
這時(shí)只要加載完成,渲染引擎就會(huì)中斷渲染立即執(zhí)行分苇。執(zhí)行完成后添诉,再恢復(fù)渲染。
同樣的:一旦使用了此屬性医寿,<script type="module">就不會(huì)按照在頁(yè)面出現(xiàn)的順序執(zhí)行栏赴,而是只要該模塊加載完成,就執(zhí)行該模塊靖秩。
-->
<script type="module" src="./foo.js" async></script>
ES6 模塊也允許內(nèi)嵌在網(wǎng)頁(yè)中须眷,語(yǔ)法行為與加載外部腳本完全一致。
<script type="module">
import utils from "./utils.js";
// other code
</script>
對(duì)于外部的模塊腳本(上例是foo.js
)沟突,有幾點(diǎn)需要注意花颗。
- 代碼是在模塊作用域之中運(yùn)行,而不是在全局作用域運(yùn)行惠拭。模塊內(nèi)部的頂層變量扩劝,外部不可見(jiàn)。
- 模塊腳本自動(dòng)采用嚴(yán)格模式职辅,不管有沒(méi)有聲明
use strict
棒呛。 - 模塊之中,可以使用
import
命令加載其他模塊(.js
后綴不可省略罐农,需要提供絕對(duì) URL 或相對(duì) URL)条霜,也可以使用export
命令輸出對(duì)外接口。 - 模塊之中涵亏,頂層的
this
關(guān)鍵字返回undefined
宰睡,而不是指向window
。也就是說(shuō)气筋,在模塊頂層使用this
關(guān)鍵字拆内,是無(wú)意義的。 - 同一個(gè)模塊如果加載多次宠默,將只執(zhí)行一次麸恍。
下面是一個(gè)示例模塊。
import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
利用頂層的this
等于undefined
這個(gè)語(yǔ)法點(diǎn)搀矫,可以偵測(cè)當(dāng)前代碼是否在 ES6 模塊之中抹沪。
const isNotModuleScript = this !== undefined;
4.1.4 本地瀏覽的報(bào)錯(cuò)
代碼結(jié)構(gòu)如下(個(gè)人習(xí)慣)
├── index.html
├── main.js
└── modules
└── foo.js
index.html中引入兩個(gè)js文件作為模塊:
<script src="./modules/foo.js" type="module"></script>
<script src="main.js" type="module"></script>
如果直接在瀏覽器中運(yùn)行代碼,會(huì)報(bào)如下錯(cuò)誤:
這個(gè)在MDN上面有給出解釋:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
- 你需要注意本地測(cè)試 — 如果你通過(guò)本地加載Html 文件 (比如一個(gè)
file://
路徑的文件), 你將會(huì)遇到 CORS 錯(cuò)誤瓤球,因?yàn)镴avascript 模塊安全性需要融欧。 - 你需要通過(guò)一個(gè)服務(wù)器來(lái)測(cè)試。
我這里使用的VSCode卦羡,VSCode中有一個(gè)插件:Live Server
- 通過(guò)插件運(yùn)行噪馏,可以將我們的代碼運(yùn)行在一個(gè)本地服務(wù)中麦到;
4.2 ES6 Module的語(yǔ)法
模塊功能主要由兩個(gè)命令構(gòu)成:export
和import
:
-
export
命令用于規(guī)定模塊的對(duì)外接口 -
import
命令用于輸入其他模塊提供的功能。
4.2.1 模塊與CommonJS模塊的區(qū)別
1. 相同點(diǎn)
與CommonJS的相同點(diǎn):一個(gè)模塊就是一個(gè)獨(dú)立的文件欠肾。該文件內(nèi)部的所有變量瓶颠,外部無(wú)法獲取。如果你希望外部能夠讀取模塊內(nèi)部的某個(gè)變量刺桃,就必須使用export
關(guān)鍵字輸出該變量粹淋。
2. 導(dǎo)出的不同
CommonJS通過(guò)module.exports導(dǎo)出的是一個(gè)對(duì)象,是module.exports
屬性淺拷貝后導(dǎo)出:
該對(duì)象只有在腳本運(yùn)行完才會(huì)生成瑟慈。
導(dǎo)出的是一個(gè)對(duì)象意味著可以將這個(gè)對(duì)象的引用在導(dǎo)入模塊中賦值給其他變量廓啊;但是最終他們指向的都是同一個(gè)對(duì)象,那么一個(gè)變量修改了對(duì)象的屬性封豪,所有的地方都會(huì)被修改;
// 導(dǎo)出
var counter = 3;
var obj = {count: 3}
function incCounter() {
counter++;
obj.count++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
obj: obj
};
// 導(dǎo)入
var mod = require('./lib');
console.log(mod.counter, mod.obj.count); // 3 3
mod.incCounter();
console.log(mod.counter, mod.obj.count); // 3 4
ES Module通過(guò)export導(dǎo)出的不是對(duì)象炒瘟,是一個(gè)個(gè)導(dǎo)出變量/函數(shù)/類本身的引用:
說(shuō)法1:
- 它的對(duì)外接口只是一種靜態(tài)定義吹埠,在代碼靜態(tài)解析階段就會(huì)生成。JS 引擎對(duì)腳本靜態(tài)分析的時(shí)候疮装,遇到模塊加載命令
import
缘琅,就會(huì)生成一個(gè)只讀引用。等到腳本真正執(zhí)行時(shí)廓推,再根據(jù)這個(gè)只讀引用刷袍,到被加載的那個(gè)模塊里面去取值。 - 換句話說(shuō)樊展,ES6 的
import
有點(diǎn)像 Unix 系統(tǒng)的“符號(hào)連接”呻纹,原始值變了,import
加載的值也會(huì)跟著變专缠。(由于 ES6 輸入的模塊變量雷酪,只是一個(gè)“符號(hào)連接”,所以這個(gè)變量是只讀的涝婉,對(duì)它進(jìn)行重新賦值會(huì)報(bào)錯(cuò)) - 所以哥力,
import
命令叫做“連接” binding 其實(shí)更合適。
說(shuō)法2:
export在導(dǎo)出一個(gè)變量時(shí)墩弯,js引擎會(huì)解析這個(gè)語(yǔ)法吩跋,并且創(chuàng)建模塊環(huán)境記錄(module environment record);
模塊環(huán)境記錄會(huì)和變量進(jìn)行
綁定
(binding)渔工,并且這個(gè)綁定是實(shí)時(shí)的锌钮;而在導(dǎo)入的地方,我們是可以實(shí)時(shí)的獲取到綁定的最新值的涨缚;
export和import綁定的過(guò)程:
還是舉上面的例子轧粟。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
上面代碼說(shuō)明策治,ES6 模塊輸入的變量counter
是活的,完全反應(yīng)其所在模塊lib.js
內(nèi)部的變化兰吟。
3. 導(dǎo)入的不同
// CommonJS模塊
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代碼實(shí)質(zhì)會(huì)整體加載fs
模塊(即加載fs
的所有方法)通惫,生成一個(gè)對(duì)象(_fs
),然后再?gòu)倪@個(gè)對(duì)象上面讀取 3 個(gè)方法混蔼。
// ES6模塊
import { stat, exists, readFile } from 'fs';
上面代碼實(shí)質(zhì)只是從fs
模塊加載 3 個(gè)方法履腋,其他方法不加載。
4.2.2 export
export關(guān)鍵字將一個(gè)模塊中的變量惭嚣、函數(shù)遵湖、類等導(dǎo)出;
1. export <decl>
方式一:分別導(dǎo)出晚吞。在語(yǔ)句聲明的前面直接加上export關(guān)鍵字:
export const name = 'coderwhy';
export const age = 18;
export let message = "my name is why";
export function sayHello(name) {
console.log("Hello " + name);
}
// export需要指定對(duì)外暴露的接口延旧,所以不能直接輸出一個(gè)值
// export 40; //error
2. export {}
方式二:統(tǒng)一導(dǎo)出。將所有需要導(dǎo)出的標(biāo)識(shí)符槽地,放到export后面的 {}
中迁沫。它與上一種寫(xiě)法是等價(jià)的,但是應(yīng)該優(yōu)先考慮使用這種寫(xiě)法捌蚊。因?yàn)檫@樣就可以在腳本尾部集畅,一眼看清楚輸出了哪些數(shù)據(jù)。
- 注意:這里的
{}
里面不是ES6的對(duì)象字面量的增強(qiáng)寫(xiě)法缅糟,{}
也不是表示一個(gè)對(duì)象的挺智; - 所以:
export {name: name}
,是錯(cuò)誤的寫(xiě)法窗宦;
const name = 'coderwhy';
const age = 18;
function sayHello(name) {
console.log("Hello " + name);
}
export {
name,
age,
sayHello
}
3. export {<> as <>}
方式三:通常情況下赦颇,export
輸出的變量就是本來(lái)的名字,但是可以使用as
關(guān)鍵字在導(dǎo)出時(shí)給標(biāo)識(shí)符
起一個(gè)別名:export {<> as <>}
export {
name as fName,
age as fAge,
sayHello as fSayHello1,
sayHello as fSayHello2, // 重命名后迫摔,sayHello可以用不同的名字輸出兩次沐扳。
}
4. export導(dǎo)出的是標(biāo)識(shí)符的地址
export
語(yǔ)句輸出的接口,與其對(duì)應(yīng)的值是動(dòng)態(tài)綁定關(guān)系句占,即通過(guò)該接口沪摄,可以取到模塊內(nèi)部實(shí)時(shí)的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代碼輸出變量foo
纱烘,值為bar
杨拐,500 毫秒之后變成baz
。
這一點(diǎn)與 CommonJS 規(guī)范完全不同擂啥。CommonJS 模塊輸出的是值的緩存哄陶,不存在動(dòng)態(tài)更新。
5. export導(dǎo)出同一個(gè)實(shí)例
function C() {
this.sum = 0;
}
export let c = new C();
不同的模塊中哺壶,加載這個(gè)模塊屋吨,得到的都是同一個(gè)實(shí)例蜒谤。對(duì)c修改,其他模塊導(dǎo)入的數(shù)據(jù)也會(huì)改變
6. export書(shū)寫(xiě)位置
export
命令可以出現(xiàn)在模塊的任何位置至扰,只要處于模塊頂層就可以敲长。如果處于塊級(jí)作用域內(nèi)灵疮,就會(huì)報(bào)錯(cuò)叶组,import
命令也是如此糙俗。這是因?yàn)樘幱跅l件代碼塊之中,就沒(méi)法做靜態(tài)優(yōu)化了直秆,違背了 ES6 模塊的設(shè)計(jì)初衷濒募。
function foo() {
export default 'bar' // SyntaxError
}
foo()
7. export書(shū)寫(xiě)次數(shù)
一個(gè)模塊中:export <decl>
、export {}
圾结、export {<> as <>}
都是可以出現(xiàn)0-n
次的
4.2.3 import
import關(guān)鍵字負(fù)責(zé)從另外一個(gè)模塊中導(dǎo)入內(nèi)容瑰剃。
import
語(yǔ)句會(huì)執(zhí)行所加載的模塊。如果同一個(gè)模塊被加載多次筝野,那么模塊里的代碼只執(zhí)行一次培他。
導(dǎo)入內(nèi)容的方式也有多種:
1. import {} from ''
方式一:選擇導(dǎo)入。import {標(biāo)識(shí)符列表} from '模塊'
遗座;
注意:
- 大括號(hào)里面的變量名,必須與被導(dǎo)入模塊對(duì)外接口的名稱相同俊扳。
- 這里的
{}
也不是一個(gè)對(duì)象途蒋,里面只是存放導(dǎo)入的標(biāo)識(shí)符列表內(nèi)容;
import { name, age, sayHello } from './modules/foo.js';
console.log(name)
console.log(age);
sayHello("Kobe");
import { name } from './modules/foo.js';
import { age } from './modules/foo.js';
// 等同于
import { name, age } from './modules/foo.js';
上面代碼中馋记,雖然name
和age
在兩個(gè)語(yǔ)句中加載号坡,但是它們對(duì)應(yīng)的是同一個(gè)foo.js
模塊。也就是說(shuō)梯醒,import
語(yǔ)句是 Singleton 模式宽堆。
1. import ''
的含義
import
語(yǔ)句會(huì)執(zhí)行所加載的模塊,因此可以有下面的寫(xiě)法茸习。
import 'lodash';
上面代碼僅僅執(zhí)行lodash
模塊畜隶,但是不導(dǎo)入任何值。
同樣的号胚,如果多次重復(fù)執(zhí)行同一句import
語(yǔ)句籽慢,那么只會(huì)執(zhí)行一次,而不會(huì)執(zhí)行多次猫胁。
import 'lodash';
import 'lodash'; // 代碼加載了兩次`lodash`箱亿,但是只會(huì)執(zhí)行一次。
2. import {<> as <>} from ''
方式二:導(dǎo)入時(shí)給標(biāo)識(shí)符起別名: import {<> as <>} from ''
import { name as wName, age as wAge, sayHello as wSayHello } from './modules/foo.js';
3. import * as <> from ''
方式三:整體導(dǎo)入弃秆。將模塊功能放到一個(gè)模塊功能對(duì)象(a module object)上届惋,用*
指定: import * as <> from ''
import * as foo from './modules/foo.js';
console.log(foo.name);
console.log(foo.age);
foo.sayHello("Kobe");
// foo.n = "add"; // Type Error: object is not extensible
// foo.f = function () {};
注意髓帽,模塊整體加載所在的那個(gè)對(duì)象,應(yīng)該是可以靜態(tài)分析的脑豹,所以不允許運(yùn)行時(shí)改變郑藏。上面的寫(xiě)法是不允許的。
4. import導(dǎo)入為只讀
import { name } from './modules/foo.js';
name = "mod"; // Syntax Error : 'name' is read-only;
name
是只讀的晨缴。但是译秦,如果name
是一個(gè)對(duì)象,改寫(xiě)其屬性是允許的击碗,并且其他模塊也可以讀到改寫(xiě)后的值筑悴。不過(guò),這種寫(xiě)法很難查錯(cuò)稍途,建議凡是輸入的變量阁吝,都當(dāng)作完全只讀,不要輕易改變它的屬性械拍。
5. import from后的路徑
import
后面的from
指定模塊文件的位置突勇,可以是相對(duì)路徑,也可以是絕對(duì)路徑坷虑,<font color=red>后綴名不能省略</font>甲馋。
如果不帶有路徑,只是一個(gè)模塊名迄损,那么必須有配置文件定躏,告訴 JavaScript 引擎該模塊的位置。
import { myMethod } from 'util';
上面代碼中芹敌,util
是模塊文件名痊远,由于不帶有路徑,必須通過(guò)配置氏捞,告訴引擎怎么取到這個(gè)模塊碧聪。
6. import命令的提升
注意,import
命令具有提升效果液茎,會(huì)提升到整個(gè)模塊的頭部逞姿,首先執(zhí)行。
foo();
import { foo } from 'my_module';
上面的代碼不會(huì)報(bào)錯(cuò)捆等,因?yàn)?code>import的執(zhí)行早于foo
的調(diào)用哼凯。這種行為的本質(zhì)是,import
命令是編譯階段執(zhí)行的楚里,在代碼運(yùn)行之前断部。
目前階段,通過(guò) Babel 轉(zhuǎn)碼班缎,CommonJS 模塊的require
命令和 ES6 模塊的import
命令蝴光,可以寫(xiě)在同一個(gè)模塊里面她渴,但是最好不要這樣做。因?yàn)?code>import在靜態(tài)解析階段執(zhí)行蔑祟,所以它是一個(gè)模塊之中最早執(zhí)行的趁耗。下面的代碼可能不會(huì)得到預(yù)期結(jié)果。
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
7. import中不能使用表達(dá)式和變量
由于import
是靜態(tài)執(zhí)行疆虚,所以不能使用表達(dá)式和變量苛败,這些只有在運(yùn)行時(shí)才能得到結(jié)果的語(yǔ)法結(jié)構(gòu)。
// 報(bào)錯(cuò)
import { 'f' + 'oo' } from 'my_module';
// 報(bào)錯(cuò)
let module = 'my_module';
import { foo } from module;
// 報(bào)錯(cuò)
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
上面三種寫(xiě)法都會(huì)報(bào)錯(cuò)径簿,因?yàn)樗鼈冇玫搅吮磉_(dá)式罢屈、變量和if
結(jié)構(gòu)。在靜態(tài)分析階段篇亭,這些語(yǔ)法都是沒(méi)法得到值的缠捌。
4.2.4 export default
1. 概述
前面我們學(xué)習(xí)的導(dǎo)出功能都是有名字的導(dǎo)出(named exports):
- 在導(dǎo)出export時(shí)指定了名字;
- 在導(dǎo)入import時(shí)需要知道具體的名字译蒂;
還有一種導(dǎo)出叫做默認(rèn)導(dǎo)出(default export)
- 默認(rèn)導(dǎo)出export時(shí)可以不需要指定名字曼月;
- 在導(dǎo)入時(shí)不需要使用
{}
,并且可以自己來(lái)指定名字柔昼; - 它也方便我們和現(xiàn)有的CommonJS等規(guī)范相互操作哑芹;
2. 導(dǎo)出與導(dǎo)入格式
也是可以導(dǎo)出變量、函數(shù)捕透、類的绩衷。
// 導(dǎo)出格式1
export default function sub(num1, num2) {
return num1 - num2;
}
// 導(dǎo)出格式2:用在非匿名函數(shù)前
export default function() {}
// 導(dǎo)出格式3:用在函數(shù)變量前
function sub() { console.log('sub'); }
export default sub;
// 函數(shù)名`sub`,在模塊外部是無(wú)效的激率。加載的時(shí)候,視同匿名函數(shù)加載勿决。
// 導(dǎo)入格式1:常用及推薦
import sub from './modules/foo.js';
console.log(sub(20, 30));
// 導(dǎo)入格式2
import * as m from './modules/foo.js';
console.log(m.default.sub(20, 30));
// 導(dǎo)入格式3
import {default as m} from './modules/foo.js';
console.log(m.sub(20, 30));
3. export default的本質(zhì)
本質(zhì)上乒躺,export default
就是輸出一個(gè)叫做default
的變量或方法,然后系統(tǒng)允許你為它取任意名字低缩。所以嘉冒,下面的寫(xiě)法是有效的。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default}; // 等同于 export default add;
// app.js
import { default as foo } from 'modules'; // 等同于 import foo from 'modules';
正是因?yàn)?code>export default命令其實(shí)只是輸出一個(gè)叫做default
的變量咆繁,所以它后面不能跟變量聲明語(yǔ)句讳推。
// 正確
export var a = 1;
// 正確
var a = 1;
export default a; // 含義是將變量`a`的值賦給變量`default`。所以玩般,最后一種寫(xiě)法會(huì)報(bào)錯(cuò)银觅。
// 錯(cuò)誤
// export default var a = 1;
// 同樣地,因?yàn)閌export default`命令的本質(zhì)是將后面的值坏为,賦給`default`變量究驴,所以可以直接將一個(gè)值寫(xiě)在`export default`之后镊绪。
// 正確
export default 42;
// 報(bào)錯(cuò)
// export 42; // export后面得跟聲明,或者{標(biāo)識(shí)符}
4. export default與export
注意:在一個(gè)模塊中洒忧,export default是可以與export同時(shí)使用的:
- export default用于指定模塊的默認(rèn)輸出蝴韭。顯然,一個(gè)模塊只能有一個(gè)默認(rèn)輸出熙侍,因此
export default
命令只能使用一次榄鉴。 - export是沒(méi)有限制的。
export <decl>
蛉抓、export {}
庆尘、export {<> as <>}
都是可以出現(xiàn)0-n
次的
// 導(dǎo)出
export default function sub(num1, num2) {
return num1 - num2;
}
export var name = "module1";
// 導(dǎo)入 在一條`import`語(yǔ)句中,同時(shí)輸入默認(rèn)接口和其他接口
import m, {name} from './modules/foo.js'; //m.sub芝雪、name
import * as m from './modules/foo.js'; // m.default.sub减余、m.name
import {default as m, name} from './modules/foo.js'; // m.sub、name
4.2.5 export和import結(jié)合
// bar.js 導(dǎo)出一個(gè)sum函數(shù)
export const sum = function(num1, num2) {
return num1 + num2;
}
// foo.js做一個(gè)中轉(zhuǎn)
// main.js直接從foo中導(dǎo)入
import { sum } from './modules/foo.js';
console.log(sum(20, 30));
如果從一個(gè)模塊中導(dǎo)入的內(nèi)容惩系,我們希望再直接導(dǎo)出出去位岔,這個(gè)時(shí)候可以使用export和import的結(jié)合,寫(xiě)成一行堡牡。
// foo.js 導(dǎo)入抒抬,但是只是做一個(gè)中轉(zhuǎn)
export { sum } from './bar.js';
// 接口改名
export { sum as barSum } from './bar.js'; // 甚至在foo.js中導(dǎo)出時(shí),我們可以變化它的名字
// 整體導(dǎo)入和導(dǎo)出
export * from './bar.js';
// 相當(dāng)于實(shí)現(xiàn)了模塊之間的繼承晤柄。注意擦剑,`export *`命令會(huì)忽略后面模塊的`default`接口。
// 默認(rèn)接口
export { default } from 'foo';
// 具名接口改為默認(rèn)接口的寫(xiě)法如下:
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
// 默認(rèn)接口也可以改名為具名接口:
export { default as es6 } from './someModule';
// ES2020 之前芥颈,有一種`import`語(yǔ)句惠勒,沒(méi)有對(duì)應(yīng)的復(fù)合寫(xiě)法。[ES2020](https://github.com/tc39/proposal-export-ns-from)補(bǔ)上了這個(gè)寫(xiě)法爬坑。
export * as ns from "mod";
// 等同于
import * as ns from "mod";
export {ns};
// 需要注意的是纠屋,寫(xiě)成一行以后,`sum`實(shí)際上并沒(méi)有被導(dǎo)入當(dāng)前模塊盾计,只是相當(dāng)于對(duì)外轉(zhuǎn)發(fā)了這個(gè)接口售担,導(dǎo)致當(dāng)前模塊不能直接使用`sum`。
為什么要這樣做呢署辉?
- 在開(kāi)發(fā)和封裝一個(gè)功能庫(kù)時(shí)族铆,通常我們希望將暴露的所有接口放到一個(gè)文件中;
- 這樣方便指定統(tǒng)一的接口規(guī)范哭尝,也方便閱讀哥攘;
- 這個(gè)時(shí)候,我們就可以使用export和import結(jié)合使用;
4.2.6 import()
1. import()的背景
前面介紹過(guò)献丑,import
命令會(huì)被 JavaScript 引擎靜態(tài)分析末捣,先于模塊內(nèi)的其他語(yǔ)句執(zhí)行。所以创橄,import
和export
命令只能在模塊的頂層箩做,是不可以在其放到邏輯代碼中(比如在if
代碼塊之中,或在函數(shù)之中)的妥畏。下面的代碼會(huì)報(bào)錯(cuò):
if (true) {
import sub from './modules/foo.js';
}
引擎處理import
語(yǔ)句是在編譯時(shí)邦邦,這時(shí)不會(huì)去分析或執(zhí)行if
語(yǔ)句,所以import
語(yǔ)句放在if
代碼塊之中毫無(wú)意義醉蚁,因此會(huì)報(bào)句法錯(cuò)誤燃辖,而不是執(zhí)行時(shí)錯(cuò)誤。
這樣的設(shè)計(jì)网棍,固然有利于編譯器提高效率黔龟,但也導(dǎo)致無(wú)法在運(yùn)行時(shí)加載模塊。在語(yǔ)法上滥玷,條件加載就不可能實(shí)現(xiàn)氏身。如果import
命令要取代 Node 的require
方法,這就形成了一個(gè)障礙惑畴。因?yàn)?code>require是運(yùn)行時(shí)加載模塊蛋欣,import
命令無(wú)法取代require
的動(dòng)態(tài)加載功能。
const path = './' + fileName;
const myModual = require(path);
// 上面的語(yǔ)句就是動(dòng)態(tài)加載如贷,`require`到底加載哪一個(gè)模塊陷虎,只有運(yùn)行時(shí)才知道。`import`命令做不到這一點(diǎn)杠袱。
ES2020提案 引入import()
函數(shù)尚猿,支持動(dòng)態(tài)加載模塊。
import(specifier)
上面代碼中楣富,import
函數(shù)的參數(shù)specifier
凿掂,指定所要加載的模塊的位置。import
命令能夠接受什么參數(shù)菩彬,import()
函數(shù)就能接受什么參數(shù),兩者區(qū)別主要是后者為動(dòng)態(tài)加載潮梯。
2. 語(yǔ)法
import()
返回一個(gè) Promise 對(duì)象骗灶。下面是一個(gè)例子。
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => { // 加載模塊成功以后秉馏,這個(gè)模塊會(huì)作為一個(gè)對(duì)象耙旦,當(dāng)作`then`方法的參數(shù).
//.then({export1, export2} => { // 可以使用對(duì)象解構(gòu)賦值的語(yǔ)法,獲取輸出接口萝究。
//.then({default: theDefault} => { // 如果是default,那么需要解構(gòu)重命名
module.loadPageInto(main); // module.default來(lái)使用默認(rèn)導(dǎo)出
})
.catch(err => {
main.textContent = err.message;
});
// 如果想同時(shí)加載多個(gè)模塊,可以采用下面的寫(xiě)法狰晚。
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
// 返回值是Promise對(duì)象芜抒,所以也可以用在async函數(shù)中
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
import()
函數(shù)可以用在任何地方,不僅僅是模塊渊跋,非模塊的腳本也可以使用。它是運(yùn)行時(shí)執(zhí)行,也就是說(shuō)侨舆,什么時(shí)候運(yùn)行到這一句,就會(huì)加載指定的模塊绢陌。另外挨下,import()
函數(shù)與所加載的模塊沒(méi)有靜態(tài)連接關(guān)系,這點(diǎn)也是與import
語(yǔ)句不相同脐湾。import()
類似于 Node 的require
方法臭笆,區(qū)別主要是前者是異步加載,后者是同步加載秤掌。
3. 適用場(chǎng)合
-
按需加載愁铺。
import()
可以在需要的時(shí)候,再加載某個(gè)模塊机杜。比如放在click
事件的監(jiān)聽(tīng)函數(shù)之中帜讲,只有用戶點(diǎn)擊了按鈕,才會(huì)加載這個(gè)模塊椒拗。 -
條件加載
import()
可以放在if
代碼塊似将,根據(jù)不同的情況,加載不同的模塊蚀苛。 -
動(dòng)態(tài)的模塊路徑
import()
允許模塊路徑動(dòng)態(tài)生成在验。import(f()).then(...); // 根據(jù)函數(shù)`f`的返回結(jié)果,加載不同的模塊堵未。
4.2.7 應(yīng)用: 公共頭文件
介紹const
命令的時(shí)候說(shuō)過(guò)腋舌,const
聲明的常量只在當(dāng)前代碼塊有效。如果想設(shè)置跨模塊的常量(即跨多個(gè)文件)渗蟹,或者說(shuō)一個(gè)值要被多個(gè)模塊共享块饺,可以采用下面的寫(xiě)法。
// constants.js 模塊
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模塊
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模塊
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
如果要使用的常量非常多雌芽,可以建一個(gè)專門(mén)的constants
目錄授艰,將各種常量寫(xiě)在不同的文件里面,保存在該目錄下世落。
// constants/db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};
// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
然后淮腾,將這些文件輸出的常量,合并在index.js
里面。
// constants/index.js
export {db} from './db';
export {users} from './users';
使用的時(shí)候谷朝,直接加載index.js
就可以了洲押。
// script.js
import {db, users} from './constants/index.js';
4.2.8 與CommonJS模塊化的差異
- CommonJS 模塊輸出的是一個(gè)值的拷貝(module.exports的淺拷貝),ES6 模塊輸出的是值的引用圆凰。
- CommonJS 模塊是運(yùn)行時(shí)加載杈帐,ES6 模塊是編譯(解析)時(shí)加載。
運(yùn)行時(shí)加載意味著是js引擎在 執(zhí)行js代碼的過(guò)程中 加載模塊送朱;所以require可以與變量娘荡、表達(dá)式等運(yùn)行時(shí)代碼結(jié)合使用
-
編譯時(shí)(解析)時(shí)加載,意味著import不能和運(yùn)行時(shí)相關(guān)的內(nèi)容放在一起使用:
- 比如from后面的路徑需要?jiǎng)討B(tài)獲仁徽印炮沐;
- 比如不能將import放到if等語(yǔ)句的代碼塊中;
- 所以我們有時(shí)候也稱ES Module是靜態(tài)解析的回怜,而不是動(dòng)態(tài)或者運(yùn)行時(shí)解析的大年;
- CommonJS 模塊的
require()
是同步加載模塊,ES6 模塊的import
命令是異步加載玉雾,有一個(gè)獨(dú)立的模塊依賴的解析階段翔试。- 同步的就意味著一個(gè)文件沒(méi)有加載結(jié)束之前,后面的代碼都不會(huì)執(zhí)行复旬;
- 異步的意味著:不會(huì)阻塞主線程繼續(xù)執(zhí)行垦缅;
- JS引擎在遇到
import
時(shí)會(huì)去獲取這個(gè)js文件的過(guò)程是異步的 - 設(shè)置了
type=module
的script標(biāo)簽,相當(dāng)于加上了async
屬性驹碍; - 如果我們后面有普通的script標(biāo)簽以及對(duì)應(yīng)的代碼壁涎,那么ES Module對(duì)應(yīng)的js文件和代碼不會(huì)阻塞它們的執(zhí)行;
- JS引擎在遇到
CommonJS代碼:
console.log("main代碼執(zhí)行");
const flag = true;
if (flag) {
// 同步加載foo文件志秃,并且執(zhí)行一次內(nèi)部的代碼
const foo = require('./foo');
console.log("if語(yǔ)句繼續(xù)執(zhí)行");
}
ES Module代碼:
<script src="main.js" type="module"></script>
<!-- 這個(gè)js文件的代碼不會(huì)被阻塞執(zhí)行 -->
<script src="index.js"></script>
四怔球、CommonJS模塊與ES6模塊的混編
4.3 CommonJS模塊加載ES6模塊
通常情況下,CommonJS不能加載ES Module
- 因?yàn)镃ommonJS是同步加載的浮还,但是ES Module必須經(jīng)過(guò)靜態(tài)分析等竟坛,無(wú)法在這個(gè)時(shí)候執(zhí)行JavaScript代碼;
- 但是這個(gè)并非絕對(duì)的钧舌,某些平臺(tái)在實(shí)現(xiàn)的時(shí)候可以對(duì)代碼進(jìn)行針對(duì)性的解析担汤,也可能會(huì)支持;
可以使用import()
這個(gè)方法加載
(async () => {
await import('./my-app.mjs');
})();
上面代碼可以在 CommonJS 模塊中運(yùn)行洼冻。
require()
不支持 ES6 模塊的一個(gè)原因是崭歧,它是同步加載,而 ES6 模塊內(nèi)部可以使用頂層await
命令碘赖,導(dǎo)致無(wú)法被同步加載驾荣。
4.2 ES6模塊加載CommonJS模塊
多數(shù)情況下,ES Module可以加載CommonJS普泡,但是只能整體加載播掷,不能只加載單一的輸出項(xiàng)。
- ES Module在加載CommonJS時(shí)撼班,會(huì)將其module.exports導(dǎo)出的內(nèi)容作為default導(dǎo)出方式來(lái)使用歧匈;
- 這個(gè)依然需要看具體的實(shí)現(xiàn),比如webpack中是支持的砰嘁、Node最新的Current(v14.13.1)版本也是支持的件炉;
// foo.js
const address = 'foo的address';
module.exports = {
address
}
// main.js
import foo from './modules/foo.js';
console.log(foo.address);
還有一種變通的加載方法,就是使用 Node.js 內(nèi)置的module.createRequire()
方法矮湘。
// cjs.cjs
module.exports = 'cjs';
// esm.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjs = require('./cjs.cjs');
cjs === 'cjs'; // true
上面代碼中斟冕,ES6 模塊通過(guò)module.createRequire()
方法可以加載 CommonJS 模塊。但是缅阳,這種寫(xiě)法等于將 ES6 和 CommonJS 混在一起了磕蛇,所以不建議使用。
4.3 使模塊同時(shí)支持兩種模塊化導(dǎo)入
一個(gè)模塊同時(shí)要支持 CommonJS 和 ES6 兩種格式十办,也很容易秀撇。
如果原始模塊是 ES6 格式,那么需要給出一個(gè)整體輸出接口向族,比如export default obj
呵燕,使得 CommonJS 可以用import()
進(jìn)行加載。
如果原始模塊是 CommonJS 格式件相,那么可以加一個(gè)包裝層再扭。
import cjsModule from '../index.js';
export const foo = cjsModule.foo;
上面代碼先整體輸入 CommonJS 模塊,然后再根據(jù)需要輸出具名接口适肠。
你可以把這個(gè)文件的后綴名改為.mjs
霍衫,或者將它放在一個(gè)子目錄,再在這個(gè)子目錄里面放一個(gè)單獨(dú)的package.json
文件侯养,指明{ type: "module" }
敦跌。
如果是Node.js中,還有一種做法是在package.json
文件的exports
字段逛揩,指明兩種格式模塊各自的加載入口柠傍。
"exports":{
"require": "./index.js",
"import": "./esm/wrapper.js"
}
上面代碼指定require()
和import
辩稽,加載該模塊會(huì)自動(dòng)切換到不一樣的入口文件惧笛。
五、Node.js中的模塊化
5.1 Node中支持 ES6 Module
JavaScript 現(xiàn)在常用的有兩種模塊逞泄。
- ES6 模塊患整,簡(jiǎn)稱 ESM拜效;
- CommonJS 模塊,簡(jiǎn)稱 CJS各谚。
CommonJS 模塊是 Node.js 專用的紧憾,與 ES6 模塊不兼容。語(yǔ)法上面昌渤,兩者最明顯的差異是赴穗,CommonJS 模塊使用require()
和module.exports
,ES6 模塊使用import
和export
膀息。
從 Node.js v13.2 版本開(kāi)始般眉,Node.js 已經(jīng)默認(rèn)打開(kāi)了 ES6 模塊支持,需要進(jìn)行以下操作:
- 方式一:文件以
.mjs
結(jié)尾潜支,表示使用的是ES Module甸赃; - 方式二:在package.json中配置字段
type: module
,一旦設(shè)置了以后冗酿,該目錄里面的 JS 腳本辑奈,就被解釋用 ES6 模塊。- 如果這時(shí)還要使用 CommonJS 模塊已烤,那么需要將 CommonJS 腳本的后綴名都改成
.cjs
鸠窗。
- 如果這時(shí)還要使用 CommonJS 模塊已烤,那么需要將 CommonJS 腳本的后綴名都改成
- 如果沒(méi)有
type
字段,或者type
字段為commonjs
胯究,則.js
腳本會(huì)被解釋成 CommonJS 模塊稍计。
在之前的版本(比如v12.19.0)中,也是可以正常運(yùn)行的裕循,但是輸出臺(tái)會(huì)報(bào)一個(gè)警告:The ESM Module loader is experimental
Node.js 遇到 ES6 模塊臣嚣,默認(rèn)啟用嚴(yán)格模式,不必在每個(gè)模塊文件頂部指定"use strict"
剥哑。
總結(jié)為一句話:
-
.mjs
文件總是以 ES6 模塊加載 -
.cjs
文件總是以 CommonJS 模塊加載 -
.js
文件的加載取決于package.json
里面type
字段的設(shè)置硅则。
注意,ES6 模塊與 CommonJS 模塊盡量不要混用株婴。require
命令不能加載.mjs
文件怎虫,會(huì)報(bào)錯(cuò),只有import
命令才可以加載.mjs
文件困介。反過(guò)來(lái)大审,.mjs
文件里面也不能使用require
命令,必須使用import
座哩。
5.2 Node.js包模塊的入口文件設(shè)置
5.2.1 package.json 的 main 字段
package.json
文件有兩個(gè)字段可以指定模塊的入口文件:main
和exports
徒扶。比較簡(jiǎn)單的模塊,可以只使用main
字段根穷,指定模塊加載的入口文件姜骡。
舉例:指定入口文件导坟,格式為ESM
// ./node_modules/es-module-package/package.json
{
"type": "module",
"main": "./src/index.js"
}
上面代碼指定項(xiàng)目的入口腳本為./src/index.js
,它的格式為 ES6 模塊圈澈。如果沒(méi)有type
字段乍迄,index.js
就會(huì)被解釋為 CommonJS 模塊。
然后士败,import
命令就可以加載這個(gè)模塊。
// ./my-app.mjs
import { something } from 'es-module-package';
// 實(shí)際加載的是 ./node_modules/es-module-package/src/index.js
上面代碼中褥伴,運(yùn)行該腳本以后谅将,Node.js 就會(huì)到./node_modules
目錄下面,尋找es-module-package
模塊重慢,然后根據(jù)該模塊package.json
的main
字段去執(zhí)行入口文件饥臂。
這時(shí),如果用 CommonJS 模塊的require()
命令去加載es-module-package
模塊會(huì)報(bào)錯(cuò)似踱,因?yàn)?CommonJS 模塊不能處理export
命令隅熙。
5.2.2 package.json 的 exports 字段
exports
字段的優(yōu)先級(jí)高于main
字段。它有多種用法核芽。
1. 給腳本或子目錄起別名
package.json
文件的exports
字段可以指定腳本或子目錄的別名囚戚。
// ./node_modules/es-module-package/package.json
{
"exports": {
"./submodule": "./src/submodule.js", //給腳本文件 src/submodule.js 起別名
"./features/": "./src/features/",// 給子目錄 ./src/features/ 起別名
}
}
通過(guò)別名加載:
import submodule from 'es-module-package/submodule';
// 加載 ./node_modules/es-module-package/src/submodule.js
import feature from 'es-module-package/features/x.js';
// 加載 ./node_modules/es-module-package/src/features/x.js
如果沒(méi)有指定別名轧简,就不能用“模塊+腳本名”這種形式加載腳本驰坊。
// 報(bào)錯(cuò)
import submodule from 'es-module-package/private-module.js';
// 不報(bào)錯(cuò)
import submodule from './node_modules/es-module-package/private-module.js';
2. main 的別名.
exports
字段的別名如果是.
就代表了是模塊的主入口,優(yōu)先級(jí)高于main
字段哮独,并且可以直接簡(jiǎn)寫(xiě)成exports
字段的值拳芙。
{
"exports": {
".": "./main.js"
}
}
// 等同于
{
"exports": "./main.js"
}
由于exports
字段只有支持 ES6 的 Node.js 才認(rèn)識(shí),所以可以用來(lái)兼容舊版本的 Node.js皮璧。
{
"main": "./main-legacy.cjs",
"exports": {
".": "./main-modern.cjs"
}
}
上面代碼中舟扎,老版本的 Node.js (不支持 ES6 模塊)的入口文件是main-legacy.cjs
,新版本的 Node.js 的入口文件是main-modern.cjs
悴务。
3. 條件加載
利用.
這個(gè)別名睹限,可以為 ES6 模塊和 CommonJS 指定不同的入口。目前讯檐,這個(gè)功能需要在 Node.js 運(yùn)行的時(shí)候邦泄,打開(kāi)--experimental-conditional-exports
標(biāo)志。
{
"type": "module",
"exports": {
".": {
"require": "./main.cjs", // 別名`.`的`require`條件指定`require()`命令的入口文件(即 CommonJS 的入口)
"default": "./main.js" // 別名`.`的`default`條件指定其他情況的入口(即 ES6 的入口)裂垦。
}
}
}
上面的寫(xiě)法可以簡(jiǎn)寫(xiě)如下
{
"exports": {
"require": "./main.cjs",
"default": "./main.js"
}
}
注意顺囊,如果同時(shí)還有其他別名,就不能采用簡(jiǎn)寫(xiě)蕉拢,否則或報(bào)錯(cuò)特碳。
{
// 報(bào)錯(cuò)
"exports": {
"./feature": "./lib/feature.js",
"require": "./main.cjs",
"default": "./main.js"
}
}
5.3 Node.js原生模塊完全支持ES6 Module
Node.js 的內(nèi)置模塊可以整體加載诚亚,也可以加載指定的輸出項(xiàng)。
// 整體加載
import EventEmitter from 'events';
const e = new EventEmitter();
// 加載指定的輸出項(xiàng)
import { readFile } from 'fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
}
});
5.4 加載路徑
ES6 模塊的加載路徑必須給出腳本的完整路徑午乓,不能省略腳本的后綴名站宗。import
命令和package.json
文件的main
字段如果省略腳本的后綴名,會(huì)報(bào)錯(cuò)益愈。
// ES6 模塊中將報(bào)錯(cuò)
import { something } from './index';
為了與瀏覽器的import
加載規(guī)則相同梢灭,Node.js 的.mjs
文件支持 URL 路徑。
import './foo.mjs?query=1'; // 加載 ./foo 傳入?yún)?shù) ?query=1
上面代碼中蒸其,腳本路徑帶有參數(shù)?query=1
敏释,Node 會(huì)按 URL 規(guī)則解讀。同一個(gè)腳本只要參數(shù)不同摸袁,就會(huì)被加載多次钥顽,并且保存成不同的緩存。由于這個(gè)原因靠汁,只要文件名中含有:
蜂大、%
、#
蝶怔、?
等特殊字符奶浦,最好對(duì)這些字符進(jìn)行轉(zhuǎn)義。
目前踢星,Node.js 的import
命令只支持加載本地模塊(file:
協(xié)議)和data:
協(xié)議财喳,不支持加載遠(yuǎn)程模塊。另外斩狱,腳本路徑只支持相對(duì)路徑耳高,不支持絕對(duì)路徑(即以/
或//
開(kāi)頭的路徑)。
5.5 內(nèi)部變量
ES6 模塊應(yīng)該是通用的所踊,同一個(gè)模塊不用修改泌枪,就可以用在瀏覽器環(huán)境和服務(wù)器環(huán)境。為了達(dá)到這個(gè)目標(biāo)秕岛,Node.js 規(guī)定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內(nèi)部變量碌燕。
首先,就是this
關(guān)鍵字继薛。ES6 模塊之中修壕,頂層的this
指向undefined
;CommonJS 模塊的頂層this
指向當(dāng)前模塊遏考,這是兩者的一個(gè)重大差異慈鸠。
其次,以下這些頂層變量在 ES6 模塊之中都是不存在的灌具。
arguments
require
module
exports
__filename
__dirname
六、循環(huán)加載
“循環(huán)加載”(circular dependency)指的是,a
腳本的執(zhí)行依賴b
腳本漫贞,而b
腳本的執(zhí)行又依賴a
腳本。
// a.js
var b = require('b');
// b.js
var a = require('a');
通常芦昔,“循環(huán)加載”表示存在強(qiáng)耦合,如果處理不好咕缎,還可能導(dǎo)致遞歸加載,使得程序無(wú)法執(zhí)行料扰,因此應(yīng)該避免出現(xiàn)凭豪。
但是實(shí)際上,這是很難避免的记罚,尤其是依賴關(guān)系復(fù)雜的大項(xiàng)目,很容易出現(xiàn)a
依賴b
壳嚎,b
依賴c
桐智,c
又依賴a
這樣的情況。這意味著烟馅,模塊加載機(jī)制必須考慮“循環(huán)加載”的情況说庭。
對(duì)于 JavaScript 語(yǔ)言來(lái)說(shuō),目前最常見(jiàn)的兩種模塊格式 CommonJS 和 ES6郑趁,處理“循環(huán)加載”的方法是不一樣的刊驴,返回的結(jié)果也不一樣。
6.1 CommonJS 模塊的循環(huán)加載
CommonJS 模塊的重要特性是加載時(shí)執(zhí)行寡润,即腳本代碼在require
的時(shí)候捆憎,就會(huì)全部執(zhí)行。一旦出現(xiàn)某個(gè)模塊被"循環(huán)加載"梭纹,就只輸出已經(jīng)執(zhí)行的部分躲惰,還未執(zhí)行的部分不會(huì)輸出。
讓我們來(lái)看变抽,Node 官方文檔里面的例子础拨。
// a.js
exports.done = false; // 先輸出一個(gè)`done`變量
var b = require('./b.js'); // 然后加載另一個(gè)腳本文件b.js。注意绍载,此時(shí)代碼就停在這里诡宗,等待`b.js`執(zhí)行完畢,再往下執(zhí)行击儡。
console.log('在 a.js 之中塔沃,b.done = %j', b.done); // b.js執(zhí)行完畢,返回來(lái)a.js接著往下執(zhí)行阳谍,直到執(zhí)行完畢芳悲。
exports.done = true;
console.log('a.js 執(zhí)行完畢');
// b.js
exports.done = false;
/*
執(zhí)行到這一行立肘,會(huì)去加載a.js,這時(shí)名扛,就發(fā)生了“循環(huán)加載”谅年。系統(tǒng)會(huì)去a.js模塊對(duì)應(yīng)對(duì)象的exports屬性取值,可是因?yàn)閍.js還沒(méi)有執(zhí)行完肮韧,從exports屬性只能取回已經(jīng)執(zhí)行的部分融蹂,而不是最后的值。
此時(shí):a.js已經(jīng)執(zhí)行的部分弄企,只有一行:exports.done = false; 即對(duì)于b.js來(lái)說(shuō)超燃,它從a.js只輸入一個(gè)變量done=false 。
*/
var a = require('./a.js');
console.log('在 b.js 之中拘领,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執(zhí)行完畢');
// b.js接著往下執(zhí)行意乓,等到全部執(zhí)行完畢,再把執(zhí)行權(quán)交還給a.js约素。
我們寫(xiě)一個(gè)腳本main.js届良,驗(yàn)證這個(gè)過(guò)程。
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
執(zhí)行main.js
圣猎,運(yùn)行結(jié)果如下:
$ node main.js
在 b.js 之中士葫,a.done = false
b.js 執(zhí)行完畢
在 a.js 之中,b.done = true
a.js 執(zhí)行完畢
在 main.js 之中, a.done=true, b.done=true
上面的代碼證明了兩件事:
- 在
b.js
之中送悔,a.js
沒(méi)有執(zhí)行完畢慢显,只執(zhí)行了第一行。 -
main.js
執(zhí)行到第二行時(shí)欠啤,不會(huì)再次執(zhí)行b.js
荚藻,而是輸出緩存的b.js
的執(zhí)行結(jié)果,即它的第四行exports.done = true;
總之洁段,CommonJS 輸入的是被輸出值的拷貝鞋喇,不是引用。
另外眉撵,由于 CommonJS 模塊遇到循環(huán)加載時(shí)侦香,返回的是當(dāng)前已經(jīng)執(zhí)行的部分的值,而不是代碼全部執(zhí)行后的值纽疟,兩者可能會(huì)有差異罐韩。所以,輸入變量的時(shí)候污朽,必須非常小心散吵。
var a = require('a'); // 安全的寫(xiě)法
var foo = require('a').foo; // 危險(xiǎn)的寫(xiě)法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一個(gè)部分加載時(shí)的值
};
上面代碼中,如果發(fā)生循環(huán)加載,require('a').foo
的值很可能后面會(huì)被改寫(xiě)矾睦,改用require('a')
會(huì)更保險(xiǎn)一點(diǎn)晦款。
6.2 ES6 模塊的循環(huán)加載
ES6 處理“循環(huán)加載”與 CommonJS 有本質(zhì)的不同。ES6 模塊是動(dòng)態(tài)引用枚冗,如果使用import
從一個(gè)模塊加載變量(即import foo from 'foo'
)缓溅,那些變量不會(huì)被緩存,而是成為一個(gè)指向被加載模塊的引用赁温,需要開(kāi)發(fā)者自己保證坛怪,真正取值的時(shí)候能夠取到值。
請(qǐng)看下面這個(gè)例子股囊。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
上面代碼中袜匿,a.mjs
加載b.mjs
,b.mjs
又加載a.mjs
稚疹,構(gòu)成循環(huán)加載居灯。執(zhí)行a.mjs
,結(jié)果如下内狗。
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
上面代碼中怪嫌,執(zhí)行a.mjs
以后會(huì)報(bào)錯(cuò),foo
變量未定義其屏,這是為什么喇勋?
讓我們一行行來(lái)看缨该,ES6 循環(huán)加載是怎么處理的:
- 首先偎行,執(zhí)行
a.mjs
以后,引擎發(fā)現(xiàn)它加載了b.mjs
贰拿,因此會(huì)優(yōu)先執(zhí)行b.mjs
蛤袒,然后再執(zhí)行a.mjs
。 - 接著膨更,執(zhí)行
b.mjs
的時(shí)候妙真,已知它從a.mjs
輸入了foo
接口,這時(shí)不會(huì)去執(zhí)行a.mjs
荚守,而是認(rèn)為這個(gè)接口已經(jīng)存在了珍德,繼續(xù)往下執(zhí)行。 - 執(zhí)行到第三行
console.log(foo)
的時(shí)候矗漾,才發(fā)現(xiàn)這個(gè)接口根本沒(méi)定義锈候,因此報(bào)錯(cuò)。
解決這個(gè)問(wèn)題的方法敞贡,就是讓b.mjs
運(yùn)行的時(shí)候泵琳,foo
已經(jīng)有定義了。這可以通過(guò)將foo
寫(xiě)成函數(shù)來(lái)解決。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' } // const foo = () => 'foo'; 仍然會(huì)執(zhí)行報(bào)錯(cuò)获列。函數(shù)表達(dá)式谷市,就不具有提升作用
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
這時(shí)再執(zhí)行a.mjs
就可以得到預(yù)期結(jié)果。
$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar
這是因?yàn)?strong>函數(shù)具有提升作用击孩,在執(zhí)行import {bar} from './b'
時(shí)迫悠,函數(shù)foo
就已經(jīng)有定義了,所以b.mjs
加載的時(shí)候不會(huì)報(bào)錯(cuò)溯壶。
這也意味著及皂,如果把函數(shù)foo
改寫(xiě)成函數(shù)表達(dá)式,也會(huì)報(bào)錯(cuò)且改。
6.3 代碼示例
我們?cè)賮?lái)看 ES6 模塊加載器SystemJS給出的一個(gè)例子验烧。
// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
counter++;
return n === 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
return n !== 0 && even(n - 1);
}
上面代碼中,even.js
里面的函數(shù)even
有一個(gè)參數(shù)n
又跛,只要不等于 0碍拆,就會(huì)減去 1,傳入加載的odd()
慨蓝。odd.js
也會(huì)做類似操作感混。
運(yùn)行上面這段代碼,結(jié)果如下礼烈。
$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
上面代碼中弧满,參數(shù)n
從 10 變?yōu)?0 的過(guò)程中,even()
一共會(huì)執(zhí)行 6 次此熬,所以變量counter
等于 6庭呜。第二次調(diào)用even()
時(shí),參數(shù)n
從 20 變?yōu)?0犀忱,even()
一共會(huì)執(zhí)行 11 次募谎,加上前面的 6 次,所以變量counter
等于 17阴汇。
這個(gè)例子要是改寫(xiě)成 CommonJS数冬,就根本無(wú)法執(zhí)行,會(huì)報(bào)錯(cuò)搀庶。
// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
var even = require('./even').even;
module.exports = function (n) {
return n != 0 && even(n - 1);
}
上面代碼中拐纱,even.js
加載odd.js
,而odd.js
又去加載even.js
哥倔,形成“循環(huán)加載”秸架。這時(shí),執(zhí)行引擎就會(huì)輸出even.js
已經(jīng)執(zhí)行的部分(不存在任何結(jié)果)未斑,所以在odd.js
之中咕宿,變量even
等于undefined
币绩,等到后面調(diào)用even(n - 1)
就會(huì)報(bào)錯(cuò)。
$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function
七府阀、了解:AMD和CMD規(guī)范
7.1. CommonJS規(guī)范缺點(diǎn)
CommonJS加載模塊是同步的:
- 同步的意味著只有等到對(duì)應(yīng)的模塊加載完畢缆镣,當(dāng)前模塊中的內(nèi)容才能被運(yùn)行;
- 這個(gè)在服務(wù)器不會(huì)有什么問(wèn)題试浙,因?yàn)榉?wù)器加載的js文件都是本地文件董瞻,加載速度非常快田巴;
如果將它應(yīng)用于瀏覽器呢钠糊?
- 瀏覽器加載js文件需要先從服務(wù)器將文件下載下來(lái),之后在加載運(yùn)行壹哺;
- 那么采用同步的就意味著后續(xù)的js代碼都無(wú)法正常運(yùn)行抄伍,即使是一些簡(jiǎn)單的DOM操作;
所以在瀏覽器中管宵,我們通常不使用CommonJS規(guī)范:
- 當(dāng)然在webpack中使用CommonJS是另外一回事截珍;
- 因?yàn)樗鼤?huì)將我們的代碼轉(zhuǎn)成瀏覽器可以直接執(zhí)行的代碼;
在早期為了可以在瀏覽器中使用模塊化箩朴,通常會(huì)采用AMD或CMD:
- 但是目前一方面現(xiàn)代的瀏覽器已經(jīng)支持ES Modules岗喉,另一方面借助于webpack等工具可以實(shí)現(xiàn)對(duì)CommonJS或者ES Module代碼的轉(zhuǎn)換;
- AMD和CMD已經(jīng)使用非常少了炸庞,所以這里我們進(jìn)行簡(jiǎn)單的演練钱床;
7.2. AMD規(guī)范
7.2.1 AMD與Require.js
AMD主要是應(yīng)用于瀏覽器的一種模塊化規(guī)范:
- AMD是Asynchronous Module Definition(異步模塊定義)的縮寫(xiě);
- 它采用的是異步加載模塊埠居;
- 事實(shí)上AMD的規(guī)范還要早于CommonJS查牌,但是CommonJS目前依然在被使用,而AMD使用的較少了拐格;
我們提到過(guò)僧免,規(guī)范只是定義代碼的應(yīng)該如何去編寫(xiě)刑赶,只有有了具體的實(shí)現(xiàn)才能被應(yīng)用:
- AMD實(shí)現(xiàn)的比較常用的庫(kù)是require.js和curl.js捏浊;
7.2.2 Require.js的使用
第一步:下載require.js
- 下載地址:https://github.com/requirejs/requirejs
- 找到其中的require.js文件;
第二步:定義HTML的script標(biāo)簽引入require.js和定義入口文件:
- data-main屬性的作用是在加載完src的文件后會(huì)加載執(zhí)行該文件
<script src="./lib/require.js" data-main="./index.js"></script>
第三步:編寫(xiě)如下目錄和代碼(個(gè)人習(xí)慣)
├── index.html
├── index.js
├── lib
│ └── require.js
└── modules
├── bar.js
└── foo.js
index.js
(function() {
require.config({
baseUrl: '',
paths: {
foo: './modules/foo',
bar: './modules/bar'
}
})
// 開(kāi)始加載執(zhí)行foo模塊的代碼
require(['foo'], function(foo) {
})
})();
modules/bar.js
- 如果一個(gè)模塊不依賴其他撞叨,那么直接使用define(function)即可
define(function() {
const name = "coderwhy";
const age = 18;
const sayHello = function(name) {
console.log("Hello " + name);
}
return {
name,
age,
sayHello
}
})
modules/foo.js
define(['bar'], function(bar) {
console.log(bar.name);
console.log(bar.age);
bar.sayHello('kobe');
})
7.3 CMD規(guī)范
7.3.1 CMD與SeaJS
CMD規(guī)范也是應(yīng)用于瀏覽器的一種模塊化規(guī)范:
- CMD 是Common Module Definition(通用模塊定義)的縮寫(xiě)金踪;
- 它也采用了異步加載模塊,但是它將CommonJS的優(yōu)點(diǎn)吸收了過(guò)來(lái)牵敷;
- 但是目前CMD使用也非常少了胡岔;
CMD也有自己比較優(yōu)秀的實(shí)現(xiàn)方案:
- SeaJS
7.3.2 SeaJS的使用
1. 下載SeaJS
- 下載地址:https://github.com/seajs/seajs
- 找到dist文件夾下的sea.js
2. 引入sea.js和啟動(dòng)模塊
-
seajs
是指定主入口文件的,也稱為啟動(dòng)模塊
<script src="./lib/sea.js"></script> <!--在調(diào)用 seajs 之前枷餐,必須先引入 sea.js 文件-->
<script>
seajs.use('./index.js');
/*
通過(guò) seajs.use() 函數(shù)可以啟動(dòng)模塊
- ('模塊id' [,callback]) 加載一個(gè)模塊靶瘸,并執(zhí)行回調(diào)函數(shù)
- (['模塊1', '模塊2'] [,callback]) 加載多個(gè)模塊,并執(zhí)行回調(diào)函數(shù)
- callback 參數(shù)是可選的。格式為:function( 模塊對(duì)象 ){ 業(yè)務(wù)代碼 };
- seajs.use 理論上只用于加載啟動(dòng)怨咪,不應(yīng)該出現(xiàn)在 define 中的模塊代碼里
- seajs.use 和 DOM ready 事件沒(méi)有任何關(guān)系屋剑。要想保證 文檔結(jié)構(gòu)加載完畢再執(zhí)行你的 js 代碼,一定要在seajs.use內(nèi)部通過(guò) window.onload 或者 $(function(){})
*/
</script>
3. 編寫(xiě)如下目錄和代碼(個(gè)人習(xí)慣)
├── index.html
├── index.js
├── lib
│ └── sea.js
└── modules
├── bar.js
└── foo.js
4. 定義模塊define
- 在CMD規(guī)范中诗眨,一個(gè)模塊就是一個(gè)js文件
module是一個(gè)對(duì)象唉匾,存儲(chǔ)了模塊的元信息,具體如下:
module.id——模塊的ID匠楚。
module.dependencies——一個(gè)數(shù)組巍膘,存儲(chǔ)了此模塊依賴的所有模塊的ID列表。
module.exports——與exports指向同一個(gè)對(duì)象芋簿。
module.uri
define 是一個(gè)全局函數(shù)峡懈,用來(lái)定義模塊:define( factory )
- 對(duì)象
{}
:這種方式,外部會(huì)直接獲取到該對(duì)象 - 字符串
""
: 同上 - 函數(shù):
define(function(require, exports, module){ 模塊代碼 });
為了減少出錯(cuò)与斤,定義函數(shù)的時(shí)候直接把這三個(gè)參數(shù)寫(xiě)上
5. 導(dǎo)出接口exports和module.exports
功能:通過(guò)給 exports或module.exports動(dòng)態(tài)的掛載變量逮诲、函數(shù)或?qū)ο螅獠繒?huì)獲取到該接口
exports 等價(jià)于 module.exports幽告。exports能做什么梅鹦,module.exports就能做什么
可以通過(guò)多次給exports 掛載屬性向外暴露
不能直接給 exports 賦值
如果想暴露單個(gè)變量、函數(shù)或?qū)ο罂梢酝ㄟ^(guò)直接給 module.exports 賦值 即可
6. 依賴模塊require
/*
模塊標(biāo)識(shí)/模塊id
- 模塊標(biāo)識(shí)就是一個(gè)`字符串`冗锁,用來(lái)`標(biāo)識(shí)模塊`
- 模塊標(biāo)識(shí) 可以不包含后綴名.js
- 以 ./或 ../ 開(kāi)頭的相對(duì)路徑模塊齐唆,相對(duì)于 require 所在模塊的路徑
- 不以 ./ 或 ../ 開(kāi)頭的頂級(jí)標(biāo)識(shí),會(huì)相對(duì)于模塊的基礎(chǔ)路徑解析(配置項(xiàng)中的base)
- 絕對(duì)路徑如http://127.0.0.1:8080/js/a.js冻河、/js/a.js
*/
requeire('模塊id')
/*
1.用于根據(jù)一個(gè)模塊id加載/依賴該模塊
2.參數(shù)必須是一個(gè)字符串
3.該方法會(huì)得到 要加載的模塊中的 module.exports 對(duì)象
*/
- 只能在模塊環(huán)境define中使用箍邮,define(factory)的構(gòu)造方法第一個(gè)參數(shù)必須命名為 require
- 不要重命名require函數(shù)或者在任何作用域中給 require 重新賦值
- 在一個(gè)模塊系統(tǒng)中,
require
加載過(guò)的模塊會(huì)被緩存 - 默認(rèn)
require
是同步加載模塊的
require.async
SeaJS會(huì)在html頁(yè)面打開(kāi)時(shí)通過(guò)靜態(tài)分析一次性記載所有需要的js文件叨叙,如果想要某個(gè)js文件在用到時(shí)才下載锭弊,可以使用require.async:
require.async('/path/to/module/file', function(m) {
//code of callback...
});
這樣只有在用到這個(gè)模塊時(shí),對(duì)應(yīng)的js文件才會(huì)被下載擂错,也就實(shí)現(xiàn)了JavaScript代碼的按需加載味滞。
SeaJS高級(jí)配置
- alias:別名配置
- paths:路徑配置
- vars:變量配置
- map:映射配置
- preload:預(yù)加載項(xiàng)
- debug:調(diào)試模式
- base:基礎(chǔ)路徑
- charset:文件編碼
代碼示例
index.js
define(function(require, exports, module) {
const foo = require('./modules/foo');
})
bar.js
define(function(require, exports, module) {
const name = 'lilei';
const age = 20;
const sayHello = function(name) {
console.log("你好 " + name);
}
module.exports = {
name,
age,
sayHello
}
})
foo.js
define(function(require, exports, module) {
const bar = require('./bar');
console.log(bar.name);
console.log(bar.age);
bar.sayHello("韓梅梅");
})