模塊化開發(fā)
--- 當(dāng)下最重要的前端開發(fā)范式之一
所謂模塊化,只是思想或者理論,不是具體的某個(gè)特定的實(shí)現(xiàn)
模塊化的演變過程
-
第一階段:文件劃分方式
早起的模塊化完全依賴約定
-
缺點(diǎn)
污染全局作用域
命名沖突
無法管理模塊的依賴關(guān)系
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Modular evolution stage 1</title> </head> <body> <h1>模塊化演變(第一階段)</h1> <h2>基于文件的劃分模塊的方式</h2> <p> 具體做法就是將每個(gè)功能及其相關(guān)狀態(tài)數(shù)據(jù)各自單獨(dú)放到不同的文件中, 約定每個(gè)文件就是一個(gè)獨(dú)立的模塊, 使用某個(gè)模塊就是將這個(gè)模塊引入到頁面中,然后直接調(diào)用模塊中的成員(變量 / 函數(shù)) </p> <p> 缺點(diǎn)十分明顯: 所有模塊都直接在全局工作,沒有私有空間,所有成員都可以在模塊外部被訪問或者修改, 而且模塊一段多了過后,容易產(chǎn)生命名沖突, 另外無法管理模塊與模塊之間的依賴關(guān)系 </p> <script src="module-a.js"></script> <script src="module-b.js"></script> <script> // 命名沖突 method1(); // 模塊成員可以被修改 name = 'foo'; </script> </body> </html>// module a 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù) var name = 'module-a'; function method1() { console.log(name + '#method1'); } function method2() { console.log(name + '#method2'); }// module a 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù) var name = 'module-a'; function method1() { console.log(name + '#method1'); } function method2() { console.log(name + '#method2'); }// module b 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù) var name = 'module-b'; function method1() { console.log(name + '#method1'); } function method2() { console.log(name + '#method2'); } -
第二階段:命名空間方式
每個(gè)模塊掛載到對(duì)象上
-
缺點(diǎn)
內(nèi)部的所有成員任然可以被修改和訪問
無法管理模塊的依賴關(guān)系
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Modular evolution stage 2</title>
</head>
<body>
<h1>模塊化演變(第二階段)</h1>
<h2>每個(gè)模塊只暴露一個(gè)全局對(duì)象,所有模塊成員都掛載到這個(gè)對(duì)象中</h2>
<p>
具體做法就是在第一階段的基礎(chǔ)上,通過將每個(gè)模塊「包裹」為一個(gè)全局對(duì)象的形式實(shí)現(xiàn),
有點(diǎn)類似于為模塊內(nèi)的成員添加了「命名空間」的感覺。
</p>
<p>
通過「命名空間」減小了命名沖突的可能,
但是同樣沒有私有空間,所有模塊成員也可以在模塊外部被訪問或者修改,
而且也無法管理模塊之間的依賴關(guān)系。
</p>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1();
moduleB.method1();
// 模塊成員可以被修改
moduleA.name = 'foo';
</script>
</body>
</html>
// module a 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)
var moduleA = {
name: 'module-a',
method1: function () {
console.log(this.name + '#method1');
},
method2: function () {
console.log(this.name + '#method2');
},
};
// module b 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)
var moduleB = {
name: 'module-b',
method1: function () {
console.log(this.name + '#method1');
},
method2: function () {
console.log(this.name + '#method2');
},
};
- 第三階段:立即執(zhí)行函數(shù)
每個(gè)模塊放在一個(gè)立即執(zhí)行函數(shù)中,將外部要用到的對(duì)象掛載到全局邊梁上
優(yōu)點(diǎn):
避免了大量的對(duì)象被掛載到全局,防止私有成員被訪問
可以通過參數(shù)傳遞依賴
缺點(diǎn)
- 掛載的時(shí)候還是會(huì)有命名沖突
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Modular evolution stage 3</title>
</head>
<body>
<h1>模塊化演變(第三階段)</h1>
<h2>
使用立即執(zhí)行函數(shù)表達(dá)式(IIFE:Immediately-Invoked Function
Expression)為模塊提供私有空間
</h2>
<p>
具體做法就是將每個(gè)模塊成員都放在一個(gè)函數(shù)提供的私有作用域中,
對(duì)于需要暴露給外部的成員,通過掛在到全局對(duì)象上的方式實(shí)現(xiàn)
</p>
<p>
有了私有成員的概念,私有成員只能在模塊成員內(nèi)通過閉包的形式訪問。
</p>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1();
moduleB.method1();
// 模塊私有成員無法訪問
console.log(moduleA.name); // => undefined
</script>
</body>
</html>
// module a 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)
(function () {
var name = 'module-a';
function method1() {
console.log(name + '#method1');
}
function method2() {
console.log(name + '#method2');
}
window.moduleA = {
method1: method1,
method2: method2,
};
})();
// module b 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)
(function () {
var name = 'module-b';
function method1() {
console.log(name + '#method1');
}
function method2() {
console.log(name + '#method2');
}
window.moduleB = {
method1: method1,
method2: method2,
};
})();
模塊化規(guī)范
歷史
早期的模塊化規(guī)范,Commonjs 規(guī)范不適合瀏覽器,在 node 中同步加載模塊依賴。
- 一個(gè)文件就是一個(gè)模塊
- 每個(gè)模塊都有單獨(dú)的作用域
- 通過 module.exports 導(dǎo)出模塊
- 通過 require 函數(shù)載入模塊
于是在瀏覽器中又提出了 AMD(Asyncchronous Module Definition)
Require.js 實(shí)現(xiàn)了這個(gè)規(guī)范。目前大多數(shù)的對(duì)三方庫支持 AMD 規(guī)范缺缺點(diǎn):
使用起來很復(fù)雜
當(dāng)我們模塊劃分很細(xì)的時(shí)候 js 文件就會(huì)請(qǐng)求的很頻繁
AMD 規(guī)范是前端模塊化演進(jìn)道路上的一步,的歷史長(zhǎng)河中進(jìn)了一步,是一種妥協(xié)的實(shí)現(xiàn)方式,不算是最終的姐解決方案。除此之外,同一時(shí)期,淘寶推出了 Sea.js 庫,實(shí)現(xiàn)的是 CMD 標(biāo)準(zhǔn),但是后面讓 Require.js 兼容了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Modular evolution stage 5</title>
</head>
<body>
<h1>模塊化規(guī)范的出現(xiàn)</h1>
<h2>Require.js 提供了 AMD 模塊化規(guī)范,以及一個(gè)自動(dòng)化模塊加載器</h2>
<script src="lib/require.js" data-main="main"></script>
</body>
</html>
require.config({
paths: {
// 因?yàn)?jQuery 中定義的是一個(gè)名為 jquery 的 AMD 模塊
// 所以使用時(shí)必須通過 'jquery' 這個(gè)名稱獲取這個(gè)模塊
// 但是 jQuery.js 并不一定在同級(jí)目錄下,所以需要指定路徑
jquery: './lib/jquery',
},
});
require(['./modules/module1'], function (module1) {
module1.start();
});
// 兼容 CMD 規(guī)范(類似 CommonJS 規(guī)范)
define(function (require, exports, module) {
// 通過 require 引入依賴
var $ = require('jquery');
// 通過 exports 或者 module.exports 對(duì)外暴露成員
module.exports = function () {
console.log('module 2~');
$('body').append('<p>module2</p>');
};
});
// 因?yàn)?jQuery 中定義的是一個(gè)名為 jquery 的 AMD 模塊
// 所以使用時(shí)必須通過 'jquery' 這個(gè)名稱獲取這個(gè)模塊
// 但是 jQuery.js 并不在同級(jí)目錄下,所以需要指定路徑
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' });
module2();
},
};
});
最佳實(shí)踐
隨著技術(shù)的發(fā)展,JavaScript 標(biāo)準(zhǔn)逐漸完善,模塊化被統(tǒng)一成了瀏覽器端的 ES Module,nodejs 中遵循 CommonJs 規(guī)范
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>ES Module - 模塊的特性</title>
</head>
<body>
<!-- 通過給 script 添加 type = module 的屬性,就可以以 ES Module 的標(biāo)準(zhǔn)執(zhí)行其中的 JS 代碼了 -->
<script type="module">
console.log('this is es module');
</script>
<!-- 1. ESM 自動(dòng)采用嚴(yán)格模式,忽略 'use strict' -->
<script type="module">
console.log(this);
</script>
<!-- 2. 每個(gè) ES Module 都是運(yùn)行在單獨(dú)的私有作用域中 -->
<script type="module">
var foo = 100;
console.log(foo);
</script>
<script type="module">
console.log(foo);
</script>
<!-- 3. ESM 是通過 CORS 的方式請(qǐng)求外部 JS 模塊的 -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- 4. ESM 的 script 標(biāo)簽會(huì)延遲執(zhí)行腳本 等同于defer屬性 -->
<script type="module" src="demo.js"></script>
<p>需要顯示的內(nèi)容</p>
</body>
</html>
ES Module 核心功能
import export 注意
默認(rèn)導(dǎo)出的是字面量對(duì)象,非默認(rèn)導(dǎo)出則不是,且必須為{}中的成員
導(dǎo)入的語法中的{}為固定的語法,并非解構(gòu)
導(dǎo)出到外部的都是內(nèi)存地址的引用,并非值的拷貝
導(dǎo)出的成員都是只讀的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>ES Module 導(dǎo)出與導(dǎo)入 - 注意事項(xiàng)</title>
</head>
<body>
<script type="module" src="app.js"></script>
</body>
</html>
// CommonJS 中是先將模塊整體導(dǎo)入為一個(gè)對(duì)象,然后從對(duì)象中結(jié)構(gòu)出需要的成員
// const { name, age } = require('./module.js')
// ES Module 中 { } 是固定語法,就是直接提取模塊導(dǎo)出成員
import { name, age } from './module.js';
console.log(name, age);
// 導(dǎo)入成員并不是復(fù)制一個(gè)副本,
// 而是直接導(dǎo)入模塊成員的引用地址,
// 也就是說 import 得到的變量與 export 導(dǎo)入的變量在內(nèi)存中是同一塊空間。
// 一旦模塊中成員修改了,這里也會(huì)同時(shí)修改,
setTimeout(function () {
console.log(name, age);
}, 1500);
// 導(dǎo)入模塊成員變量是只讀的
// name = 'tom' // 報(bào)錯(cuò)
// 但是需要注意如果導(dǎo)入的是一個(gè)對(duì)象,對(duì)象的屬性讀寫不受影響
// name.xxx = 'xxx' // 正常
var name = 'jack';
var age = 18;
// var obj = { name, age }
// export default { name, age }
// 這里的 `{ name, hello }` 不是一個(gè)對(duì)象字面量,
// 它只是語法上的規(guī)則而已
export { name, age };
// export name // 錯(cuò)誤的用法
// export 'foo' // 同樣錯(cuò)誤的用法
setTimeout(function () {
name = 'ben';
}, 1000);
import 額外注意
導(dǎo)入模塊的時(shí)候 import from 后的路徑必須帶文件后綴,不支持自動(dòng)定位 index
導(dǎo)入本地模塊的時(shí)候 import from 后面的相對(duì)路徑必須是'./' or '../'; 絕對(duì)路徑支持從項(xiàng)目跟目錄查找 以'/'開頭; 或者完整的 url,不然會(huì)被按照導(dǎo)入三方模塊處理
加載這個(gè)模塊并不提取任何成員
import {} from './module.js';
// 簡(jiǎn)寫
import './module.js';
- 全部導(dǎo)出成員
import * as mod from './module.js';
- import from 后面不支持變量
var modulePath = './module.js'
import {name} from modulePath; // 報(bào)錯(cuò)
- 塊級(jí)作用域下不能使用 import
if (true) {
import { name } from './module.js'; // 報(bào)錯(cuò)
}
- 動(dòng)態(tài)導(dǎo)入需要使用全局提供的 import 函數(shù)
import('./module.js').then(module => {
console.log(module);
});
- 直接導(dǎo)入導(dǎo)出
export { for, bar } from './module.js';
- ES Module 瀏覽器環(huán)境 Polyfill
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>ES Module 瀏覽器環(huán)境 Polyfill</title>
</head>
<body>
<script
nomodule
src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"
></script>
<script
nomodule
src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"
></script>
<script
nomodule
src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"
></script>
<script type="module">
import { foo } from './module.js';
console.log(foo);
</script>
</body>
</html>
注意:script 標(biāo)簽的 nomodule 屬性可以在沒有 ES Module 的環(huán)境中執(zhí)行,動(dòng)態(tài)編譯,不推薦直接在生產(chǎn)環(huán)境使用
-
在 node 中使用 ES module
- 在 node 環(huán)境中運(yùn)行 ES module,把 js 文件后綴改為'.mjs',在命令行中執(zhí)行 node --experimental-modules index.mjs
// 第一,將文件的擴(kuò)展名由 .js 改為 .mjs; // 第二,啟動(dòng)時(shí)需要額外添加 `--experimental-modules` 參數(shù); import { foo, bar } from './module.mjs'; console.log(foo, bar); // 此時(shí)我們也可以通過 esm 加載內(nèi)置模塊了 import fs from 'fs'; fs.writeFileSync('./foo.txt', 'es module working'); // 也可以直接提取模塊內(nèi)的成員,內(nèi)置模塊兼容了 ESM 的提取成員方式 import { writeFileSync } from 'fs'; writeFileSync('./bar.txt', 'es module working'); // 對(duì)于第三方的 NPM 模塊也可以通過 esm 加載 import _ from 'lodash'; _.camelCase('ES Module'); // 不支持,因?yàn)榈谌侥K都是導(dǎo)出默認(rèn)成員 // import { camelCase } from 'lodash' // console.log(camelCase('ES Module')) -
在 ES module 中使用 Commonjs
// es-module.mjs import mod from './commonjs.js' console.log(mod) // 正常打印 // commonJS 始終只會(huì)導(dǎo)出一個(gè)默認(rèn)成員 // commonjs.js module.export = { foo: 'commonjs exports value' } // 該語法為上面語法的簡(jiǎn)寫 export.foo = 'commonjs exports value' 不能直接提取 commonjs 的成員,import 不是解構(gòu)導(dǎo)出對(duì)象
import { foo } from './commonjs.js';
console.log(foo); // 報(bào)錯(cuò)
- 不能在 CommonJS 模塊中通過 require 載入 ES Module
// es-module.js
export const foo = 'es module export value';
const mod = require('./es-module.mjs');
console.log(mod); // 報(bào)錯(cuò)
- commonjs 和 ES Module 在 node 中區(qū)別
common js
// 加載模塊函數(shù)
console.log(require);
// 模塊對(duì)象
console.log(module);
// 導(dǎo)出對(duì)象別名
console.log(exports);
// 當(dāng)前文件的絕對(duì)路徑
console.log(__filename);
// 當(dāng)前文件所在目錄
console.log(__dirname);
ES Module
// ESM 中沒有模塊全局成員了
// // 加載模塊函數(shù)
// console.log(require)
// // 模塊對(duì)象
// console.log(module)
// // 導(dǎo)出對(duì)象別名
// console.log(exports)
// // 當(dāng)前文件的絕對(duì)路徑
// console.log(__filename)
// // 當(dāng)前文件所在目錄
// console.log(__dirname)
// -------------
// require, module, exports 自然是通過 import 和 export 代替
// __filename 和 __dirname 通過 import 對(duì)象的 meta 屬性獲取
// const currentUrl = import.meta.url
// console.log(currentUrl)
// 通過 url 模塊的 fileURLToPath 方法轉(zhuǎn)換為路徑
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__filename);
console.log(__dirname);
- 新版本 node 中進(jìn)一步支持 ESModule
// 在 package.json 中設(shè)置type,默認(rèn)以ES Module方式工作,不用在改擴(kuò)展名了 .mjs=>.js
{
"type": "module"
}
// 如果要繼續(xù)支持commonjs規(guī)范,則可以把js文件后綴改為'.cjs'
- 舊版本 node 中也可以通過 babel-node 支持 ES Module 特性
$ yarn add @babel/node @babel/core @babel/preset-env -D
// or
$ yarn add @babel/node @babel/core @babel/plugin-transform-modules-commonjs -D
$ yarn babel-node [文件名]
// .babelrc
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
// or
{
"presets": ["@babel/preset-env"]
}