require-from-string

最近几天遇到了一个需求,就是在项目启动的时候,需要动态生成一些代码并将这些生成的代码进行引入,一开始我的做法是把生成的代码写入文件,再require这些文件,成功之后再将文件删除。做完之后才发现,这些文件其实没必要去创建,可不可以直接从内存中引入,这样就减少了两次的文件io,而且require其实也是将文件读入内存再进行解析,那么从内存中直接引入也是存在理论上的可能性的。

在这里记录一下具体的做法以及背后的原理分析。

require-from-string

其实这个功能很久之前就被开发出来了,只需要从npm仓库中下载“require-from-string”这个包就可以了,然后就可以直接将代码字符串传入函数中就可以了。

这里有一点要注意的就是,如果你通过require-from-string引入的代码中还通过require引入了其他的module,那么这些module的相对路径要相对你调用require-from-string的地方

下面是简单的官方示例:

1
2
3
4
var requireFromString = require('require-from-string');

requireFromString('module.exports = 1');
//=> 1

API

requireFromString(code, [filename], [options])

code

Required
Type: string

Module code.

filename

Type: string
Default: ''

Optional filename.

options

Type: object

appendPaths

Type: Array

List of paths, that will be appended to module paths. Useful, when you want to be able require modules from these paths.

prependPaths

Type: Array

Same as appendPaths, but paths will be prepended.

原理

这个包使用起来非常简单,在使用之后,我们再去看一下他的源码

源码也非常简单,只有一个index.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
'use strict';

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

module.exports = function requireFromString(code, filename, opts) {
if (typeof filename === 'object') {
opts = filename;
filename = undefined;
}

opts = opts || {};
filename = filename || '';

opts.appendPaths = opts.appendPaths || [];
opts.prependPaths = opts.prependPaths || [];

if (typeof code !== 'string') {
throw new Error('code must be a string, not ' + typeof code);
}

var paths = Module._nodeModulePaths(path.dirname(filename));

var parent = module.parent;
var m = new Module(filename, parent);
m.filename = filename;
m.paths = [].concat(opts.prependPaths).concat(paths).concat(opts.appendPaths);
m._compile(code, filename);

var exports = m.exports;
parent && parent.children && parent.children.splice(parent.children.indexOf(m), 1);

return exports;
};

这段代码里面最关键的就是引用的Nodejs的module核心模块。

这里面用到了Module模块的几个变量和方法。

属性/方法desc
module.children就是这个module require的那些module
module.exports就是module 暴露给引用者的内容
module.filename文件名
module.loadedload完成没,比如前面的循环引用的例子中,就会出现load没完成的时候
module.parent第一个require 这个module的module
module.require(id)就是require的真身,没怎么用过,貌似可以在load完成后再让其require,但是在其他module中只能看到exports,所以module本身需要被export。

Module contructor

1
2
3
4
5
6
7
8
9
10
11
12
13
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._compile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {
...(omit here)...
// create wrapper function
var wrapper = Module.wrap(content);

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

if (process._debugWaitConnect && process._eval == null) {
if (!resolvedArgv) {
// we enter the repl if we're not given a filename argument.
if (process.argv[1]) {
resolvedArgv = Module._resolveFilename(process.argv[1], null);
} else {
resolvedArgv = 'repl';
}
}

// Set breakpoint on module start
if (filename === resolvedArgv) {
delete process._debugWaitConnect;
const Debug = vm.runInDebugContext('Debug');
Debug.setBreakPoint(compiledWrapper, 0, 0);
}
}
var dirname = path.dirname(filename);
var require = internalModule.makeRequireFunction.call(this);
var args = [this.exports, require, this, filename, dirname];
var depth = internalModule.requireDepth;
if (depth === 0) stat.cache = new Map();
var result = compiledWrapper.apply(this.exports, args);
if (depth === 0) stat.cache = null;
return result;
};

require原理

如果对上述代码的理解还不够,可以看一下require的源码,其实require-from-string就是截取了require源码中对于js文件的处理,并且省略了一些细节的结果,省了文件类型判断,模块的cache,paths的生成等。

具体的原理解析可以看这里

总结得讲,就是

  • require其实是调用了module的_load方法,该方法首先会根据入参生成文件的绝对路径,然后用这个绝对路径作为键去查cache(其实就是个对象),如果找到,直接返回,如果找不到继续编译。

  • 编译首先会找是不是核心模块,如果是则返回核心模块的require结果,如果不是,继续。

  • 接下来就是require-from-string中的步骤,new Module(filename,parent)。

  • 在接下来就是一些变量的赋值,比如isMain,如果是node命令运行这个文件,那他就是入口文件,process.mainModule就是当前module。

  • 接下来就是将这个module存到cache。

  • 调用tryModuleLoad(module,filename),该方法会调用module.load,如果调用失败,则从cache中删除当前模块。

  • 那么module.load做了什么?Module._nodeModulePaths(path.dirname(filename))方法生成路径,再根据路径获取扩展名,然后对于不同的文件类型调用不同的方法,json就用JSON.parse,js就调用了module._compile方法

  • 这个_compile会对模块进行解析

  • 最终就是调用NativeModule.wrapper去将打包好的模块放进闭包中

    1
    2
    3
    4
    5
    6
    7
    8
    NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
    };

    NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
    ];