node 写一个自动监听文件并读写配置的脚本

3,549 阅读8分钟

前言

我的 github/blog,给个小星星咯~

最近因为工作,需要写一个脚本来自动读取文件夹下的某个文件,把其中的内容写到另一个新生成的文件中。因为这种场景还是挺常见的,网络上也搜不到好的(手把手教学的)解决方案,这对于还没学过 node.js 的前端小白来说,很不友好啊~

于是这篇文章就手把手教你写一个这样的脚本,都会尽量解释清楚,保证你看了就会!

场景举例

假如有这么一个项目,其文件目录如下:

|-- app1
    |-- config.json
    |-- index.js
|-- app2
    |-- config.json
    |-- index.js
|-- app3
    |-- config.json
    |-- index.js
|-- app4
    |-- config.json
    |-- index.js
|-- config.all.js
|-- package.json

index.js 中的内容是啥与本文无关,只是做个样子,但是在每个 app 文件夹中都有一个 config.json 文件,这就是我们需要读的配置文件,现在我们要做的就是写一个 node 脚本去监听当前这个目录的文件变动,并且实时地去读各个 app 下的配置文件,并写入 config.all.js 文件中。

现在假设配置文件 config.json 内容大概如下:

{
  "name""vortesnail",
  "github""github.com/vortesnail",
  "age""24",
  "address""earth",
  "hobby": ["sing""dance""rap""code"]
}

各个 app 文件下的 config.json 内容可不一致,比较符合我们的实际项目场景。

脚本编写

安装 chokidar

因为用原生的 fs.watch 会有很多问题,并且有很大的局限,我们采用第三方模块 chokidar 来进行文件的监听。

npm install chokidar

创建脚本文件

现在在根目录下创建我们的脚本文件: auto-config.js ,当然,名字随你。
先引用我们的第三方模块 chokidar ,以及 node 核心模块 fspath 以及 process

const chokidar = require('chokidar')
const fs = require('fs')
const path = require('path')
const process = require('process')
const PROJECT_PATH = process.cwd()

PROJECT_PATH 表示当前目录路径。

使用 chokidar.watch

这里需要注意我们是 chokidar.watch('.', {}).on(). 代表当前跟路径,使用 PROJECT_PATH 会有问题,有知道的大佬可以评论交流一下!

const chokidar = require('chokidar')
const fs = require('fs')
const path = require('path')
const process = require('process')
const PROJECT_PATH = process.cwd()

chokidar.watch('.', {
  persistenttrue,
  ignored/(^|[\/\\])\..|auto-config.js|config.all.js|node_modules/,
  depth1
}).on('all', (event, pathname) => {
  console.log(event, pathname)
  // do something later...
})
  • persistent: 与原生 fs.watch 一样,表示是否保护进程不退出持久监听,默认值为true。
  • ignored: 所要忽略监听的文件或文件夹。
  • depth: 只监听当前目录以及下一级子目录。

使用 fs.readdirSync

使用 fs.readdirSync(PROJECT_PATH) 可读取当前目录的文件列表,是数组形式,数组内容为每一个文件或文件夹的名字。更新我们的代码:

chokidar.watch('.', {
  persistent: true,
  ignored: /(^|[\/\\])\..|auto-config.js|config.all.js|node_modules/,
  depth: 0
}).on('all', (event, pathname) => {
  console.log(event, pathname)
- // do something later...
+ const rootFilenames = fs.readdirSync(PROJECT_PATH)
+ console.log(rootFilenames)
})

现在已经可以在当前目录执行 node auto-config.js 来查看当前控制台的打印了,会发现循环打印了当前目录下的文件名字数组:

[
  'app1',
  'app2',
  'app3',
  'app4',
  'auto-config.js',
  'config.all.js',
  'node_modules',
  'package-lock.json',
  'package.json'
]

循环的原因是 chokidar 第一次会监听当前目录所有文件的 add 事件,都有哪些事件详情可看这: event

循环遍历每个文件夹并获取子目录文件列表

获得了当前目录的文件,我们需要先筛选出文件夹,再对该文件夹(比如我们的 app1app2 文件夹)使用上面使用过的 fs.readdirSync([路径]) 来获取配置文件所在目录的文件列表, [路径] 可通过字符串拼接得到。

chokidar.watch('.', {
    // ...
}).on('all', (event, pathname) => {
  console.log(event, pathname)
  const rootFilenames = fs.readdirSync(PROJECT_PATH)
- console.log(rootFilenames)
+ rootFilenames.forEach(function(file) {
+   const newPath = path.join(PROJECT_PATH, `/${file}/`)
+   const subFilenanme = fs.readdirSync(newPath)
+   console.log(subFilenanme)
+ })
})

但是现在会报错,因为对于 fs.readdirSync 来说,若读取的当前路径为一个文件而不是一个文件夹,就会发生错误并终止程序的运行。故我们需要对其做一个判断。

读取文件状态 fs.stat

使用 fs.stat(path,callback) ,而不是 fs.statSync ,我们可以处理错误发生后的一些操作。

  • callback 有两个参数: (err,stats),stats 是一个 fs.Stats 对象。
  • stats.isDirectory() 可判断是否是文件夹。

更新代码如下:

chokidar.watch('.', {
    // ...
}).on('all', (event, pathname) => {
  console.log(event, pathname)
  const rootFilenames = fs.readdirSync(PROJECT_PATH)
  rootFilenames.forEach(function(file{
    const newPath = path.join(PROJECT_PATH, `/${file}/`)
    fs.stat(newPath, function(err, stats{
      if(err){
        console.log(file + 'is not a directory...')
      } else {
        const isDir = stats.isDirectory() //是文件夹
        if (isDir) {
          const subFilenanmes = fs.readdirSync(newPath)
          console.log(subFilenanmes)
        }
      }
    })
  })
})

现在已经可以获取到子目录的文件列表了,接下来可以判断是否找到我们需要读取的文件,并且读文件了。

使用 fs.readFileSync 与 fs.writeFileSync

我们需要一个变量来存储读取到的值,这里我们使用

let content = ''

这里我只是简单的读取 .json 文件,并将其内容后添加一个 , 并全部写入到新生成的 config.all.js 文件中。

添加代码如下:

chokidar.watch('.', {
  persistent: true,
  ignored: /(^|[\/\\])\..|auto-config.js|config.all.js|node_modules/,
  depth: 0
}).on('all', (event, pathname) => {
  console.log(event, pathname)
+ let content = ''
  const rootFilenames = fs.readdirSync(PROJECT_PATH)
  rootFilenames.forEach(function(file) {
    const newPath = path.join(PROJECT_PATH, `/${file}/`)
    fs.stat(newPath, function(err, stats) {
      if(err){
        console.log(file + 'is not a directory...')
      } else {
        const isDir = stats.isDirectory() //是文件夹
        if (isDir) {
          const subFilenanmes = fs.readdirSync(newPath)
-         console.log(subFilenanmes)
+         subFilenanmes.forEach(function(file) {
+           if (file === 'config.json') {
+             const data = fs.readFileSync(path.join(newPath, file), 'utf-8') //读取文件内容
+             content += data + ',' + '\n'
+           }
+           fs.writeFileSync(path.join(PROJECT_PATH, 'config.all.js'), `module.exports={data: [${content}]}`)
+         })
        }
      }
    })
  })
+ console.log(`配置表 config.all.js 已自动生成...`)
})

到目前为止,这个读写脚本就算完成了,你不信你执行 node auto-config.js ,再打开根目录下 config.all.js 文件看看,是不是把所有 app 目录下的 config.json 中的文件写入到里面了,而且你任意修改一下当前目录以及子目录的任一文件内容,都会重新生成配置表。

处理瑕疵

最后的的打印因为第一次监听会生成很多很多。。。这看起来太丑了,可以加一个防抖,只让它输出一次。
另外,还可以在适合的地方加一些提示,现放出完整代码:

const chokidar = require('chokidar')
const fs = require('fs')
const path = require('path')
const process = require('process')
const PROJECT_PATH = process.cwd()

chokidar.watch('.', {
  persistenttrue,
  ignored/(^|[\/\\])\..|auto-config.js|config.all.js|node_modules/,
  depth0
}).on('all', (event, pathname) => {
  console.log(event, pathname)
  let content = ''
  const rootFilenames = fs.readdirSync(PROJECT_PATH)
  rootFilenames.forEach(function(file{
    const newPath = path.join(PROJECT_PATH, `/${file}/`)
    fs.stat(newPath, function(err, stats{
      if(err){
        console.log(file + 'is not a directory...')
      } else {
        const isDir = stats.isDirectory() //是文件夹
        if (isDir) {
          const subFilenanmes = fs.readdirSync(newPath)
          subFilenanmes.forEach(function(file{
            if (file === 'config.json') {
              const data = fs.readFileSync(path.join(newPath, file), 'utf-8'//读取文件内容
              content += data + ',' + '\n'
            }
            fs.writeFileSync(path.join(PROJECT_PATH, 'config.all.js'), `module.exports={data: [${content}]}`)
          })
        }
      }
    })
  })

  success()
})

function debounce(func, wait{
  var timeout;
  return function ({
    var context = this;
    var args = arguments;
    clearTimeout(timeout)
    timeout = setTimeout(function(){
      func.apply(context, args)
    }, wait);
  }
}

const success = debounce(() => {
  console.log('配置表 config.all.js 已自动生成...')
}, 500)

现在你再试试 node auto-config.js ,看看效果吧~

webpack打包配置

有的时候,我们不仅仅是只在项目中使用而已,我们需要打包出一个脚本文件,丢到 nginx 环境中去,在那个根目录打开我们的脚本,自动时时刻刻监听文件的变动,生成配置表,彻底解放双手!

打包配置很简单,不要慌!

安装必要插件

无非就是一些 webpack 打包需要的依赖而已,比较重要的是 node-loader 这个包。

npm install -D webpack webpack-cli node-loader

webpack配置

根目录创建 webpack.auto.js

const path = require('path');

module.exports = {
  target"node",
  entry: {
    script: path.resolve(__dirname, "auto-config.js"),
  },
  output: {
    publicPath'',
    filename'[name].js',
    path: path.resolve(__dirname, "build"),
  },
  module: {
    rules: [
      {
        test/\.node$/,
        use'node-loader'
      }
    ],
  },
  node: {
    fs'empty',
    child_process'empty',
    tls'empty',
    net'empty'
  },
};

比较重要的地方就是 target: node ,以及入口文件要写对,因为 fsevents 中有 .node 文件,我们需要对其处理,需要一个 node-loader 做识别转译。

修改 package.json

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
+ "build:auto": "webpack -p --progress --config webpack.auto.js"
},

打包

现在,你在控制台执行 npm run build:auto ,一个可以监听并读写的小脚本就这样完成了!尽情去使用吧。把打出来的包丢到任意目录,执行:

node auto-config.js
// 没权限的话需要要加 sudo
sudo node auto-config.js

完美!

结语

自我认为该写法还有很大改进地方,奈何自己水平有限,如果有大佬有更好的意见,非常希望您能在评论区说出来,让更多像我这样“求知若渴”的同学得到成长,感谢!🙏

推荐阅读:
这一次,彻底理解 https 原理