碼文不易,轉載請帶上本文鏈接,感謝~ https://www.cnblogs.com/echoyya/p/14577243.html
在開發以及面試中,總是會遇到有關模塊化相關的問題,始終不是很明白,不得要領,例如以下問題,回答起來也是模棱兩可,希望通過這篇文章,能夠讓大家了解十之一二,首先拋出問題:
- 導出模塊時使用
module.exports/exports或者export/export default; - 有時加載一個模塊會使用
require奇怪的是也可以使用import??它們之間有何區別呢?
於是有了菜鳥解惑的搜嘍過程。。。。。。
模塊化規范:即為 JavaScript 提供一種模塊編寫、模塊依賴和模塊運行的方案。降低代碼復雜度,提高解耦性
Script 標簽
其實最原始的 JavaScript 文件加載方式,就是Script 標簽,如果把每一個文件看做是一個模塊,那么他們的接口通常是暴露在全局作用域下,也就是定義在 window 對象中,不同模塊的接口調用都是一個作用域中,一些復雜的框架,會使用命名空間的概念來組織這些模塊的接口。
缺點:
- 污染全局作用域
- 開發人員必須主觀解決模塊和代碼庫的依賴關系
- 文件只能按照script標簽的書寫順序進行加載
- 在大型項目中各種資源難以管理,長期積累的問題導致代碼庫混亂不堪
默認情況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標簽就會停下來,等到執行完腳本,再繼續向下渲染。如果是外部腳本,還必須加入腳本下載的時間。
如果腳本體積很大,下載和執行的時間就會很長,因此造成瀏覽器堵塞,用戶會感覺到瀏覽器“卡死”了,沒有任何響應。這顯然是很不好的體驗,所以瀏覽器允許腳本異步加載。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
<script>標簽添加defer或async屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行后面的命令。
defer:要等到整個頁面在內存中正常渲染結束,才會執行;多個腳本時,按順序執行
async:一旦下載完,渲染引擎就會中斷渲染,執行這個腳本再繼續渲染。多個腳本時,不能保證按執行順序
總結一句話:defer是“渲染完再執行”,async是“下載完就執行”。
CommonJS規范(同步加載模塊)
- 服務器端實現:**Node.js **
- 瀏覽器端實現:**Browserify **,也稱為Commonjs的瀏覽器的打包工具
- 通過
require方法同步加載所依賴的模塊,通過exports或module.exports導出需要暴露的數據。一個文件就是一個模塊 - CommonJS 規范包括了模塊(modules)、包(packages)、系統(system)、二進制(binary)、控制台(console)、編碼(encodings)、文件系統(filesystems)、套接字(sockets)、單元測試(unit testing)等部分。
加載模塊
使用require函數 加載模塊(即被依賴模塊的 module.exports對象)。
- 按路徑加載模塊
- 通過查找 node_modules 目錄加載模塊
- 加載緩存:Node.js 是根據實際文件名緩存,而不是 require() 提供的參數緩存的,如
require('express')和require('./node_modules/express')加載兩次,也不會重復加載,盡管兩次參數不同,解析到的文件卻是同一個。 - 核心模塊擁有最高的加載優先級,換言之如果有模塊與其命名沖突,Node.js 總是會加載核心模塊。
- 更多關於require函數的用法和特點,博主此前另外總結過一篇博文,NodeJs 入門到放棄 — 入門基本介紹(一)
導出模塊
exports.屬性 = 值
exports.方法 = 函數
- Node.js 為每個模塊提供一個 exports 變量,指向 module.exports。相對於在每個模塊頭部,有一行這樣的命令:
var exports = module.exports; - exports對象 和 module.exports對象,指同一個內存空間, module.exports對象才是真正的暴露對象
- 而
exports對象 是 module.exports對象的引用,不能改變指向,只能添加屬性和方法,若直接改變exports 的指向,等於切斷了 exports 與 module.exports 的聯系,返回空對象 - console.log(module.exports === exports); // true
- 更多關於exports函數的用法和特點,博主此前另外總結過一篇博文,NodeJs 入門到放棄 — 入門基本介紹(一)
另外的用法:
// singleobjct.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
}
exports.Hello = Hello;
此時獲取 Hello 對象require('./singleobject').Hello,略顯冗余,可以用下面方法簡化。
// hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
}
module.exports = Hello;
就可以直接獲得這個對象:
// gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('Yu');
hello.sayHello();
以下同樣一段代碼(圖為對應的目錄結構),分別運行在服務器端和瀏覽器端,看看有神馬區別?

// module1.js
module.exports = {
foo(){
console.log('module1的foo()函數運行了');
}
}
// module2.js
module.exports = function() {
console.log('module2的foo()函數運行了');
}
// module3.js
exports.foo = function () {
console.log('module3的foo()函數運行了');
}
exports.bar = function () {
console.log('module3的bar()函數運行了');
}
// main.js
let module1 = require('./module1')
let module2 = require('./module2')
let module3 = require('./module3')
module1.foo()
module2()
module3.foo()
module3.bar()
服務器端實現 NodeJs
cd響應的目錄,直接命令行執行:node main.js

瀏覽器端實現 Browserify
Browserify 本身也是一個 NodeJS 模塊,npm安裝后可以使用 browserify 命令。分析文件中require 方法調用來遞歸查找所依賴的其他模塊。把輸入文件所依賴的所有模塊文件打包成單個文件並輸出。
npm安裝命令 :npm install -g browserify
打包命令:browserify 入口文件 -o 打包文件 如:browserify ./src/main.js -o ./dist/build.js
想要運行在瀏覽器端,要有一個入口的hmtl文件。創建index.html,並引入上述打包生成的build.js文件 <script src="./dist/build.js"></script>

CommonJS 特點
- 同步加載方式,適用於服務端,因為模塊都放在服務器端,對於服務端來說模塊加載較快,不適合在瀏覽器環境中使用,因為同步意味着阻塞加載。
- 所有代碼都運行在模塊作用域,不會污染全局作用域。
- 模塊可以多次加載,但只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。
- 模塊加載的順序,按照其在代碼中出現的順序。
AMD(Asynchronous Module Definition)
采用異步方式加載模塊,模塊的加載不影響它后面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之后,這個回調函數才會運行。推崇依賴前置
require.js 是目前 AMD 規范最熱門的一個實現
AMD 也采用 require語句加載模塊,但是不同於 CommonJS,它要求兩個參數:require([module], callback);
-
[module]:是一個數組,成員就是要加載的模塊
-
callback:加載成功之后的回調函數;
require(['math'], function (math) {
math.add(2, 3);
});
創建模塊 及 規范模塊加載
模塊必須采用 define() 函數來定義。
- 若一個模塊不依賴其他模塊。可以直接定義在 define() 函數中。
// math.js
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
- 若這個模塊還依賴其他模塊,那么 define() 函數的第一個參數,必須是一個數組,指明該模塊的依賴性。當 require() 函數加載test模塊時,就會先加載 math.js 模塊。
// dataService.js
define(['math'], function (math) {
function doSomething() {
let result = math.add(2, 9);
console.log(result);
}
return {
doSomething
};
});
- 設置一個主模塊,統一調度當前項目中所有依賴模塊
// main.js
(function () {
require.config ({
// baseUrl:'',
paths:{
dataService:'./dataService',
math:'./math'
}
})
require(['dataService'], function (dataService) {
dataService.doSomething()
});
})();
- 在index.html中引入require.js,並設置data-main入口主模塊
<!-- index/html -->
<script data-main="./js/main.js" src="./js/require.js"></script>
- 來來來,瀏覽器看看效果了:打印出了兩數字相加的結果

- 本案例中所有源碼,目錄結構及每個模塊的作用,如下圖所示(源碼同上1234步驟):

加載非規范的模塊
理論上require.js加載的模塊,必須是按照 AMD 規范用 define() 函數定義的模塊。但實際上,雖然已經有一部分流行的函數庫(比如 jQuery )符合 AMD 規范,更多的庫並不符合。那么require.js 如何能夠加載非規范的模塊呢?
這樣的模塊在用 require() 加載之前,要先用 require.config()方法,定義它們的一些特征。
例如,underscore 和 backbone 這兩個庫,都沒有采用 AMD 規范編寫。如果要加載的話,必須先定義它們的特征。
require.config({
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
require.config() 接受一個配置對象,這個對象有一個 shim 屬性,專門用來配置不兼容的模塊。每個模塊要定義:
-
exports :輸出的變量名,表示這個模塊外部調用時的名稱;
-
deps: 數組,表示該模塊的依賴性。
如jQuery 的插件還可以這樣定義:
shim: {
'jquery.scroll': {
deps: ['jquery'],
exports: 'jQuery.fn.scroll'
}
}
AMD特點
- AMD允許輸出的模塊兼容CommonJS
- 異步並行加載,不阻塞 DOM 渲染。
推崇依賴前置,也就是提前執行(預執行),在模塊使用之前就已經執行完畢。
CMD(Common Module Definition)
- CMD 是通用模塊加載,要解決的問題與 AMD 一樣,只不過是對依賴模塊的執行時機不同 ,
推崇就近依賴。 - sea.js 是 CMD 規范的一個實現代表庫
- 定義模塊使用全局函數define,接收一個 factory 參數,可以是一個函數,也可以是一個對象或字符串;
-
factory 是函數時有三個參數,function(require, exports, module):
-
require:函數用來獲取其他模塊提供的接口require(模塊標識ID) -
exports: 對象用來向外提供模塊接口 -
module:對象,存儲了與當前模塊相關聯的屬性和方法
-
// 定義 a.js 模塊,同時可引入其他依賴模塊,及導出本模塊
define(function(require, exports, module) {
var $ = require('jquery.js')
exports.price= 200;
});
- factory 為對象、字符串時,表示模塊的接口就是該對象、字符串。比如可以定義一個 JSON 數據模塊:
// 定義 foo.js
define({"foo": "bar"});
// 導入使用
define(function(require, exports, module) {
var obj = require('foo.js')
console.log(obj) // {foo: "bar"}
});
-
下面通過一個案例分析,深入了解一下CMD模塊化規范,具體的用法:
- 定義1,2,3,4,四個簡單模塊,定義主模塊main.js, 以及一個index.html
cmd從語法上分析,結合了AMD模塊定義的特點,同時又沿用了CommonJs 模塊導入和導出的特點- 由於代碼比較雜,所以還是看圖理解一下吧,圖上均有標注每個文件的用途,圖二為瀏覽器執行效果:


AMD 與 CMD 的區別
-
AMD 是提前執行,CMD 是延遲執行。
-
AMD 是依賴前置,CMD 是依賴就近。
// AMD
define(['./a', './b'], function(a, b) { // 在定義模塊時 就要聲明其依賴的模塊
a.doSomething()
// ....
b.doSomething()
// ....
})
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// ...
var b = require('./b') // 可以在用到某個模塊時 再去require
b.doSomething()
// ...
})
UMD(Universal Module Definition)
- UMD是AMD和CommonJS的糅合
- UMD的實現很簡單:
- 先判斷是否支持Node.js模塊(exports是否存在),存在則使用Node.js模塊模式。
- 再判斷是否支持AMD(define是否存在),存在則使用AMD方式加載模塊。
- 前兩個都不存在,則將模塊公開到全局(window或global)。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define([],factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
return {};
});
ES6模塊化
ES6 模塊的設計思想,是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。
ES6 中,import引用模塊,使用export導出模塊。默認情況下,Node.js默認是不支持import語法的,通過babel項目將 ES6 模塊 編譯為 ES5 的 CommonJS。因此Babel實際上是將import/export翻譯成Node.js支持的require/exports。
// 導入
import Vue from 'vue'
import App from './App'
// 導出
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
export function multiply() {...};
export var year = 2018;
export default ...
剛剛講到使用babel將import編譯為nodejs支持的require,即可使用node命令執行,而瀏覽器默認是不支持import和require的,此時還需要借助另一個工具,即上文中,在講述CommonJs時,提到的,browserify,下面請看完整的案例分析:
- 安裝必要包,babel,及browserify
npm install babel-cli -gnpm install babel-preset-es2015 --save-devnpm install browserify -g
- 創建
.babelrc文件,並設置編譯格式為es2015 - 自定義一個模塊,導出數據,並在主模塊中加載執行
babel ./src -d ./build命令將import編譯為requirebrowserify ./build/main.js -o ./dist/main.js編譯為瀏覽器識別語法,最終引入index.html文件中

- 編譯命令及瀏覽器運行效果:

模塊化規范大總結
| CommonJS | AMD | CMD | ES6 | |
|---|---|---|---|---|
| 引用模塊 | require | require | require | import |
| 暴露接口 | module.exports || exports | define函數返回值 return | exports | export |
| 加載方式 | 運行時加載,同步加載 | 並行加載,提前執行,異步加載 | 並行加載,按需執行,異步加載 | 編譯時加載,異步加載 |
| 實現模塊規范 | NodeJS | RequireJS | SeaJS | 原生JS |
| 適用 | 服務器 | 瀏覽器 | 瀏覽器 | 服務器/瀏覽器 |
問題回歸:"require"與"import"的區別
說了這么多,還是要回到文章一開始提到的問題,"require"與"import"兩種引入模塊方式,到底有神馬區別,大致可以分為以下幾個方面(可能總結的也不是很全面):
寫法上的區別
require/exports 的用法只有以下三種簡單的寫法:
const fs = require('fs')
exports.fs = fs
module.exports = fs
import/export 的寫法就多種多樣:
import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs'
export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'
輸入值的區別
require輸入的變量,基本類型數據是賦值,引用類型為淺拷貝,可修改
import輸入的變量都是只讀的,如果輸入 a 是一個對象,允許改寫對象屬性。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操作
執行順序
require:不具有提升效果,到底加載哪一個模塊,只有運行時才知道。
const path = './' + fileName;
const myModual = require(path);
import:具有提升效果,會提升到整個模塊的頭部,首先執行。import的執行早於foo的調用。本質就是import命令是編譯階段執行的,在代碼運行之前。
foo();
import { foo } from 'my_module';
import()函數:ES2020提案引入,支持動態加載模塊。import()函數接受一個參數,指定所要加載的模塊的位置,參數格式同import命令,兩者區別主要是import()為動態加載。可用於按需加載、條件加載、動態的模塊路徑等。
它是運行時執行,也就是說,什么時候運行到這一句,就會加載指定的模塊,返回一個 Promise 對象。import()加載模塊成功以后,該模塊會作為一個對象,當作then方法的參數。可以使用對象解構賦值,獲取輸出接口。
// 按需加載
button.addEventListener('click', event => {
import('./dialogBox.js')
.then({export1, export2} => { // export1和export2都是dialogBox.js的輸出接口,解構獲得
// do something...
})
.catch(error => {})
});
// 條件加載
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
// 動態的模塊路徑
import(f()).then(...); // 根據函數f的返回結果,加載不同的模塊。
使用表達式和變量
require:很顯然是可以使用表達式和變量的
let a = require('./a.js')
a.add()
let b = require('./b.js')
b.getSum()
import靜態執行,不能使用表達式和變量,因為這些都是只有在運行時才能得到結果的語法結構。
// 報錯
import { 'f' + 'oo' } from 'my_module';
// 報錯
let module = 'my_module';
import { foo } from module;
// 報錯
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
而require/exports 和 import/export 本質上的區別,實際上也就是CommonJS規范與ES6模塊化的區別
1、瀏覽器在不做任何處理時,默認是不支持import和require
2、babel會將ES6模塊規范轉化成Commonjs規范
3、webpack、gulp以及其他構建工具會對Commonjs進行處理,使之支持瀏覽器環境
它們有三個重大差異。
CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
CommonJS 模塊的
require()是同步加載模塊,ES6 模塊的import命令是異步加載,有一個獨立的模塊依賴的解析階段。
導致第二個差異是因為 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成
CommonJS:運行時加載
- 只能在運行時確定模塊的依賴關系,以及輸入和輸出的變量,一個模塊就是一個對象,輸入時必須查找對象屬性。
// CommonJS模塊
let { stat, exists, readfile } = require('fs');
// 等同於
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6 :編譯時加載或者靜態加載
- ES6 模塊不是對象,而是通過
export命令顯式指定輸出的代碼,再通過import命令輸入。 - 可以在編譯時就完成模塊加載,引用時只加載需要的方法,其他方法不加載。效率要比 CommonJS 模塊的加載方式高。
import { stat, exists, readFile } from 'fs';
