macOS和node.js之間的小誤會

謹以此文獻給個人摯友喬治G同窗!!!願他打敗一切惡魔和狗腿

事情的原由是這樣的,喬治G在他的電腦上作了一個小測試,但結果和預期的大不相同。javascript

那麼咱們先來看看這個小測試都寫了什麼:html

一共三個文件,代碼總計不超過15行java

parent.jsnode

class Parent {}

module.exports = Parent

son.jswebpack

//加載時把模塊文件名首字母大寫了(不正確的)
const Parent = require('./Parent')

class Son extends Parent {}

module.exports = Son

test.jsgit

//加載時把模塊名首字母大寫(不正確的)
const ParentIncorrect = require('./Parent')
//經過正確的模塊文件名加載(正確)
const Parent = require('./parent')

const Son = require('./son')

const ss = new Son()

//測試結果
console.log(ss instanceof Parent) // false
console.log(ss instanceof ParentIncorrect) // true

喬治G同窗有如下疑問:github

  1. son.jstest.js裏都有錯誤的文件名(大小寫問題)引用,爲何不報錯?
  2. 測試結果,爲何ss instanceof ParentIncorrect === true?不報錯我忍了,爲何還認賊做父,說本身是那個經過不正確名字加載出來的模塊的instance?

若是同窗你對上述問題已經瞭然於胸,恭喜你,文能提筆安天,武能上馬定乾坤;上炕認識娘們,下炕認識鞋!web

但若是你也不是很清楚爲何?那麼好了,我有的說,你有的看。bootstrap

其實斷症(裝逼範兒的debug)之法和中醫看病也有類似指出,望、聞、問、切四招能夠按需選取一二來尋求答案。ubuntu

代碼很少,看了一會,即使沒有個人註釋,相信仔細的同窗也都發現真正的文件名和代碼中引入時有出入的,那麼這裏確定是有問題的,問題記住,咱們繼續

這個就算了,代碼我也聞不出個什麼鬼來

來吧,軟件工程裏很重要的一環,就是溝通,不見得是和遇到bug的同事,多是本身,多是QA,固然也多是PM或者你的老闆。你沒問出本身想知道的問題;他沒說清楚本身要回答的;都完蛋。。。。

那麼我想知道什麼呢?下面兩件事做爲debug的入口比較合理:

  1. 操做系統
  2. 運行環境 + 版本
  3. 你怎麼測試的,命令行仍是其餘什麼手段

答曰:macOS; node.js > 8.0;命令行node test.js

激動人心的深入到來了,我要動手了。(爲了完整的描述debug過程,我會僞裝這下面的全部事情我事先都是不知道的)

準備電腦,完畢

準備運行環境node.js > 9.3.0, 完畢

復刻代碼,完畢

運行,日了狗,果真沒報錯,並且運行結果就是喬治G說的那樣。

爲了證實我沒瞎,我又嘗試在test.jsrequire了一個壓根不存在的文件require('./nidayede'),運行代碼。

還好此次報錯了Error: Cannot find module './nidayede',因此我沒瘋。這點真使人高興。

因而有了第一個問題

爲何狗日的模塊名大小寫都錯了,還能加載?

會不會和操做系統有關係?來咱們再找臺ubuntu試試,果真,到了ubuntu上,大小寫問題就是個問題了,Error: Cannot find module './Parent'。(經朋友提醒,windows也是默認大小寫不敏感的,因此以前舉例說windows會報錯,應該也是我本身早前修改過註冊表緣故)。

那麼macOS到底在幹什麼?連個大小寫都分不出來麼?因而趕忙google(別問我爲何不baidu)

圖片描述

原來人家牛逼的 OS X默認用了 case-insensitive的文件系統( 詳細文檔)。

but why?這麼反人類的設計究竟是爲了什麼?

圖片描述

更多解釋,來,走你

因此,這就是你不報錯的理由?(對node.js指責道),但這就是所有真相了。

但事情沒完

那認賊做父又是個什麼鬼?

依稀有聽過node.js裏有什麼緩存,是那個東西引發的麼?因而抱着試試看的心情,我把const ParentIncorrect = require('./Parent')const Parent = require('./parent')換了下位置,心想,這樣最早按照正確的名字加載,會不會就對了呢?

果真,仍是不對。靠猜和裝逼是不可以真正解決問題的

那比比ParentIncorrectParent呢?因而我寫了console.log(ParentIncorrect === Parent),結果爲false。因此他倆還真的不是同一個東西,那麼說明問題可能在引入的部分嘍?

因而一個裝逼看node.js源碼的想法誕生了(其實不看,問題最終也能想明白)。 日了狗,懷着忐忑的心情,終於clone了一把node.js源碼(花了很久,真tm慢)

來,咱們一塊兒進入神祕的node.js源碼世界。既然咱們的問題是有關require的,那就從她開始吧,不過找到require定義的過程須要點耐心,這裏不詳述,只說查找的順序吧

src/node_main.cc => src/node.cc => lib/internal/bootstrap_node.js => lib/module.js

找到咯,就是這個lib/module.js,進入正題:

lib/module.js => require

Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, /* isMain */ false);
};
好像沒什麼卵用,對不對?她就調用了另外一個方法 _load,永不放棄,繼續

lib/module.js => _load

Module._load = function(request, parent, isMain) {
  //debug代碼,麼卵用,跳過
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

  if (isMain && experimentalModules) {
    //...
    //...
    //這段是給ES module用的,不看了啊
  }

  //獲取模塊的完整路徑
  var filename = Module._resolveFilename(request, parent, isMain);

  //緩存在這裏啊?好激動有沒有?!?終於見到她老人家了
  //原來這是這樣的,簡單的一批,毫無神祕感啊有木有
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  //加載native但非內部module的,不看
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  //構造全新Module實例了
  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  //先把實例引用加緩存裏
  Module._cache[filename] = module;

  //嘗試加載模塊了
  tryModuleLoad(module, filename);

  return module.exports;
};
彷佛到這裏差很少了,不過咱們再深刻看看 tryModuleLoad

lib/module.js => tryModuleLoad

function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    //加載模塊
    module.load(filename);
    threw = false;
  } finally {
    //要是加載失敗,從緩存裏刪除
    if (threw) {
      delete Module._cache[filename];
    }
  }
}
接下來就是真正的 load了,要不咱們先停一停?

好了,分析問題的關鍵在於不忘初心,雖然到目前爲止咱們前進的比較順利,也很爽對不對?。但咱們的此行的目的並非爽,好像是有個什麼疑惑哦!因而,咱們再次梳理下問題:

  1. son.js裏用首字母大寫(不正確)的模塊名引用了parent.js
  2. test.js裏,引用了兩次parent.js,一次用徹底一致的模塊名;一次用首字母大寫的模塊名。結果發現son instanceof require('./parent') === false

既然沒報錯的問題前面已經解決了,那麼,如今看起來就是加載模塊這個部分可能出問題了,那麼問題究竟是什麼?咱們怎麼驗證呢?

這個時候我看到了這麼一句話var cachedModule = Module._cache[filename];,文件名是做爲緩存的key,來吧,是時候看看Module._cache裏存的模塊key都是什麼牛鬼蛇神了。因而怎麼可以查看到Module._cache就是咱們的下一個探索目標。那麼咱們就得順着剛纔發現的,真正的load繼續看下去了。

lib/module.js => load

Module.prototype.load = function(filename) {
  debug('load %j for module %j', filename, this.id);

  assert(!this.loaded);
  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;

  //ES6 module相關,不看
  if (ESMLoader) {
    ...
    ...
    ...
  }
};
順着這條路,咱們如今應該去找那個 Module._extensions['.js']的實現了

lib/module.js => Module._extensions

Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};
至此,咱們尚未發現如何從開發者角度訪問到 _cache的蹤影,因此繼續向下走

lib/module.js => _compile

Module.prototype._compile = function(content, filename) {

  content = internalModule.stripShebang(content);

  // create wrapper function
  //爲了保證每一個模塊獨立的做用域,這個有個wrapper的過程,
  //相信瞭解browserify、webpack工做原理的朋友懂得
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  ...
  ...
  var dirname = path.dirname(filename);
  //這個步驟是關鍵,看到了require,請容許我草率的決定進去看看這個makeRequireFunction
  var require = internalModule.makeRequireFunction(this);
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result;
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
                              require, this, filename, dirname);
  } else {
    result = compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  }
  if (depth === 0) stat.cache = null;
  return result;
};

lib/internal/module.js => makeRequireFunction

function makeRequireFunction(mod) {
  const Module = mod.constructor;

  function require(path) {
    try {
      exports.requireDepth += 1;
      return mod.require(path);
    } finally {
      exports.requireDepth -= 1;
    }
  }

  function resolve(request, options) {
    return Module._resolveFilename(request, mod, false, options);
  }

  require.resolve = resolve;

  function paths(request) {
    return Module._resolveLookupPaths(request, mod, true);
  }

  resolve.paths = paths;

  require.main = process.mainModule;

  // Enable support to add extra extension types.
  require.extensions = Module._extensions;

  //開心,我看到Module._cache被賦值到require上了
  //接下來只要知道這個require是否是咱們在使用時的那個就行了
  require.cache = Module._cache;

  return require;
}
我在這裏能夠明確告訴你,是的,這裏的 require,就是咱們代碼裏用到的 require。線索就在上面那步 Module.prototype._compile裏,請仔細看 var wrapper = Module.wrap(content);result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);兩行內容

注:其實若是你熟讀文檔, 上述尋找_cache訪問方法的過程是沒必要要的,但爲了保持敘事完整,我仍是裝了個逼,請見諒

打完收工,如今咱們已經知道如何查看_cache裏的內容了,因而我在test.js裏最後面加了一句console.log(Object.keys(require.cache)),咱們看看打出了什麼結果

false
true
[ '/Users/admin/codes/test/index.js',
  '/Users/admin/codes/test/Parent.js',
  '/Users/admin/codes/test/parent.js',
  '/Users/admin/codes/test/son.js' ]
真相已經呼之欲出了, Module._cache裏真的出現了兩個 [p|P]arentmacOS默認不區分大小寫,因此她找到的實際上是同一個文件;但 node.js當真了,一看文件名不同,就當成不一樣模塊了),因此最後問題的關鍵就在於 son.js裏到底引用時用了哪一個名字(上面咱們用了首字母大寫的 require('./Parent.js')),這才致使了 test.js認賊做父的梗。

若是咱們改改son.js,把引用換成require('./parEND.js'),再次執行下test.js看看結果如何呢?

false
false
[ '/Users/haozuo/codes/test/index.js',
  '/Users/haozuo/codes/test/Parent.js',
  '/Users/haozuo/codes/test/parent.js',
  '/Users/haozuo/codes/test/son.js',
  '/Users/haozuo/codes/test/parENT.js' ]

沒有認賊做父了對不對?再看Module._cache裏,原來是parENT.js也被當成一個單獨的模塊了。

因此,假設你的模塊文件名有n個字符,理論上,在macOS大小寫不敏感的文件系統裏,你能讓node.js將其弄出最大2n次方個緩存來

是否是很慘!?還好macOS仍是能夠改爲大小寫敏感的,格盤重裝系統;新建分區都行。

問題雖然不難,但探究問題的決心和思路仍是重要的。

最後祝願你們前程似錦!!

相關文章
相關標籤/搜索