请不要使用require引入单个文件
发布于 2 个月前 作者 blackmatch 1648 次浏览 来自 分享

前言

Node.js的模块化是基于CommonJS规范实现的,我们通常会使用module.exports来导出一个模块,用require来引入一个模块。其实在Node.js中,一个文件就是一个模块,更多时候我们使用require来引入一些NPM包。例如:

const _ = require('lodash')

// codes

但是有时候我们也需要引入一些文件,最常见的文件就是.json文件,例如:

const package = require('./package.json')

// codes

用这种方式引入单个文件在大多数情况下是可行的,但是如果引入的文件是一个可能会在程序启动后发生变化的文件就会有问题了。

require的缓存机制

当程序启动后,Node.js会在当前进程缓存所有用require引入过的内容,并保存在全局对象require.cache中。所以,如果使用require引入一个动态文件,在程序运行过程中就无法获取最新的文件内容了。

我们来看一个测试:

const fs = require('fs')

const test = () => {
  // 首次引用config.json
  const config1 = require('./config.json')
  console.log('first require config')
  console.log(config1)

  console.log('require cache:')
  console.log(require.cache)

  // 修改config.json
  const str = fs.readFileSync('./config.json', 'utf8')
  const obj = JSON.parse(str)
  obj.age = 20
  fs.writeFileSync('config.json', JSON.stringify(obj, null, 2))

  // 第二次引用config.json
  const config2 = require('./config.json')
  console.log('second require config')
  console.log(config2)
}

test()

config.json文件中的内容为:

{
  "name": "blackmatch",
  "age": 18
}

输出结果:

first require config
{ name: 'blackmatch', age: 18 }
require cache:
{ '/Users/blackmatch/Desktop/blackmatch/demos/require-cache-demo/test.js':
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/blackmatch/Desktop/blackmatch/demos/require-cache-demo/test.js',
     loaded: false,
     children: [ [Object] ],
     paths:
      [ '/Users/blackmatch/Desktop/blackmatch/demos/require-cache-demo/node_modules',
        '/Users/blackmatch/Desktop/blackmatch/demos/node_modules',
        '/Users/blackmatch/Desktop/blackmatch/node_modules',
        '/Users/blackmatch/Desktop/node_modules',
        '/Users/blackmatch/node_modules',
        '/Users/node_modules',
        '/node_modules' ] },
  '/Users/blackmatch/Desktop/blackmatch/demos/require-cache-demo/config.json':
   Module {
     id: '/Users/blackmatch/Desktop/blackmatch/demos/require-cache-demo/config.json',
     exports: { name: 'blackmatch', age: 18 },
     parent:
      Module {
        id: '.',
        exports: {},
        parent: null,
        filename: '/Users/blackmatch/Desktop/blackmatch/demos/require-cache-demo/test.js',
        loaded: false,
        children: [Object],
        paths: [Object] },
     filename: '/Users/blackmatch/Desktop/blackmatch/demos/require-cache-demo/config.json',
     loaded: true,
     children: [],
     paths:
      [ '/Users/blackmatch/Desktop/blackmatch/demos/require-cache-demo/node_modules',
        '/Users/blackmatch/Desktop/blackmatch/demos/node_modules',
        '/Users/blackmatch/Desktop/blackmatch/node_modules',
        '/Users/blackmatch/Desktop/node_modules',
        '/Users/blackmatch/node_modules',
        '/Users/node_modules',
        '/node_modules' ] } }
second require config
{ name: 'blackmatch', age: 18 }

从输出结果可以看出:

  • config.json文件被缓存到了require.cache全局对象中了。
  • 在config.json文件被修改后,第二次使用require引入文件无法获取该文件最新的内容。

这就是require的缓存机制。

使用读取文件的方式引入动态文件

同样基于上面的例子,我们使用读取文件的方式引入config.json,代码如下:

const fs = require('fs')

const test2 = () => {
  // 首次引用config.json
  const str1 = fs.readFileSync('./config.json', 'utf8')
  const config1 = JSON.parse(str1)
  console.log('first require config')
  console.log(config1)

  // 修改config.json
  const str = fs.readFileSync('./config.json', 'utf8')
  const obj = JSON.parse(str)
  obj.age = 20
  fs.writeFileSync('config.json', JSON.stringify(obj, null, 2))

  // 第二次引用config.json
  const str2 = fs.readFileSync('./config.json', 'utf8')
  const config2 = JSON.parse(str2)
  console.log('second require config')
  console.log(config2)
}

test2()

输出结果:

first require config
{ name: 'blackmatch', age: 18 }
second require config
{ name: 'blackmatch', age: 20 }

这次就能获取config.json最新的内容了。

总结

  • 引入一个动态文件的场景比较少,但养成使用读取文件的方式来引入单个文件是个好习惯。
  • 由于require的缓存机制,在需要热更新的场景可能需要另辟蹊径,必要时重启进程。
18 回复

require的缓存是可以清的,require json文件一般是在该文件不会在运行时变化的情况(比如配置文件)

@dislido 是可以通过delete require.cache[key]的方式清除缓存,但是查了一些资料说这样直接清除可能会存在一些隐患。我们有个项目就是一个json配置文件,程序运行后每天定时被修改一次,这种情况是比较少见的。

@blackmatch 这种情况应该要使用数据库了吧

@aov2005 是的,一开始的时候没那么复杂的,随着需求的叠加和修改,架构和重构跟不上了。

@blackmatch 光 delete 这个是清不掉的

@atian25 感谢大佬提醒。

正确清除require.cache的姿势可以参考这篇老文 Node.js Web应用代码热更新的另类思路 只热更新配置文件的情况可以hold住,附一个demo,但不建议这么使用,不规范且不便管理

@Claude-Ray 这篇文章一样有问题,module.parent 只记录第一次被引用时的父模块,实际上一个模块可能被很多个父模块引用,这里其实也只清理掉了第一个父模块的引用而已,其余的引用会导致模块本身依旧无法被回收

要做这个热更新,得从入口开始自己构建一个模块引用关系,很麻烦的一件事,还会涉及到模块本身的 timer,socket 等资源的释放,基本上是一条死路

@hyj1991 嗯嗯,这块问题才是真的隐患,不过文章末尾提到了,我就没再强调

不过由于 Node.js 本身缺乏对有效的留存对象的扫描机制,因此并不能100%的消除类似 setInterval 导致的老模块的资源无法释放的问题。也是由于这样的局限性,目前我们提供的 YOG2 框架中,主要还是将此技术应用于开发调试期,通过热更新实现快速开发。而生产环境的代码更新依然使用重启或者 PM2 的 hot reload 功能来保证线上服务的稳定性。

一行 delete require.cache 引发的内存泄漏血案 https://zhuanlan.zhihu.com/p/34702356

热更新真感觉没啥意义,一个 SLB 搞定的事

@atian25 SLB?傻老板?(光速逃……哈哈哈哈哈

@atian25 虽然没搞过热更新,但是感觉没必要,但是我们的领导(不懂技术)一直很推崇,因为线上环境的限制搞不了SLB,我们的领导觉得每次更新版本都要重启容器太low了。。。。

@blackmatch 你老板是不是不知道啥叫 k8s

@atian25 二十年前貌似写过一点点代码吧。之后就一直做管理。Node.js JavaScript这些都只是知道有这个名字而已,每次系统出问题都甩锅给Node.js,更别说k8s了。。。架构师换了好几个,都没啥作为。。。而且每次换架构师,老板都特意强调这是在IBM工作了十几年的大牛,我表示然并卵。。。

回到顶部