本文作者:高峰,360奇舞團前端工程師,W3C WoT工作組成員。
ES6中引入了模塊(Modules)的概念,相信大家都已經挺熟悉的了,在日常的工作中應該也都有使用。
本文會簡單介紹一下ES模塊的優點、基本用法以及常見問題。
着重介紹3個使用ES模塊的常見問題:
-
如何在瀏覽器中下快速使用export/import?
-
如何在Node下快速使用export/import?
-
當心,不要修改export輸出的對象,盡管你能改
一、ES模塊的優點
ES模塊的引入主要有以下幾個優點:
-
可以將代碼分割成功能獨立的更小的文件。
-
有助於消除命名沖突。
-
不再需要對象作為命名空間(比如Math對象),不會污染全局變量。
-
ES6 模塊在編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量,從而可以進行靜態優化。
二、ES模塊的基本用法
模塊功能中主要有以下幾個關鍵詞:export、import、as、default、*。
-
export用於規定輸出模塊的對外接口
-
import用於輸入模塊提供的接口
-
as用於重命名輸出和輸入接口
-
default用於指定模塊輸出的默認接口
-
*表示輸入模塊的所有接口。
2.1 export
2.1.1 常規用法
export輸出規定模塊的對外接口,有四種常規用法:
// 用法1:直接輸出一個變量聲明、函數聲明或者類聲明
export var m = 1;
export function m() {};
export class M {};
// 用法2:輸出內容為大括號包裹的一組變量,
// 注意不要被迷惑,export不能直接輸出常規的對象,下面會給出錯誤示例。
var m1 = 1;
var m2 = 2;
export {m1, m2};
// 用法3:輸出指定變量,並重命名,則外部引入時得到的是as后的名稱。
var n = 1;
export {n as m};
// 用法4:使用default輸出默認接口,default后可跟值或變量
export default 1;
var m = 1
export default m;
2.1.2 錯誤用法
需要注意的是,在使用export時會經常出現以下錯誤用法。如下代碼所示:
// 用法1
export 1;
export {m: '1'};
// 用法2
var m = 1;
export m;
// 用法3
function foo() {
export default 'bar' // SyntaxError
}
其中錯誤用法1和用法2相同,export必須輸出一個接口,不能輸出一個值(哪怕是對象也不行)或者一個已賦值的變量,已賦值的變量對應的也是一個值。上述常規用法中,export default后之所以可以直接跟值是因為default為輸出的接口。
錯誤用法3是因為export只能出現在模塊的頂層作用域,不能存在塊級作用域中。如果出現在塊級作用域的話,就沒法做靜態優化了,這違背ES6中模塊的設計初衷了。
2.2 import
import命令用於引入模塊提供的接口,有以下幾種常見用法:
// 用法1:僅執行 my_module 模塊,不輸入任何值(可能沒啥用,但是是合法的)
import 'my_module';
// 用法2:輸入 my_module 的默認接口, 默認接口重命名為 m
import m from 'my_module';
// 用法3:輸入 my_module 的 m 接口
import { m } from 'my_module';
// 用法4:輸入 my_module 的 m 接口,使用as重命名m接口
import { m as my_m} from 'my_module';
// 用法5:導入所有接口
import * as all from 'my_module';
需要注意的是,如果多次重復執行同一句import語句,那么只會執行一次,而不會執行多次。如下兩種均不會多次執行。
// 用法1:重復引入 my_module,只執行一次
import 'my_module';
import 'my_module';
// 用法2:多次引入不同的接口,只執行一次
import { m1 } from 'my_module';
import { m2 } from 'my_module';
此外,import命令輸入的變量都是只讀的,加載后不能修改接口。
import { m } from 'my_module';
m = 1; // SyntaxError: "m" is read-only
如果m是一個對象,改寫m的屬性是可以的。但是筆者不建議這么做,具體內容第三部分會詳細說。
錯誤用法
需要注意的是,import也必須在頂級作用域內,並且其中不能使用表達式和變量。其常見的錯誤用法示例如下:
// 用法1:不能使用表達式
import { 'm' + '1' } from 'my_module';
// 用法2:不能使用變量
let module = 'my_module';
import { m } from module;
// 用法3:不能用於條件表達式
if (x === 1) {
import { m } from 'module1';
} else {
import { m } from 'module2';
}
三、常見的使用問題
3.1 如何在瀏覽器中下快速使用import?
各大瀏覽器已經開始逐步支持ES模塊了,如果我們想在瀏覽器中使用模塊,可以在script標簽上添加一個type="module"的屬性來表示這個文件是以module的方式來運行的。如下:
// myModule.js
export default {
name: 'my-module'
}
// script腳本引入
<script type="module">
import myModule from './myModule.js'
console.log(myModule.name) // my-module
</script>
不過,由於ES的模塊功能還沒有完全支持,在不支持的瀏覽器下,我們需要一些回退方案,可以通過nomodule屬性來指定某腳本為回退方案。如下,在支持的瀏覽器中進行提示。
<script type="module">
import myModule from './myModule.js'
</script>
<script nomodule>
alert('你的瀏覽器不支持ES模塊,請先升級!')
</script>
如上,當瀏覽器支持type=module時,會忽略帶有nomodule的script;如果不支持,則忽略帶有type=module的腳本,執行帶有nomodule的腳本。
在使用type=module引入模塊時還有一點需要注意的,module的文件默認為defer,也就是說該文件不會阻塞頁面的渲染,會在頁面加載完成后按順序執行。
3.2 如何在Node下快速使用export/import?
相信大家都遇到過如下錯誤:

當我們直接在node下執行包含ES模塊的的代碼時,就會看到如上報錯,這是因為Node還沒有原生支持ES模塊。但有的時候我們又想在Node下使用,那么該如何做呢?
下面介紹兩種快捷的方法,一種是Node原生支持的,一種需要借助Babel進行編譯。
3.2.1 Node原生支持
Node從9.0版本開始支持ES模塊,可以在flag模式下使用ES模塊,不過這還處於試驗階段(Stability: 1 - Experimental)。其用法也比較簡單,執行腳本或者啟動時加上--experimental-modules即可。不過這一用法要求import/export的文件后綴名必須為*.mjs。
node --experimental-modules test-my-module.mjs
// test-my-module.mjs
import myModule from './myModule.mjs'
console.log(myModule.name) // my-module
這是Node原生支持的方法,但是對文件的后綴名有限制,但是現階段,我們在項目中的代碼應該還是以.js為后綴居多,所以大多數情況下我們還是會通過編譯使用ES模塊。
下面我們就介紹下如何快速編譯並使用ES模塊。
3.2.2 借助babel-node執行包含ES模塊代碼的文件
平時我們可能會借助構建工具對ES模塊,可能是借助Webpack/Rollup等構建工具進行編譯,這些工具配置起來都相對繁瑣。
有時,我們只想簡單的執行某些代碼,而其中又包含ES模塊代碼,就會發生問題,因為node默認不支持。這時候如果進行一堆配置來使其支持的話,又太過麻煩。
下面我給大家介紹一種看起來更加快捷的方法。
-
安裝babel-cli和babel-preset-env,並將其保存為開發依賴。
-
在根目錄創建.babelrc文件,在其中添加如下配置。
{
"presets": ["env"]
}
-
通過./node_modules/.bin/babel-node index.js或npx babel-node index.js執行腳本。其中babel-node為babel-cli自帶。
怎么樣,是不是相當快捷了,而且近乎於0配置。
3.3 當心,不要修改export輸出的對象
前面有提到如果export輸出的接口是一個對象,那么是可以修改這個對象的屬性的。
而我的建議是,盡管你能改,也不要修改。
大家可能都會有這樣一個常規的用法,即在編寫某個組件時,可能會存在包含基礎配置的代碼,我們姑且稱其為options.js,其輸出一堆配置文件。
// options.js
export default {
// 默認樣式
style: {
color: 'green',
fontSize: 14,
}
}
如果你沒有類似需求,你可以想象下,你現在要把EChart的某個圖表抽象成自己代碼庫里的組件,那么這時候應該就有一大堆基礎配置文件了。
既然稱其為基礎配置,那么言外之意就是,根據組件的用法不同,會一定程度上對配置進行修改。比如我們會在引入后將顏色改為紅色。
// use-options.js
import options from "./options.js";
console.log(options); // { style: { color: 'green', fontSize: 14 } }
options.style.color = "red";
這時候就需要格外注意了,如果我們直接對輸入的默認配置對象進行修改,就可能會導致一些bug。
因為export輸出的值是動態綁定的,如果我們修改了其中的值,就會導致其他地方再次引入該值時會發生變化,此時的默認配置就不是我們所設想的默認配置了。如上例,我們再次引入基礎配置后,就會發現顏色的默認值已經變成紅色了。
// use-options-again.js
import useOptions from "./use-options.js
import options from "./options.js";
console.log(options); // { style: { color: 'red', fontSize: 14 } }
所以,筆者建議,當我們有需求對輸入的對象接口進行改變時,可以先對其進行深度復制,然后在進行修改,這樣就不會導致上述所說的問題了。如下所示:
// use-options.js
import _ from "./lodash.js";
import options from "./options.js";
const myOptions = _.cloneDeep(options);
console.log(myOptions); // { style: { color: 'green', fontSize: 14 } }
myOptions.style.color = "red";
四、總結
本文只是簡單點的介紹了下ES模塊的基本用法,還有一些用法,如import和export的結合使用等,這些大家可以結合MDN或者其他網站進行了解。本文主要是介紹了以下筆者及身邊的同事在使用ES模塊時會存在的一些疑問,希望對大家有一點幫助。
參考內容
-
Export | MDN
-
Import | MDN
-
7 Different Ways to Use ES Modules Today!
-
Import, Export, Babel, and Node
