需求背景

公司有8个前端项目,共使用四个公共包,基础包(baqi)、医生公共包(baqi-doctor)、患者公共包(baqi-patient)、聊天包(baqi-chat)。
开发分支上使用file引入公共包(file引入的方式可以查看这一篇),到测试和生产上需要使用版本号来引入这几个公共包。这就需要一个方便的publish公共包的工具,写了一个node脚本publish.js

实现

主流程

  1. 计算下一个版本号
  2. 设置npm代理为淘宝
  3. 设置baqi包、baqi-xxx包中package.json的version号
  4. publish baqi包和baqi-xxx包
  5. 替换前端8个项目引入baqi包和baqi-xxx包的版本号
  6. 判断是否安装cnpm,若没安装cnpm,则安装
  7. 新开4个进程,执行cnpm sync命令,同步新发布的包到淘宝代理
  8. 新开8个进程,进入8个前端项目执行npm install

需要注意的是

  1. 可以使用http的get请求获取npm包的信息,url为http://registry.npm.taobao.org/baqi/latest,其中baqi为包名
  2. 需要设置taobao的npm代理,不然执行npm命令进程偶尔会卡住
  3. 设置taobao代理后无法publish,在publish时指定registry为npmjs官方url
  4. publish和进入各项目执行npm install时可以开多个进程执行,提高效率,使用await Promise.all(promiseArr)保证执行顺序
  5. 如在linux下使用,路径会错误,可以使用path.sep()来代替路径的斜杠
  6. 使用process.cwd()来代替__dirname,减少因路径带来的问题

package.json增加执行脚本

"scripts": {
   
  "pb": "node bin/publish.js"
},

使用方法

npm run pb <version>

version可选,若输入version,按照指定的版本号publish,若不输入version,自动计算版本号

代码

主流程代码

#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const {
    getRequest, execCmdSync, execCmd } = require('./util')

const cwd = process.cwd()
const modules = ['baqi', 'baqi-chat', 'baqi-doctor', 'baqi-patient']
const projects = [
  'doctor_web',
  'patient_web',
  'heart-doctor',
  'heart-patient',
  'tumour-doctor',
  'tumour-patient',
  'mini-hospital',
  'mh-doctor'
]
let version = process.argv[2]

main()

async function main () {
   
  if (version === undefined) {
   
    console.log('查找版本号!')
    version = await computeVersion()
    console.log('版本号为:' + version)
  }
  console.log('设置npm代理')
  execCmdSync('npm config set registry https://registry.npm.taobao.org')
  console.log('替换baqi包、baqi-xxx包的version号\n将baqi-xxx包中引入baqi的方式替换成version引入')
  for (let i = 0; i < modules.length; i++) {
   
    replaceVersionAndImport(modules[i])
  }
  console.log('publish baqi包和baqi-xxx包')
  const publishArr = []
  for (let i = 0; i < modules.length; i++) {
   
    console.log('正在publish ' + modules[i])
    const cdPath = path.join(cwd, '../' + modules[i])
    publishArr.push(execCmd('cd ' + cdPath + ' && npm publish --registry http://registry.npmjs.org'))
  }
  await Promise.all(publishArr) // 不catch,报错就中断
  console.log('将baqi-xxx包中引入baqi的方式替换成file引入')
  for (let i = 1; i < modules.length; i++) {
   
    replaceImportToFile(modules[i])
  }
  console.log('替换所有项目引入baqi包和baqi-xxx包的版本号')
  for (let i = 0; i < projects.length; i++) {
   
    replaceImport(projects[i])
  }
  console.log('判断是否安装cnpm')
  checkCnpm()
  console.log('新开4个进程,同步cnpm')
  let cnpmSyncArr = []
  for (let i = 0; i < modules.length; i++) {
   
    cnpmSyncArr.push(execCmd('cnpm sync ' + modules[i]))
  }
  await Promise.all(cnpmSyncArr) // 不catch,报错就中断
  console.log('新开8个进程,进入各项目执行npm install')
  const npmInstallArr = []
  for (let i = 0; i < projects.length; i++) {
   
    const lockFilePath = path.join(cwd, '../' + projects[i] + '/package-lock.json')
    if (fs.existsSync(lockFilePath)) {
   
      console.log('删除文件' + lockFilePath)
      fs.unlinkSync(lockFilePath)
    }
    const cdPath = path.join(cwd, '../' + projects[i])
    npmInstallArr.push(execCmd('cd ' + cdPath + ' && npm install'))
  }
  try {
   
    await Promise.all(npmInstallArr)
  } catch (e) {
   
    console.error(e)
  }
  console.log('----------搞定了----------')
}

其他函数代码

function checkCnpm () {
   
  try {
   
    const out = execCmdSync('where cnpm')
    if (out.startsWith('信息')) {
   
      execCmdSync('npm install -g cnpm')
    }
  } catch (e) {
   
    execCmdSync('npm install -g cnpm')
  }
}

async function computeVersion () {
   
  let maxVersion = ''
  for (let i = 0; i < modules.length; i++) {
   
    const moduleInfo = await getRequest('http://registry.npm.taobao.org/' + modules[i] + '/latest')
    const packageVersion = moduleInfo.version
    if (packageVersion > maxVersion) {
   
      maxVersion = packageVersion
    }
  }
  let lastVersion = parseInt(maxVersion.substring(maxVersion.lastIndexOf('.') + 1)) + 1
  maxVersion = maxVersion.substring(0, maxVersion.lastIndexOf('.') + 1) + lastVersion
  return maxVersion
}

function replaceImportToFile (moduleName) {
   
  let filePath = path.join(cwd, '../' + moduleName + '/package.json')
  let fileStr = readPackageJson(filePath)
  fileStr = fileStr.replace(/("baqi"\s*:\s*").*?(")/g, '$1file:../baqi$2')
  writePackageJson(filePath, fileStr)
}

function replaceImport (projectName) {
   
  let filePath = path.join(cwd, '../' + projectName + '/package.json')
  let fileStr = readPackageJson(filePath)
  fileStr = fileStr.replace(/("baqi.*\s*:\s*").*?(")/g, '$1' + version + '$2')
  writePackageJson(filePath, fileStr)
}

function replaceVersionAndImport (moduleName) {
   
  let filePath = path.join(cwd, '../' + moduleName + '/package.json')
  let fileStr = readPackageJson(filePath)
  fileStr = fileStr.replace(/("version": ").*?(")/, '$1' + version + '$2')
  fileStr = fileStr.replace(/("baqi.*\s*:\s*").*?(")/, '$1' + version + '$2')
  writePackageJson(filePath, fileStr)
}

function writePackageJson (packageJsonPath, str) {
   
  fs.writeFileSync(packageJsonPath, str, 'utf8')
}

function readPackageJson (packageJsonPath) {
   
  const fileStr = fs.readFileSync(packageJsonPath, 'utf8')
  return fileStr
}

util.js

const http = require('http')
const execSync = require('child_process').execSync
const exec = require('child_process').exec

function execCmdSync (cmdStr) {
   
  console.log('执行:' + cmdStr)
  const out = execSync(cmdStr, {
    encoding: 'utf8' })
  return out
}
function execCmd (cmdStr) {
   
  return new Promise((resolve,rejcect) => {
   
    console.log('执行:' + cmdStr)
    exec(cmdStr, {
    encoding: 'utf8' }, function (error, stdout, stderr) {
   
      error ? rejcect(error) : resolve(stdout)
    })
  })
}
function getRequest (url) {
   
  return new Promise(resolve => {
   
    console.log('请求URL:' + url)
    http.get(url, res => {
   
      let html = ''
      res.on('data', function (data) {
   
        html += data
      })
      res.on('end', function () {
   
        console.log('返回:' + html)
        try {
   
          resolve(JSON.parse(html))
        } catch (e) {
   
          resolve(html)
        }
      })
    })
  })
}
module.exports = {
   
  exec,
  execCmd,
  getRequest
}

结语

实现了一键发布4个公共包并且修改引用的功能,但有时候只改动某一个地方确要发布4个公共包,下一步计划增加只发布某一个公共包的功能