likes
comments
collection
share

读取配置文件的最佳方式

作者站长头像
站长
· 阅读数 45

配置文件无处不在,有 .json.yaml.yml.js.ts.cjs.mts 等等,如果配置过多让配置文件支持类型提示,无疑是最友好的体验,所以提供配置文件的优先级应该是这样的:TS 文件 > JS 文件 > 其他类型文件。

这个时候我们会遇到一个问题,前端目前有两种模块规范—— ESM 和 CJS,我们如何正确安全的加载配置文件呢。为此我们可以参考社区优秀项目的做法,比如 VuePress 它就是通过 bundle-require 来读取文件的,非常方便,我在写库(比如 vitepress-export-pdf )的时候也用到了这个优秀的插件。

如果一切顺利那么文章到此结束,事实上这种方式有潜在的风险,那就是它临时生成的文件很有可能打断正常项目进展,为此专门有人提了一个 issue Tmp .mjs file in the current directory breaks other tools #33,因为提问者没有提供更加具体的事例,作者就没有回复。

但是我在最近写 vite-plugin-fake-server 的时候复现了上面问题的实际例子,这个问题在也存在 Vite 运行时,修改mock文件会生成很多个xxx.mjs文件

简单来讲就是:项目监听了一个文件夹,bundle-require 在读取文件的时候会创建临时文件,意外触发了项目监听事件导致无限循环,这个问题还挺不好解决的,于是我想探索另外一种方式。

bundle-require 读取文件的方式

首先用 esbuild 打包输出符合 CJS 或者 ESM 格式的文件,为了避免重复加载模块缓存问题,这些临时文件有一个随机文件名,然后通过 import 或者 require 来读取临时文件的内容。

读取配置文件的最佳方式

因为 Node.js 无法识别 TypeScript 文件,所以需要类似 esbuild 的文件打包工具,打包成 JS 文件。如何能从输出的 string 中得到真正的代码呢。

通过 require.extensions 自定义加载器

// https://gist.github.com/jamestalmage/df922691475cff66c7e6
require.extenstions['.js'] = function (module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename);
}

vite-plugin-mock-dev-server 就是通过这种办法来读取 CJS 文件的。但是因为 require.extensions 已经被废弃,虽然所有的 Node.js 中都保留它的代码,但终归不是一个好办法,尤其是它会拖慢加载模块的速度。

require-from-string

我们会发现在 require.extensions 中调用了 module._compile,我们何不直接使用这个 API 来读取字符串呢,答案是可以的,虽然这个 API 通过下划线开头被标记为 Module 类的内部方法,但是我们一样可以调用,require-from-string 就是使用了这个 API 来读取字符串内容的,这个方法还有一个好处就是不会把读取的模块加载到 require.cache 中,重复读取不需要删除缓存。

简单封装就可以这么使用:

var requireFromString = require('require-from-string');

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

如果你想在 CJS 和 ESM 模块使用这个 API 请使用 import-from-string 里面的 requireFromString 方法。

import-from-string

不写入文件读取文件内容,对于符合 CJS 规范的字符串我们找到了办法,那么对于符合 ESM 规范的字符串呢?

查看 Node.js 的 Modules: ECMAScript modules 章节我们会有惊喜(实际上我是从这个页面最先找到的灵感 Is there a way to use eval() with esm?)。

读取配置文件的最佳方式

下面这种代码是有效的:

import 'data:text/javascript,console.log("hello!");';

根据 import(data:text/javascript,) throw SyntaxError 的描述,使用 Buffer.from 更是更加安全的方式。

配合动态 import 我们就能实现类似 requireFromString 的功能了,不过这种办法有以下缺点:

  1. 不支持相对路径
  2. 不支持第三方模块
  3. 不支持 import.meta.url

上面 1 和 2 两个缺点可以通过使用绝对路径来解决,第三个替换下变量就行了,这在 esbuild 非常容易实现,如此我们就能实现和 requireFromString 函数类似的 importFromString。

这就是 import-from-string 的原理。

module-from-string

requireFromString 和 importFromString 也有那么一点点不足,比如:

requireFromString 中的 _compile 是私有方法,直接调用好像不是很专业。 importFromString 的 import.meta 因为我们只替换了 import.meta.url,这多少有点限制。

有没有终极解决办法呢,答案是有的,那就是 module-from-stringmodule-from-string 使用了更加现代化的 API,它代表着从字符串读取模块的未来。

module-from-string 中,读取 CJS 模块内容的字符串使用的是 runInNewContext API 实现的,这个 API 的缺点是加载动态 import 只是实验性支持。

读取符合 ESM 规范的字符串使用的是 vm.SourceTextModule 来实现的,这个 API 的缺点是这个 API 是实验性质的,而且也是动态 import 也是实验性质的。

module-from-string 最大的好处就是完美,不足之处就是 API 是实验性质的,需要再等等,才能直接在 Node.js 中使用。


说好的今天,加班这个点了还只有我一个人。

2023 年 11 月 11 日 10:14

转载自:https://juejin.cn/post/7300550397781180425
评论
请登录