require-from-string
最近几天遇到了一个需求,就是在项目启动的时候,需要动态生成一些代码并将这些生成的代码进行引入,一开始我的做法是把生成的代码写入文件,再require这些文件,成功之后再将文件删除。做完之后才发现,这些文件其实没必要去创建,可不可以直接从内存中引入,这样就减少了两次的文件io,而且require其实也是将文件读入内存再进行解析,那么从内存中直接引入也是存在理论上的可能性的。
在这里记录一下具体的做法以及背后的原理分析。
require-from-string
其实这个功能很久之前就被开发出来了,只需要从npm仓库中下载“require-from-string”这个包就可以了,然后就可以直接将代码字符串传入函数中就可以了。
这里有一点要注意的就是,如果你通过require-from-string引入的代码中还通过require引入了其他的module,那么这些module的相对路径要相对你调用require-from-string的地方。
下面是简单的官方示例:
1 | var requireFromString = require('require-from-string'); |
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 | ; |
这段代码里面最关键的就是引用的Nodejs的module核心模块。
这里面用到了Module模块的几个变量和方法。
属性/方法 | desc |
---|---|
module.children | 就是这个module require的那些module |
module.exports | 就是module 暴露给引用者的内容 |
module.filename | 文件名 |
module.loaded | load完成没,比如前面的循环引用的例子中,就会出现load没完成的时候 |
module.parent | 第一个require 这个module的module |
module.require(id) | 就是require的真身,没怎么用过,貌似可以在load完成后再让其require,但是在其他module中只能看到exports,所以module本身需要被export。 |
Module contructor
1 | function Module(id, parent) { |
module._compile
1 | // Run the file contents in the correct scope or sandbox. Expose |
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
8NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];