深刻理解ES6 Modules

深刻了解 ES6 Modules

當下, 咱們幾乎全部的項目都是基於 webpack、rollup 等構建工具進行開發的,模塊化已是常態。javascript

咱們對它並不陌生,今天,咱們就再系統的回顧一下ES6的模塊機制, 並總結下經常使用的操做和最佳實踐, 但願對你有所幫助。html

一些簡單的背景

隨用隨取, 是一種咱們都但願實現的機制。前端

在 Javascript 中也同樣,把一個大的 Javascript 程序分割成不一樣的部分, 哪一個部分要被用到,就取那一部分。java

在很長一段時間內, NodeJS 擁有這樣的能力, 後來, 愈來愈多的庫和框架也擁有了模塊化的能力, 好比 CommonJS, 或者基於AMD模型的實現(好比RequireJs),還有後續的Webpack, Babel等。webpack

到2015年,一個標準的模塊化系統誕生了,這就是咱們今天要說的主角 - ES6 模型系統。 es6

一眼看上去, 咱們不發現, ES6的模型系統和CommonJS語法很是的類似,畢竟ES6 的模型系統是從CommonJS時代走過來的, 深受CommonJS 影響。 web

看個簡單的例子,好比在CommonJs中: (https://flaviocopes.com/commo...express

//file.js
module.exports = value;

// 引入value
const value = require('file.js')

而在ES6中:segmentfault

// const.js
export const value = 'xxx';


import { value } from 'const.js'

語法是很是類似的。promise

下面咱們就主要看 import 和 export,和幾個相關的特性,瞭解ES6 Modules的更多方面。

模塊化的好處

模塊化的好處主要是兩點:

1. 避免全局變量污染
2. 有效的處理依賴關係

隨着時代的演進, 瀏覽器原生也開始支持es6 import 和 export 語法了。

image.png

先看個簡單的例子:

<script type="module">
  import { addTextToBody } from '/util.js';

  addTextToBody('Modules are pretty cool.');
</script>

// util.js 
export function addTextToBody(text) {
  const div = document.createElement('div');
  div.textContent = text;
  document.body.appendChild(div);
}

若是要處理事件,也是同樣, 看個簡單的例子:

<button id="test">Show Message</button>
<script type="module" crossorigin src="/showImport.js"></script>

// showImport.js
import { showMessage } from '/show.js'

document.getElementById('test').onclick = function() {
  showMessage();
}

// show.js
export function showMessage() {
  alert("Hello World!")
}

若是你想跑這個demo, 注意要起個簡單的服務:

$ http-server

不然,你會看到一個CORS拋錯。

至於拋錯的具體緣由和其餘細節,不是本文討論的重點, 感興趣的能夠閱讀以下連接瞭解詳情。

https://jakearchibald.com/201...

嚴格模式

https://developer.mozilla.org...

'use strict' 聲明咱們都不陌生, 在es5 時代咱們也常用, 通常是在文件頂部加這個聲明,目的就是禁用Javascript中不太友好的一部分,有助於咱們寫更嚴謹的代碼。

這個特性,在es6語法中是默認開啓的, 若是代碼裏面有不太嚴格的代碼,則會報錯,例如:

嚴格模式下JS報錯

下面是我從MDN中摘取的一些在嚴格模式中被禁用的部分:

  • Variables can’t be left undeclared
  • Function parameters must have unique names (or are considered syntax errors)
  • with is forbidden
  • Errors are thrown on assignment to read-only properties
  • Octal numbers like 00840 are syntax errors
  • Attempts to delete undeletable properties throw an error
  • delete prop is a syntax error, instead of assuming delete global[prop]
  • eval doesn’t introduce new variables into its surrounding scope
  • eval and arguments can’t be bound or assigned to
  • arguments doesn’t magically track changes to method parameters
  • arguments.callee throws a TypeError, no longer supported
  • arguments.caller throws a TypeError, no longer supported
  • Context passed as this in method invocations is not 「boxed」 (forced) into becoming an Object
  • No longer able to use fn.caller and fn.arguments to access the JavaScript stack
  • Reserved words (e.g protected, static, interface, etc) cannot be bound

exports 的幾種用法

ES6模塊只支持靜態導出,你只能夠在模塊的最外層做用域使用export,不可在條件語句中使用,也不能在函數做用域中使用。

從分類上級講, exports 主要有三種:

  1. Named Exports (Zero or more exports per module)
  2. Default Exports (One per module)
  3. Hybrid Exports

exports 總覽:

// Exporting individual features
export let name1, name2, …, nameN; // also var, const
export let name1 = …, name2 = …, …, nameN; // also var, const
export function functionName(){...}
export class ClassName {...}

// Export list
export { name1, name2, …, nameN };

// Renaming exports
export { variable1 as name1, variable2 as name2, …, nameN };

// Exporting destructured assignments with renaming
export const { name1, name2: bar } = o;

// Default exports
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };

// Aggregating modules
export * from …; // does not set the default export
export * as name1 from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;

下面我就介紹一下常見的 exports用法。

1. Named exports (導出每一個函數/變量)

具名導出,這種方式導出多個函數,通常使用場景好比 utils、tools、common 之類的工具類函數集,或者全站統一變量等。

只須要在變量或函數前面加 export 關鍵字便可。

//------ lib.js ------
export const sqrt = Math.sqrt;

export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js 使用方式1 ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

//------ main.js 使用方式2 ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

咱們也能夠直接導出一個列表,例如上面的lib.js能夠改寫成:

//------ lib.js ------
const sqrt = Math.sqrt;
function square(x) {
    return x * x;
}
function add (x, y) {
    return x + y;
}
export { sqrt, square, add }

2. Default exports (導出一個默認 函數/類)

這種方式比較簡單,通常用於一個類文件,或者功能比較單一的函數文件使用。

一個模塊中只能有一個export default默認輸出。

export default與export的主要區別有兩個:

不須要知道導出的具體變量名, 導入(import)時不須要{}.

//------ myFunc.js ------
export default function () {};

//------ main.js ------
import myFunc from 'myFunc';
myFunc();

導出一個類

//------ MyClass.js ------
class MyClass{}

export default MyClass;

//------ Main.js ------
import MyClass from 'MyClass';

注意這裏默認導出不須要用{}。

3. Mixed exports (混合導出)

混合導出,也就是 上面第一點和第二點結合在一塊兒的狀況。比較常見的好比 Lodash,都是這種組合方式。

//------ lib.js ------
export var myVar = ...;
export let myVar = ...;
export const MY_CONST = ...;

export function myFunc() {
  // ...
}
export function* myGeneratorFunc() {
  // ...
}
export default class MyClass {
  // ...
}

// ------ main.js ------
import MyClass, { myFunc } from 'lib';

再好比lodash例子:

//------ lodash.js ------
export default function (obj) {
  // ...
};
export function each(obj, iterator, context) {
  // ...
}
export { each as forEach };

//------ main.js ------
import _, { forEach } from 'lodash';

4. Re-exporting (別名導出)

通常狀況下,export輸出的變量就是在原文件中定義的名字,但也能夠用 as 關鍵字來指定別名,這樣作通常是爲了簡化或者語義化export的函數名。

//------ lib.js ------
export function getUserName(){
  // ...
};
export function setName(){
  // ...
};

//輸出別名,在import的時候能夠同時使用原始函數名和別名
export {
  getName as get, //容許使用不一樣名字輸出兩次
  getName as getNameV2,
  setName as set
}

5. Module Redirects (中轉模塊導出)

有時候爲了不上層模塊導入太多的模塊,咱們可能使用底層模塊做爲中轉,直接導出另外一個模塊的內容以下:

//------ myFunc.js ------
export default function() {...};
 
//------ lib.js ------
export * from 'myFunc';
export function each() {...};
 
//------ main.js ------
import myFunc, { each } from 'lib';

export 只支持在最外層靜態導出、只支持導出變量、函數、類,以下的幾種用法都是錯誤的。

`錯誤`的export用法:

//直接輸出變量的值
export 'Mark';

// 未使用中括號 或 未加default
// 當只有一個導出數,需加default,或者使用中括號
var name = 'Mark';
export name;

//export不要輸出塊做用域內的變量
function () {
  var name = 'Mark';
  export  { name };
}

import的幾種用法

import的用法和export是一一對應的,可是import支持靜態導入和動態導入兩種方式,動態import支持晚一些,兼容性要差一些。

image.png

下面我就總結下import的基本用法:

1. Import All things

當export有多個函數或變量時,如文中export的第一點,可使用 * as 關鍵字來導出全部函數及變量,同時 as 後面跟着的名稱作爲 該模塊的命名空間。

//導出lib的全部函數及變量
import * as lib from 'lib';

//以 lib 作爲命名空間進行調用,相似於object的方式
console.log(lib.square(11)); // 121

2. Import a single/multiple export from a module

從模塊文件中導入單個或多個函數,與 * as namepage 方式不一樣,這個是按需導入。以下例子:

//導入square和 diag 兩個函數
import { square, diag } from 'lib';

// 只導入square 一個函數
import { square } from 'lib';

// 導入默認模塊
import _ from 'lodash';

// 導入默認模塊和單個函數,這樣作主要是簡化單個函數的調用
import _, { each } from 'lodash';

3. Rename multiple exports during import

和 export 同樣,也能夠用 as 關鍵字來設置別名,當import的兩個類的名字同樣時,可使用 as 來重設導入模塊的名字,也能夠用as 來簡化名稱。
好比:

// 用 as 來 簡化函數名稱
import {
  reallyReallyLongModuleExportName as shortName,
  anotherLongModuleName as short
} from '/modules/my-module.js';

// 避免重名
import { lib as UserLib} from "alib";
import { lib as GlobalLib } from "blib";

4. Import a module for its side effects only

有時候咱們只想import一個模塊進來,好比樣式,或者一個類庫。

// 導入樣式
import './index.less';

// 導入類庫
import 'lodash';

5. Dynamic Imports

靜態import在首次加載時候會把所有模塊資源都下載下來.

咱們實際開發時候,有時候須要動態import(dynamic import)。

例如點擊某個選項卡,纔去加載某些新的模塊:

// 當動態import時,返回的是一個promise
import('lodash')
  .then((lodash) => {
    // Do something with lodash.
  });

// 上面這句實際等同於
const lodash = await import('lodash');

es7的新用法:

async function run() {
    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'),
        ]);
}

run();

總結

以上, 我總結了ES6 Module 的簡單背景和 常見的import , export 用法, 但這遠遠不是它的所有, 篇幅有限,若是想了解更多, 能夠看下面的延伸閱讀部分(質量都還不錯, 能夠看看)。

最後

若是以爲內容有幫助,能夠關注個人公衆號 「 前端e進階 」,掌握最新資訊,一塊兒學習, 一塊兒成長!

clipboard.png

延伸閱讀:

ECMAScript 6 modules: the final syntax

JavaScript modules

dynamic-import

Node中沒搞明白require和import,你會被坑的很慘

https://www.zhangxinxu.com/wordpress/2018/08/browser-native-es6-export-import-module/

相關文章
相關標籤/搜索