開發者須要瞭解的nodejs中require的機制

原文地址:medium.freecodecamp.org/requiring-m…javascript

image

node中採用了兩個核心模塊來管理模塊依賴:html

  • require模塊:全局可見,不須要額外使用require('require')
  • module模塊:全局可見,不須要額外使用require('module')

能夠認爲require模塊是一個command,module模塊是所需模塊的organizer。 在Node中引用模塊並非一件複雜的事情:const config = require('/path/to/file'); require模塊暴露出一個函數(就像上面看到的那樣)。當require()函數傳入一個path參數的時候,node會依次執行以下步驟:java

  • Resolving : 找到path的絕對路徑。
  • Loading: 肯定文件的內容。
  • Wrapping:構造私有的做用域。Wrapping能夠確保每次require文件的時候,require和exports都是私有的。
  • Evaluating:evaluating環節是VM處理已加載文件的最後一個環節。
  • Caching:爲了不引用相同的文件狀況下,不重複執行上面的步驟。

本文中筆者將經過案例講解上面提到的不一樣階段以及這些階段對於開發者開發node模塊的影響。 首先經過終端建立一個文件夾mkdir ~/learn-node && cd ~/learn-node 下面本文全部的命令都是在~/learn-node中執行。node

Resolving a local path

首先來介紹下module對象。讀者能夠經過REPL來看到module對象 c++

image

每一個module對象都有id屬性用來區分不一樣的模塊。id屬性通常都是模塊對應的絕對路徑,在REPL中會簡單的設置爲<repl>。Node模塊和系統磁盤上的文件是一一對應的。引用模塊實際上會將文件中的內容加載到內存中。node支持經過多種方式來引用文件(好比說經過相對路徑或者預配置路徑),在把文件中內容加載到內存以前,須要先找到文件的絕對路徑。 當不指定路徑直接引用find-me模塊的時候:require('find-me'); node會依次遍歷module.paths指定的路徑來尋找find-me模塊: json

image

上面的路徑是從當前路徑到根目錄全部目錄下的node_modules文件夾的路徑,除此以外也包括一些遺留的可是已經不推薦使用的路徑。若是node在上述路徑中都找不到find-me.js就會拋出一個「cannot find module error.」的異常。 api

image

若是在當前文件夾下建立一個node_modules文件夾,並建立一個find-me.js文件,這時require('find-me')就可以找到find-me了。 瀏覽器

image

若是其餘路徑下也存在find-me.js 好比在用戶的home目錄下的node_modules文件夾下面存在另一個find-me.js: 緩存

image

當在learn-code目錄下執行require('find-me'),因爲在learn-code下的node_modules目錄下有一個find-me.js,此時用戶的home目錄下的find-me.js並不會加載執行。 app

image

若是咱們從~/learn-code目錄下刪除node_modules文件夾,再執行引用find-me,則會使用用戶home目錄下的node_modules下的fine-me:

image

Requiring a folder

模塊不必定只是一個文件,讀者也能夠建立一個find-me文件夾,而且在文件夾中建立index.js,require('find-me')的時候會引用index.js:

image

注意此時因爲當前目錄下有了find-me, 則此時引用find-me會忽略用戶home目錄下的node_modules。當引用目錄的時候,默認狀況下會尋找index.js,可是咱們能夠經過package.json中的main屬性來指示用那個文件。舉個例子,爲了讓require('find-me')可以解析到find-me文件夾下的其餘文件,咱們須要在find-me目錄下加一個package.json,並指定應該解析到哪一個文件:

image

require.resolve

若是隻想解析模塊但不執行模塊,可使用require.resolve函數。resolverequire函數的表現除了不執行文件以外,其餘方面表現是一致的。當文件找不到的時候仍然會拋出一個異常,在找到文件的狀況下會返回文件的絕對路徑。

image

resolve函數能夠用來檢測是否安裝了某個模塊,並在檢查到模塊的狀況下使用已安裝的模塊。

Relative and absolute paths

除了從node_modules中解析出模塊,咱們也能夠把模塊放在任何地方,經過相對路徑(./或者../打頭)或者絕對路徑(/打頭)的方式來引用該模塊。 好比,若是find-me.js在lib目錄下而不是在node_modules目錄下,咱們能夠經過這種方式來引用find-me:require('./lib/find-me');

Parent-child relation between files

建立一個lib/util.js並加入一行console.log來作區分,同時輸出module對象:

image

在index.js也加入相似的代碼,後面咱們經過node執行index.js。在index.js中引用lib/util.js:

image

在node中執行index.js:

image

注意index模塊(id: '.')lib/util模塊的父模塊。可是輸出結果中lib/util模塊並無顯示在index模塊的子模塊中。取而代之的是一個[Circular]的值,由於這兒是一個循環引用。此時若是node打印lib/utilindex的子模塊的話,則會進入到死循環。這也能夠解釋了爲何須要簡單的用[Circular]來代替lib/util。 那麼若是在lib/util模塊中引用index模塊會發生什麼。這就是node中所容許的的循環引用。

爲了可以更好的理解循環依賴,首先須要瞭解一些關於module對象上的一些概念。

exports, module.exports, and synchronous loading of modules

任何模塊中exports都是一個特別的對象。注意到上面的結果中,每次打印module對象,都會有一個爲空對象的exports屬性。咱們能夠在這個特別的exports對象上加入一些屬性。好比爲index.jslib/index.js暴露id屬性。

image

如今再執行index.js, 就能看到每一個文件的module對象上新增的屬性:

image

這裏爲了簡潔,筆者刪除了輸出結果中的一些屬性,可是能夠看到exports對象如今就有了咱們以前定義的屬性。你能夠在exports對象上增長任意數量的屬性,也能夠把整個exports對象替換成其餘東西。好比說想要把exports對象換成一個函數能夠以下:

image

再執行index.js就能夠看到exports對象變成了一個函數:

image

這裏把exports對象替換成函數並非經過exports = function(){}來完成的。實際上咱們也不能這麼作,由於模塊中的exports對象只是module.exports的引用,而module.exports纔是負責暴露出來的屬性。當咱們給exports對象從新賦值的時候,會斷開對module.exports的引用,這種狀況下只是引入了一個新的變量而不是修改module.exports屬性。

當咱們引入某個模塊,require函數返回的其實是module.exports對象。舉個例子,把index.js中require('./lib/util')修改成:

image

上面的代碼把lib/util中暴露出來的屬性賦值給UTIL常量。當咱們執行index.js時,最後一行會返回以下結果:UTIL: { id: 'lib/util' }

下面來談談每一個module對象上的loaded屬性。到目前爲止,每次咱們打印module對象的時候,loaded屬性都是爲false。module對象經過loaded屬性來記錄哪些模塊已經加載(loaded爲true),哪些模塊還未加載(loaded爲false)。能夠經過setImmediate方法來再下一個event loop中看到模塊已經加載完成的信息。

image

輸出結果以下:

image

再延遲的console.log中咱們能夠看到lib/util.jsindex.js已經被徹底加載。 當node加載模塊完成後,exports對象也會變成已完成狀態。requiring和loading的過程是同步的。這也是爲何咱們可以在一個循環以後可以看到模塊加載完成信息的緣由。

同時這也意味着咱們不能異步的修改exports對象。好比咱們不能像下面這麼作:

image

Circular module dependency

下面來回答前面提到的循環依賴的問題:若是模塊1依賴模塊2,同時模塊2又依賴模塊1,這時候會發生什麼呢? 爲了找到答案,咱們在lib目錄下建立兩個文件,module1.jsmodule2.js,並讓他們互相引用:

image

當執行module1.js的時候,會看到以下結果:

image

咱們在module1尚未徹底加載成功的狀況下引用module2,因爲module2中在module1尚未徹底加載成功的狀況就引用module1,此時在module2中可以獲得的exports對象是循環依賴以前的所有屬性(也就是require('module2')以前)。此時只能訪問到a屬性,由於bc屬性在require('module2')以後。

node在循環依賴這塊的處理十分簡單。你能夠引用哪些尚未徹底加載的模塊,可是隻能拿到一部分屬性。

JSON and C/C++ addons

經過require函數咱們能夠原生的加載JSONC++擴展。使用的時候甚至不須要指定擴展名。在文件擴展名沒有指定的狀況下,node首先會嘗試加載.js的文件。若是.js的文件沒有找到,則會嘗試加載.json文件,若是找到.json文件則會解析.json文件。若是.json文件也沒有找到,則會嘗試加載.node文件。可是爲了不語義模糊,開發者應該在非.js的狀況下指定文件的擴展名。

加載.json文件對於管理靜態配置、或者週期性的從外部文件中讀取配置的場景是十分有用的。好比咱們有以下json文件:

image

咱們能夠直接使用它:

image

運行上面的代碼會輸出:Server will run at http://localhost:8080 若是node找不到.js.json的狀況下,會尋找.node文件,並採用解析node擴展的方式來解析.node文件。

Node 官方文檔中有一個c++寫的擴展案例。該案例暴露了一個hello()函數,執行hello()函數會輸出world。你可使用node-gyp.cc文件編譯、構建成.node文件。開發者須要配置binding.gyp來告訴node-gyp該作什麼。在構建addon.node成功後,就能夠像引用其餘模塊同樣使用:

image

require.extensions能夠看到目前只支持三種類型的擴展:

image

能夠看到每種類型都有不一樣的加載函數。對於.js文件使用module._compile方法,對於.json文件使用JSON.parse方法,對於.node文件使用process.dlopen方法。

All code you write in Node will be wrapped in functions

node中對模塊的包裹經常被誤解,在理解node對模塊的包裹以前,先來回顧下exports/module.exports的關係。 咱們能夠用exports來暴露屬性,可是不能直接替換exports對象,由於exports對象只是對module.exporst的引用。

image

準確的來講,exports對象對於每一個模塊來講是全局的,定義爲module對象上屬性的引用。 在解釋node包裝過程前,咱們再來問一個問題。 在瀏覽器中,當咱們在全局環境中申明一個變量:var answer = 42; 在定義answer變量以後的腳本中,answer變量就屬於全局變量。 在node中並非這樣的。當咱們在一個模塊中定義了一個變量,另外的模塊並不能直接訪問該模塊中的變量,那麼在node中變量是如何被局部化的呢?

答案很簡單。在編譯模塊以前,node把模塊代碼包裝在一個函數中,咱們能夠經過module對象上的wrapper屬性來看到這個函數:

image

node並不會直接執行你寫在文件中的代碼。而是執行包裹函數的代碼,包裹函數會把你寫的代碼包裝在函數體中。這就保證了任何模塊中的頂級變量對於別的模塊來講是局部的。

wrapper函數有5個參數:exports,require,module,__filename__dirname。這也是爲何對於每一個模塊來講,這些變量都像是全局的緣由,實際上對每一個模塊來講,這些變量都是獨立的。

當node執行包裝函數的時候,這些變量都已經被正確賦值。exports被定義爲module.exports的引用,requiremodule都指向待執行的函數,__filename__dirname表示了被包裹模塊的文件名和目錄的路徑。

若是你運行了一個出錯的模塊,立馬就能看到包裹函數。

image

能夠看到報錯的是wrapper函數的第一行。除此以外,因爲每一個模塊都被函數包裹了一遍,咱們能夠經過arguments來訪問wrapper函數全部的參數。

image

第一個參數是exports對象,一開始是一個空對象,接着是require/module對象,這兩個對象不是全局變量,都是與index.js相關的實例化對象。最後兩個參數表示文件路徑和文件夾路徑。

包裹函數的返回值是module.exporst。在包裹函數內部,咱們能夠經過exports對象來修改module.exports的屬性,可是不能對exports從新賦值,由於exports只是一個引用。

上面描述的等價於下面的代碼:

image

若是咱們修改了exports對象,則exports對象再也不是module.exports的引用。這種引用的方式不只在這裏能夠正常工做,在javascript中都是能夠正常工做的。

The require object

require對象並無什麼特殊的。require是一個函數對象,接受模塊名或者路徑名,並返回module.exports對象。若是咱們想的話,能夠隨意的覆蓋require對象。 好比爲了測試,咱們但願能夠mock require函數的默認行爲,返回一個模擬的對象,而不是引用模塊返回module.exports對象。對require進行賦值能夠實現這一目的:

image

在對require進行從新賦值以後,每次調用require('something')都會返回mock對象。 require對象也有自身的屬性。前面咱們已經看到過了用於解析模塊路徑的resolve屬性以及require.extensions屬性。 除此以外,還有require.main屬性用來區別當前模塊是被引用仍是直接運行的。好比說咱們在print-in-frame.js文件中有一個printInFrame函數:

image

這個函數接受一個數值類型的參數numberic和一個字符串類型的參數header,函數中首先根據size參數打印指定個數*的frame,並在frame中打印header。 咱們能夠有兩種方式來使用這個函數:

  1. 命令行直接調用:~/learn-node $ node print-in-frame 8 Hello,命令行中給函數傳入8和Hello,打印一個8個*組成的frame,並在frame中輸出hello
  2. require方式調用:假設print-in-frame.js暴露出一個printInFrame函數,咱們能夠這樣調用:
    image

這樣會在5個*組成的frame 中打印Hey。 咱們須要某種方式來區分當前模塊是命令行單獨調用仍是被其餘模塊引用的。這種狀況,咱們能夠經過require.main 來作判斷:

image

這樣咱們能夠經過這個條件表達式來實現上述應用場景:

image

若是當前模塊沒有以模塊的方式被其餘模塊引用,咱們能夠根據命令行參數process.argv來調用printInFrame函數。不然,咱們設置module.exports參數爲printInFrame函數。

All modules will be cached

理解模塊緩存是十分重要的。咱們來經過一個簡單的例子來說解下,好比說咱們有一個以下的字符畫的js文件:

image

咱們但願每次require文件的時候都能顯示字符畫。好比咱們引用兩次字符畫的js,但願能夠輸出兩次字符畫:

image

第二次引用並不會輸出字符畫,由於此時模塊已經被緩存了。在第一次引用後,咱們能夠經過require.cache來查看模塊緩存狀況。cache對象是一個簡單的鍵值對,每次引用的模塊都會被緩存在這個對象上。cache上的值就是每一個模塊對應的module對象。咱們能夠從require.cache上移除module對象來讓緩存失效。若是咱們從緩存中緩存中移除module對象,從新require的時候,node依然會從新加載該模塊,並從新緩存該模塊。 可是,對於這種狀況,上面的修改緩存的方式並非最好的方法。最簡單的方法是把ascii-art.js包裝在函數中而後暴露出去,這樣的話,當咱們引用ascii-art.js的時候,會獲得一個函數,每次執行的時候都會輸出字符畫。

image
相關文章
相關標籤/搜索