1.為什麼需要模組化
隨著前端應用的日益複雜,我們的專案程式碼已經逐漸膨脹到了不得不花大量時間去管理的程度了。而模組化就是一種最主流的程式碼組織方式,它透過把複雜的程式碼按照功能的不同劃分為不同的模組單獨維護,從而提高開發效率、降低維護成本。模組化可以使你能夠更容易地重用程式碼。你可以建立一個模組來完成一個特定的功能,然後在多個地方重用這個模組,而不是複製和貼上程式碼。
2.沒有工具和規範時模組化的演進歷史
2.1檔案劃分
最早期的模組化是透過檔案劃分的方式,將不同的檔案劃分為不同的模組,一個檔案就對應一個模組,如下圖就有2個模組a和b。
想要使用模組的時候就用script標籤引入該模組
<script src="module-a.js"></script>
<script src="module-b.js"></script>
這種方式存在的問題
- 難以管理模組之間的依賴關係
- 多個模組的變數名會出現衝突
- 外部可以修改模組的內容
2.2名稱空間
為了解決以上出現的問題,又有了一種新的模組化方式,便是名稱空間,透過將每個模組包裹成一個全域性物件來實現,這樣的確解決了命名衝突問題,但是仍然存在外部可以修改模組內部內容的問題
使用模組
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
//模組成員可以被修改
moduleA.name = 'foo‘
</script>
2.3立即執行函式
用立即執行函式實現了私有成員的方式,外部無法修改內部的變數,透過掛載到window物件上來完成模組化的暴露
3.模組化規範
前面所提到的幾種早期模組化方式都有一個問題,就是必須透過script指令碼標籤來使用模組,但是如果隨著專案規模的增大,忘記加入script標籤或者引入了已經刪除的模組,就會出現一些問題。也就是說,最好要把引入模組化這個工作放到js程式碼中去完成,而不只是在html中引入
3.1 CommonJS
NodeJS裡的CommonJS規範是一個很好的模組化方式,CommonJS包含以下幾個特徵
- 一個檔案就是一個模組
- 每個模組都有單獨的作用域
- 透過 module.exports 匯出成員
- 透過 require 函式載入模組
特徵中的第4個,require是同步的載入,在Node中只會在啟動的時候載入,執行的時候只是去使用,而到了瀏覽器端,每一次重新整理頁面都會導致大量的同步模式請求出現,這就無法使用了。
3.2 AMD(Asynchronous Module Definition)
AMD(Asynchronous Module Definition)是 RequireJS 在推廣過程中對模組定義的規範化產出,。由於不是JavaScript原生支援,使用AMD規範進行頁面開發需要用到對應的庫函式,也就是require.js。
AMD這個規範約定每一個模組都必須透過 define 這個函式定義,預設可以接收兩個引數,也可以傳遞三個引數:
- 第一個引數是模組的名字;
- 第二個引數是一個陣列,用於宣告模組依賴項;
- 第三個引數是一個函式,函式的引數與前面的依賴項一一對應,每一項分別為依賴項這個模組匯出的成員,這個函式的作用可以以理解為為當前的這個模組提供一個私有的空間。如果需要在這個模組當中向外部匯出一些成員,可以透過 return 實現
AMD也可以透過require方法來載入對應的模組,require與define的區別是,require只是用來載入,而define是定義一個模組
案例
src
├── index.html
├── index.js
├── lib
│ └── require.js // 使用require.js 庫
└── modules
├── dataServe.js
└── example.js
- dataServe
// 匯入example
define(['example'], function (example) {
let msg = "data"
function showMsg () {
console.log(msg, example.getName());
}
return { showMsg }
})
- example.js
define(function () {
let name = "w"
function getName () { return name }
return { getName }
})
- index.js
(function () {
requirejs.config({
paths: {
example: './modules/example',
dataServe: './modules/dataServe'
}
})
requirejs(['dataServe'], function (d) {
d.showMsg()
})
})()
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script data-main="./index.js" src="lib/require.js"></script>
</body>
</html>
問題:
- 使程式碼複雜度提高
- 如果模組劃分的過於細緻,同一個頁面的請求會過多,頁面效率低下
3.3 CMD+Sea.js
與CommonJS基本保持一致,但是後來也被require.js相容了
3.4 ES Module
ES Module是現在最常用的模組化解決方案,仍然採用了與CommonJS相似的import和export來完成模組的匯入和匯出
在html中,只需要在script標籤里加入type="module"就可以匯入模組
<script type="module"> console.log('this is es module') </script>
與普通script標籤不同的地方是:
- es模組會自動開啟嚴格模式,忽略掉use strict。
- es模組都有單獨的作用域
- es模組透過CORS方式請求,如果請求的資源不支援CORS會報跨域錯誤
- es模組等於在指令碼上加入defer屬性,讓指令碼等同步內容載入完後非同步按順序執行
<script type="module">
//es模組會自動開啟嚴格模式
console.log(this); //undefined
</script>
<script type="module">
//es模組都有單獨的作用域
let a = 1
console.log(a); //1
</script>
<script type="module">
//es模組都有單獨的作用域
let a = 2
console.log(a); //2
</script>
匯出:使用export關鍵詞來完成匯出
普通匯出:
方式一 用{}包裹需要匯出的變數,函式或者類,如果想要改名,可以在匯出時用as來改
const name = "why";
const age = 18;
function sum(a, b) {
return a + b;
}
class Person {
constructor(name) {
this.name = name;
}
}
//3.統一匯出時使用as關鍵字給變數起別名
export { name as bName, age, sum as bSum, Person };
方式二 export直接放在變數,函式,類宣告之前
export const name = "why";
export const age = 18;
export function sum(a, b) {
return a + b;
}
export class Person {
constructor(name) {
this.name = name;
}
}
預設匯出
方式一:不使用{}包裹變數,函式,類
const height = 1.88;
export default height;
方式二:使用{}包裹變數,函式,類,但必須透過as改變名字為default
const height = 1.88;
export {
height as default
};
匯入:使用import關鍵詞來完成匯入
方式一:分別匯入,可以透過as來起別名
import {
name as barName,
age,
sum,
Person as BarPerson,
} from "./bar.js";
方式二:整體匯入,透過as來起別名,然後分別使用
import * as baz from "./baz.js";
console.log(baz.name, baz.age);
baz.sum(1, 2);
const person2 = new baz.Person("lily");
console.log(person2);
方式三:匯入預設匯出的變數,不加{}包裹
import height from "./demo.js";
console.log(height);
匯入匯出注意點:
- ES Module匯出的變數並非變數的值本身,而是一個引用,所以匯入的變數的值會受原模組的影響
- 匯入的變數是隻讀的,不能進行賦值更改
- import非同步實現,會有一個獨立的模組依賴的解析階段
- 不能與CommonJS相似地在匯入路徑中省略.js(可透過打包配置改善)
舉例:匯入的變數的值會受原模組的影響
在匯入中使用匯出:把import from改成export from
常用於集中匯出,方便後續匯入資源
與CommonJS的互動
在node環境下,雖說一般都是CommonJS規範的模組化,但是node也做了相容可以讓ES Module正常使用,只要把原來的.js檔案改為.mjs就可以正常使用import語法了。import匯入的時候還可以匯入CommonJS的模組,只是所有CommonJS模組都會被當作預設匯出的方式來匯入。但是在CommonJS裡面,無法使用require去匯入ES Module匯出的內容,也就是在下面的b.js裡面會報錯
- a.mjs
import b from './b.js'
console.log(b.name); // 1234
export let a = 4
- b.js
const a = require('./a.mjs') // 報錯
module.exports = {
name: '1234'
}