阅读 315

利用 Node.js 中的 Module 类,执行字符串形式代码的方法

mini-css-extract-plugin 中有一个直接执行字符串形式的 commonjs 代码,在编译阶段获取 css-loader 产物的方法。学习了一下这个方法所做的事情。

在写编译工具的时候,我们经常需要提取文件中的特定内容来完成后续的编译。举例来说假如我们正在写一个编译插件,需要提取 .vue 文件中 data 的初始值,如果想要用 babel 之类的工具来解析文件通过 AST 来获取,会是很麻烦的一件事。

如果 .vue 文件写的还比较简单,例如这种:

或许比较好处理,我们只需要提取 data 函数中 return 的对象就可以了。但是只要稍微复杂一点,我们就无能为力了,例如 data 初始值中如果执行了一个函数:

由于写法非常灵活,想要继续通过静态分析来找到内容是非常困难的了。这时候一个更好的方法,就是在编译阶段直接执行一下这段 JS 文件,同时执行一下 data 方法,那么就可以直接拿到 data 的返回值了。现在问题来了,如何能在编译阶段执行一下这段代码呢?

简陋方法

之前我一直的做法是编译成 commonjs 文件:

之后 new 一个 Function,传入一些假的 exportsmodule 等方法,例如:

非常容易看出来,这种方法最大的问题是使用了假的 require 方法。这里直接传入 Node.js 提供的 require 方法也是不行的,因为 require 可能写相对路径或者包名称,而我们传入的 require 方法是以当前文件来查找的,会找不到对应的依赖。

mini-css-extract-plugin 插件所用的方法

最近看 webpack 的 mini-css-extract-plugin 插件时,发现这个插件为了提取 css-loader 输出的样式,实现了一个 evalModuleCode 函数,这个函数的位置在这里:github.com/webpack-con…,内容是这样的:

使用起来大概是这样的:

如果执行一下,我们就可以拿到代码中输出的内容了:

我们来看下这个函数如何实现的:

  • 首先引入了 module 模块。
  • 之后创建了一个 NativeModule 实例。
  • 接下来设置了模块的 pathsfilename
  • 最后执行了 _compile 方法。

这个 module 模块是一个 Node.js 提供的模块,文档地址在这里:nodejs.org/api/modules…,注意这个跟我们直接使用的 module.exports 并不是同一个(module.exports 中的 moduleNativeModule 的一个实例)。

但是我们如果看这个文档,会发现文档中的内容只有非常有限的几个方法,并没有提及到上面的任何操作😓。

于是我们只能看代码了。

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 方法是如何实现的: