無模塊化
簡單的將所有的 js 文件統(tǒng)統(tǒng)放在一起,然后通過 <script>
標簽引入。
- 優(yōu)點:
- 相比于使用一個js文件,這種多個js文件實現(xiàn)最簡單的模塊化的思想是進步的。
- 缺點:
- 污染全局作用域。因為每一個模塊都是暴露在全局的,簡單的使用,會導致全局變量命名沖突,當然,我們也可以使用命名空間的方式來解決。
- 對于大型項目,各種js很多,開發(fā)人員必須手動解決模塊和代碼庫的依賴關系,后期維護成本較高。
- 依賴關系不明顯,不利于維護。 比如 main.js 需要使用 jquery,但是,從上面的文件中,我們是看不出來的,如果 jquery 忘記了,那么就會報錯。
<!-- 頁面內(nèi)嵌的腳本 -->
<script type="application/javascript">
// module code
</script>
<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>
上面代碼中,由于瀏覽器腳本的默認語言是 JavaScript,因此 type="application/javascript" 可以省略。
- 默認情況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到
<script>
標簽就會停下來,等到執(zhí)行完腳本,再繼續(xù)向下渲染。如果是外部腳本,還必須加入腳本下載的時間。如果腳本體積很大,下載和執(zhí)行的時間就會很長,因此造成瀏覽器堵塞,用戶會感覺到瀏覽器“卡死”了,沒有任何響應。 - 瀏覽器允許腳本異步加載,下面就是兩種異步加載的語法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代碼中,
<script>
標簽打開 defer 或 async 屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執(zhí)行,而是直接執(zhí)行后面的命令。
- defer 與 async 的區(qū)別是:
- defer 要等到整個頁面在內(nèi)存中正常渲染結束(DOM 結構完全生成,以及其他腳本執(zhí)行完成),才會執(zhí)行
- async 一旦下載完,渲染引擎就會中斷渲染,執(zhí)行這個腳本以后,再繼續(xù)渲染
- 一句話,defer 是 “渲染完再執(zhí)行”,async 是 “下載完就執(zhí)行”
- 如果有多個 defer 腳本,會按照它們在頁面出現(xiàn)的順序加載,而多個 async 腳本是不能保證加載順序的
CommonJS 規(guī)范
1. 概述
- 是一個 JavaScript 模塊化的規(guī)范,
Nodejs
環(huán)境所使用的模塊系統(tǒng)就是基于CommonJS
規(guī)范實現(xiàn)的,我們現(xiàn)在所說的CommonJS
規(guī)范也大多是指 Node 的模塊系統(tǒng),前端的 webpack 也是對 CommonJS 原生支持。 - CommonJS 規(guī)范,每一個文件就是一個模塊,其內(nèi)部定義的變量是屬于這個模塊的,不會對外暴露,也就是說不會污染全局變量。
- 有四個重要的環(huán)境變量為模塊化的實現(xiàn)提供支持:
module
、exports
、require
、global
。實際使用時,用module.exports
定義當前模塊對外輸出的接口(不推薦直接用 exports ),用require
加載模塊。
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
上面代碼中,變量 x 和函數(shù) addX,是當前文件 example.js 私有的,其他文件不可見。
- 如果想在多個文件分享變量,必須定義為
global
對象的屬性。
global.warning = true
上面代碼的 warning 變量,可以被所有文件讀取。當然,這樣寫法是不推薦的。
- CommonJS 規(guī)范規(guī)定,每個模塊內(nèi)部,module 變量代表當前模塊。這個變量是一個對象,它的 exports 屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的 module.exports 屬性。
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports = {
x: x,
addX: addX
};
上面代碼通過 module.exports 輸出變量 x 和函數(shù) addX。
-
require
方法用于加載模塊
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
-
CommonJS 規(guī)范特點
- 所有代碼都運行在模塊作用域,不會污染全局作用域
- CommonJS 模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存
- CommonJS 模塊加載的順序,按照其在代碼中出現(xiàn)的順序
- 由于 Node.js 主要用于服務器編程,模塊文件一般都已經(jīng)存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以 CommonJS 規(guī)范比較適用
-
優(yōu)點:
- CommonJS 規(guī)范在服務器端率先完成了 JavaScript 的模塊化,解決了依賴、全局變量污染的問題,這也是 js 運行在服務器端的必要條件。
-
缺點:
- 由于 CommonJS 是同步加載模塊的,在服務器端,文件都是保存在硬盤上,所以同步加載沒有問題,但是對于瀏覽器端,需要將文件從服務器端請求過來,那么同步加載就不適用了,所以,CommonJS 是不適用于瀏覽器端的。
2. module 對象
- Node 內(nèi)部提供一個 Module 構建函數(shù)。所有模塊都是 Module 的實例。
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
- 每個模塊內(nèi)部,都有一個 module 對象,代表當前模塊。它有以下屬性。
module.id 模塊的識別符,通常是帶有絕對路徑的模塊文件名。
module.filename 模塊的文件名,帶有絕對路徑。
module.loaded 返回一個布爾值,表示模塊是否已經(jīng)完成加載。
module.parent 返回一個對象,表示調(diào)用該模塊的模塊。
module.children 返回一個數(shù)組,表示該模塊要用到的其他模塊。
module.exports 表示模塊對外輸出的值。
- 如果在命令行下調(diào)用某個模塊,比如 node something.js ,那么 module.parent 就是 null 。如果是在腳本之中調(diào)用,比如 require('./something.js') ,那么 module.parent 就是調(diào)用它的模塊。利用這一點,可以判斷當前模塊是否為入口腳本。
if (!module.parent) {
// ran with `node something.js`
app.listen(8088, function() {
console.log('app listening on port 8088');
});
} else {
// used with `require('/.something.js')`
module.exports = app;
}
- module.exports 屬性表示當前模塊對外輸出的接口,其他文件加載該模塊,實際上就是讀取 module.exports 變量。
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
setTimeout(function() {
module.exports.emit('ready');
}, 1000);
上面模塊會在加載后1秒后,發(fā)出 ready 事件。其他文件監(jiān)聽該事件,可以寫成下面這樣。
var a = require('./a');
a.on('ready', function() {
console.log('module a is ready');
});
3. exports 變量
- 為了方便,Node 為每個模塊提供一個 exports 變量,指向 module.exports。這等同在每個模塊頭部,有一行這樣的命令。
var exports = module.exports;
造成的結果是,在對外輸出模塊接口時,可以向exports對象添加方法。
exports.area = function (r) {
return Math.PI * r * r;
};
exports.circumference = function (r) {
return 2 * Math.PI * r;
};
- 注意,不能直接將 exports 變量指向一個值,因為這樣等于切斷了 exports 與 module.exports 的聯(lián)系。
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
上面代碼中,hello 函數(shù)是無法對外輸出的,因為 module.exports 被重新賦值了
- 這意味著,如果一個模塊的對外接口,就是一個單一的值,不能使用 exports 輸出,只能使用 module.exports 輸出,如下。
module.exports = function (x) { console.log(x); };
4. require 命令
4.1 基本用法
Node 使用 CommonJS 模塊規(guī)范,內(nèi)置的 require 命令用于加載模塊文件
- require 命令的基本功能是,讀入并執(zhí)行一個 JavaScript 文件,然后返回該模塊的 exports 對象。如果沒有發(fā)現(xiàn)指定模塊,會報錯。
// example.js
var invisible = function () {
console.log("invisible");
}
exports.message = "hi";
exports.say = function () {
console.log(message);
}
運行以下命令輸出 exports 對象
var example = require('./example.js');
example
// {
// message: "hi",
// say: [Function]
// }
- 如果模塊輸出的是一個函數(shù),那就不能定義在 exports 對象上面,而要定義在 module.exports 變量上面。
module.exports = function () {
console.log("hello world");
};
require('./example2.js')();
4.2 加載規(guī)則
- require 命令用于加載文件,后綴名默認為 .js。
var foo = require('foo');
// 等同于
var foo = require('foo.js');
- 根據(jù)參數(shù)的不同格式,require命令去不同路徑尋找模塊文件。
- (1)如果參數(shù)字符串以
/
開頭,則表示加載的是一個位于絕對路徑的模塊文件。比如,require('/home/marco/foo.js') 將加載 /home/marco/foo.js。 - (2)如果參數(shù)字符串以
./
開頭,則表示加載的是一個位于相對路徑(跟當前執(zhí)行腳本的位置相比)的模塊文件。比如,require('./circle') 將加載當前腳本同一目錄的 circle.js。 - (3)如果參數(shù)字符串不以
./
或/
開頭,則表示加載的是一個默認提供的核心模塊(位于 Node 的系統(tǒng)安裝目錄中
),或者一個位于各級 node_modules 目錄的已安裝模塊
(全局安裝或局部安裝)。
- (1)如果參數(shù)字符串以
- 舉例來說,腳本 /home/user/projects/foo.js 執(zhí)行了 require('bar.js') 命令,Node 會依次搜索以下文件。
- (4)如果參數(shù)字符串不以
./
或/
開頭,而且是一個路徑,比如 require('example-module/path/to/file'),則將先找到 example-module 的位置,然后再以它為參數(shù),找到后續(xù)路徑。 - (5)如果指定的模塊文件沒有發(fā)現(xiàn),Node 會嘗試為文件名添加 .js 、 .json 、 .node 后,再去搜索。 .js 件會以文本格式的 JavaScript 腳本文件解析, .json 文件會以 JSON 格式的文本文件解析, .node 文件會以編譯后的二進制文件解析。
- (6)如果想得到 require 命令加載的確切文件名,使用 require.resolve() 方法。
- (4)如果參數(shù)字符串不以
/usr/local/lib/node/bar.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
這樣設計的目的是,使得不同的模塊可以將所依賴的模塊本地化
4.3 目錄的加載規(guī)則
- 通常,我們會把相關的文件會放在一個目錄里面,便于組織。這時,最好為該目錄設置一個入口文件,讓 require 方法可以通過這個入口文件,加載整個目錄。
在目錄中放置一個 package.json 文件,并且將入口文件寫入 main 字段。下面是一個例子。
// package.json
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
- require 發(fā)現(xiàn)參數(shù)字符串指向一個目錄以后,會自動查看該目錄的 package.json 文件,然后加載 main 字段指定的入口文件。如果 package.json 文件沒有 main 字段,或者根本就沒有 package.json 文件,則會加載該目錄下的 index.js 文件或 index.node 文件。
4.4 模塊的緩存
- 第一次加載某個模塊時,Node 會緩存該模塊。以后再加載該模塊,就直接從緩存取出該模塊的 module.exports 屬性。
require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"
上面代碼中,連續(xù)三次使用 require 命令,加載同一個模塊。第二次加載的時候,為輸出的對象添加了一個 message 屬性。但是第三次加載的時候,這個 message 屬性依然存在,這就證明 require 命令并沒有重新加載模塊文件,而是輸出了緩存。
- 如果想要多次執(zhí)行某個模塊,可以讓該模塊輸出一個函數(shù),然后每次 require 這個模塊的時候,重新執(zhí)行一下輸出的函數(shù)。
所有緩存的模塊保存在 require.cache 之中,如果想刪除模塊的緩存,可以像下面這樣寫。
// 刪除指定模塊的緩存
delete require.cache[moduleName];
// 刪除所有模塊的緩存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
});
- 注意,緩存是根據(jù)絕對路徑識別模塊的,如果同樣的模塊名,但是保存在不同的路徑,require 命令還是會重新加載該模塊。
5. 環(huán)境變量 NODE_PATH
- Node 執(zhí)行一個腳本時,會先查看環(huán)境變量 NODE_PATH。它是一組以冒號分隔的絕對路徑。在其他位置找不到指定模塊時,Node 會去這些路徑查找。
可以將 NODE_PATH 添加到 .bashrc。
export NODE_PATH="/usr/local/lib/node"
所以,如果遇到復雜的相對路徑,比如下面這樣。
var myModule = require('../../../../lib/myModule');
- 有兩種解決方法,一是將該文件加入 node_modules 目錄,二是修改 NODE_PATH 環(huán)境變量,package.json 文件可以采用下面的寫法。
{
"name": "node_path",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "NODE_PATH=lib node index.js"
},
"author": "",
"license": "ISC"
}
- NODE_PATH 是歷史遺留下來的一個路徑解決方案,通常不應該使用,而應該使用 node_modules 目錄機制。
4.6 模塊的循環(huán)加載
- 如果發(fā)生模塊的循環(huán)加載,即A加載B,B又加載A,則B將加載A的不完整版本。
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
上面代碼是三個 JavaScript 文件。其中,a.js 加載了 b.js,而 b.js 又加載 a.js。這時, Node 返回 a.js 的不完整版本,所以執(zhí)行結果如下。
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
修改 main.js,再次加載 a.js 和 b.js。
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
執(zhí)行上面代碼
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
main.js a2
main.js b2
- 上面代碼中,第二次加載 a.js 和 b.js 時,會直接從緩存讀取 exports 屬性,所以 a.js 和 b.js 內(nèi)部的 console.log 語句都不會執(zhí)行了。
4.7 require.main
- require 方法有一個 main 屬性,可以用來判斷模塊是直接執(zhí)行,還是被調(diào)用執(zhí)行。
直接執(zhí)行的時候(node module.js),require.main 屬性指向模塊本身。
require.main === module
// true
調(diào)用執(zhí)行的時候(通過 require 加載該腳本執(zhí)行),上面的表達式返回 false 。
5. 模塊的加載機制
- CommonJS 模塊輸出的是一個值的拷貝,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值
- CommonJS 模塊是運行時加載,加載的是一個對象(即
module.exports
屬性),該對象只有在腳本運行完才會生成
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代碼輸出內(nèi)部變量 counter 和改寫這個變量的內(nèi)部方法 incCounter。然后,在 main.js 里面加載這個模塊。
// main.js
var com = require('./lib')
console.log(com.counter) // 3
com.incCounter()
console.log(com.counter) // 3
上面代碼說明,
lib.js
模塊加載以后,它的內(nèi)部變化就影響不到輸出的com.counter
了。這是因為com.counter
是一個原始類型的值,會被緩存。除非寫成一個函數(shù),才能得到內(nèi)部變動后的值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
上面代碼中,輸出的 counter 屬性實際上是一個取值器函數(shù)?,F(xiàn)在再執(zhí)行 main.js ,就可以正確讀取內(nèi)部變量 counter 的變動了。
$ node main.js
3
4
5.1 require 的內(nèi)部處理流程
- require 命令是 CommonJS 規(guī)范之中,用來加載其他模塊的命令。它其實不是一個全局命令,而是指向當前模塊的 module.require 命令,而后者又調(diào)用 Node 的內(nèi)部命令 Module._load 。
Module._load = function(request, parent, isMain) {
// 1. 檢查 Module._cache,是否緩存之中有指定模塊
// 2. 如果緩存之中沒有,就創(chuàng)建一個新的Module實例
// 3. 將它保存到緩存
// 4. 使用 module.load() 加載指定的模塊文件,
// 讀取文件內(nèi)容之后,使用 module.compile() 執(zhí)行文件代碼
// 5. 如果加載/解析過程報錯,就從緩存刪除該模塊
// 6. 返回該模塊的 module.exports
};
上面的第 4 步,采用 module.compile() 執(zhí)行指定模塊的腳本,邏輯如下。
Module.prototype._compile = function(content, filename) {
// 1. 生成一個require函數(shù),指向module.require
// 2. 加載其他輔助方法到require
// 3. 將文件內(nèi)容放到一個函數(shù)之中,該函數(shù)可調(diào)用 require
// 4. 執(zhí)行該函數(shù)
};
上面的第 1 步和第 2 步, require 函數(shù)及其輔助方法主要如下。
require(): 加載外部模塊
require.resolve():將模塊名解析到一個絕對路徑
require.main:指向主模塊
require.cache:指向所有緩存的模塊
require.extensions:根據(jù)文件的后綴名,調(diào)用不同的執(zhí)行函數(shù)
一旦 require 函數(shù)準備完畢,整個所要加載的腳本內(nèi)容,就被放到一個新的函數(shù)之中,這樣可以避免污染全局環(huán)境。該函數(shù)的參數(shù)包括 require、module、exports ,以及其他一些參數(shù)。
(function (exports, require, module, __filename, __dirname) {
// YOUR CODE INJECTED HERE!
});
- Module._compile 方法是同步執(zhí)行的,所以 Module._load 要等它執(zhí)行完成,才會向用戶返回 module.exports 的值
CommonJS 規(guī)范 require 方法簡單實現(xiàn)
- 通過讀取文件內(nèi)容將內(nèi)容包裝到一個自執(zhí)行函數(shù)中,默認返回 module.exports 做為函數(shù)的結果。
const a = `function (exports, require, module, __filename, __dirname) {
let a = 1;
module.exports = 'hello';
return module.exports;
}(exports, require, module, xxxx, xxx)`;
function Module(id) {
this.id = id;
// 代表的是模塊的返回結果
this.exports = {};
}
Module._cache = {};
Module.wrapper = [
`(function(exports, require, module, __filename, __dirname) {`,
`})`
];
Module._extensions = {
'.js'(module) {
let content = fs.readFileSync(module.id, 'utf8');
content = Module.wrapper[0] + content + Module.wrapper[1];
// 需要讓函數(shù)字符串變成真正的函數(shù)
let fn = vm.runInThisContext(content);
let exports = module.exports; // {}
let dirname = path.dirname(module.id);
// 讓包裝的函數(shù)執(zhí)行 require 時會讓包裝的函數(shù)執(zhí)行,并且把this改變
fn.call(exports, exports, req, module, module.id, dirname);
},
'.json'(module) {
let content = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(content);
}
};
Module._resolveFilename = function (filename) {
let absPath = path.resolve(__dirname, filename);
// 查看路徑是否存在 如果不存在 則增加 .js 或者 .json 后綴
let isExists = fs.existsSync(absPath);
if (isExists) {
return absPath;
} else {
let keys = Object.keys(Module._extensions);
for (let i = 0; i < keys.length; i++) {
let newPath = absPath + keys[i];
let flag = fs.existsSync(newPath);
if (flag) {
return newPath;
}
}
throw new Error('module not exists');
}
};
Module.prototype.load = function () {
let extname = path.extname(this.id);
// module.exports = 'hello'
Module._extensions[extname](this);
}
function req(filename) { // 默認傳入的文件名可能沒有增加后綴,如果沒有后綴我就嘗試增加.js .json
// 解析出絕對路徑
filename = Module._resolveFilename(filename);
// 創(chuàng)建一個模塊
// 這里加載前先看一眼 是否加載過了
let cacheModule = Module._cache[filename]; // 多次引用同一個模塊只運行一次
if (cacheModule) {
return cacheModule.exports; // 返回緩存的結果即可
}
let module = new Module(filename);
Module._cache[filename] = module
// 加載模塊
module.load();
return module.exports;
};
ES6 加載規(guī)則
- 瀏覽器加載 ES6 模塊,也使用
<script>
標簽,但是要加入type="module"
屬性。
<script type="module" src="./foo.js"></script>
- 上面代碼在網(wǎng)頁中插入一個模塊 foo.js ,由于 type 屬性設為 module ,所以瀏覽器知道這是一個 ES6 模塊。
- 瀏覽器對于帶有 type="module" 的
<script>
,都是異步加載,不會造成堵塞瀏覽器,即等到整個頁面渲染完,再執(zhí)行模塊腳本,等同于打開了<script>
標簽的 defer 屬性。
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
- 如果網(wǎng)頁有多個
<script type="module">
,它們會按照在頁面出現(xiàn)的順序依次執(zhí)行。 -
<script>
標簽的 async 屬性也可以打開,這時只要加載完成,渲染引擎就會中斷渲染立即執(zhí)行。執(zhí)行完成后,再恢復渲染。
<script type="module" src="./foo.js" async></script>
- 一旦使用了 async 屬性,
<script type="module">
就不會按照在頁面出現(xiàn)的順序執(zhí)行,而是只要該模塊加載完成,就執(zhí)行該模塊。 - ES6 模塊也允許內(nèi)嵌在網(wǎng)頁中,語法行為與加載外部腳本完全一致。
<script type="module">
import utils from "./utils.js";
// other code
</script>
對于外部的模塊腳本(上例是 foo.js ),有幾點需要注意。
- 代碼是在模塊作用域之中運行,而不是在全局作用域運行。模塊內(nèi)部的頂層變量,外部不可見。
- 模塊腳本自動采用嚴格模式,不管有沒有聲明 use strict 。
- 模塊之中,可以使用 import 命令加載其他模塊( .js 后綴不可省略,需要提供絕對 URL 或相對 URL),也可以使用 export 命令輸出對外接口。
- 模塊之中,頂層的 this 關鍵字返回 undefined ,而不是指向 window 。也就是說,在模塊頂層使用 this 關鍵字,是無意義的。
- 同一個模塊如果加載多次,將只執(zhí)行一次。
示例
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 這個語法點,可以偵測當前代碼是否在 ES6 模塊之中。
const isNotModuleScript = this !== undefined;
ES6 模塊與 CommonJS 模塊的差異
- ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態(tài)分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執(zhí)行時,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的 import 有點像 Unix 系統(tǒng)的“符號連接”,原始值變了,import 加載的值也會跟著變。因此,ES6 模塊是動態(tài)引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。
以之前 2-CommonJS 規(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
再舉一個例子
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);