Skip to content
文章目录

node工具开发常用工具和代码

直接和 node 相关

从完整的物理路径名中获取文件名

包含文件类型后缀

js
import { basename } from 'path'
// 获取文件名
const newFileName = basename(myFile.filepath)

获取文件内容

js
import { readFileSync } from 'fs'

const filePath = 'D:/demo/test.txt'
const content = rf.readFileSync(filePath, 'utf-8')

判断是文件还是文件夹

js
import { readdirSync, statSync, accessSync } from 'fs'

const fileOrDirFullPath = '文件或目录的全路径名'
const stats = statSync(fileOrDirFullPath)
if (stats.isDirectory()) {
  console.log('是目录')
} else if (stats.isFile()) {
  console.log('是文件')
}

判断文件/目录是否存在

js
import { accessSync, constants } from 'fs'

const fileOrDirFullPath = '文件或目录的全路径'
try {
  accessSync(fileOrDirFullPath, constants.F_OK)
  console.log('文件或目录存在')
} catch (err) {
  console.log('文件或目录不存在')
}

判断是否可读/可写

是否可写

js
import { accessSync, constants } from 'fs'

const fileOrDirFullPath = '文件或目录的全路径'
try {
  accessSync(fileOrDirFullPath, constants.R_OK)
  console.log('文件或目录可读')
} catch (err) {
  console.log('文件或目录不可读')
}

是否可读

js
import { accessSync, constants } from 'fs'

const fileOrDirFullPath = '文件或目录的全路径'
try {
  accessSync(fileOrDirFullPath, constants.W_OK)
  console.log('文件或目录可写')
} catch (err) {
  console.log('文件或目录不可写')
}

判断文件是否存在且写

js
import { accessSync, constants } from 'fs'

const fileOrDirFullPath = '文件全路径'
try {
  accessSync(fileOrDirFullPath, constants.F_OK | constants.W_OK)
  console.log('文件存在且可写')
} catch (err) {
  console.log('文件不存在或不可写')
}

获取系统路径分隔符

ts
import { sep } from 'path'

console.log(sep)

系统路径组装

js
import { join } from 'path'
/*
 * 文件下载
 * __dirname: 表示开发者写 __dirname 这个语句所在文件的这个文件目录的绝对路径
 */
const root = join(__dirname, '../../upload')

各种路径

js
import { join } from 'path'

join('/foo', 'bar', 'baz/asdf', 'quux', '..')
// 返回: '/foo/bar/baz/asdf'

join('foo', {}, 'bar')
// 抛出 'TypeError: Path must be a string. Received {}'

获取当前路径的上级目录

js
import { join } from 'path'
const parentFullPath = join(__dirname, '../')

__dirname./ 的区别

__dirname总是指向被执行 js 文件的绝对路径

当在/react-admin-server/routers/file-upload.js 文件中写了 __dirname, 它的值就是/react-admin-server/routers

相反./会返回你执行 node 命令的路径,例如你的工作路径

假设有如下目录结构

/react-admin-server
  /routers
    file-upload.js
js
// file-upload.js
var path = require('path')
console.log(path.resolve('.'))
console.log(path.resolve(__dirname))

然后在file-upload.js 中,有如下代码,然后在终端执行了下面命令

cd /react-admin-server/routers
node file-upload.js

. 是你的当前工作目录,在这个例子中就是/react-admin-server/routers__dirnamefile-upload.js 的文件路径,在这个例子中就是 /react-admin-server/routers

然而,如果我们的工作目录是/react-admin-server

cd /react-admin-server
node routers/file-upload.js

将会得到

/react-admin-server
/react-admin-server/routers

此时,.指向我们的工作目录,即/react-admin-server__dirname 还是指向 /react-admin-server/routers

js
//__dirname表示当前文件所在的根路径,也就是/routers,然后..表示向上退一级,也就是到了项目根目录,然后public/upload表示到了这个目录
const dirPath = path.join(__dirname, '..', 'public/upload')

遍历指定目录的所有文件和文件夹

js
import { resolve, join, sep } from 'path'
import { readdirSync, statSync } from 'fs'
import { DefaultTheme } from 'vitepress'

/**
 * 判断是否为markdown文件
 *
 * @param   {string}  fileName  文件名
 *
 * @return  {[boolean]}         有返回值则表示是markdown文件,否则不是
 */
function isMarkdownFile(fileName: string) {
  return fileName.match(/.+\.md$/)
}
// 获取docs目录的完整名称(从根目录一直到docs目录)
const docsDirFullPath = join(__dirname, '../')
// 获取docs目录的完整长度
const docsDirFullPathLen = docsDirFullPath.length
/**
 * 获取dirOrFileFullName中第一个/docs/后的所有内容
 *
 * 如:
 * /a-root/docs/test 则 获取到 /test
 * /a-root-docs/docs/test 则 获取到 /test
 * /a-root-docs/docs/docs/test 则 获取到 /docs/test
 *
 * @param   {string}  dirOrFileFullName  文件或者目录名
 *
 * @return  {[type]}                     [return description]
 */
function getDocsDirNameAfterStr(dirOrFileFullName: string) {
  // 使用docsDirFullPathLen采用字符串截取的方式,避免多层目录都叫docs的问题
  return `${sep}${dirOrFileFullName.substring(docsDirFullPathLen)}`
}
interface SidebarGenerateConfig {
  /**
   * 需要遍历的目录. 默认:articles
   */
  dirName?: string
  /**
   * 忽略的文件名. 默认: index.md
   */
  ignoreFileName?: string
  /**
   * 忽略的文件夹名称. 默认: ['demo','asserts']
   */
  ignoreDirNames?: string[]
}
export function getSidebarData(sidebarGenerateConfig: SidebarGenerateConfig = {}) {
  const {
    dirName = 'articles',
    ignoreFileName = 'index.md',
    ignoreDirNames = ['demo', 'asserts'],
  } = sidebarGenerateConfig
  // 获取目录的绝对路径
  const dirFullPath = resolve(__dirname, `../${dirName}`)
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const obj = {}
  allDirAndFileNameArr.map(dirName => {
    let subDirFullName = join(dirFullPath, dirName)
    const property = getDocsDirNameAfterStr(subDirFullName).replace(/\\/g, '/') + '/'
    const arr = getSideBarItemTreeData(subDirFullName, 1, 2, ignoreFileName, ignoreDirNames)
    obj[property] = arr
  })
  // console.log('sidebarData')
  // console.log(obj)
  return obj
}

interface SideBarItem {
  text: string
  collapsible?: boolean
  collapsed?: boolean
  items?: SideBarItem[]
  link?: string
}
function getSideBarItemTreeData(
  dirFullPath: string,
  level: number,
  maxLevel: number,
  ignoreFileName: string,
  ignoreDirNames: string[]
): SideBarItem[] {
  // 获取所有文件名和目录名
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const result: SideBarItem[] = []
  allDirAndFileNameArr.map((fileOrDirName: string, idx: number) => {
    const fileOrDirFullPath = join(dirFullPath, fileOrDirName)
    const stats = statSync(fileOrDirFullPath)
    if (stats.isDirectory()) {
      if (!ignoreDirNames.includes(fileOrDirName)) {
        const text = fileOrDirName.match(/^[0-9]{2}-.+/) ? fileOrDirName.substring(3) : fileOrDirName
        // 当前为文件夹
        const dirData: SideBarItem = {
          text,
          collapsed: false,
        }
        if (level !== maxLevel) {
          dirData.items = getSideBarItemTreeData(fileOrDirFullPath, level + 1, maxLevel, ignoreFileName, ignoreDirNames)
        }
        if (dirData.items) {
          dirData.collapsible = true
        }
        result.push(dirData)
      }
    } else if (isMarkdownFile(fileOrDirName) && ignoreFileName !== fileOrDirName) {
      // console.log(fileOrDirName)
      // 当前为文件
      const matchResult = fileOrDirName.match(/(.+)\.md/)
      let text = matchResult ? matchResult[1] : fileOrDirName
      text = text.match(/^[0-9]{2}-.+/) ? text.substring(3) : text
      // console.log(text)
      const fileData: SideBarItem = {
        text,
        link: getDocsDirNameAfterStr(fileOrDirFullPath).replace('.md', '').replace(/\\/g, '/'),
      }
      result.push(fileData)
    }
  })
  return result
}

interface NavGenerateConfig {
  /**
   * 是否启用路由匹配显示激活状态. 默认:false
   */
  enableDirActiveMatch: boolean
  /**
   * 需要遍历的目录. 默认:articles
   */
  dirName?: string
  /**
   * 最大遍历层级. 默认:1
   */
  maxLevel?: number
}

export function getNavData(navGenerateConfig: NavGenerateConfig) {
  const { enableDirActiveMatch, dirName = 'articles', maxLevel = 1 } = navGenerateConfig
  const dirFullPath = resolve(__dirname, `../${dirName}`)
  const result = getNavDataArr(dirFullPath, 1, maxLevel, enableDirActiveMatch)
  // console.log('navData')
  // console.log(result)
  return result
}

/**
 * 获取顶部导航数据
 *
 * @param   {string}     dirFullPath  当前需要遍历的目录绝对路径
 * @param   {number}     level        当前层级
 * @param   {number[]}   maxLevel     允许遍历的最大层级
 * @param   {boolean}    enableActiveMatch 是否启用路由匹配显示激活状态
 *
 * @return  {NavItem[]}               导航数据数组
 */
function getNavDataArr(
  dirFullPath: string,
  level: number,
  maxLevel: number,
  enableActiveMatch: boolean
): DefaultTheme.NavItem[] {
  // 获取所有文件名和目录名
  const allDirAndFileNameArr = readdirSync(dirFullPath)
  const result: DefaultTheme.NavItem[] = []
  allDirAndFileNameArr.map((fileOrDirName: string, idx: number) => {
    const fileOrDirFullPath = join(dirFullPath, fileOrDirName)
    const stats = statSync(fileOrDirFullPath)
    const link = getDocsDirNameAfterStr(fileOrDirFullPath).replace('.md', '').replace(/\\/g, '/')
    // console.log(fileOrDirFullPath)
    // console.log(link)
    const text = fileOrDirName.match(/^[0-9]{2}-.+/) ? fileOrDirName.substring(3) : fileOrDirName
    if (stats.isDirectory()) {
      // 当前为文件夹
      const dirData: DefaultTheme.NavItem = {
        text,
        link: `${link}/`,
      }
      if (level !== maxLevel) {
        // @ts-ignore
        dirData.items = getNavDataArr(fileOrDirFullPath, level + 1, maxLevel, enableActiveMatch)
      }
      if (enableActiveMatch) {
        dirData.activeMatch = link + '/'
      }
      result.push(dirData)
    } else if (isMarkdownFile(fileOrDirName)) {
      // 当前为文件
      const fileData: DefaultTheme.NavItem = {
        text,
        link: link,
      }
      if (enableActiveMatch) {
        fileData.activeMatch = link + '/'
      }
      result.push(fileData)
    }
  })
  return result
}

第三方工具

文件复制

shell
pnpm add cp
js
cp(src, dest, cb)

cp.sync(src, dest)

文件/目录删除

文件/目录删除

pnpm add rimraf
js
import rimraf from 'rimraf'

import { rimraf, rimrafSync, native, nativeSync } from 'rimraf'

rimraf 文档

提问与回答

shell
pnpm add inquirer
ts
import inquirer, { QuestionCollection } from 'inquirer'

interface TemplateInfo {
  // 模板压缩文件下载地址
  downloadUrl: string
  // 模板描述
  desc: string
}
const repositoryList: Record<string, TemplateInfo> = {
  vue3: {
    downloadUrl: 'https://gitcode.net/pzy_666/front-project-template/-/raw/master/zip/vue3.zip?inline=false',
    desc: 'vue3项目基础模板',
  },
  'vue3-element-plus': {
    downloadUrl:
      'https://gitcode.net/pzy_666/front-project-template/-/raw/master/zip/vue3-element-plus.zip?inline=false',
    desc: 'vue3项目基础模板上整合进element-plus',
  },
}

const projectTemplateChoices = Object.keys(repositoryList).map(propertyName => ({
  name: repositoryList[propertyName].desc,
  value: propertyName,
}))

const PROMPT_LIST: QuestionCollection = [
  {
    type: 'input',
    message: '请输入项目名',
    name: 'projectName',
    default: 'demo',
  },
  {
    type: 'list',
    message: '请选择需要的项目模板',
    name: 'templateName',
    choices: projectTemplateChoices,
  },
]
inquirer.prompt<IPromptOption>(PROMPT_LIST).then(async answer => {
  console.log(answer)
})

在 node 脚本中执行 shell 命令

pnpm add shelljs
js
import shell from 'shelljs'

// 检测当前环境是否有yalc命令
if (!shell.which('yalc')) {
  shell.echo('抱歉执行该脚本需要 yalc')
  shell.exit(1)
}

/**
 * 执行yalc dir命令
 */
shell.exec('yalc dir', function (code: number, stdout: string, stderr: string) {
  const yalcRepo = stdout.replace('\n', '').replace('\r\n', '')
  if (!yalcRepo) {
    shell.echo(`执行出错:未获取到yalc仓库地址`)
    shell.exit(1)
  }
  if (code === 0) {
    sucCall && sucCall(yalcRepo)
  } else {
    shell.echo(`执行出错:${stderr}`)
    shell.exit(1)
  }
})

帮助与版本与命令行参数获取

shell
pnpm add commander
ts
import { Command } from 'commander'

const program = new Command('yalc-clean')
program.usage(`your-package-name [-f]
e.g. 
yalc-clean @pzy/my-button     # 从yalc仓库删除 @pzy/my-button 包, 如果 @pzy/my-button 包被其他项目引用,则会删除失败
yalc-clean @pzy/my-button -f  # 从yalc仓库中强制删除 @pzy/my-button 包, 无论 @pzy/my-button 包是否被其他项目引用
`)
program.description(`用于删除yalc仓库中的包.
1. 内部会先执行: yalc dir 用于获取yalc仓库目录.
2. 再执行: yalc installations show your-package-name 查看包的引用情况,如果存在引用则询问是否强制删除. 并告知强制删除命令. 然后终止程序执行.
3. 如果 your-package-name 没有被其他项目引用或你使用的是强制删除, 则先执行: yalc installations clean your-package-name 进行清除,
4. 最后再将 your-package-name 从yalc仓库中删除
`)
program.option('-f --force', '无论包是否被其他项目引用,都强制删除')

program.version('0.0.1', '-v, --version', '输出当前版本')
program.helpOption('-h --help', '显示命令帮助信息')

program.parse(process.argv)

const yalcPackageName = program.args[0]

const forceDel = program.opts().force

文件下载与解压

shell
pnpm add download-git-repo
js
import download from 'download-git-repo'

async function demo() {
  const downloadUrl = `direct:${repositoryList[answer.templateName].downloadUrl}`
  const targetPath = path.resolve(CURRENT_PATH, answer.projectName) // 目标路径
  await download(downloadUrl, targetPath, {}, (err: any) => {
    if (err) {
      console.log(err)
    } else {
        cd ${answer.projectName}
        pnpm install
        pnpm run dev

        `)
    }
  })
}

进度信息

js
import ora from 'ora'

const newOra = ora('开始创建项目...').start()
newOra.fail(`项目创建失败`)
newOra.succeed(`项目创建成功`)