嵌入式設(shè)備專屬Node:ShadowNode源碼解析 -- module模塊

ShadowNode 是一款可以運(yùn)行于嵌入式設(shè)備的js運(yùn)行時(shí),基于Samsung的iotjs項(xiàng)目開發(fā),和node相比,其具有更小的內(nèi)存占用和更快的啟動速度,不過作為contributor之一,給我很直觀的感受就是ShadowNode具有極快的編譯速度,開發(fā)起來也更加順暢。我從2018年10月開始利用業(yè)余時(shí)間參與ShadowNode的開發(fā)和維護(hù),為其提交了數(shù)個(gè)補(bǔ)丁和特性,因此也逐漸對其有了一定的了解,在此我將對ShadowNode從源碼的角度對其進(jìn)行解析以及我個(gè)人對ShadowNode的一些疑惑和思考。因?yàn)榇蟛糠謱?shí)現(xiàn)與node一致,而且團(tuán)隊(duì)也一直希望將ShadowNode做到與node兼容,因此此解析也適用于理解node源碼。在此也希望ShadowNode能越來越普及,并為node社區(qū)開拓一片新的領(lǐng)域。

本文主要講述ShadowNode中module模塊的實(shí)現(xiàn)。module是node中最重要的模塊之一,在ShadowNode中也是如此。和node一樣,ShadowNode也支持CommonJS的模塊形式,實(shí)現(xiàn)方式略有不同,但使用方式與node基本一致。

module模塊在node和ShadowNode中的重要性不言而喻,從啟動時(shí)便要用于加載腳本。廢話不多說,接下來先解析一波源碼,我們從入口文件ShadowNode/src/js/iotjs.js(為什么還要叫iotjs呢?其實(shí)我一直想向團(tuán)隊(duì)建議改個(gè)名字,比如snode啥的@yorkie:) )開始,這里面包含了一個(gè)IIFE的函數(shù),ShadowNode/src/js/iotjs.js line: 23

function Module(id) {
    this.id = id;
    this.exports = {};
}

Module.cache = {};
Module.require = function(id) {
    if (id === 'native') {
      return Module;
    }

    if (Module.cache[id]) {
      return Module.cache[id].exports;
    }

    var module = new Module(id);

    Module.cache[id] = module;
    module.compile();

    return module.exports;
};

可以看到這里定義了一個(gè)Module類,但這還不是我們?nèi)粘J褂玫哪莻€(gè)module模塊,這里進(jìn)行了if (id === 'native')的判斷,后面會用到,然后從緩存中獲取模塊,我們可以看到不管是node還是ShadowNode,緩存的概念都是一以貫之的,這極大地提升了模塊加載的性能,最后將模塊返回。而后移到ShadowNode/src/js/iotjs.js line: 51

var module = Module.require('module');

這里調(diào)用了上述Module類的require靜態(tài)方法來加載真正的module模塊所在地:ShadowNode/src/js/module.js,并運(yùn)行compile成員方法,里面會調(diào)用process.compileModule()方法,這是用c代碼實(shí)現(xiàn)的內(nèi)置process模塊,在此我不詳細(xì)講述process的內(nèi)容,之后會用專門的篇幅進(jìn)行解析。compileModule() 用于將模塊載入內(nèi)存,成為運(yùn)行時(shí)的一部分,也就可以用于運(yùn)行與調(diào)用了。簡單來說,這入口文件主要執(zhí)行了諸如:ShadowNode/src/js/iotjs.js line: 384

global.console = Module.require('console');
global.Buffer = Module.require('buffer');
global.Promise = Module.require('promise');

以及:ShadowNode/src/js/iotjs.js line: 496

process.exit = function(code) {
    ...

等我們熟悉的全局模塊、方法以及常量的定義與加載操作,為系統(tǒng)啟動做足準(zhǔn)備工作,但這不是我們現(xiàn)在所關(guān)心的,因此移步至

ShadowNode/src/js/iotjs.js line: 603

var m = Module.require('module');
m.runMain();

這里再次加載了一個(gè)上述真正的module模塊實(shí)現(xiàn)文件并執(zhí)行了其靜態(tài)的runMain方法,因此我們移步至ShadowNode/src/js/module.js:line: 335

iotjs_module_t.runMain = function() {
  if (process.debuggerWaitSource) {
    var fn = process.debuggerSourceCompile();
    fn.call();
  } else {
    var filename = mainModule.filename = process.argv[1];
    mainModule.exports = iotjs_module_t.load(filename, null);
  }
  while (process._onNextTick());
};

我們現(xiàn)在著重關(guān)注以下代碼:

var filename = mainModule.filename = process.argv[1];
mainModule.exports = iotjs_module_t.load(filename, null);

這里將process.argv[1]所指代的變量作為文件名,也就是當(dāng)執(zhí)行$ iotjs xxx.js時(shí)需要加載的文件,也就是說這里會加載用戶指定的文件進(jìn)行解析并運(yùn)行,緊接著調(diào)用iotjs_module_t.load(filename, null);來執(zhí)行加載操作,看一下load方法的實(shí)現(xiàn):

ShadowNode/src/js/module.js:line: 220

iotjs_module_t.load = function(id, parent) {
  if (process.builtin_modules[id]) {
    iotjs_module_t.curr = id;
    return Native.require(id);
  }
  var module = new iotjs_module_t(id, parent);
  var modPath = iotjs_module_t.resolveModPath(module.id, module.parent);

  var cachedModule = iotjs_module_t.cache[modPath];
  if (cachedModule) {
    iotjs_module_t.curr = modPath;
    return cachedModule.exports;
  }

  if (!modPath) {
    throw new Error('Module not found: ' + id);
  }

  var stat = process._loadstat();
  var startedAt;
  if (stat) {
    startedAt = Date.now();
  }

  module.filename = modPath;
  module.dirs = [modPath.substring(0, modPath.lastIndexOf('/') + 1)];
  iotjs_module_t.cache[modPath] = module;
  iotjs_module_t.curr = modPath;

  var ext = modPath.substr(modPath.lastIndexOf('.') + 1);
  if (ext === 'jsc') {
    module.compile(true);
  } else if (ext === 'json') {
    var source = process.readSource(modPath);
    module.exports = JSON.parse(source);
  } else if (ext === 'node') {
    var native = process.openNativeModule(module.filename);
    module.exports = native;
  } else {
    /** Treat any other file as js file */
    module.compile();
  }

  if (stat) {
    var relPath = modPath.replace(cwd, '');
    var consume = Math.floor(Date.now() - startedAt);
    console.log(`load "${relPath}" ${consume}ms`);
  }
  return module.exports;
};

這個(gè)方法也是全局require方法所執(zhí)行的模塊加載操作,其中的加載流程和node相同,首先查詢是否是內(nèi)置模塊,如果是,則直接返回內(nèi)置模塊,如果不是,則解析模塊名,并對緩存進(jìn)行查詢,這里使用絕對路徑作為緩存存儲的鍵以避免重復(fù)緩存,如果緩存中存在,則直接返回,否則解析模塊文件并加載,這里會識別jscjson、node的文件以使用對應(yīng)方式進(jìn)行解析,否則,其他文件都將作為js文件進(jìn)行解析。最終將module.exports返回。至此,模塊就被加載了。

那么問題來了,全局的require函數(shù)是怎么就能直接使用了呢?這也是我剛開始看源代碼時(shí)心中所帶的問題。到現(xiàn)在好像也沒有看到有相關(guān)的操作,那接下就可以分析一下上述代碼的compile方法了!以下是compile成員方法的實(shí)現(xiàn):ShadowNode/src/js/module.js:line: 272

function _makeRequireFunction(mod) {
  var Module = mod.constructor;
  function require(id) {
    return mod.require(id);
  }

  function _resolve(request) {
    if (!request || typeof request !== 'string') {
      throw new TypeError('module must be a non-null string');
    }

    if (process.builtin_modules[request]) {
      return request;
    }

    var path = Module.resolveModPath(request, mod);
    if (!path) {
      throw new Error('Module not found: ' + request);
    }
    return path;
  }
  require.resolve = _resolve;
  require.main = mainModule;
  require.cache = Module.cache;

  return require;
}


iotjs_module_t.prototype.compile = function(snapshot) {
  var __filename = this.filename;
  var __dirname = path.dirname(__filename);
  var fn;
  if (!snapshot) {
    fn = process.compile(__filename);
  } else {
    fn = process.compileSnapshot(__filename);
    if (typeof fn !== 'function')
      throw new TypeError('Invalid snapshot file.');
  }

  var _require = _makeRequireFunction(this);

  fn.apply(this.exports, [
    this.exports,             // exports
    _require,                 // require
    this,                     // module
    undefined,                // native
    __filename,               // __filename
    __dirname                 // __dirname
  ]);
};

這里并沒有很復(fù)雜的實(shí)現(xiàn),通過process.compile(__filename)process.compileSnapshot(__filename)創(chuàng)建運(yùn)行的事例,并組裝好require等參數(shù),通過fn.apply(...)exports、require、module__filename等我們熟悉的全局函數(shù)和對象傳入,至此,我們最熟悉的那些模塊函數(shù)也就可以用了。不過到此為止,好像還缺了點(diǎn)什么,對,還沒說ShadowNode模塊是怎么尋址的呢!這里我們從iotjs_module_t.resolveModPath(...)方法開始,這個(gè)方法在iotjs_module_t.load(...)require.resolve(...)方法中用于模塊尋址:

ShadowNode/src/js/module.js:line: 166

iotjs_module_t.resolveModPath = function(id, parent) {
  if (parent != null && id === parent.id) {
    return false;
  }

  var filepath = false;
  if (id[0] === '/') {
    filepath = iotjs_module_t._resolveFilepath(id, false);
  } else if (parent === null) {
    filepath = iotjs_module_t._resolveFilepath(id, cwd);
  } else if (id[0] === '.') {
    var root = path.dirname(parent.filename);
    filepath = iotjs_module_t._resolveFilepath(id, root);
  } else {
    var dirs = iotjs_module_t.resolveDirectories(id, parent);
    filepath = iotjs_module_t.resolveFilepath(id, dirs);
  }

  if (filepath &&
    (filepath.indexOf('./') > 0 || filepath.indexOf('../') > 0)) {
    return iotjs_module_t.normalizePath(filepath);
  }
  return filepath;
};

parent是指調(diào)用目標(biāo)模塊的模塊,也屬于module的實(shí)例,而后根據(jù)模塊路徑的形式和傳入的parent值指定模塊尋址的起點(diǎn),比如當(dāng)parent === null時(shí)傳入cwd作為尋址起點(diǎn),也就是腳本運(yùn)行的當(dāng)前目錄。接下來是iotjs_module_t._resolveFilepath(...)ShadowNode/src/js/module.js:line: 129

iotjs_module_t._resolveFilepath = function(id, root, ext_index) {
  var modulePath = root ? path.join(root, id) : id;
  var filepath;
  var exts = ['.js', '.json', '.node'];
  if (ext_index === undefined) {
    ext_index = 0;
  }

  // id[.ext]
  if (filepath = tryPath(modulePath, exts[ext_index])) {
    return filepath;
  }

  // id/index[.ext]
  if (filepath = tryPath(modulePath + '/index', exts[ext_index])) {
    return filepath;
  }

  // 3. package path id/
  var jsonpath = modulePath + '/package.json';
  filepath = iotjs_module_t.tryPath(jsonpath);
  if (filepath) {
    var pkgSrc = process.readSource(jsonpath);
    var pkgMainFile = JSON.parse(pkgSrc).main;

    // pkgmain[.ext]
    if (filepath = tryPath(modulePath + '/' + pkgMainFile, exts[ext_index])) {
      return filepath;
    }
  }
  ext_index++;
  if (ext_index < exts.length) {
    return iotjs_module_t._resolveFilepath(id, root, ext_index);
  }
};

此函數(shù)將目標(biāo)模塊的路徑進(jìn)行組合并嘗試讀取模塊文件,在這里會識別js、json、node 三種格式的文件以及index.*默認(rèn)文件,若讀取失敗,則嘗試讀取package.json中依賴的模塊,最終返回完整的模塊路徑。后續(xù)對模塊地址進(jìn)行整理即返回,模塊的尋址也就結(jié)束了。

以上內(nèi)容描述了ShadowNode中module模塊的實(shí)現(xiàn)過程,包括全局對象的構(gòu)建、模塊尋址、緩存優(yōu)化等,但其中有一些細(xì)節(jié)比如process.compile(...)如何對模塊文件進(jìn)行編譯以及snapshot構(gòu)建等問題沒有深入論述,后續(xù)隨著我參與項(xiàng)目構(gòu)建的深入我還會繼續(xù)詳解。

作為一個(gè)開源愛好者,也是一名noder,我對ShadowNode的關(guān)注由來已久,但真正參與構(gòu)建也就近兩個(gè)月的事情,一直以來我對這個(gè)項(xiàng)目保留了一些疑問和不解,對此我也特地和ShadowNode作者@yorkie有過一次詳談,一方面從性能角度來看,js并不優(yōu)良的性能以及它的運(yùn)行環(huán)境對系統(tǒng)資源的巨大消耗決定了其絕對不是構(gòu)建嵌入式設(shè)備應(yīng)用的絕佳選擇,開源社區(qū)對類似運(yùn)行時(shí)的diss也基本集中在這方面;另一方面從生態(tài)的角度來看,雖然js的生態(tài)非常完備,尤其是在node和npm崛起之后,但嵌入式設(shè)備應(yīng)用開發(fā)本身也并不是一個(gè)巨大的需求,因此對于構(gòu)建這樣一個(gè)類node且運(yùn)行于嵌入式設(shè)備的運(yùn)行時(shí)是否具有現(xiàn)實(shí)意義,我一直是存疑的。對此,yorkie也給了解答,構(gòu)建ShadowNode的動機(jī)很簡單,其實(shí)就是看中js本身所具有的巨大生態(tài)支撐,而其他并沒有太多考慮(事實(shí)上也不值得考慮太多),yorkie還用了Android的例子,選擇Java作為其開發(fā)語言并不是看中Java的性能,而是其強(qiáng)大的生態(tài)。確實(shí),這沒毛病,而且最終Android也反過去助長了Java生態(tài)的增長。盡管這一點(diǎn)也并沒有絕對說服我,但ShadowNode的最終目標(biāo)在于社區(qū)建設(shè)和生態(tài)構(gòu)建,且對未來發(fā)展有更多的憧憬與期待而非該技術(shù)本身這一點(diǎn),也還是令我信服的。

以上是我對ShadowNode實(shí)現(xiàn)的簡單闡述及我個(gè)人粗淺的看法與理解,有錯(cuò)誤或遺漏的部分歡迎指正 : )

2018-12-20

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容