Node.js module system (一) - module.exports 及 require 運作原理

在 Node.js 中,若有載入多支 JS 檔需求時,並不能像以往前端開發一樣,用 <script> 標籤引入多支檔案,且它也沒有 HTML 檔案來支持你這麼做。Node.js 須要使用 require 的方式來載入其他的 JS 檔案,這也是模組系統的主要使用方法。

什麼是模組?

在 Node.js 中的模組(Module),指的就是單一支一支的 JS 檔案,透過互相引入的方式來達成繫節關係。

簡單範例

資料夾結構:
 nodejs-module
  ∟ A.js
  ∟ B.js

1
2
3
4
// A.js
console.log('A.js 開始執行了!')
require('./B.js')
console.log('A.js 結束執行了!')
1
2
// B.js
console.log('B.js文件被載入且執行了!')

執行 node A.js

1
2
3
A.js 開始執行了!
B.js文件被載入且執行了!
A.js 結束執行了!

也就是說,當你執行某支檔案時,程式碼執行到 require 字眼時,Node.js 就會執行被 require 那支檔案內的程式碼。

在 Node.js 中沒有全域作用域,只有模組作用域

資料夾結構:
 nodejs-module
  ∟ A.js
  ∟ B.js

1
2
3
4
5
6
// A.js
var foo = 'A'
console.log('A.js 開始執行')
require('./B.js')
console.log('A.js 結束執行')
console.log('foo 的值是' + foo)
1
2
3
4
// B.js
console.log('B.js 開始執行')
var foo = 'B'
console.log('B.js 結束執行')

在這個例子當中,若是以以往 <script> 載入的概念來看是像這樣的

1
2
<script src="A.js"></script>
<script src="B.js"></script>

全域的 foo 變數會被後來載入的 B.js 內的 foo 覆蓋,預期應 foo 應該會是 B。但是在 Node.js 中是這樣嗎?

試著來輸出結果看看

1
2
3
4
5
A.js 開始執行
B.js 開始執行
B.js 結束執行
A.js 結束執行
foo 的值是A

延續上面的例子,如果我們在 A.js 中定義一個函數,並試著在 B.js 呼叫這個函數,能夠成功嗎?

1
2
3
4
5
6
7
8
9
// A.js
console.log('A.js 開始執行')

function add (x, y) {
return x + y
}

require('./B.js')
console.log('A.js 結束執行')
1
2
3
4
// B.js
console.log('B.js 開始執行')
console.log(add(10, 5))
console.log('B.js 結束執行')

輸出結果

1
2
3
console.log(add(10, 5))
^
ReferenceError: add is not defined

由上面的例子我們可以理解所謂模組作用域其實就是 檔案作用域,只要不是寫在自己本身的變數或函式,即使我載入了你,我還是不能取用你的東西的,載入就是單純執行對方內部的程式碼而已。

注意事項

  • require 時副檔名可以省略

    1
    require('./B.js') // === require('./B')
  • 載入自己寫的模組(JS檔),相對路徑不得省略

    1
    require('B.js') // Cannot find module 'B'

模組間的通信

上面的例子或許讓你感到很困惑,既然載入了它,不就是為了要取用它的部分成員嗎?如果只是執行內部的程式碼,而不能取用成員,為何還要載入它呢?

是的,Node.js 中預設模組是封閉了,你可以載入我,但你取用不到我任何的成員,內部無法取用外部; 外部也無法取用內部。

但有時候載入模組的目的不只是單純為了執行裡面的程式碼,更重要的是取用裡面的成員阿!

那,該怎麼做?

導出 (exports)

Node 中的 require 方法有兩個作用

  1. 載入模組並執行裡面的程式碼 (也就是上面的例子)
  2. 拿到被加載模組所導出的接口物件
    • 在每個模組中都提供一個物件 exports,我們可以將它導出,並讓其他模組接收,而它預設是一個空的物件

資料夾結構:
 nodejs-module
  ∟ A.js
  ∟ B.js

1
2
3
// A.js
var result = require('./B')
console.log(result)
1
2
// B.js
console.log('B.js 被加載執行了')

執行 node A.js 結果

1
2
B.js 被加載執行了
{}

A.js 利用一個變數 require 了 B.js 的接口物件。結果發生了兩件事

  1. 執行了 B.js 檔案內的程式碼
  2. result 變數接收了 B.js 的接口物件,由於 B.js 沒有導出任何成員,因此 A.js 接到的是空物件。

你可以試著在任一模組裡,印出 exports 物件,證明它預設就是一個空物件

1
2
// B.js
console.log(exports)

執行 node B,果然得到一個空物件

1
{}

導出成員

了解上面的原理後,接著我們可以開始導出成員了,只需要將想被導出的成員放入 exports 物件中,接著讓須要取用的模組 require 即可

1
2
3
4
5
6
7
8
9
10
11
// B.js
exports.foo = 'B'

exports.add = function(x, y) {
return x + y
}
// 此時的 exports 是
// {
// foo: 'B',
// add: function(x, y) { return x + y}
// }
1
2
3
4
5
// A.js
var B_exports = require('./B')

console.log(B_exports.foo)
console.log(B_exports.add(10, 5))

輸出 node A.js 結果

1
2
B
15