說個事, 親,你知道當你運行js的時候,發生錯誤的時候下面的信息表明的是什麼嗎?html
like前端
SyntaxError: Unexpected token . at exports.runInThisContext (vm.js:53:16) at Module._compile (module.js:414:25) at Object.Module._extensions..js (module.js:442:10) at Module.load (module.js:356:32) at Function.Module._load (module.js:313:12) at Function.Module.runMain (module.js:467:10) at startup (node.js:136:18)
母雞吧,實際上,這個nodeJS中Module模塊的內容。 說道模塊,這就牽扯到整個js現存的那些優秀的模塊機制呢。首當其衝的要數,AMD,而後是NodeJS的加載模塊.下面,咱們來了解一下內部的機制,肯定上面的錯誤,到底表明什麼.node
AMD 全稱爲: Asynchronous Module Definition(異步模塊定義)jquery
AMD規範應該是之前 前端 最常使用的一個模塊化規範. 他經過異步的方式加載js 腳本,而且執行相關的內容.
咱們先來看一下基本的AMD格式. 他其實就提供了一個全局函數-define.json
define([arr], function(para){//...});
該函數接受兩個參數segmentfault
第一個參數: 接受的是數組類型, 裏面定義的參數是相關的js文件的路徑或者js文件的alias. 路徑或者alias 表明着一個引入的js模塊. 好比: ['/arr/script.js','/js/demo.js','jquery']
,不論是定義路徑仍是定義alias, 最終, 都會被插件解析爲 實際的js代碼. 這個具體就涉及路徑的解析了, 咱們下文在說一下api
第二個參數: 接受的是函數. 而且函數能夠帶入參數. 參數的位置, 和前面數組引入的模塊位置一致. 而後咱們就能夠直接使用模塊內容。define(['/arr/isArray.js','jquery'],function(isArray,$){//...})
咱們接着就能夠直接使用定義好的模塊alias就能夠了。數組
因爲AMD 僅僅提出了一個define函數用來異步加載腳本. 可是服務端的場景,這顯然就有點雞肋了. 因此, nodeJS 基本上參考了 CommonJS Modules/1.1 proposal. 他爲了更精確的表達 server 端模塊化的機制. 定義了3個全局的變量 exports
, require
, module
. 須要注意的是... 其實exports 並非單一的一塊,他實際上是.緩存
var exports = module.exports = {};
即, 其實nodeJS模塊交互只有require 和 module 在進行。 咱們來具體看一下 , nodeJS 端 進行模塊化的機制吧.
首先,咱們得明白什麼叫作模塊?app
A module encapsulates related code into a single unit of code.
看到這段話後,更覺更懵逼了. 能不能說人話~
其實, 模塊就是可以完成必定工做的函數,對象,甚至基本的數據類型(好比:String,Number等);
來, 咱們能夠寫一個demo:
var sayHello = function(){ return 'hello'; } var move = function(){ return 'Now, I am moving'; }
上面兩個函數咱們就能夠說,是模塊.
Ok~ 如今咱們已經寫了一個簡單的模塊了, 那接下來該怎麼導出這個模塊呢?
這裏咱們就須要使用exports方法進行導出便可.
//dist.js // var exports = module.exports = {}; exports.sayHello = function(){ return 'hello'; } exports.move = function(){ return 'Now, I am moving'; }
這裏,咱們須要注意一下:exports = module.exports.
因爲是對象,咱們還能夠利用對象原本的特徵, 經過字面量形式書寫
//dist.js //下面的module.exports 不能使用exports代替 module.exports = { sayHello : function(){ return 'hello'; } move : function(){ return 'Now, I am moving'; } } //若是你寫成以下 exports = {//...} //那麼你的exports關鍵字已經和module.exports斷開聯繫了.
可是, 現實狀況是, 不推薦這樣直接 將 exports 用字面量表達. 由於這樣形成將一開始寫入的內容給覆蓋掉.
exports.getName = function(){ return "jimmy"; }; exports.flag = "It will be overloaded"; //上面全部的都將會被覆蓋掉 module.exports = { //這裏只能使用 getName : function(){ return "sam" }, sayName :function(){ return "Michael" } }
因此,推薦的兩點:
若是一開始使用exports.xxx
導出的話, 後面就不要使用exports = {}
導出.
能夠在最後部分直接使用 exports = {}
進行導出, 這樣的, 可以讓你的代碼更清晰.
OK, 基本模塊樣式咱們已經寫完了. 如今就輪到如何引用模塊了.
最後上面3個關鍵字,就只剩下了 require
. 那require的工做機制是怎樣的呢?
var require = function(path) { // 經過路徑查找文件, 並解析 return module.exports; };
因此, 如今模塊機制的難點不在是 模塊是怎麼 引用的, 而變成了 路徑解析的問題, 咱們能夠放到後面再進行討論。 咱們如今能夠梳理一下, 模塊內容傳遞的 Process.
app.js => module.exports => require => 自定義變量
因此,一個模塊 就經歷了以上的流程傳遞到你最後引用的變量裏面了。 咱們來看一下總體的demo.
//dist.js module.exports = { sayHello : function(){ return 'hello'; } move : function(){ return 'Now, I am moving'; } } //main.js var action = require('../dist.js'); console.log(action.sayHello()); //hello console.log(action.move()); //Now, I am moving
經過模塊機制, 咱們能夠很容易的瞭解到. require其實就是一個包裝函數. 在函數體內部進行 一些列的路徑轉換. 好比, 路徑解析, 包的緩存,模塊的加載,內置模塊等等。
咱們稍微膚淺一點,看一下. require是怎樣進行路徑加載的吧.
這裏,咱們依照官方的說明.前提是:
在Y路徑下,使用require(X) 引用. 會按一下步驟進行解析
若是X是內置的模塊,好比http,net等. 直接返回. over
若是X帶上'/'或者'./'或者'../'.
會根據X所在的父目錄,肯定X所在的絕對位置.
先假設X是文件,而後按照順序依次查找下列文件
x
x.js
x.json
x.node
若是找到則返回
若是X是目錄,則依次查找下列文件:
X/package.json(main 字段)
X/index.js
X/index.json
X/index.node
若是找到則返回.
若是X不是以'/'或'./'或'../'開頭. 則會根據X所在的父目錄,對node_modules進行回朔遍歷. 接着,經過上述肯定X爲文件或者目錄的方式,進行查找.
若是上述的流程都沒有找到則會拋出錯誤(Not Found)
這裏,咱們具體來看一下 node_modules的查找. 假設在路徑/usr/app/shop 下運行 require('bar'); 以後, 程序遍歷的結果是.
首先, 假設bar是文件,查找路徑爲
/usr/app/shop/node_modules/bar
/usr/app/shop/node_modules/bar.js
/usr/app/shop/node_modules/bar.json
/usr/app/shop/node_modules/bar.node
若是,在該目錄下沒有找到,則會進行回朔(../).則遍歷路徑爲:
/usr/app/shop/node_modules
/usr/app/node_modules
/usr/node_modules
/node_modules
若是假設爲目錄. 相似,查找爲:
bar/package.json(main)
bar/index.js
bar/index.json
bar/index.node
一樣,也有路徑回朔(../). 如上,這裏就不贅述了
實際上, nodeJS的壯大, 其一是其自己的異步機制和事件mode 優點帶動的, 其二就是其自己優秀的模塊機制. 經過Modules 模塊, nodeJS將其自己的擴展性,提的老高老高. 上述路徑解析,其實就是nodeJS Modules機制中的一部分. 詳情能夠參考一下:modules詳情
其實,咱們寫的每個js文件,在run的時候,都會包裹一層Modules.具體情形就是:
(function (exports, require, module, __filename, __dirname) { // 模塊源碼 return exports; });
實際上,module其實就是Modules的一個實例,在源碼中定義的Modules函數實際內容,並不複雜:
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if (parent && parent.children) { parent.children.push(this); } this.filename = null; this.loaded = false; this.children = []; } module.exports = Module;
能夠說,咱們全部的模塊都是創建在Module這一個構造函數上的. 那這些對象,咱們應該怎麼獲取呢?
實際上,clever的童鞋,已經意識到了, module在運行的時候已經傳進來了,咱們能夠直接調用.
一個簡單的demo:
app.js
console.log('module.id: ', module.id); console.log('module.exports: ', module.exports); console.log('module.parent: ', module.parent); console.log('module.filename: ', module.filename); console.log('module.loaded: ', module.loaded); console.log('module.children: ', module.children); console.log('module.paths: ', module.paths);
運行: ndoe app.js
結果,爲:
module.id: . module.exports: {} module.parent: null module.filename: /Users/jimmy_thr/Documents/code/shopping/app/sam.js module.loaded: false module.children: [] module.paths: [ //內容過多忽略 ]
有興趣的童鞋,能夠本身運行試一試.
那每一個屬性對應的是什麼內容呢?
property | effect |
---|---|
id | 引用的模塊名--當沒有父模塊時爲:. 有則爲絕對路徑 |
exports | 就是使用module.exports 導出的方法或者變量 |
parent | 很簡單,就是父模塊.也就是另一個module實例 |
filename | 模塊的絕對路徑 |
loaded | 用來表示,模塊是否已經所有加載(沒太多用處) |
children | 數組類型,表示子模塊 |
paths | 包含模塊可能存在的位置,以備下次require的時候搜索 |
能夠看出,經過run以後, 有3個global對象,分別爲,require,module,exports. 那實際上,他們3者的關係是什麼呢?
咱們來看一下源碼裏面是怎麼作的吧.
這是require 方法的具體細節:
Module.prototype.require = function(path) { return Module._load(path, this); };
實際上, require 只是一層皮, 裏面套的是Module的_load方法.
代碼內有不少debug和alert, 去掉檢測的內容,咱們來看一下內部機理.
Module._load = function(request, parent, isMain) { // 計算絕對路徑 var filename = Module._resolveFilename(request, parent); // 第一步:若是有緩存,取出緩存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; // 第二步:是否爲內置模塊 if (NativeModule.exists(filename)) { return NativeModule.require(filename); } // 第3.1步:加載模塊,生成模塊實例,存入緩存 var module = new Module(filename, parent); Module._cache[filename] = module; // 第3.2步: 載入模塊內容 try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } // 第四步:輸出模塊的exports屬性 return module.exports; };
這下大概清楚了,實際上, 在路徑解析以前,其實Module 還會對內置模塊進行其餘的檢測.
實際順序爲:
是否已經緩存
是否爲內置模塊
加載模塊
生成模塊實例,存入緩存
路徑解析
最終返回module.exports
這裏,咱們也能夠看到NodeJS 模塊加載的另一個機制.
只要require事後的模塊都會被保存在緩存當中. 當須要再次引用的時候,則會直接從緩存中獲取.
Module裏面自定義了不少路徑的處理和緩存的處理。 咱們這裏, 只關注一下. module.load的內容. 源碼以下
Module.prototype.load = function(filename) { this.filename = filename; this.paths = Module._nodeModulePaths(path.dirname(filename)); var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; Module._extensions[extension](this, filename); this.loaded = true; };
這裏很簡單,用來肯定文件後綴的加載:
X
X.js
X.json
X.node
首先,在理解內部機制以前,咱們須要瞭解一下關於path 模塊。 該模塊一般使用來處理文件路徑的.
path.basename(p[, ext])
返回基本的文件名. 若是ext有參數,則表示不帶指定尾綴返回. 好比:usr/home/app.js
=>app.js
. 若是指定ext爲.js
則返回app
. 更好的理解方式爲: p-ext
path.dirname(p)
返回目錄名. usr/home/app.js
=>usr/home
path.extname(p)
返回文件名的後綴。一般是最後一個'.'到字符串最後. index.html
=>.html
。若是沒有'.'則會返回一個空字符.index
=>''
path.format(pathObject)
將路徑對象轉化爲字符串路徑. 即.path.format({//...})
. Object能夠帶的屬性有:
root
dir
base
ext
name
一個簡單的demo:
path.format({ root : "/", dir : "/home/user/dir", //後面不用加`/`系統會自動補充 base : "file.txt", name : "file", ext : ".txt" }); // returns '/home/user/dir/file.txt'
實際上, 咱們只須要使用一部分便可。
俺,經常使用的組合爲: dir + base. 或者 dir+name+ext
path.isAbsolute(path)
用來檢查路徑是否爲絕對路徑。絕對路徑很好理解, 1. 看你的路徑是否在根目錄上. 2. 看你的路徑的開頭是不是/
。/usr/path
=> true, shop/app.js
=>false
path.join(path1[, ...])
使用/
來鏈接多個字符,並對..
或者.
進行路徑轉化. 這是一個比較重要的方法. 經常用在路徑處理.
path.join('/foo', 'bar', 'baz/sam', 'quux', '..'); 返回爲: '/foo/bar/baz/sam'
path.normalize(p)
對路徑字符串解釋, 會處理..
和.
。
path.normalize('/usr/home/../sam'); 返回: '/usr/sam'
path.parse(pathString)
該方法和path.format相反,是將路徑字符串轉化爲路徑對象
path.parse('/home/user/dir/file.txt') // returns // { // root : "/", // dir : "/home/user/dir", // base : "file.txt", // ext : ".txt", // name : "file" // }
path.resolve([from ...], to)
組合全部的路徑,找出絕對路徑. 若是路徑中不存在以/
開頭,或者根目錄的話,則以當前js文件所在的目錄爲起始參考路徑. NodeJS官方給出一種更好理解的方式:
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile') // cd foo/bar // cd /tmp/file/ // cd .. // cd a/../subfile 最後返回: /tmp/subfile
path.relative(fromPath, toPath)
計算出,相對於fromPath 到 toPath的相對路徑。兩個參數須要是絕對路徑. 在MAC下面開頭須要爲/
. 若是不是, 則會默認以執行的js文件所在目錄進行轉化.
path.relative('/usr/home/sam','/usr/app') 返回: ../../app
總結一下:
回到load方法。 該方法主要就是對尾綴進行不一樣的處理策略:
var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; Module._extensions[extension](this, filename); this.loaded = true;
再反觀,源碼對不一樣後綴的處理
Module._extensions['.js'] = function(module, filename) {...} Module._extensions['.json'] = function(module, filename) {...} Module._extensions['.node'] = function(module, filename) {...}
找到文件以後,再經過vm模塊,進行編譯處理.
最後, 在_compile函數裏, 對scope和sandbox進行處理後,爭取運行文件.
Module.prototype._compile = function(content, filename) { var self = this; var args = [self.exports, require, self, filename, dirname]; return compiledWrapper.apply(self.exports, args); };
最後就編譯爲,咱們前文所述的那樣:
(function (exports, require, module, __filename, __dirname) { // 模塊源碼 return exports; });
經過上文,咱們也可以很好地理解。 出錯的時候,下面的信息到底意味着什麼了.
SyntaxError: Unexpected token . at exports.runInThisContext (vm.js:53:16) at Module._compile (module.js:414:25) at Object.Module._extensions..js (module.js:442:10) at Module.load (module.js:356:32) at Function.Module._load (module.js:313:12) at Function.Module.runMain (module.js:467:10) at startup (node.js:136:18)
轉載請註明原文連接和做者: https://segmentfault.com/a/1190000004868...