node的模塊化管理是JavaScript語言向服務器語言邁進的重要一步役拴。
使用過Node.js的程序員對require
肯定很熟悉:
var http = require('http');
var fs = require('fs');
下面我們看看node的模塊化是如何做的。
創(chuàng)建模塊
定義模塊有兩種方式:
- module.exports方式
創(chuàng)建custom_hello.js
var hello = function() {
console.log("hello!");
}
module.exports = hello;
- exports方式
創(chuàng)建custom_goodbye.js
exports.goodbye = function() {
console.log("bye!");
}
這兩種方式有什么區(qū)別呢括享,我們看看如何使用這兩個模塊:
創(chuàng)建測試程序app.js
var hello = require('./custom_hello');
var gb = require('./custom_goodbye');
hello();
gb.goodbye();
我們看到了使用起來有些不同之處,需要注意一下珍促。
導出多個函數(shù)和隱藏內(nèi)部實現(xiàn)
很明顯铃辖,exports只是module上的普通變量,所以我們可以在上面掛載我們想要的函數(shù)猪叙,需要隱藏的函數(shù)娇斩,我們不掛載就好了。這樣就方便的實現(xiàn)了功能隱藏穴翩。
// Private
var TWO = 2;
function sum(x, y) {
return x + y;
}
// Public
module.exports = {
x: 5,
add2: function(num) {
return sum(TWO, num);
},
addX: function(num) {
return sum(module.exports.x, num);
}
}
require的實現(xiàn)
require的定義在Module.js中
// Loads a module at the given file path. Returns that module's
// `exports` property.
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);
};
require只是Module._load的簡單封裝犬第,下面我們看看Module的作用。
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有兩個作用:
- 保存加載的文件芒帕。
- 加載文件歉嗓。
Module._load
算法為:
檢查是否有緩存。
- 如果有背蟆,則返回
cachedModule.exports;
- 如果是native模塊鉴分,使用NativeModule加載。
- 都不是带膀,則創(chuàng)建一個新的模塊志珍,存在緩存,返回exports
Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = Module._resolveFilename(request, parent);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
tryModuleLoad(module, filename);
return module.exports;
};
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}
Module.prototype._compile
module.load(filename);
函數(shù)最終會調(diào)用_compile
函數(shù)垛叨。模塊加載是同步運行的碴裙,這個和瀏覽器的模塊加載器requirejs是不同的,在node中我們可以方便的使用同步代碼加載模塊点额。最后我們的目標是產(chǎn)生如下的包裝舔株。這個我們自己寫的模塊機制是不是很像。
(function (exports, require, module, __filename, __dirname) {
// YOUR CODE INJECTED HERE!
});
模塊加載算法
這里說的算法實際上是查找文件的算法还棱,主要代碼在_findPath
中载慈。
Module._findPath = function(request, paths) {
var exts = Object.keys(Module._extensions);
if (request.charAt(0) === '/') {
paths = [''];
}
var trailingSlash = (request.slice(-1) === '/');
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
// For each path
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename;
if (!trailingSlash) {
// try to join the request to the path
filename = tryFile(basePath);
if (!filename && !trailingSlash) {
// try it with each of the extensions
filename = tryExtensions(basePath, exts);
}
}
if (!filename) {
filename = tryPackage(basePath, exts);
}
if (!filename) {
// try it with each of the extensions at "index"
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
return false;
};
我們看到最后會緩存filename
在Module._pathCache = {};
,由于代碼調(diào)用比較多珍手,我們看看偽代碼會更清晰办铡。
require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"
LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP
LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
a. Parse X/package.json, and look for "main" field.
b. let M = X + (json main field)
c. LOAD_AS_FILE(M)
2. If X/index.js is a file, load X/index.js as JavaScript text. STOP
3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
4. If X/index.node is a file, load X/index.node as binary addon. STOP
LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_AS_FILE(DIR/X)
b. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
c. DIR = path join(PARTS[0 .. I] + "node_modules")
b. DIRS = DIRS + DIR
c. let I = I - 1
5. return DIRS
從上面的算法可以看到辞做,模塊加載機制會嘗試在不同的路徑搜索我們需要的模塊,比較智能寡具,但是也比較耗時間啊秤茅。下面我們來看一個對node模塊加載機制的改進。
提升node的模塊加載性能
提升的思路來源自這篇文章:faster-node-app-require童叠。上面的算法每次都需要在不同的目錄去找模塊框喳,如果我們固定下來路徑,就可以大大減少時間啦厦坛∥蹇澹可以在這里下載測試代碼。我們看看怎么實現(xiàn)的吧杜秸。
var Module = require('module');
var fs = require('fs');
var exists = fs.existsSync;
var _require = Module.prototype.require;
var SAVE_FILENAME =
process.env.CACHE_REQUIRE_PATHS_FILE ?
process.env.CACHE_REQUIRE_PATHS_FILE :
'./.cache-require-paths.json';
var nameCache = exists(SAVE_FILENAME) ? JSON.parse(fs.readFileSync(SAVE_FILENAME, 'utf-8')) : {};
var currentModuleCache;
var pathToLoad;
Module.prototype.require = function cachePathsRequire(name) {
currentModuleCache = nameCache[this.filename];
if (!currentModuleCache) {
currentModuleCache = {};
nameCache[this.filename] = currentModuleCache;
}
if (currentModuleCache[name] &&
// Some people hack Object.prototype to insert their own properties on
// every dictionary (for example, the 'should' testing framework). Check
// that the key represents a path.
typeof currentModuleCache[name] === 'string') {
pathToLoad = currentModuleCache[name];
} else {
pathToLoad = Module._resolveFilename(name, this);
currentModuleCache[name] = pathToprintCacheLoad;
}
return _require.call(this, pathToLoad);
};
從上面的代碼中可以看到放仗,把路徑緩存在./.cache-require-paths.json
文件中了,所以下次就不用一次次嘗試了撬碟。這樣第一次啟動還是會慢诞挨,所以還可以有優(yōu)化空間,我們提前生成這個文件就好了呢蛤。在我的機器上亭姥,優(yōu)化過的代碼除了第一次以外,會比非優(yōu)化的快兩倍顾稀。