[JS] JavaScript 模組(ES Module, ESM)
- 👍 Modules @ JavaScript.info
- JavaScript modules @ MDN
- ES Module Syntax @ Rollup
TL;DR
- module 的內容只有在第一次被 import 的時候會被執行(evaluated),而且它會是 singleton,也就是說,如果 export 的是物件,這個物件只會有一個,在任何 module 都會改到同一個物件(參考:A module code is evaluated only the first time when imported)。
- 如果是在瀏覽中使用 ESModule,則每個 script 都會以與
defer
相同的方式執行。
ESM
// default 後面直接是 JavaScript 物件,可以接 key-value
export default { foo: 'bar' };
import esm from './modules'; // { foo: 'bar' }
// named imports and exports
// 代表匯出 foo 和 bar 這兩個變數,不能用 key-value pair
export { foo, bar };
import { foo, bar } from './modules';
// re-export
export { foo, bar } from './modules';
實名匯出 - 直接定義變數並匯出
匯出:
// util.js
// 直接定義並匯出變數
export const deviceName = 'iPhone';
export const mobilesOnSale = ['Samsung', 'Apple', 'Huawei'];
export const offers = {
priceCurrency: 'TWD',
price: '26,900',
};
export const logPrice = (price) => {
console.log('price: ', price);
};
export function logDeviceName(deviceName) {
console.log(deviceName);
}
匯入的名稱需要和匯出時相對應
// 在另一隻檔案只需要使用 import{} 即可匯入
import { deviceName, mobilesOnSale, offers, logPrice, logDeviceName } from './utils';
實名匯出 - 先定義好變數再匯出
也可以先把變數定義好,接著透過 export {}
把變數匯出,這種做法可以在匯出時透過 as
修改匯出的名稱:
// util.js
// 先定義好變數後匯出
const deviceName = 'iPhone';
const mobilesOnSale = ['Samsung', 'Apple', 'Huawei'];
const offers = {
priceCurrency: 'TWD',
price: '26,900',
};
const logPrice = (price) => {
console.log('price: ', price);
};
function logDeviceName(deviceName) {
console.log(deviceName);
}
// 匯出時可以進行名稱的修改
export { deviceName, mobilesOnSale, offers as productDetail, logPrice, logDeviceName };
要特別留意這裡 export 後的大括號並不是物件,而是匯出用的語法,千萬不要在裡面使用 key-value 這樣的寫法!
匯入時同樣只需要根據匯出時的變數對應使用 import{ }
即可:
// 在另一隻檔案只需要使用 import{ } 即可匯入
import {
deviceName as device,
productDetail, // 要用匯出時的名稱
} from './utils';
預設匯出(Default Export)
當模組只有一個單一的 export 時才建議使用 export default
,雖然可以但不建議同時使用 default export 又使用 named export。
預設匯出的語法是在 export
後加上 default
,default
後則直接帶入你想要匯出的東西即可,例如:個變數:
const logPrice = (price) => {
console.log('price: ', price);
};
export default logPrice;
匯入的時候可以自己隨意取名,這裡我們取做 showPrice
,就可以直接使用:
import showPrice from './utils';
showPrice(1000);
要特別留意的是,和實名匯出不同,如果你在 export default
後接的是 {}
,這個 {}
表示的就是物件,裡面放的就會是物件的屬性名稱和屬性值,因此不能再用 as
去修改名稱,例如:
const deviceName = 'iPhone';
const mobilesOnSale = ['Samsung', 'Apple', 'Huawei'];
const logPrice = (price) => {
console.log('price: ', price);
};
// export default 後直接帶入要匯出的東西
// 這裡是會直接匯出「物件」,因此不能在裡面使用 "as" 語法
export default {
deviceName, // deviceName: deviceName 的縮寫
mobilesOnSale, // mobilesOnSale: mobilesOnSale 的縮寫
logPrice, // logPrice: logPrice 的縮寫
};
匯入時也會是一個物件,若要使用裡面的資料,需要使用物件的方式來操作:
// myPhone 會是物件
import myPhone from './utils';
// 透過物件的方式操作
console.log('Device:', myPhone.deviceName);
// 也可以透過解構賦值將需要的屬性取出
const { logPrice } = myPhone;
logPrice(24900);
和實名匯出的 export {}
不同, export default {}
後的 {}
表示的真的就是個物件。匯入時也是匯入整個物件。
匯入一整個模組 import module
import 'lodash'; // 「執行」 lodash module 中內容,且不存成變數
import _ from 'lodash'; // 載入 default 並取名為 _
import { map, reduce } from 'lodash'; // 只載入特定模組
import { map as _map } from 'lodash'; // 載入特定模組並重新命名
import _, { map } from 'lodash'; // 同時載入 default 與特定模組
import { default as _, map } from 'lodash'; // 效果同上
import * as _ from 'lodash'; // 載入全部的模組,並放到名為 _ 的 namespace 中
// 動態載入
import('./modules.js').then(({ default: DefaultExport, NamedExport }) => {
// do something with modules.
});
動態載入(Dynamic Import)
動態載入(Dynamic Import) @ pjchender > Webpack 學習筆記
觀念
- 在模組內的程式總是會套用
strict mode
(因此不需要額外定義"use strict"
) - 模組並沒有共享全域空間,每一個模組都有一個屬於它自己的作用域(scope),和其他模組溝通時,需要透過
export
來暴露變數,沒有匯出的函式是無法使用的。
greet.js
// greet.js
function greet() {
console.log('Hello');
}
greet();
app.js
// app.js
/**
* 執行 app.js 時會出現 'Hello'
* 但是在 app.js 中並沒有辦法呼叫到 greet() 這個函式
**/
require('./greet.js');
基本語法
模組的匯入與匯出
匯出:使用 export {...}
// 匯出 calc.js
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;
export { str, fn, num };
匯入: 搭配 import {...} from ...
,大括號裡面的變量名,必須與被匯入的模組接口名稱相同
// 匯入,{ } 裡的變量名要與被匯入的 module 中的變數對應
import { str, fn, num } from './calc';
console.log('import', str, num);
console.log(fn(3, 4));
另外,匯入 module 時實際上仍參照到原本的變數,因此呼叫 onSale
時,price
的值會改變:
// 匯出 export.js
let price = 1000;
function onSale() {
price *= 0.9;
}
export { price, onSale };
// 匯入
import { price, onSale } from './export';
console.log('price', price); // 1000
onSale();
console.log('price', price); // 900
onSale();
console.log('price', price); // 810
as:為變數建立重新命名
匯出或匯入時均可使用 as
重新命名模組:
// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;
export { str as name, fn, num as year };
// 匯入
import { name, fn as multiply, year } from './calc';
console.log('import', name, year);
console.log(multiply(3, 2));
default
當匯入的 module 具有 default
時,就不需要知道原模組中輸出的函數名:
- 不論匯出的 function 有無名稱,匯入時都視為匿名函式;匯入時可以為該匿名函數指定任意名字
- 匯入時不使用大括號
{ }
(一般的匯入在匯入時需使用{ }
指定要匯入的變數名) export default
命令其實只是輸出一個as default
的變量
// 匯出
var multiply = (x, y) => x * y;
export default multiply; // 等同於 export { multiply as default }
// 匯入
/**
* 等同於 import { default as multiply } from './calc';
* 因為是 "as" 所以名稱可以自取,不用是 multiply
**/
import multiply from './calc'; //
console.log(multiply(3, 2));
- 由於
export default
命令其實只是輸出一個as default
的變量,所以它後面不能跟變量聲明語句:
// 正確
export var a = 1;
// 正確
var a = 1;
export default a;
// 錯誤
export default var a = 1;
另外,可以同時使用 export { }
和 export default
來同時匯出 default 和特定內容:
// 匯出 export.js
let price = 1000;
function onSale() {
price *= 0.9;
}
let defaultText = 'This is default text';
export { price, onSale };
export default defaultText;
// export { defaultText as default, price, onSale} // 等同於上面兩句
// 匯入
import defaultValue, { price, onSale } from './export';
console.log('defaultValue', defaultValue); // This is default text
console.log('price', price); // 1000
onSale();
console.log('price', price); // 900
import * as ...: 匯入該模組中的所有內容
// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;
export { str, fn, num };
匯入的內容會是一包物件:
// 匯入
import * as calc from './calc'; // calc 會是一包物件
console.log('import', calc.str, calc.num);
console.log(calc.fn(2, 6));
匯入(執行)模組但不賦予變數
使用 import './module'
可以匯入(執行)模組但不賦予變數:
// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;
console.log('calc'); // 這行會被執行
export { str, fn, num };
import 'lodash'; // 載入(執行)整個模組但不賦予變數
import './calc'; //
console.log('import', num); // undefined
在瀏覽器中使用 ESM
在瀏覽器中使用 module 有幾個需要留意的地方:
- 這種方式需要使用 serve 的方式,也就是只能用 HTTP(S) 的方式,不能直接點兩下用檔案的方式開啟。
- 載入的 module path 一定要寫完整的路徑並加副檔名,不能省略
.js
。
在瀏覽器中使用 module 時,預設載入的方式和 defer
一樣,它們會等到 DOM 準備好後,才開始「依序」執行:
<body>
<main>
<!-- ... -->
<!-- module 會依照順序被執行 -->
<script type="module" src="main.js"></script>
<script type="module" src="./1.js"></script>
<script type="module" src="./2.js"></script>
</main>
</body>
如果這個 module 和其他的 module 間沒有相依的關係,也可以使用 async
的方式,但只能用 inline 的方式:
<!-- https://javascript.info/modules-intro#async-works-on-inline-scripts -->
<body>
<main>
<!-- ... -->
<script async type="module">
import { counter } from './analytics.js';
counter.count();
</script>
</main>
</body>
在 Node.js 中使用 ESM
請到這裡。
其他範例
ES6
// ES6 匯出
const sum = (a, b) => a + b;
const multiply = (a, b) => a + b;
export default { sum, multiply };
export { sum, multiply };
// ES6 匯入
import math from './math'; // ES6,沒有用 { } ,表示匯入 default 內容
console.log(math.sum(3, 5));
console.log(math.multiply(3, 5));
匯出 Constructor
匯出前先 new
// Common JS 匯出 constructor
function SumIndex() {
this.index = 2;
this.sum = function () {
console.log(this.index + 2);
};
}
module.exports = new SumIndex();
// Common JS 匯入
const math = require('./math'); // 但是如果之後在重新載入這個 module,並不會在重新引用一次
math.sum();
匯出整個 class
// Common JS 匯出 constructor
function SumIndex() {
this.index = 2;
this.sum = function () {
console.log(this.index + 2);
};
}
module.exports = SumIndex;
// Common JS 匯入
const Math = require('./math'); // 但是如果之後在重新載入這個 module,並不會在重新引用一次
let math = new Math();
math.sum();
與 Common JS 語法的小比較
模組的匯入
// ES6 寫法:用 import
import { str, fn, num } from './calc';
// CommonJS 寫法:用 require
/**
* require 只會載入檔案一次然後存放在記憶體裡面, 所以不用怕效能的問題
* 如果載入的是檔案沒寫副檔名,預設會載 .js 檔
**/
const something = require('./something'); // 找不到 ./something 時自動尋找 ./something.js
const something = require('./something.js');
const something = require('something'); // 匯入 npm 模組
模組的匯出
/**
* ES6 寫法:用 export
**/
// profile.js
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;
export { str, fn, num };
/**
* CommonJS 寫法:用 module.exports
**/
const fn (options) = {
return (req, res, next) => {
// do something here
// console.log(options.name)
return next()
}
}
module.exports = fn
Code Style
當從同一個路徑匯入時,不要拆成多個 import
@ airbnb 10.4
// bad
import foo from 'foo';
import { named1, named2 } from 'foo';
// good
import foo, { named1, named2 } from 'foo';
將所有的 import 放在最上方
@ airbnb 10.7
由於 import 具有變量提升(hoisted)的特性,因此應該放在文件最上方:
// bad
import foo from 'foo';
foo.init();
import bar from 'bar';
// good
import foo from 'foo';
import bar from 'bar';
foo.init();
命名規則
- 小寫駝峰(camelCase):使用
export-default
時使用小寫駝峰命名與建檔 - 大寫駝峰(PascalCase):當匯出的是純物件(pure object )、建構式(constructor)、類(class)、singleton 或 function library:
23.7 @ airbnb
檔名應該和 export default 的名稱相同
// file 1 contents
class CheckBox { ... }
export default CheckBox;
// file 2 contents
export default function fortyTwo() { return 42; }
// file 3 contents
export default function insideDirectory() {}
import CheckBox from './CheckBox';
import fortyTwo from './fortyTwo';
import insideDirectory from './insideDirectory';
23.6 A base filename should exactly match the name of its default export. @ airbnb
參考資料
- 模組的語法 @ ECMAScript 6 入門 by 阮一峰
- Import and Export @ MDN
- Modules @ Exploring ES6
- Accelerated ES6 JavaScript Training @ Udemy
- Module Pattern @ PJCHENder Gist