node.js监听文件变化的实现方法


Posted in Javascript onApril 17, 2019

前言

随着前端技术的飞速发展,前端开发也从原始的刀耕火种,向着工程化效率化的方向发展。在各种开发框架之外,打包编译等技术也是层出不穷,开发体验也是越来越好。例如HMR,让我们的更新可以即时可见,告别了手动F5的情况。其实现就是监听文件变化自动调用构建过程。下面就关注下如何实现node监听文件变化。

场景

假定要监听index.js,每当内容更改重新编译。

我们就用简单的console来标识执行编译。下面就是实现该功能。

node原生API

fs.watchFile

翻下node的文档就会看到一个满足我们需求的Apifs.watchFile(毕竟是文件相关的操作,很大可能就在fs模块下面了)。

fs.watchFile(filename[, options], listener)
  • filename 显然就是文件名
  • options 可选 对象 包含以下两个属性
    • persistent 文件被监听时进程是否继续,默认true
    • interval 多长时间轮训一次目标文件,默认5007毫秒
  • listener 事件回调 包含两个参数
    • current 当前文件stat对象
    • prev 之前文件stat对象

看完参数信息,不知道大家有没有从其参数属性中得到点什么特别的信息。特别是interval选项和listener中的回调参数。

监控filename对应文件,每当访问文件时会触发回调。

这里每当访问文件时会触发,实际指的是每次切换之后再次进入文件,然后保存之后,无论是否做了修改都会出发回调。

另外轮询事件和文件对象,是不是可以猜测,其实现监听的原理,固定时间轮询文件状态,然后将前后的状态返回,将判断交给使用者。

所以node也建议,如果要获取文件修改,那么需要根据stat对象的修改时间来进行对比,即比较 curr.mtime 和 prev.mtime。

这样就有点问题,我们先看下例子,会更清晰一点。

const fs = require('fs')
const filePath = './index.js'
console.log(`正在监听 ${filePath}`);
fs.watchFile(filePath, (cur, prv) => {
 if (filePath) {
  // 打印出修改时间
  console.log(`cur.mtime>>${cur.mtime.toLocaleString()}`)
  console.log(`prv.mtime>>${prv.mtime.toLocaleString()}`)
  // 根据修改时间判断做下区分,以分辨是否更改
  if (cur.mtime != prv.mtime){
   console.log(`${filePath}文件发生更新`)
  }
 }
})

然后测试结果如下:

// 运行
node watch1.js
// 1、访问index.js 不做修改,然后保存
// 2、切换文件,再次访问,不做修改,只报错
// 3、编辑内容,并保存

node.js监听文件变化的实现方法

可以看到1、2两步,并没有实际修改内容,然而我们并没有办法区分。只要你是切换之后再保存,修改时间戳mtime就发生变化。

另外响应时间真的很慢,毕竟是轮询。

对于这些问题,其实官网也给了一句话:

Using fs.watch() is more efficient than fs.watchFile and fs.unwatchFile. fs.watch should be used instead of fs.watchFile and fs.unwatchFile when possible.

能用fs.watch的情况就不要用watchFile了。一是效率,二是不能准确获知修改状态 三是只能监听单独文件
对于实际开发过程中,显然我们想要关注的是源文件夹的变动。

fs.watch

首先用法如下:

fs.watch(filename[, options][, listener])

跟fs.watchFile比较类似。

  • filename 显然就是文件名
  • options 可选 对象或者字符串 包含以下三个属性
    • persistent 文件被监听时进程是否继续,默认true
    • recursive 是否监控所有子目录,默认false 即当前目录,true为所有子目录。
    • encoding 指定传递给回调事件的文件名称,默认utf8
  • listener 事件回调 包含两个参数
    • eventType 事件类型,rename 或者 change
    • filename 当前变更文件的文件名

options如果是字符串,指的是encoding。

监听filename对应的文件或者文件夹(recursive参数也体现出来这一特性),返回一个fs.FSWatcher对象。

该功能的实现依赖于底层操作系统的对于文件更改的通知。 所以就存在一个问题,可能不同平台的实现不太相同。
如下示例1:

const fs = require('fs')
const filePath = './' 
console.log(`正在监听 ${filePath}`);
fs.watch(filePath,(event,filename)=>{
 if (filename){
  console.log(`${filename}文件发生更新`)
 }
})

一个比较明显的优势就体现出来了:响应比较及时,相比于轮询,效率肯定更高。

不过这样修改并保存的时候回发现同样有点问题。

直接保存,显示两次更新

修改文件之后,同样显示两次更新(mac系统上是两次,其他系统可能有所差别)

node.js监听文件变化的实现方法

这样可能是于操作系统对文件修改的事件支持有关,在保存的时候出发了不止一次。

下面聚焦于回调事件的参数,event对应事件类型,是否可以判断事件类型为change呢,才执行呢,忽略空保存。

const fs = require('fs')
const filePath = './'  
console.log(`正在监听 ${filePath}`);
fs.watch(filePath,(event,filename)=>{
  console.log(`event类型${event}`)
  if (filename && event == 'change') {
    console.log(`${filename}文件发生更新`)
  }
})

不过实际上,空的保存event也是change,另外不同平台event的实现可能也有所不同。这种方式要pass掉。

校验变更时间

显然从上面的例子可以看到,变更时间依然不可控。因为每次保存,node对应stat对象依然会修改。

对比文件内容

只能选择这种方式来判断是否是否更新。例如md5:

const fs = require('fs'),
  md5 = require('md5');
const filePath = './'  
let preveMd5 = null

console.log(`正在监听 ${filePath}`);
fs.watch(filePath,(event,filename)=>{
  var currentMd5 = md5(fs.readFileSync(filePath + filename))
  if (currentMd5 == preveMd5) {
    return
  }
  preveMd5 = currentMd5
  console.log(`${filePath}文件发生更新`)
})

先保存当前文件md5值,每次文件变化时(即保存操作响应之后),每次都获取文件的md5然后进行对比,看是否发生变化。

node.js监听文件变化的实现方法

不过这样可以看到,当初次保存时,都会执行一次,因为初始值为null的缘故。这样可以加个兼容,根据是否第一次保存来判断好了。

优化

对于不同的操作系统,可能保存时触发的回调不止一个(mac上到没出现)。为了避免这种实时响应对应的频繁触发,可以引入debounce函数来保证性能。

const fs = require('fs'),
  md5 = require('md5');
let preveMd5 = null,
  fsWait = false
const filePath = './'  
console.log(`正在监听 ${filePath}`);
fs.watch(filePath,(event,filename)=>{
  if (filename){
    if (fsWait) return;
    fsWait = setTimeout(() => {
      fsWait = false;
    }, 100)
    var currentMd5 = md5(fs.readFileSync(filePath + filename))
    if (currentMd5 == preveMd5){
      return 
    }
    preveMd5 = currentMd5
    console.log(`${filePath}文件发生更新`)
  }
})

结束语

到这里,node监听文件的实现就结束了。做个学习笔记,来做个参考记录。实现起来并不难,但是要实际应用的话需要考虑的方面就比较多了。还是推荐开源框架node-watch、chokidar等,各方面实现的都比较完善。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

参考文章

  • node文档
  • How to Watch for Files Changes in Node.js
  • Nodejs Monitor File Changes
Javascript 相关文章推荐
写出更好的JavaScript之undefined篇(上)
Nov 22 Javascript
jquery 防止表单重复提交代码
Jan 21 Javascript
js算法中的排序、数组去重详细概述
Oct 14 Javascript
jquery实现非叠加式的搜索框提示效果
Jan 07 Javascript
JS 在指定数组中随机取出N个不重复的数据
Jun 10 Javascript
Javascript常用字符串判断函数代码分享
Dec 08 Javascript
jQuery实现平滑滚动到指定锚点的方法
Mar 20 Javascript
javascript中for/in循环及使用技巧
Sep 01 Javascript
在 Typescript 中使用可被复用的 Vue Mixin功能
Apr 17 Javascript
vue canvas绘制矩形并解决由clearRec带来的闪屏问题
Sep 02 Javascript
vue引入静态js文件的方法
Jun 20 Javascript
jQuery加PHP实现图片上传并提交的示例代码
Jul 16 jQuery
vue中格式化时间过滤器代码实例
Apr 17 #Javascript
postman自定义函数实现 时间函数的思路详解
Apr 17 #Javascript
vue指令之表单控件绑定v-model v-model与v-bind结合使用
Apr 17 #Javascript
记一次Vue.js混入mixin的使用(分权限管理页面)
Apr 17 #Javascript
详解js获取video任意时间的画面截图
Apr 17 #Javascript
解决vue跨域axios异步通信问题
Apr 17 #Javascript
自定义Vue中的v-module双向绑定的实现
Apr 17 #Javascript
You might like
php实现网页上一页下一页翻页过程详解
2019/06/28 PHP
PHP单文件上传原理及上传函数的封装操作示例
2019/09/02 PHP
自动生成文章摘要的代码[JavaScript 版本]
2007/03/20 Javascript
Windows系统下使用Sublime搭建nodejs环境
2015/04/13 NodeJs
详解js中构造流程图的核心技术JsPlumb
2015/12/08 Javascript
利用JQuery操作iframe父页面、子页面的元素和方法汇总
2017/09/10 jQuery
基于滚动条位置判断的简单实例
2017/12/14 Javascript
解决Mac安装thrift因bison报错的问题
2018/05/17 Javascript
Bootbox将后台JSON数据填充Form表单的实例代码
2018/09/10 Javascript
Vue 实例事件简单示例
2019/09/19 Javascript
VuePress 中如何增加用户登录功能
2019/11/29 Javascript
一个小示例告诉你Python语言的优雅之处
2014/07/04 Python
Python 抓取动态网页内容方案详解
2014/12/25 Python
python使用PyGame播放Midi和Mp3文件的方法
2015/04/24 Python
使用python 和 lint 删除项目无用资源的方法
2017/12/20 Python
Python 函数返回值的示例代码
2019/03/11 Python
python买卖股票的最佳时机(基于贪心/蛮力算法)
2019/07/05 Python
详解PANDAS 数据合并与重塑(join/merge篇)
2019/07/09 Python
Python操作SQLite/MySQL/LMDB数据库的方法
2019/11/07 Python
Python异步编程之协程任务的调度操作实例分析
2020/02/01 Python
Jupyter Notebook 文件默认目录的查看以及更改步骤
2020/04/14 Python
python Django 反向访问器的外键冲突解决
2020/05/20 Python
Python函数参数分类原理详解
2020/05/28 Python
Python Dict找出value大于某值或key大于某值的所有项方式
2020/06/05 Python
意大利奢侈品购物网站:Giglio
2018/01/05 全球购物
全球领先的美容用品专卖店:Beauty Plus Salon
2018/09/04 全球购物
有趣的睡衣和礼物:LazyOne
2019/11/27 全球购物
如何实现一个自定义类的序列化
2012/05/22 面试题
电大自我鉴定
2013/10/27 职场文书
最受欢迎的自我评价
2013/12/22 职场文书
违反校纪校规检讨书
2014/02/15 职场文书
2016大一新生军训感言
2015/12/08 职场文书
解决go在函数退出后子协程的退出问题
2021/04/30 Golang
mybatis调用sqlserver存储过程返回结果集的方法
2021/05/08 SQL Server
Vue接口封装的完整步骤记录
2021/05/14 Vue.js
「海贼王」112.9万粉丝纪念图标公布
2022/03/21 日漫