node的模块化管理是JavaScript语言向服务器语言迈进的重要一步。
使用过Node.js的程序员对require
肯定很熟悉:
var http = require('http');
var fs = require('fs');
下面我们看看node的模块化是如何做的。
创建模块
定义模块有两种方式:
- module.exports方式
创建custom_hello.js
var hello = function() {
console.log("hello!");
}
module.exports = hello;
- exports方式
创建custom_goodbye.js
exports.goodbye = function() {
console.log("bye!");
}
这两种方式有什么区别呢,我们看看如何使用这两个模块:
创建测试程序app.js
var hello = require('./custom_hello');
var gb = require('./custom_goodbye');
hello();
gb.goodbye();
我们看到了使用起来有些不同之处,需要注意一下。
导出多个函数和隐藏内部实现
很明显,exports只是module上的普通变量,所以我们可以在上面挂载我们想要的函数,需要隐藏的函数,我们不挂载就好了。这样就方便的实现了功能隐藏。
// 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的实现
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加载。
- 都不是,则创建一个新的模块,存在缓存,返回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);
函数最终会调用_compile
函数。模块加载是同步运行的,这个和浏览器的模块加载器requirejs是不同的,在node中我们可以方便的使用同步代码加载模块。最后我们的目标是产生如下的包装。这个我们自己写的模块机制是不是很像。
(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 = {};
,由于代码调用比较多,我们看看伪代码会更清晰。
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。上面的算法每次都需要在不同的目录去找模块,如果我们固定下来路径,就可以大大减少时间啦。可以在这里下载测试代码。我们看看怎么实现的吧。
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
文件中了,所以下次就不用一次次尝试了。这样第一次启动还是会慢,所以还可以有优化空间,我们提前生成这个文件就好了。在我的机器上,优化过的代码除了第一次以外,会比非优化的快两倍。