JavaScript模塊化

爲什麼要使用模塊化?

當我們一個項目越做越大的時候,維護起來肯定沒那麼方便,且多人協作的去進行開發,當中肯定會遇到很多的問題,例如:

  1. 方法的覆蓋: 很有可能你定義的一些函數會覆蓋公共類中同名的函數,因爲你可能根本就不知道公共類中有哪些函數,也不知道是如何命名的。
  2. 這些公共的組件: 但是你又不知道這些組件又會依賴哪些模塊,同時在維護這些公共方法的時候,會新增一些依賴或者刪除一些依賴,那麼每個引入這些公共方法的地方都需要去對應的新增或者刪除。等等,還會存在很多的問題。

我們使用模塊化就是爲了讓各個模塊之間相對獨立,可能每個文件就是一個功能塊,能滿足於某項特定的功能,這樣我們在引用某項功能的時候就會很方便。

CommonJS

Node 應用由模塊組成,採用 CommonJS 模塊規範。

每個文件就是一個模塊,有自己的作用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其他文件不可見,CommonJS規範加載模塊是同步的,也就是說,加載完成纔可以執行後面的操作,Node.js主要用於服務器編程,模塊一般都是存在本地硬盤中,加載比較快,所以Node.js採用CommonJS規範。且CommonJS模塊輸出的是值的緩存, 運行時加載。
CommonJS規範規定,每個模塊內部,module變量代表當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的module.exports屬性。

// tools.ts 
const add = (a: number, b: number) =>{
  return a + b
}

const reduce = (a: number, b: number) => {
  return a - b
}

const multy = (a: number, b: number) => {
	return a * b
}

exports.add = add
exports.reduce = reduce

export default multy

// 等價於
module.exports = {
  add,
  reduce
}
// app.ts
const tools = require('./tools.ts')
tools.add(2, 3)       // 5
tools.reduce(3, 2)    // 1

Node內部提供一個Module構建函數。所有模塊都是Module的實例。

// Module 構造函數
function Module(id, parent) {
  this.id = id          //模塊的識別符,通常是帶有絕對路徑的模塊文件名
  this.exports = {}     //表示模塊對外輸出的值
  this.parent = parent  //返回一個對象,表示調用該模塊的模塊
  ...
}
// tools.ts 
const add = (a: number, b: number) =>{
  return a + b
}

exports.add = add
console.log(module)

// 輸出
Module {
  id: '.',
  exports: { add: [Function: add] },
  parent: null,
  filename: 'C:\\Users\\viruser.v-desktop\\Desktop\\zz\\aa.js', //模塊的文件名,帶有絕對路徑
  loaded: false,      //返回一個布爾值,表示模塊是否已經完成加載
  children: [],       //返回一個數組,表示該模塊要用到的其他模塊
  paths: [ 
    'C:\\Users\\viruser.v-desktop\\Desktop\\zz\\node_modules',
    'C:\\Users\\viruser.v-desktop\\Desktop\\node_modules',
    'C:\\Users\\viruser.v-desktop\\node_modules',
    'C:\\Users\\node_modules',
    'C:\\node_modules' 
  ] 
}

如果在命令行下調用某個模塊,比如node tools.js,那麼module.parent就是null。如果是在腳本之中調用,比如require(’./tools.js’),那麼module.parent就是調用它的模塊。利用這一點,可以判斷當前模塊是否爲入口腳本。

if (!module.parent) {
  // ran with `node something.js`
  app.listen(8088, function() {
    console.log('app listening on port 8088')
  })
} else {
  // used with `require('/.something.js')`
  module.exports = app
}

爲了方便,Node爲每個模塊提供一個exports變量,指向module.exports。這等同在每個模塊頭部,有一行這樣的命令。造成的結果是,在對外輸出模塊接口時,可以向exports對象添加方法。注意,不能直接將exports變量指向一個值,因爲這樣等於切斷了exports與module.exports的聯繫。

const exports = module.exports

AMD

大多數的同學都應該瞭解RequireJS,而且RequireJS是基於AMD規範的。AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之後,這個回調函數纔會運行。且同樣是運行時加載的,用require.config()指定引用路徑等,用define()定義模塊,用require()加載模塊, 但是不同於CommonJS,它要求兩個參數:

定義模塊

// define([module], callback)
define(['myLib'], () =>{
	function foo(){
		console.log('mylib')
	}
	return {
		foo : foo
 }
})
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  				//實際路徑爲js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});

使用模塊

// require([module], callback)
require(['myLib'], mod => {
  mod.foo()
})

// myLib

爲什麼要使用AMD規範呢?

因爲AMD是專門爲瀏覽器中js環境設計的規範。它吸取了CommonJS的一些優點,但是沒有全部都照搬過來。也是非常容易上手。

CMD

CMD在很多地方和AMD有相似之處,在這裏我只說兩者的不同點。首先,CMD規範和CommonJS規範是兼容的,相比AMD,它簡單很多。遵循CMD規範的模塊,可以在Node.js中運行。SeaJS是推薦是用CMD的寫法,那麼就使用SeaJS來編寫一個簡單的例子:

// AMD寫法  AMD的依賴需要前置書寫
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
  // 等於在最前面聲明並初始化了要用到的所有模塊
  a.doSomething()
  if (false) {
    // 即便沒用到某個模塊 b,但 b 還是提前執行了
    b.doSomething()
  }
})

// CMD寫法 CMD的依賴就近書寫即可,不需要提前聲明
define(function(require, exports, module) {
  var a = require('./a')  //在需要時申明  同步
  a.doSomething()
  if (false) {
    var b = require('./b')
    b.doSomething()
	}
	require.async('a', math =>{  //異步
    a.add(1, 2);
  })
})

/** sea.js **/
// 定義模塊 math.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  var add = function(a,b){
    return a+b
  }
  exports.add = add
})
// 加載模塊
seajs.use(['math.js'], function(math){
  var sum = math.add(1+2)
})

CMD規範我們可以發現其API職責專一,例如同步加載和異步加載的API都分爲require和require.async,而AMD的API比較多功能。

UMD

UMD是AMD和CommonJS的糅合

AMD模塊以瀏覽器第一的原則發展,異步加載模塊。
CommonJS模塊以服務器第一原則發展,選擇同步加載,它的模塊無需包裝(unwrapped modules)。
這迫使人們又想出另一個更通用的模式UMD (Universal Module Definition)。希望解決跨平臺的解決方案。

UMD先判斷是否支持Node.js的模塊(exports)是否存在,存在則使用Node.js模塊模式。
在判斷是否支持AMD(define是否存在),存在則使用AMD方式加載模塊。

(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 () {
  //module ...
})

ESModule

歷史上,JavaScript一直沒有模塊(module)體系,無法將一個大程序拆分成互相依賴的小文件,再用簡單的方法拼裝起來。其他語言都有這項功能,比如Ruby的 require 、Python的 import ,甚至就連CSS都有 @import ,但是JavaScript任何這方面的支持都沒有,這對開發大型的、複雜的項目形成了巨大障礙。

ES6模塊的設計思想,是儘量的靜態化,使得編譯時就能確定模塊的依賴關係,以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時確定這些東西。比如,CommonJS模塊就是對象,輸入時必須查找對象屬性。

模塊功能主要由兩個命令構成: export 和 import。export 命令用於規定模塊的對外接口,import 命令用於輸入其他模塊提供的功能

// a.js
var num1 = 1
var num2 = 2

export { num1, num2 }
// b.js
import { num1, num2 } from './a.js'

function add(num1, num2) {
  return num1 + num2
}

console.log(add(num1, num2))

如果想爲輸入的變量重新取一個名字,import命令要使用 as 關鍵字,將輸入的變量重命名。

import { num1 as snum } from './a'

mport 命令具有提升效果,會提升到整個模塊的頭部

add();
import { add} from './tools'

如果在一個模塊之中,先輸入後輸出同一個模塊, import 語句可以與 export 語句寫在一起。

export { es6 as default } from './a'

// 等同於
import { es6 } from './a'
export default es6

還可以使用整體加載,即用星號( * )指定一個對象,所有輸出值都加載在這個對象上面, 但不包括default

import * as tools from './a'
import multy from './a
tools.add(2, 1)  		//3
tools.reduce(2, 1)  //1
multy(2,1)					//2

ES6模塊加載的實質

ES6模塊加載的機制,與CommonJS模塊完全不同。CommonJS模塊輸出的是一個值的拷貝,而ES6模塊輸出的是值的引用。

CommonJS模塊輸出的是被輸出值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值

// lib.js
var counter = 3
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
}
// main.js
var mod = require('./lib')
console.log(mod.counter) // 3
mod.incCounter()
console.log(mod.counter) // 3

lib.js 模塊加載以後,它的內部變化就影響不到輸出的 mod.counter 了。這是因爲 mod.counter 是一個原始類型的值,會被緩存。除非寫成一個函數,才能得到內部變動後的值

// lib.js
var counter = 3
function incCounter() {
  counter++
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
}
// main.js
var mod = require('./lib')
console.log(mod.counter) // 3
mod.incCounter()
console.log(mod.counter) // 4

ES6模塊的運行機制與CommonJS不一樣,它遇到模塊加載命令 import 時,不會去執行模塊,而是隻生成一個動態的只讀引用。等到真的需要用到時,再到模塊裏面去取值,換句話說,ES6的輸入有點像Unix系統的”符號連接“,原始值變了,import 輸入的值也會跟着變。因此,ES6模塊是動態引用,並且不會緩存值,模塊裏面的變量綁定其所在的模塊。

發佈了6 篇原創文章 · 獲贊 1 · 訪問量 1511
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章