前端进阶 – node基本操作:path + fs

很多前端同学没有服务端或者客户端经验,对node操作望而生畏,这大可不必。

正如大神Winter所说:所谓编程,无非IO。一般意义上的Web前端,处理的是基于http与浏览器(或宿主)的本地缓存、各种API的输入输出。Node作为一个服务端平台,面向的并不是浏览器,而是服务器本身,所以node可以操作的,是整个服务端资源,其中包括前端并不常接触的网络IO、磁盘IO、数据库IO等等;要学习这些操作,看看node文档、写个demo玩一下,相信你很快就能掌握。

进一步讲,我们日常基于webpack/gulp等工具对前端项目进行打包等等这些操作,就是通过node和各种工具包,对自己电脑上的代码资源进行测试、重构、压缩,输出不同需求的代码包。如果把这个流程放到服务端操作,并整合发布审核流程,就是我们常说的CI/CD。

本文主要展示node自带的几个基础接口能力。

process

Node主进程。

process.env

% node
Welcome to Node.js v15.12.0.
Type ".help" for more information.
> process.env

复制代码

process是node主进程,process.env中带有运行时各种环境变量。其中常用的有

process.env.PWD   // 启动脚本时的目录
process.env.HOME  // 当前用户目录
process.env.USER  // 当前用户名
process.env.SHELL // 当前终端名称
process.env.PATH  // 系统全局变量
复制代码

process.argv

脚本执行的完整命令+参数。

path

// js
const path = require('path')
复制代码

path用于处理各种本地路径问题。

path.join 路径拼接

> path.join('/baseDir', 'a', 'b')
'/baseDir/a/b'
> path.join('/baseDir', 'a', '../b')
'/baseDir/b'
复制代码

path.resolve 路径处理

> path.resolve('/baseDir', 'a', 'b')
'/baseDir/a/b'
> path.resolve('/baseDir', 'a', '../b')
'/baseDir/b'
复制代码

path.relative 相对路径

> path.relative('/baseDir', '/baseDir/a')
'a'
> path.relative('/baseDir', '/someElseDir/a')
'../someElseDir/a'
> path.relative('/baseDir/a/aa', '/baseDir/b/bb')
'../../b/bb'
复制代码

path.dirname 获取所在目录名

> path.dirname('/baseDir/a/b')
'/baseDir/a'
> path.dirname('/baseDir/a/b/')
'/baseDir/a'
复制代码

由于Windows系统和Mac Linux的路径格式不一样,所以要处理路径问题时,一定要使用path,如非必要不要尝试自己拼接

fs

const fs = require('fs')
复制代码

fs也就是file system的缩写,故名思义就是操作文件的工具。为了方便,这里都只介绍异步方法。同步方法直接加Sync,具体请查阅node文档

fs.exists 文件或目录是否存在

> fs.exists('/baseDir/a/b.html', r => console.log('exists', r))
> exists false // 该文件确实不存在

> fs.exists('~/11111.zip', r => console.log('exists', r))
> exists false // 不支持使用 ~/ 

> fs.exists(path.join(process.env.HOME, '11111.zip'), r => console.log('exists', r))
> exists true  // 使用环境变量拼接完整地址

> fs.exists(process.env.HOME, r => console.log('exists', r))
> exists true  // 也可以检查目录是否存在
复制代码

fs.stat 路径详情

文件详情信息

> fs.stat(path.join(process.env.HOME, '11111.zip'), (err,stat) => console.log('\nstat', stat, '\nerror', err))

stat Stats {
  dev: 16777221,
  mode: 33188,
  nlink: 1,
  uid: 501,
  gid: 20,
  rdev: 0,
  blksize: 4096,
  ino: 12006418,
  size: 332419104,
  blocks: 649264,
  atimeMs: 1615184984501.856,
  mtimeMs: 1615184993657.0806,
  ctimeMs: 1615185011988.9607,
  birthtimeMs: 1615184984501.856,
  atime: 2021-03-08T06:29:44.502Z,
  mtime: 2021-03-08T06:29:53.657Z,
  ctime: 2021-03-08T06:30:11.989Z,
  birthtime: 2021-03-08T06:29:44.502Z
} 
error null
复制代码

目录详情信息

> fs.stat(process.env.HOME, (err,stat) => console.log('\nstat', stat, '\nerror', err))

stat Stats {
  dev: 16777221,
  mode: 16877,
  nlink: 78,
  uid: 501,
  gid: 20,
  rdev: 0,
  blksize: 4096,
  ino: 363143,
  size: 2496,
  blocks: 0,
  atimeMs: 1624841990808.2407,
  mtimeMs: 1624841990597.5269,
  ctimeMs: 1624841990597.5269,
  birthtimeMs: 1603169031408.2742,
  atime: 2021-06-28T00:59:50.808Z,
  mtime: 2021-06-28T00:59:50.598Z,
  ctime: 2021-06-28T00:59:50.598Z,
  birthtime: 2020-10-20T04:43:51.408Z
} 
error null
复制代码

路径不存在

> fs.stat('/baseDir/a/b', (err,stat) => console.log('\nstat', stat, '\nerror', err))

stat undefined 
error [Error: ENOENT: no such file or directory, stat '/baseDir/a/b'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'stat',
  path: '/baseDir/a/b'
}
复制代码

判断路径类型

> fs.stat(process.env.HOME, (err,stat) => console.log('\nisDir', stat.isDirectory(), 'isFile', stat.isFile(), '\nerror', err))

isDir true isFile false 
error null
复制代码
> fs.stat(path.join(process.env.HOME, '11111.zip'), (err,stat) => console.log('\nisDir', stat.isDirectory(), 'isFile', stat.isFile(), '\nerror', err))

isDir false isFile true 
error null
复制代码

PS:根据我自己的印象,在不同的node版本中,对于不存在的路径,fs.stat反馈存在差异。建议先使用fs.exists判断存在性。

fs.readFile 读取文件内容

使用该方法前,应当先确定文件是否存在。

> fs.readFile(path.join(process.env.HOME, 'testnpm/build.js'), (err, content) => console.log('\ncontent',content,'\nerror', err))

content <Buffer 63 6f 6e 73 6f 6c 65 2e 6c 6f 67 28 27 62 75 69 6c 64 27 29 0a> 
error null
复制代码

上面的代码没有指定编码格式,所以返回文件内容是Buffer二进制格式。下面使用utf-8格式按文本读取文件:

> fs.readFile(path.join(process.env.HOME, 'testnpm/build.js'), 'utf-8', (err, content) => console.log('\ncontent',content,'\nerror', err))

content console.log('build')
 
error null
复制代码

fs.writeFile 写入文件内容

使用该方法前,应当先确定文件是否存在。

> fs.writeFile(path.join(process.env.HOME, 'a.txt'), 'hello fs', err => console.log('\nerr', err))

err null
复制代码
# 查看文件
% cat a.txt 
hello fs
复制代码

fs.unlink 删除文件

> fs.unlink(path.join(process.env.HOME, 'a.txt'), err => console.log(err))

> null
复制代码

如果目录不存在,则报错:

> fs.unlink('/xxxxxx/xxxxxx.js', err => console.log(err))

> [Error: ENOENT: no such file or directory, unlink '/xxxxxx/xxxxxx.js'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'unlink',
  path: '/xxxxxx/xxxxxx.js'
}
复制代码

fs.mkdir 创建目录

> fs.mkdir(path.join(process.env.HOME, 'fs'), err => console.log('err', err))

> err null
复制代码

如果目录已存在,则报错

> fs.mkdir('/usr', err => console.log('err', err))

> err [Error: EEXIST: file already exists, mkdir '/usr'] {
  errno: -17,
  code: 'EEXIST',
  syscall: 'mkdir',
  path: '/usr'
}
复制代码

fs.rmdir 删除目录

> fs.rmdir(path.join(process.env.HOME, 'fs'), err => console.log('err', err))

> err null
复制代码

报错情况

该操作比较危险,报错情况相对复杂。

目录不存在

> fs.rmdir('/xxxxxx', err => console.log('err', err))

> err [Error: ENOENT: no such file or directory, rmdir '/xxxxxx'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'rmdir',
  path: '/xxxxxx'
}
复制代码

权限不足

> fs.rmdir('/usr', err => console.log('err', err))

> err [Error: EPERM: operation not permitted, rmdir '/usr'] {
  errno: -1,
  code: 'EPERM',
  syscall: 'rmdir',
  path: '/usr'
}
复制代码

非空目录

> fs.rmdir(path.join(process.env.HOME, 'testnpm'), err => console.log('err', err))

> err [Error: ENOTEMPTY: directory not empty, rmdir '/Users/js/testnpm'] {
  errno: -66,
  code: '{ recursive: true }',
  syscall: 'rmdir',
  path: '/Users/js/testnpm'
}
复制代码

在较高版本的node中,fs.rmdir可以使用{ recursive: true }参数项,递归删除整个目录,避免ENOTEMPTY错误。但有同学反馈node12不支持,我还没有对此问题作详细研究。考虑到兼容,还是手动递归、删除为好。

fs.readdir 读取目录中的文件列表

> fs.readdir('/usr', (err, res) => console.log(res))

> [
  'X11',        'X11R6',
  'bin',        'lib',
  'libexec',    'local',
  'sbin',       'share',
  'standalone'
]
复制代码

fs + path 递归读取全部文件

const path = require('path')
const fs = require('fs')

const read = p => {
    p = path.resolve(process.env.PWD, p)
    const exists = fs.existsSync(p)
    if(!exists) {
        return []
    }
    const stat = fs.statSync(p)
    if(stat.isFile()) {
        return [p]
    } else {
        return fs.readdirSync(p)
            .filter(f => !/^\./.test(f))
            .map(pp => path.join(p, pp))
            .map(p => read(p))
            .reduce((r, v) => [...r, ...v], [])
    }
}

console.log(read('./').map(f => f.replace(process.env.HOME, '')))
复制代码

技巧点:

  • 使用path.resolve + process.env.PWD 实现从脚本运行点出发,补全完成目录。
  • 使用fs.exists fs.stat判断路径存在性和类型,返回不同的数据。
  • 在使用path.readdir获取到文件名称列表后,通过正则过滤掉 . .. 两类操作路径符。本例也会过滤掉以.起头命名的路径,如.gitignore文件。
  • 使用递归减少代码量
  • 使用reduce打平数组。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享