前言:在寫Vue代碼的時候,經(jīng)常會看到import、export、require等關鍵字,這次特地學習了解一下,等用到的時候避免出問題。
1. 模塊化的概念
- 模塊化就是將程序劃分成一個個小的模塊;
- 每個模塊中有自己的邏輯代碼,變量有自己的作用域;
- 其它模塊可以使用自己暴露的變量、函數(shù)、對象等;
- 也可以通過某種方式,導入其它模塊中的變量、函數(shù)、對象等;
按照這種結構劃分開發(fā)程序的過程,就是模塊化開發(fā)的過程;
網(wǎng)頁開發(fā)早期,js僅作為一種腳本語言,并不復雜,只需要將js代碼寫到
<script>標簽中即可,沒有必要放到多個文件中來編寫。但是現(xiàn)在,js越來越復雜了:
- ajax的出現(xiàn),前后端開發(fā)分離,后端返回數(shù)據(jù)后,前端需要通過js進行頁面渲染;
- SPA(單頁面應用)的出現(xiàn),使前端頁面變的很復雜,包括路由、狀態(tài)管理等復雜的需求,需要js實現(xiàn);
- Node的實現(xiàn),js編寫復雜的后端程序,沒有模塊化是硬傷;
所以,模塊化是前端技術發(fā)展的必要產(chǎn)物。但是js本身,直到ES6(2015)才推出了自己的模塊化方案。在此之前,為了讓js支持模塊化,出現(xiàn)了很多不同的模塊化規(guī)范:AMD、 CMD、 CommonJS等。
既然稱之為模塊化,那么模塊需要支持導出和導入,需要有相應的方法或者關鍵字配合實現(xiàn),下面就是通過模塊的導出和導入進行學習。
2. CommonJS 和 Node
2.1 CommonJS介紹
CommonJS是一個規(guī)范,最初是在瀏覽器以外的地方使用,被命名為ServerJS,后來為了提現(xiàn)它的廣泛性,改成了CommonJS,平時也會簡稱為CJS。
- Node 是CommonJS在服務器端一個具有代表性的實現(xiàn);
- Browserify是CommonJS在瀏覽器中的一種實現(xiàn);
- webpack打包工具具備CommonJS的支持和轉換;
所以,Node中對CommonJS進行了支持和實現(xiàn),讓我們在開發(fā)node的過程中可以方便的進行模塊化開發(fā):
- Node中每個js文件都是單獨的模塊;
- 這個模塊中包括CommonJS規(guī)范的核心變量
exports、module.exports、require;
可以使用這些關鍵字來進行模塊化開發(fā):
- exports 和 module.exports用于對模塊中的內容進行導出;
- require函數(shù)用于導入其它模塊(自定義模塊、系統(tǒng)模塊、第三方庫模塊);
2.2 使用exports導出
test.js文件:
// 每一個js文件就是一個模塊
const name = "張三";
// exports默認是空對象
console.log(exports);
// 定義一個方法
function callName(name){
console.log(name);
}
// 導出
exports.name = name;
exports.callName = callName;
main.js文件:
// 可以直接給一個變量賦值,使用的時候需要test.name,這個test就是test.js中的exports對象
// const test = require('./test.js');
// 也可以部分導出
const {name, callName} = require('./test.js');
console.log(test);
console.log(name);
callName("李四"); // 調用方法
在每個文件中都有一個exports對象,在其它文件導入某個文件時,其實就是拿到該對象的內存地址,如下圖所示:

把上面的代碼修改并驗證一下:
test.js:
// 每一個js文件就是一個模塊
const name = "張三";
// exports默認是空對象
console.log(exports);
function callName(name){
exports.name = name;
}
// 導出
exports.name = name;
exports.callName = callName;
main.js:
const test = require('./test.js');
test.callName("李四");
console.log(test.name);
輸出:
李四
可以發(fā)現(xiàn),在main.js文件中調用callName方法,修改了test.js文件中的exports.name的值,最后在main.js中輸出的結果為:李四,說明test對象確實是對exports對象的淺拷貝(引用賦值)。
但是,如果使用const {name, callName} = require('./test.js');這種方式導出,main.js中修改name的值,不會影響到test.js中的name,因為這種只是導入了name的值,上面是導入了exports這個對象。
2.3 使用module.exports導出
上面學習了exports,看樣子能完全滿足我們日常開發(fā)了,為什么還會有module.exports呢?
通過維基百科中對CommonJS規(guī)范的解析:
- CommonJS中是沒有module.exports的概念的;
- 但是為了實現(xiàn)模塊的導出,Node中使用的是Module類,每一個模塊都是Module類的一個實例,也就是一個js文件就是一個Module類實例;
- 所以在Node中真正用于導出的其實不是exports,而是module.exports;
- 因為module才是導出的真正實現(xiàn)者;
把一個文件當成一個對象的時候,Node底層就會進行new module,實際上是做了這么一步操作:module.exports = exports,所以上面的test = exports = module.exports。
把上面的代碼修改,再驗證一下:
test.js:
// 每一個js文件就是一個模塊
const name = "張三";
// exports默認是空對象
console.log(exports);
function callName(name){
module.exports.name = name;
}
// 導出
exports.name = name;
exports.callName = callName;
main.js:
const test = require('./test.js');
test.callName("李四");
console.log(test.name);
輸出:
李四
通過上面的代碼驗證,上面test = exports = module.exports是成立的。
2.3 require函數(shù)的細節(jié)
require是一個函數(shù),用來引入一個文件(模塊)中導出的對象。
require的加載過程是同步的,所以必須等到引入的文件(模塊)加載完成之后,才會繼續(xù)執(zhí)行其它代碼,會產(chǎn)生阻塞現(xiàn)象,因為引入一個文件,則該文件內部的所有代碼都會被執(zhí)行一次。
2.3.1 支持動態(tài)導入
動態(tài)導入就是可以在js語句中,使用require語法,如下所示:
let lists = ["./index.js", "./config.js"]
lists.forEach((url) => require(url)) // 動態(tài)導入
if (lists.length) {
require(lists[0]) // 動態(tài)導入
}
2.3.2 require(x)的查找規(guī)則:
x是一個核心模塊,比如path、http:
直接返回核心模塊,停止查找;x是以./ 或 ../ 開頭的
a. 將x當做一個文件在對應的目錄下查找,如果沒有寫明后綴名,則按照:x->x.js->x.json->x.node 進行查找;
b. 沒有找到對應的文件,將x作為一個目錄,查找目錄下邊的index文件,按照x/index.js->x/index.json->x/index.node進行查找;直接是一個x,并且x不是核心模塊
例如我在main.js中編寫了require('test’),它會逐級查找上一層目錄下的node_modules。
如果都沒有找到,則報錯:not found。
2.4 模塊的加載過程
- 模塊在第一次被引入的時候,模塊的js代碼會被運行一次;
- 模塊被多次引入時,會進行緩存,只執(zhí)行一次(每個模塊對象module都有一個屬性:loaded用來標記是否已經(jīng)加載過)
- 如果有循環(huán)引入,那加載順序是什么?
順序為:圖結果的深度優(yōu)先算法

3. ES Module
3.1 介紹
ES Module 是ES6推出的,即ES 2015。并且自動采用嚴格模式:use strict。
但是ES Module和CommonJS的模塊化有一些不同:
- 使用import 和 export關鍵字,不是模塊也不是函數(shù);
- 采用編譯器的靜態(tài)分析,也加入了動態(tài)引用的方式;
ES Module模塊采用export和import關鍵字來實現(xiàn)模塊化:
- export負責將模塊內的內容導出;
- import負責從其它模塊導入內容;
3.2 使用ES Modules
在瀏覽器使用ES Modules時,要在script標簽上加上type="module",且要在服務器上運行,不支持本地運行的file協(xié)議(觸發(fā)CORS)。
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 src="./index.js" type="module"></script>
</body>
</html>
index.js:
console.log('hello EsModules');
輸出:
hello EsModules
3.3 使用export和import
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 src="./index.js" type="module"></script>
</body>
</html>
index.js 導入:
console.log('hello EsModules');
// 常見的導入方式
// 方式一: import {} from '路徑'
// 注意此處的{}不是對象,導入時后邊必須要加.js,腳手架里和webpack會自動加
// import { name, age, sayhello } from './modules/foo.js'
// 方式二:導出的變量可以起別名
// import { name as Fname, age as Fage, sayhello as FsayHello } from './modules/foo.js'
// 2.1 導出時已經(jīng)起了別名的,接收要使用別名接受,可以給別名再起別名
// import {Fname as FooName,Fage as FooAge,FsayHello as FooSayHello} from './modules/foo.js'
// 方式三:import * as foo from '路徑'
import * as foo from './modules/foo.js'
console.log(foo.name);
console.log(foo.age);
foo.sayHello('彭先生')
foo.js 導出:
const name = 'pengsir'
const age = 18
const sayHello = function (name) {
console.log('姓名' + name);
}
// 1.導出方式
// 方式一:
// export const name = 'pengsir'
// export const age = 18
// export const sayHello = function (name) {
// console.log('姓名' + name);
// }
// 方式二: 常用?。。。。。?// {} 這里不是類 就和 if(){} 的大括號一樣
// {放置要導出變量的引用列表}
export {
name,
age,
sayHello
}
// 方式三:{} 導出時,可以給變量起別名
// export {
// name as Fname,
// age as Fage,
// sayHello as FsayHello
// }
輸出結果:
hello EsModules
pengsir
18
姓名彭先生
3.4 export default
上面是在導出時都指定了名字,所以導入時也需要知道具體的名字。在某些情況下很不方便,所以還有另外一種導出方式:export default:
- 默認導出時,不需要指定名字;
- 導入時很方便,可以自己指定名字;
bar.js 導出 :
// 方式四:默認導出
export default function format() {
console.log('對某一個東西,進行格式化!');
}
index.js導入:
// 方式四: 演示 export default如何導入
import utils from './modules/foo.js'
utils() // 實際是調用 format
輸出:
對某一個東西,進行格式化!
但是,一個文件只能有一個默認導出:export default。
3.5 import 函數(shù)
通過import加載的模塊,是不能放到邏輯代碼中的,只能放到最上面,比如:
let flag = true
if (flag) {
// 錯誤用法,語法錯誤,不能在邏輯在邏輯代碼中使用 import 關鍵字
import format from './modules/foo.js'
}
為什么會出現(xiàn)這個情況呢?
- ES Module在被js引擎解析時,需要知道它的依賴關系;
- 由于js代碼這時沒有運行,所以無法在進行類似于if判斷中根據(jù)代碼執(zhí)行情況進行導入;
解決辦法:使用import() 函數(shù),或者require()
// 方式五:import() 函數(shù)
// 注意:上邊使用import時是作為關鍵字使用,現(xiàn)在是作為函數(shù)使用,
// 該函數(shù)為異步函數(shù),返回值為promise
let flag = true
if (flag) {
import('./modules/foo.js').then(res => {
console.log('then里邊的回調');
console.log(res);
}, err => {
console.log(err);
})
}
3.6 異步的import
使用 type="module" 時,加載該模塊是異步加載的,就相當于給script加了一個async屬性。
示例:
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 src="./index.js" type="module"></script>
<script src="./normal.js"></script>
</body>
</html>
index.js
console.log('hello EsModules');
normal.js
console.log('我是普通的js文件');
由此可見我們的 ES Module 是異步的。
3.7 ES Module的加載過程
ES Module導出的數(shù)據(jù)是實時變化的:
- 如果在bar.js中導出一個變量A,在index.js中導入該變量,如果1秒之后bar.js中的變量A的值被修改,index.js中導入的變量也會被修改;
- 但是在index.js中不能修改導入的該變量,除非該變量是一個對象類型,因為ES Module在底層實現(xiàn)的時候,每次導出的變量發(fā)生變化,都會在模塊環(huán)境記錄中創(chuàng)建一個最新的該變量,類似const name = name,底層使用const定義的,所以導入后的變量內存地址不能發(fā)生變化,但是對象類型的值可以;

3.8 統(tǒng)一export
項目中會有很多工具函數(shù),在不同的分揀中,如果要引入的話,需要找到對應的文件來引入,可以給這些工具庫弄一個統(tǒng)一的出口,然后直接導入這個出口文件即可:
/**
* 工具的統(tǒng)一出口
*/
// 1.導出方式一:挨個導入再挨個導出
// import { sub, add } from './math.js'
// import { timeFormat } from './format.js'
// export { sub, add, timeFormat }
// 2.導出方式二:直接導出指定的
// export { sub, add } from './math.js'
// export { timeFormat } from './format.js'
// 3.導出方式三: 直接導出所有的
export * from './math.js'
export * from './format.js'
4. 總結
CommonJS和 ES Module的區(qū)別:
- CommonJS可以動態(tài)加載語句,代碼發(fā)生在運行時;ES Module是靜態(tài)的,只能聲明在文件最頂部,代碼發(fā)生在編譯時;
- Es Module混合導出,單個導出,默認導出,完全互不影響;
- Es Module導出是引用值,并且值都是可讀的,不能修改;CommonJs導出值是拷貝,可以修改;