本地构建方案

2020/12/28

不知道你们遇到过这样的困扰没有,当你在本地开开心心开发完代码后,提交到你们公司的代码管理仓库,然后开始构建时,你会遇到各种 jenkins 问题,构建超时、构建失败等(如果没有这方面的困扰请忽略此篇)。有很多原因,例如 node 版本不一致,服务器网路问题等等。无论什么原因,总之是构建不成功,此时如果能在本地构建,然后把本地构建产物运行到服务器中该多好!

# 思路

  1. 本地 build 生成 dist 产物
  2. dist 打成 tar
  3. 随代码 push 到仓库
  4. 构建,解压 tar 包。需要运维在jenkins 构建脚本中添加解压 tar 包代码

# 代码

# WebBuilder.js

commonjs

const fs = require('fs')
const path = require('path')
const crypto = require("crypto")
const shell = require('shelljs')
const tar = require('tar')

class WebBuilder {
  constructor ({ output, projectPath, commands = [], async = false }) {
    // 这是打包输出的路径(绝对路径)
    this.output = output
    // 这是项目所在的路径(绝对路径)
    this.projectPath = projectPath
    /*
    * 指令 Array<{
    *   command<string>: npm run build:xx,
    *   desc<string>: 项目描述,
    *   project<string>: 项目名称
    * }>
    * */
    this.commands = commands.filter(o => o)
    // 是否异步构建
    this.async = async

    !WebBuilder.hasDir(this.output) && WebBuilder.makeDir(this.output)
  }

  async build () {
    const successCb = async ({ from, to, commandObj }) => {
      const { desc, project } = commandObj
      WebBuilder.log('SUCCESS', desc)

      // 构建成功后创建tar
      const tarName = await this.createTar(from, project)

      const inner = () => {
        WebBuilder.makeDir(to)
        WebBuilder.copyFile(path.resolve(from, `./${tarName}`), path.resolve(to, `./${tarName}`))
        WebBuilder.log('TAR_SUCCESS', desc)
      }

      if (WebBuilder.hasDir(to)) {
        const toFiles = fs.readdirSync(to)
        if (toFiles && toFiles[0] === tarName) {
          WebBuilder.log('TAR_HOLD', desc)
        } else {
          WebBuilder.cleanDir(to)
          inner()
        }
      } else {
        inner()
      }
    }

    const errorCb = (desc, stderr) => {
      WebBuilder.log('ERROR', desc)
      WebBuilder.log(stderr)
    }

    const handler = async (params) => {
      const { code, stderr, desc } = params
      if (code === 0) {
        await successCb(params)
      } else {
        errorCb(desc, stderr)
      }
    }

    for (const item of this.commands) {
      const { project, command, desc } = item

      const from = path.join(this.projectPath, `${project}/dist`)
      const to = path.join(this.output, `${project}`)

      WebBuilder.cleanDir(from)

      WebBuilder.log('START', desc)
      if (this.async) {
        // 异步构建
        shell.exec(command, { silent: false }, function (code, stdout, stderr) {
          handler({
            commandObj: item,
            code,
            stdout,
            stderr,
            from,
            to
          })
        })
      } else {
        // 同步构建(默认)
        const child = shell.exec(command)
        await handler({
          commandObj: item,
          code: child.code,
          stdout: child.stdout,
          stderr: child.stderr,
          from,
          to
        })
      }
    }
  }

  /* 生成tar包 */
  createTar (from, project) {
    const tarName = this.getHashDigest(path.join(from, '.',  project), 16)
    return new Promise((resolve, reject) => {
      tar
        .c({
            gzip: true,
            cwd: from,
          },
          ['.']
        )
        .pipe(fs.createWriteStream(`${from}/${tarName}`))
        .on('finish', () => {
          resolve(tarName)
        })
    })
  }

  /* 生成 hash 值 */
  createHash (key) {
    const algorithm = "sha256"
    const hash =  crypto.createHash(algorithm).update(key).digest("hex")
    return hash
  }

  /**
   * @function 根据文件内容生成 hash
   * @param pathStr {string} 文件所在目录
   * @param hashDigestLength {number} hash 位数
   * */
  getHashDigest (pathStr, hashDigestLength) {
    const hashNames = []

    const inner = (pathStr) => {
      const filenames = fs.readdirSync(pathStr)
      for (const fileName of filenames) {
        const fileDir = path.join(pathStr, fileName)
        const stats = fs.statSync(fileDir)
        if (stats.isDirectory()) {
          inner(fileDir)
        }
        if (stats.isFile()) {
          const fileCont = fs.readFileSync(fileDir, 'utf-8')
          hashNames.push(this.createHash(fileCont))
        }
      }
    }
    inner(pathStr)

    hashNames.sort()
    const fullHash = this.createHash(hashNames.join(''))
    return `${fullHash.slice(0, hashDigestLength)}.tar.gz`
  }

  static hasDir (pathStr) {
    return fs.existsSync(pathStr)
  }

  static makeDir (pathStr) {
    fs.mkdirSync(pathStr)
  }

  static cleanDir (pathStr) {
    this.hasDir(pathStr) && fs.rmdirSync(pathStr, {
      recursive: true
    })
  }

  static resolve (dir) {
    return path.join(__dirname, '.', dir)
  }

  static log (type, desc) {
    const state = {
      'START': '开始构建',
      'SUCCESS': '构建成功',
      'ERROR': '构建失败',
      'TAR_SUCCESS': 'tar 创建成功',
      'TAR_HOLD': 'tar 名称未改变,无需创建',
    }
    if (type === 'START') {
      console.log('------------ 分 ------------ 隔 ------------ 线 ------------')
    }
    console.log(`[${desc}] ${state[type]}`)
  }

  static copyFile (from, to) {
    const child = shell.exec(`cp-cli ${from} ${to}`) // 同步复制
    if (child.code !== 0) {
      console.log('[复制错误]', child.stderr)
    }
  }
}

module.exports = WebBuilder

# 构建脚本 build-pkg.js

const { Command } = require('commander');
const program = new Command();
const path = require("path");
const WebBuilder = require('./WebBuilder')

// 前端工程
const PACKAGE_CONFIG = [
  {
    command: 'pp',
    desc: '部位划分',
    project: 'part-project',
  },
  {
    command: 'cp',
    desc: '施工工期',
    project: 'construction-period',
  }
]

/* 完善 program 命令 */
program
  .option('-a, --all',  '全部构建')
  .option('-s, --async',  '异步构建')

PACKAGE_CONFIG.forEach(item => {
  program.option(`--${item.command}`, item.desc)
})

program.parse();

const options = program.opts();

function getCommands () {
  if (options.all) {
    return PACKAGE_CONFIG
  }
  return Object.keys(options).map(key => PACKAGE_CONFIG.find(item => item.command === key))
}

const commands = getCommands().filter(o => o).map(item => {
  item.command = `npm run build:${item.command}`
  return item
})

const builder = new WebBuilder({
  output: path.join(__dirname, './packages'),
  projectPath: path.join(__dirname, '../packages'),
  commands,
  async: !!options.async
})
builder.build()

# package.json

运行 npm run build 执行脚本构建,传参如下:

npm run build -- -a            // 全部构建
npm run build -- -a -s         // 全部异步构建 
npm run build -- --pp          // part-project 项目构建
npm run build -- --cp          // construction-period 项目构建

npm run build -- -h            // 查看所有可用指令参数
{
  "name": "root",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "build": "node ./build/build-pkg.js",
    "start:pp": "lerna run --scope part-project dev --stream",
    "build:pp": "lerna run --scope part-project build --stream",
    "start:cp": "lerna run --scope construction-period dev --stream",
    "build:cp": "lerna run --scope construction-period build --stream",
  },
  "devDependencies": {
    "commander": "^11.0.0",
    "cp-cli": "^2.0.0",
    "shelljs": "^0.8.5",
    "tar": "^6.2.0",
  }
}

Last Updated: 2023/11/24 下午2:21:57