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ù)緩存,如果緩存中存在,則直接返回,否則解析模塊文件并加載,這里會識別jsc、json、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