【前端Tip】CommonJS規(guī)范和ES Module規(guī)范

前言:在寫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.exportsrequire;

可以使用這些關鍵字來進行模塊化開發(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對象,在其它文件導入某個文件時,其實就是拿到該對象的內存地址,如下圖所示:

image.png

把上面的代碼修改并驗證一下:
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ī)則:
  1. x是一個核心模塊,比如path、http:
    直接返回核心模塊,停止查找;

  2. x是以./ 或 ../ 開頭的
    a. 將x當做一個文件在對應的目錄下查找,如果沒有寫明后綴名,則按照:x->x.js->x.json->x.node 進行查找;
    b. 沒有找到對應的文件,將x作為一個目錄,查找目錄下邊的index文件,按照x/index.js->x/index.json->x/index.node進行查找;

  3. 直接是一個x,并且x不是核心模塊
    例如我在main.js中編寫了require('test’),它會逐級查找上一層目錄下的node_modules。

如果都沒有找到,則報錯:not found。

2.4 模塊的加載過程

  • 模塊在第一次被引入的時候,模塊的js代碼會被運行一次;
  • 模塊被多次引入時,會進行緩存,只執(zhí)行一次(每個模塊對象module都有一個屬性:loaded用來標記是否已經(jīng)加載過)
  • 如果有循環(huán)引入,那加載順序是什么?

順序為:圖結果的深度優(yōu)先算法


image.png

3. ES Module

3.1 介紹

ES Module 是ES6推出的,即ES 2015。并且自動采用嚴格模式:use strict。

但是ES Module和CommonJS的模塊化有一些不同:

  • 使用import 和 export關鍵字,不是模塊也不是函數(shù);
  • 采用編譯器的靜態(tài)分析,也加入了動態(tài)引用的方式;

ES Module模塊采用exportimport關鍵字來實現(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ā)生變化,但是對象類型的值可以;
image.png

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導出值是拷貝,可以修改;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容