本地构建方案
liz-q 2020/12/28
不知道你们遇到过这样的困扰没有,当你在本地开开心心开发完代码后,提交到你们公司的代码管理仓库,然后开始构建时,你会遇到各种 jenkins
问题,构建超时、构建失败等(如果没有这方面的困扰请忽略此篇)。有很多原因,例如 node
版本不一致,服务器网路问题等等。无论什么原因,总之是构建不成功,此时如果能在本地构建,然后把本地构建产物运行到服务器中该多好!
# 思路
- 本地
build
生成dist
产物 - 将
dist
打成tar
包 - 随代码
push
到仓库 - 构建,解压
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",
}
}