Node.js 中的 Module 类,编译过程中执行字符串形式代码的方法
mini-css-extract-plugin 中有一个直接执行字符串形式的 commonjs 代码,在编译阶段获取 css-loader 产物的方法。学习了一下这个方法所做的事情。
在写编译工具的时候,我们经常需要提取文件中的特定内容来完成后续的编译。举例来说假如我们正在写一个编译插件,需要提取 .vue 文件中 data 的初始值,如果想要用 babel 之类的工具来解析文件通过 AST 来获取,会是很麻烦的一件事。
如果 .vue 文件写的非常简单,例如这种:
1 | <script> |
或许比较好说,我们只需要提取 data 函数中 return 的对象就可以了,但是只要稍微复杂一点,我们就无能为力了,例如 data 初始值中需要执行一个函数:
1 | <script> |
由于写法非常灵活,想要继续通过静态分析来找到内容是非常困难的了。这时候一个可能更好的方法,是我们在编译阶段直接执行一下这段 JS 文件,执行一下 data 方法,那么就可以直接拿到 data 的值了。现在问题来了,如何能在编译阶段执行一下这段代码呢?
简陋方法
之前我一直的做法是编译成 commonjs 文件:
1 | function getData(more) { |
之后 new 一个 Function,传入假的 exports 方法,例如:
1 | const tmpFunc = new Function('exports', 'module', 'require', ` |
非常容易看出来,这种方法最大的问题是使用了假的 require 方法。这里直接传入 node 的 require 方法也是不行的,因为 require 可能写相对路径或者包名称,而我们传入的 require 方法缺少了路径信息,会找不到对应的依赖。
mini-css-extract-plugin 插件所用的方法
最近看 webpack 的 mini-css-extract-plugin 插件时,看到这个插件为了提取 css-loader 输出的样式,实现了一个 evalModuleCode
函数,这个函数的位置在这里:https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/src/loader.js#L40,内容是这样的:
1 | import NativeModule from 'module'; |
使用起来大概是这样的:
1 | evalModuleCode({context: __dirname}, ` |
如果执行一下,我们就可以拿到代码中输出的内容了:
我们来看下这个函数如何实现的:
- 首先引入了
module
模块。 - 之后创建了一个
NativeModule
实例。 - 接下来设置了模块的
paths
和filename
。 - 最后执行了
_compile
方法。
这个 module
模块是一个 Node.js 提供的模块,文档地址在这里:https://nodejs.org/api/modules.html#modules_the_module_object_1,注意这个跟我们直接使用的 module.exports
并不是同一个(module.exports
中的 module
是 NativeModule
的一个实例)。
但是我们如果看这个文档,会发现文档中的内容只有非常有限的几个方法,并没有提及到上面的任何操作😓。
于是我们只能看代码了。
Node.js 的 module 类
require('module')
引入的文件在 Node.js 源文件中的 lib/module.js
。这个文件又 exports
出了 internal/modules/cjs/loader
。
我们对照着 evalModuleCode
的实现,一步一步在 internal/modules/cjs/loader
文件中找对应的内容:
首先是构造函数,Module
的构造函数没有做什么特殊的内容,只是设置了一些属性。其中 updateChildren
,是为了设置不同 module
之前的对应关系,这里其实我们并没有用到:
之后是 _nodeModulePaths
方法,这个方法有 window 和 posix 两个版本,区别只是对路径的处理方式不同,所做的工作其实就是从当前目录开始,向上遍历出所有的 node_modules 文件夹路径,用于在这些目录中查找对应的包 :
例如当前我们的路径是 /home/work/code/tmp
,那么解析出的 paths
路径就是这样的:
再接下来是设置了 filename
属性,之后就是调用 _compile
方法了:
其中重点关注上面这两句,第一句 wrapSafe
做的事情是将 JS 源代码,使用函数包装一下,参数增加 requires、module 这些,类似这样:
之后在 V8 中编译执行,执行结果就得到了包装后的函数。之后执行这个方法,就可以得到最终输出的结果了。
这里比较有意思的是 module
参数,可以看到传入的是 this
,而 this
就是一个 Module
实例。
执行过程大概就是这样了,其实 我们直接用的 require
方法,也是在 Module
上定义的,evalModuleCode
是将 require
的过程简化了,感兴趣的同学可以自己看一下 require 方法是如何实现的: