NodeJS发光发热之打包hooks

1,171 阅读4分钟

让NodeJS在项目中发光发热系列文章, 没看过这个的朋友建议先看一下,不然直接这个可能会有点吃力。

写在前面

最近离职了,闲着也是闲着。就想起来了之前Node相关的文章还有一部分没写,刚好有时间,今天给续上。现如今的前端开发,通过Node可以高度自定义的为我们的项目打造一条龙服务。既然一条龙那么不仅仅是开发阶段,打包之后的事情,我们也要处理。这篇文章就聊一聊打包之后的一些用得上的hooks。同样的这篇文章也是抛砖引玉的作用,文章里涉及的都是一些简单的部分,深层次的骚操作还要各位朋友自己去挖掘。

首先来看一下大致的效果

准备工作

准备一个项目,Vue/React/Angular的都可以,我们这里以vue-base-template为项目模板

改造打包命令

因为我使用的是Vue-cli3.0版本的脚手架,它打包的命令是通过vue-cli-service build命令来执行。npm直接运行的话我们就没法加hooks了, 所有我们自己写一个Node脚本来执行打包命令, 在scripts文件夹下新建build.js,这个脚本用来组织我们所有的打包相关东西。

build.js的职责其实很简单:

  1. 组织打包参数
  2. 运行打包命令
  3. 检测打包完成之后运行自定义hooks

有了需求再去写功能,对大家来说都是小case,下面就来实现这三个需求。

  1. 打包参数

这个参数就是我们在命令行中输入的参数,也就是package.json scripts中写好的参数,例如

"scripts": {
  "build": "node scripts/build.js --mode production"
}

这里我们暂时不对参数做特殊的处理,仅仅取出来给打包命令用,如果你需要复杂的参数建议你使用commander.js,我这个项目现在还没涉及到复杂的打包参数。所以直接使用process.argv取出来就行了,关于process具体使用感兴趣可以自己看看文档

const scriptArgv = process.argv.slice(2) // 删除node scripts/build.js
const args = scriptArgv.join(' ') // 组装成正常格式
  1. 运行命令

这个对Node来说,这个很简单。通过child_process就能完美实现,我比较懒,直接用了tasksfile这个库。你也可以自己采用原生Node写,不过要注意异步的问题。

const { sh } = require('tasksfile')
// 同步执行打包命令
sh(`vue-cli-service build ${args}`, {
  silent: false
})
  1. 打包完成执行Hooks,这就是执行个方法。

下面是build.js中全部的代码。

const ora = require('ora')
const { sh } = require('tasksfile')
const { Notify } = require('./util') 
const builtHooks = require('./build-hooks')
const scriptArgv = process.argv.slice(2)
const args = scriptArgv.join(' ')

const spinner = ora(`building for ${process.env.NODE_ENV}...\n`)
spinner.start()
// real pack command
sh(`vue-cli-service build ${args}`, {
  silent: false
})
// build success
spinner.succeed("打包完成")
// notify
Notify.showNotify("打包完成", "即将进行下一步操作")
// delay 2s
setTimeout(() =>{
  // run hooks
  builtHooks()
},2000)

自定义hooks

这个发挥的空间有点大,大部分都是为了提升我们的效率的,我就写几个我自己认为常用一点的。

  1. 发布到服务器
  2. 本地预览
  3. 生成Zip文件
  4. 备份Zip文件到本地

在执行完build.js的前两步之后,接着就会运行hooks。因为hooks脚本存在的目的是为开发者最大可能性的节约时间。所以就设计为了非强制性的选项。

首先新建文件build-hooks.js

第一步设计hooks选项,同样的使用我们的老朋友inquirer

const builtHooks = () => {
  inquirer.prompt([
    {
      type: 'list',
      message: `检测到production环境打包完成,请选择下一步操作`,
      name: 'next',
      choices: [
        {
          name: '退出脚本',
          value: 0
        },
        {
          name: '发布到服务器',
          value: 1
        },
        {
          name: '本地预览',
          value: 2
        },
        {
          name: '生成Zip文件',
          value: 3
        },
        {
          name: '备份Zip文件到本地',
          value: 4
        }
      ]
    }
  ]).then(answers => {
    afterHooks.get(answers.next)()
  })
}

第二步设计不同选项对应的行为

const afterHooks = new Map([
  [0, () => {
    Log.logger('退出程序')
    process.exit(0)
  }],
  [1, () => {
    Log.logger('即将进行发布🎈')
    require('./deploy')
  }],
  [2, () => {
    Log.logger('开始本地预览💻')
    require('./server')
  }],
  [3, async () => {
    Log.logger('开始压缩zip文件👜')
    await FileUtil.zipDir()
  }],
  [4, async () => {
    Log.logger('开始备份Zip文件到本地📦')
    await Backup.doBackup()
  }]
])

是不是非常简单,简洁易懂哈哈哈。 下面我们来看一下具体的操作实现细节

发布到服务器🎈

这个在这里就不说了, 可以直接看这里

本地预览💻

简单问题了,无非是在本地搭建一个轻量级的Web服务器, 用第三方库实现的话就更简单了。代码如下server.js

const http = require('http')
const fs = require('fs')
const path = require('path')
const { Log } = require('./util')
const httpPort = 8088
const filePath = path.resolve(__dirname, '../dist/index.html')
const open = require('open')
// create current http server
http.createServer((req, res) => {
  Log.logger(req.url)
  try {
    const content = fs.readFileSync(filePath, 'utf-8')
    // deal resource
    if (req.url.indexOf('static') !== -1 || req.url.indexOf('vendor') !== -1 || req.url == "/favicon.ico") {
      const data = fs.readFileSync(path.resolve(__dirname, `../dist${req.url}`))
      return res.end(data)
    }
    // index.html
    res.setHeader('Content-Type','text/html;charset=utf-8')
    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8'
    })
  res.end(content)
  } catch (error) {
    Log.error('We cannot open "index.htm" file.')
  }
}).listen(httpPort, async () => {
  const location = `http://localhost:${httpPort}`
  Log.success(`Server listening on: ${location}, Open in the browser after 3 seconds`)
  // open default browser
  setTimeout(async ()=> {
    // 自动打开默认浏览器
    await open(location)
  }, 3000)
})

效果如下:

压缩zip文件👜

这个也没什么好说的😂, 就是通过Node压缩dist文件夹,在这里我用了zip-local来实现需求(你也可以使用Node实现, 但是我比较懒哈哈哈)代码如下

/**
   * 压缩文件夹
   * @param {*} dir 要压缩的文件夹 默认 ROOTPATH.distDir
   * @param {*} zipedPath 压缩之后 的zip存放路径
   */
  static async zipDir (dir = ROOTPATH.distDir, zipedPath = ROOTPATH.distZipPath) {
    try {
      if(fs.existsSync(zipedPath)) {
        Log.logger('zip已经存在, 即将删除压缩包')
        fs.unlinkSync(zipedPath)
      } else {
        Log.logger('即将开始压缩zip文件')
      }
      await zipper.sync.zip(dir).compress().save(zipedPath);
      Log.success('文件夹压缩成功')
    } catch (error) {
      Log.error(error)
      Log.error('压缩dist文件夹失败')
    }
  }

备份Zip文件到本地📦

压缩+备份(就是复制一份文件到指定文件夹)

我在深思熟虑之后还是决定把备份给加上,我反正是吃过没备份的亏😂, 这个选项会为我们在backups文件目录下生成一个新的以当前日期命名的压缩文件。

熟悉Node的文件系统的话这个对大家来说就很简单了。backup.js代码如下:

const path = require('path')
const { FileUtil, StringUtil, DateUtil, ROOTPATH, Log, Notify } = require('./util')

class Backup {
  /**
   * @author: etongfu
   * @description: 清空所有备份
   */
  static clearBackups () {

  }
  /**
   * @author: etongfu
   * @description: 执行备份
   * @param {type}  {*}
   * @returns:  {*}
   */
  static async doBackup () {
    try {
      // 文件名是当前日期 精确到秒
      let date = StringUtil.trim(DateUtil.getCurrentDate("YYYY-MM-DD hh:mm:ss"), 1)
      date = StringUtil.replaceAll(date,"-", "")
      date = StringUtil.replaceAll(date,":", "")
      let targetPath = path.resolve(__dirname, `../backups/${date}.backup.zip`)
      if(FileUtil.fileExist(targetPath)) {
        return Log.warning(`${targetPath}已存在,已放弃备份`)
      }
      // Zip File
      await FileUtil.zipDir(ROOTPATH.distDir, targetPath)
      Log.success(`本地备份完成, 文件:${targetPath}`)
      Notify.showNotify("本地备份", `本次备份完成, 文件地址:${targetPath}`)
    } catch (error) {
      Log.error(`备份文件失败:${error}`)
    }
  }
}
module.exports = Backup

效果如下:

总结

其实hooks完全不必和打包耦合在一起,完全可以拆出来使用,不过这个是下一个版本的需求哈哈哈😀。写完这些脚本再亲身使用了一段时间之后,真的感觉可以节省下来不少时间。免去了一些繁琐的手工操作。推荐大家尝试一下,毕竟自定义程度贼高,为团队造福。

示例代码

原文地址 如果觉得有用得话给个⭐吧