iT邦幫忙

第 12 屆 iThome 鐵人賽

0
Modern Web

從技術文章深入學習 JavaScript系列 第 25

Day 25 [模塊化] 前端模塊化:CommonJS,AMD,CMD,ES6

文章參考自

https://juejin.im/post/6844903744518389768

https://juejin.im/post/6844903576309858318

https://juejin.im/post/6844904003722018830

甚麼是模塊化開發

模塊

通常一個模塊會有各自的作用域,會向外界暴露特定的變量或者函數

將一個複雜的程序,依據規範封裝成一塊塊的文件,即是模塊

好處

  1. 代碼更加方便管理
  2. 提升代碼複用性
  3. 避免命名衝突(減少全局污染)
  4. 更符合按須加載

最原始的模塊化開發

極糟糕模塊

模塊就是一組特定功能的文件,以下foo 與bar組合成的module1.js 也可被稱為模塊

// module1.js
function foo() {
  ...
}
function bar() {
  ...
}

產生問題:

假設今天我們有好幾個模塊需要引入到同一個地方會發現,假設不同的模塊都有一個foo函數不就出事情(這個情況其實就是汙染了全局變量)

創建一個obj吧

// module1.js
let module1 = new Object({
  _count: 0,
  foo: function () {
    ...
  },
  bar: function () {
    ...
  }
})

這樣只要我們調用 foo函數 就可以利用 module1.foo ,如果有其他模塊也有foo就不會發生衝突。

產生問題:

可是到這裡我們又發現,_count可以被任意訪問,他明明就是計數器怎麼可以任意被外部改變。

立即執行函數登場

有私人的變量,外界只能透過暴露的方法獲取變量

// module1.js
(function (window) {
  let _count = 0
  function foo() {
    _count += 1
  }
  
  function getCount() {
    foo()
    console.log(_count);
  }
  // 暴露給全局
  window.module1 = {
    // ES6 增強語法
    getCount,
  }
})(window)

https://ithelp.ithome.com.tw/upload/images/20210201/20124350VxQt4QgI2D.png

引入依賴

到目前為止都還不錯,不過我們還忘了引入依賴(這裡拿JQuery當例子)

// body部分
<body>
  <script
    src="https://code.jquery.com/jquery-3.5.1.js"
    integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc="
    crossorigin="anonymous"
  ></script>
  <script src="./module1.js"></script>
</body>

js部分

// module1.js
(function (window, $) {
  let _count = 0
  function foo() {
    _count += 1
  }
  
  function getCount() {
    foo()
    console.log(_count);
  }
  function changeColor() {
    console.log(++_count);
    $('body').css('background', 'red')
  }
  window.module1 = {
    // ES6 增強語法
    getCount,
    changeColor
  }
})(window, jQuery)

https://ithelp.ithome.com.tw/upload/images/20210201/201243504KIXGXSYI0.png

看起來都不錯啊為何還需要其他規範

原因有兩個,導致難以維護

  1. 請求過多

    有一堆 ,而且依賴過多需要發發送過多請求

  2. 依賴模糊

    從HTML來看根本看不出來誰依賴誰

CommonJS

Node.js 為主要實踐者,每個模塊有各自的作用域,變量或函數都是私有,外界不可視

服務器端:模塊的加載是運行時同步加載的

瀏覽器端:模塊需要提前編譯打包處理

特點:

  1. 模塊可以多次加載,但只會在第一次加載運行一次,會將運行結果緩存,下次從其他地方導入會從緩存讀取,如果想再一次運行模塊需要清除緩存
  2. 加載的順序,按照其在代碼中出現的順序。

基本語法

  1. 暴露:

module.exports 或是 exports

  1. 引入:

require(xxx)

第三方模塊(比方說npm install xxx): xxx即是模塊名子

自定義模塊: xxx是路徑

  1. 例子:

    CommonJS模塊的加載機制是,輸入的是被輸出的值的淺拷貝。

    注意跟ES6 差很多

    // module1.js
    let counter = 5;
    let objCounter = {
      value: 5
    }
    
    function addCounter() {
      ++counter;
    }
    function addObjCounter(params) {
      ++objCounter.value 
    }
    module.exports = {
      counter,
      objCounter,
      addCounter,
      addObjCounter,
    };
    
    // module2.js
    // 注意這裡要用node
    // 所以要再控制台輸入 node module2.js (注意先cd到放module2.js的資料夾)
    
    const module1 = require('./module1');
    
    console.log(module1.counter); // 5
    console.log(module1.objCounter.value); // 5
    
    
    module1.addCounter()
    module1.addObjCounter()
    
    console.log(module1.counter); // 5
    console.log(module1.objCounter.value); // 6(因為是淺拷貝,所以會增加)
    
    

exports跟module.exports 的差別

建議使用module.exports進行導出,因為最終導出的絕對是module.exports 指向的內存地址的對象

epxorts比較像是node給你的語法糖

https://ithelp.ithome.com.tw/upload/images/20210201/20124350tsVY1gAmug.png

如果今天exports 指向新的對象

https://ithelp.ithome.com.tw/upload/images/20210201/20124350UugnLm56x8.png

導出是module.exports 指向的內存地址的對象,所以當然還是

{name: 'Mike', age: 15}

AMD

AMD (Asynchronous Module Definition),如同他的名子,處理異步加載模塊

如果是瀏覽器環境,要從服務器端加載模塊,這時就必須採用非同步模式,因此瀏覽器端一般採用AMD規範

順帶一提Commonjs處理同步,因為Node.js主要用於服務端編成,模塊文件儲存在本地硬碟,加載快速。所以通常不會造成阻塞

基本語法

導出模塊:

// 不依賴其他模塊
define(function(){
   return 模块
})

// 依賴其他模塊
define(['module1', 'module2'], function(m1, m2){
   return 模块
})

導入模塊:

require(['module1', 'module2'], function(m1, m2){
   // 使用m1和m2
})

原始模塊開發與AMD比較

  • 未使用AMD規範

目錄結構
├─alert.js
├─index.html
├─main.js
└store.js
// store.js文件
(function (window) {
  let msg = '我在store.js裡'

  function getMsg() {
    return msg
  }
  window.store = {
    getMsg,
  }
})(window)
// alerter.js文件
(function (window, store) {
  let addMsg = '我被alert添加了'

  function showMsg() {
    alert(store.getMsg() + ', ' + addMsg)
  }
  window.alerter = {
    showMsg
  }
})(window, store)
// main.js文件
(function (alerter) {
  alerter.showMsg()
})(alerter)

<!-- index.html -->
<body>
  <script src="./store.js"></script>
  <script src="./alert.js"></script>
  <script src="./main.js"></script>
</body>

https://ithelp.ithome.com.tw/upload/images/20210201/20124350vQ25QD90Jf.png

缺點:

  1. 會發送多個請求(一堆)
  2. 只看index.html根本看不出依賴誰
  3. 引入順序完全不能有錯
  • 使用AMD規範(這裡透過require.js)

require載點

https://github.com/requirejs/requirejs/blob/master/require.js

目錄結構
├─index.html
├─main.js
├─lib
|  └require.js
├─js
| ├─alerter.js
| └store.js
// store.js文件
// 定義無依賴模塊
define(function () {
  let msg = '我在store.js裡'

  function getMsg() {
    return msg
  }
  // 暴露模块
  return {
    getMsg
  } 
})

// alerter.js文件
// 定義有依賴模塊
define([
  'store',
], function(store) {
  let addMsg = '我被alert添加了'

  function showMsg() {
    alert(store.getMsg() + ', ' + addMsg)
  }
  return {
    showMsg
  }
});

// main.js文件
(function () {
  // 配置require
  require.config({
    baseUrl: 'js/', 
    paths: {
      // 映射,標註模塊名子(依賴時的數組裡的名子就是這個)
      alerter: './alerter', // 不能寫成alter.js會報錯誤
      store: './store'
        
      // 導入第三方庫
      // jquery: './libs/jquery-1.10.1' //注意:寫成jQuery會報錯
    }
  })
  require(['alerter'], function (alerter) {
    alerter.showMsg()
  })
})()

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- 引入require.js並指定js主文件的入口 -->
  <script data-main="main" src="lib/require.js"></script>
</body>
</html>

注意事項

  1. 入口文件(main)定義在index.html
  2. 入口文件配置所有導出模塊的名稱(映射關係)

https://ithelp.ithome.com.tw/upload/images/20210201/20124350pKP1f9JQX3.png

CMD

整合AMD以及CommonJS

基本語法

導出模塊:

// 不依賴其他模塊
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})

// 依賴其他模塊
define(function(require, exports, module){
  //引入依賴模塊(同步)
  var module2 = require('./module2')
  //引入依赖模塊(異步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})

導入模塊:

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

CMD例子(透過sea.js)

sea載點

https://github.com/seajs/seajs/blob/master/dist/sea.js

目錄結構
├─index.html
├─result.txt
├─lib
|  └sea.js
├─js
| ├─main.js
| ├─moduleA.js
| ├─moduleB.js
| ├─moduleC.js
| └moduleD.js
// moduleA.js文件
define(function (require, exports, module) {
  console.log('我(A)被加載了')
  //内部數據
  var data = ' 我在ModuleA裡面'
  //内部函数
  function show() {
    console.log('moduleA show() ' + data)
  }
  //向外暴露
  exports.show = show
  // 上面那樣用跟module.exports.show依樣意思
})

// moduleB.js文件
define(function (require, exports, module) {
  //内部數據
  var data = ' 我在ModuleB裡面'
  //向外暴露
  console.log('我(B)被加載了')
  exports.data = data
})

// moduleC.js文件
// 這個會被異步加載
define(function (require, exports, module) {
  const TOKEN = 'abc123'
  exports.TOKEN = TOKEN
})
// moduleD.js文件
define(function (require, exports, module) {
  //引入依賴模塊(同步)
  var moduleB = require('./moduleB')

  function show() {
    console.log('moduleD show() ' + moduleB.data)
  }
  exports.show = show
  // 異步引入依賴模塊
  require.async('./moduleC', function (moduleC) {
    console.log('異步引入moduleC  ' + moduleC.TOKEN)
  })
})

// main.js文件
define(function (require) {
  var moduleA = require('./moduleA')
  var moduleD = require('./moduleD')
  moduleA.show()
  moduleD.show() // moduleD引入
})

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="text/javascript" src="lib/sea.js"></script>
  <script type="text/javascript">
    seajs.use('./js/main')
  </script>

</body>
</html>

https://ithelp.ithome.com.tw/upload/images/20210201/20124350viNtoRRqlT.png

注意事項

  1. 入口文件(main)也是定義在index.html
  2. 在各個模塊透過require導入,不像AMD集體定義在main.js
  3. 跟CommonJS一樣有exports跟module.exports

ES6 模塊化

export

  • 第一種:
export var a = 125
export const _b = 'Mike'
export let c = 2222
  • 第二種(推薦使用這種)
var a = 125
const _b = 'Mike'
let c = 2222

export {a, _b, c}; 
  • 第三種
var a = 125
const _b = 'Mike'
let c = 2222
export {
  a as var1,
  _b as var2,
  c as var3 };

​ 注意:

export命令規定要處於模塊頂層, 假如出現在塊級作用域( { } ) 就會報錯,import同理

  • 默認輸出:
// module2.js
export default function(){
  console.log('foo')
}
// 相当于
function a(){
  console.log('foo')
}
export {a as default}; 

import可以指定任意名字

import Foo from './module2'
// 相当于
import {default as Foo} from './module2'

import

  • 第一種:
import {a, _b ,c} from './profile'
  • 第二種
import {stream1 as firstVal} from './profile'

  • 加載
import { foo } from './module1'
import { bar } from './module1'

// 相当于
import {foo,bar} from './module1'

  • 全部導入
import * as circle from './module1'
circle.foo();
circle.bar();

與CommonJS差異

  1. CommonJS模塊輸出的是一個值的淺拷貝,ES6模塊輸出的是值的引用(賦值)
  2. CommonJS模塊是運行時加載,ES6模塊是編譯時加載

第一個差異:

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4 (會指向lib模塊的counter(內存地址))

第二個差異:

  • 運行時加載: CommonJS 模塊就是對象;即在輸入時是先加載整個模塊,生成一個對象,然後再從這個對像上面讀取方法,這種加載稱為“運行時加載”。

  • 編譯時加載: ES6模塊不是對象,而是通過export命令顯式指定輸出的代碼,import時採用靜態命令的形式。即在import時可以指定加載某個輸出值(可能會導致變量提升),而不是加載整個模塊,這種加載稱為“編譯時加載”。

延伸閱讀

Module 的语法

Module 的加载实现


上一篇
Day 24 [編程03] [譯文] 如何在JavaScript 中更好地使用數組
下一篇
Day 26 [其他04] ES6的Symbol竟然那么强大,面试中的加分点啊
系列文
從技術文章深入學習 JavaScript29

尚未有邦友留言

立即登入留言