├── imgs ├── 初始化.png ├── 测试环境部署.png ├── 线上环境部署.png ├── 部署命令.png └── 安装deploy.png ├── .gitignore ├── package.json ├── lib ├── init.js └── deploy.js ├── bin └── index.js ├── README.md └── utils └── index.js /imgs/初始化.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dadaiwei/fe-deploy-cli/HEAD/imgs/初始化.png -------------------------------------------------------------------------------- /imgs/测试环境部署.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dadaiwei/fe-deploy-cli/HEAD/imgs/测试环境部署.png -------------------------------------------------------------------------------- /imgs/线上环境部署.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dadaiwei/fe-deploy-cli/HEAD/imgs/线上环境部署.png -------------------------------------------------------------------------------- /imgs/部署命令.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dadaiwei/fe-deploy-cli/HEAD/imgs/部署命令.png -------------------------------------------------------------------------------- /imgs/安装deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dadaiwei/fe-deploy-cli/HEAD/imgs/安装deploy.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # production 7 | /dist 8 | 9 | /deploy/config 10 | 11 | .vscode/ 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fe-deploy-cli", 3 | "version": "1.0.2", 4 | "description": "A cli tool for web deploy.", 5 | "bin": { 6 | "deploy": "./bin/index.js" 7 | }, 8 | "keywords": [ 9 | "web", 10 | "node-ssh", 11 | "deploy" 12 | ], 13 | "author": "dadaiwei", 14 | "license": "ISC", 15 | "engines": { 16 | "node": ">=9.0.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/dadaiwei/fe-deploy-cli" 21 | }, 22 | "dependencies": { 23 | "archiver": "^3.1.1", 24 | "chalk": "^3.0.0", 25 | "commander": "^4.1.0", 26 | "download-git-repo": "^3.0.2", 27 | "inquirer": "^7.0.3", 28 | "node-ssh": "^7.0.0", 29 | "ora": "^4.0.3", 30 | "semver": "^7.1.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const download = require('download-git-repo'); 6 | const ora = require('ora'); 7 | const { successLog, infoLog, errorLog, underlineLog } = require('../utils/index'); 8 | let tmp = 'deploy'; 9 | const deployPath = path.join(process.cwd(), './deploy'); 10 | const deployConfigPath = `${deployPath}/deploy.config.js`; 11 | const deployGit = 'dadaiwei/fe-deploy-cli-template'; 12 | 13 | // 检查部署目录及部署配置文件是否存在 14 | const checkDeployExists = () => { 15 | if (fs.existsSync(deployPath) && fs.existsSync(deployConfigPath)) { 16 | infoLog('deploy目录下的deploy.config.js配置文件已经存在,请勿重新下载'); 17 | process.exit(1); 18 | return; 19 | } 20 | downloadAndGenerate(deployGit); 21 | }; 22 | 23 | // 下载部署脚本配置 24 | const downloadAndGenerate = templateUrl => { 25 | const spinner = ora('开始生成部署模板'); 26 | spinner.start(); 27 | download(templateUrl, tmp, { clone: false }, err => { 28 | if (err) { 29 | console.log(); 30 | errorLog(err); 31 | process.exit(1); 32 | } 33 | spinner.stop(); 34 | successLog('模板下载成功,模板位置:deploy/deploy.config.js'); 35 | infoLog('请配置deploy目录下的deploy.config.js配置文件'); 36 | console.log('注意:请删除不必要的环境配置(如只需线上环境,请删除dev测试环境配置)'); 37 | process.exit(0); 38 | }); 39 | }; 40 | 41 | module.exports = () => { 42 | checkDeployExists(); 43 | }; 44 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const inquirer = require('inquirer'); 6 | const packageJson = require('../package.json'); 7 | const deployPath = path.join(process.cwd(), './deploy'); 8 | const deployConfigPath = `${deployPath}/deploy.config.js`; 9 | const { checkNodeVersion, checkDeployConfig, underlineLog } = require('../utils/index'); 10 | 11 | const version = packageJson.version; 12 | const requiredNodeVersion = packageJson.engines.node; 13 | 14 | const versionOptions = ['-V', '--version']; 15 | 16 | checkNodeVersion(requiredNodeVersion, 'fe-deploy'); 17 | 18 | const program = require('commander'); 19 | 20 | program 21 | .version(version) 22 | .command('init') 23 | .description('初始化部署相关配置') 24 | .action(() => { 25 | require('../lib/init')(); 26 | }); 27 | 28 | const agrs = process.argv.slice(2); 29 | 30 | const firstArg = agrs[0]; 31 | 32 | // 非version选项且有配置文件时,进入部署流程 33 | if (!versionOptions.includes(firstArg) && fs.existsSync(deployConfigPath)) { 34 | deploy(); 35 | } 36 | 37 | // 无参数时默认输出help信息 38 | if (!firstArg) { 39 | program.outputHelp(); 40 | } 41 | 42 | // 部署流程 43 | function deploy() { 44 | // 检测部署配置是否合理 45 | const deployConfigs = checkDeployConfig(deployConfigPath); 46 | if (!deployConfigs) { 47 | process.exit(1); 48 | } 49 | 50 | // 注册部署命令 51 | deployConfigs.forEach(config => { 52 | const { command, projectName, name } = config; 53 | program 54 | .command(`${command}`) 55 | .description(`${underlineLog(projectName)}项目${underlineLog(name)}部署`) 56 | .action(() => { 57 | inquirer.prompt([ 58 | { 59 | type: 'confirm', 60 | message: `${underlineLog(projectName)}项目是否部署到${underlineLog(name)}?`, 61 | name: 'sure' 62 | } 63 | ]).then(answers => { 64 | const { sure } = answers; 65 | if (!sure) { 66 | process.exit(1); 67 | } 68 | if (sure) { 69 | const deploy = require('../lib/deploy'); 70 | deploy(config); 71 | } 72 | }); 73 | 74 | }); 75 | }); 76 | } 77 | 78 | // 解析参数 79 | program.parse(process.argv); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fe-deploy-cli 2 | 前端轻量化部署脚手架,支持测试、线上等多环境部署,支持环境配置扩展,配置好后仅需一条命令即可完成整个部署流程。 3 | 4 | ## git地址: 5 | https://github.com/dadaiwei/fe-deploy-cli 6 | 7 | ## npm地址: 8 | https://www.npmjs.com/package/fe-deploy-cli 9 | 10 | ## 博客 11 | https://juejin.im/post/5e1bfbadf265da3e3077005e 12 | 13 | ## 适用对象 14 | 目前还在采用手工部署又期望快速实现轻量化部署的小团队或者个人项目,毕竟像阿里这种大公司都有完善的前端部署平台。 15 | 16 | ## 使用指南 17 | https://github.com/dadaiwei/fe-deploy-cli/blob/master/README.md 18 | 19 | ## 前提条件 20 | 能通过ssh连上服务器即可 21 | 22 | ## 安装 23 | 全局安装fe-deploy-cli 24 | ``` 25 | npm i fe-deploy-cli -g 26 | ``` 27 | 查看版本,表示安装成功。 28 | 29 | ![安装fe-deploy-cli](./imgs/安装deploy.png) 30 | 31 | ## 使用 32 | ### 1.初始化部署模板 33 | ``` 34 | deploy init 35 | ``` 36 | 37 | ![初始化](./imgs/初始化.png) 38 | 39 | ### 2.配置部署环境 40 | 部署配置文件位于deploy文件夹下的`deploy.config.js`, 41 | 一般包含`dev`(测试环境)和`prod`(线上环境)两个配置,再有多余的环境配置形式与之类似,只有一个环境的可以删除另一个多余的配置(比如只有`prod`线上环境,请删除`dev`测试环境配置)。 42 | 43 | 具体配置信息请参考配置文件注释: 44 | ``` 45 | module.exports = { 46 | privateKey: '', // 本地私钥地址,位置一般在C:/Users/xxx/.ssh/id_rsa,非必填,有私钥则配置 47 | passphrase: '', // 本地私钥密码,非必填,有私钥则配置 48 | projectName: 'hivue', // 项目名称 49 | dev: { // 测试环境 50 | name: '测试环境', 51 | script: "npm run build-dev", // 测试环境打包脚本 52 | host: '10.240.176.99', // 开发服务器地址 53 | port: 22, // ssh port,一般默认22 54 | username: 'root', // 登录服务器用户名 55 | password: '123456', // 登录服务器密码 56 | distPath: 'dist', // 本地打包dist目录 57 | webDir: '/var/www/html/dev/hivue', // // 测试环境服务器地址 58 | }, 59 | prod: { // 线上环境 60 | name: '线上环境', 61 | script: "npm run build", // 线上环境打包脚本 62 | host: '10.240.176.99', // 开发服务器地址 63 | port: 22, // ssh port,一般默认22 64 | username: 'root', // 登录服务器用户名 65 | password: '123456', // 登录服务器密码 66 | distPath: 'dist', // 本地打包dist目录 67 | webDir: '/var/www/html/prod/hivue' // 线上环境web目录 68 | } 69 | // 再还有多余的环境按照这个格式写即可 70 | } 71 | ``` 72 | 73 | ### 3.查看部署命令 74 | 配置好`deploy.config.js`,运行 75 | ``` 76 | deploy --help 77 | ``` 78 | 查看部署命令 79 | 80 | ![部署命令](./imgs/部署命令.png) 81 | 82 | ### 4.测试环境部署 83 | 测试环境部署采用的时`dev`的配置 84 | ``` 85 | deploy dev 86 | ``` 87 | 先有一个确认,确认后进入部署流程,完成6步操作后,部署成功!!! 88 | 89 | ![测试环境部署](./imgs/测试环境部署.png) 90 | 91 | ### 5.线上环境部署 92 | 线上环境部署采用的时`prod`的配置 93 | ``` 94 | deploy prod 95 | ``` 96 | 部署流程和测试环境相同: 97 | 98 | ![线上环境部署](./imgs/线上环境部署.png) 99 | 100 | 感谢大家支持,欢迎star,O(∩_∩)O。 101 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const chalk = require('chalk'); 5 | const semver = require('semver'); 6 | 7 | const DEPLOY_SCHEMA = { 8 | name: '', 9 | script: "", 10 | host: '', 11 | port: 22, 12 | username: '', 13 | password: '', 14 | webDir: '' 15 | }; 16 | 17 | const PRIVATE_KEY_DEPLOY_SCHEMA = { 18 | name: '', 19 | script: "", 20 | host: '', 21 | port: 22, 22 | webDir: '' 23 | }; 24 | 25 | // 开始部署日志 26 | function startLog(...content) { 27 | console.log(chalk.magenta(...content)); 28 | } 29 | 30 | // 信息日志 31 | function infoLog(...content) { 32 | console.log(chalk.blue(...content)); 33 | } 34 | 35 | // 成功日志 36 | function successLog(...content) { 37 | console.log(chalk.green(...content)); 38 | } 39 | 40 | // 错误日志 41 | function errorLog(...content) { 42 | console.log(chalk.red(...content)); 43 | } 44 | 45 | // 下划线重点输出 46 | function underlineLog(content) { 47 | return chalk.blue.underline.bold(`${content}`); 48 | } 49 | 50 | // 检查node版本是否符合特定范围 51 | function checkNodeVersion(wanted, id) { 52 | if (!semver.satisfies(process.version, wanted)) { 53 | errorLog(`You ar using Node ${process.version}, but this version of ${id} requres Node ${wanted} .\nPlease upgrage your Node version.`); 54 | process.exit(1); 55 | } 56 | } 57 | 58 | // 检查配置是否符合特定schema 59 | function checkConfigScheme(configKey, configObj, privateKey) { 60 | let deploySchemaKeys = null; 61 | const configKeys = Object.keys(configObj); 62 | const neededKeys = []; 63 | const unConfigedKeys = []; 64 | let configValid = true; 65 | if (privateKey) { 66 | deploySchemaKeys = Object.keys(PRIVATE_KEY_DEPLOY_SCHEMA); 67 | } else { 68 | deploySchemaKeys = Object.keys(DEPLOY_SCHEMA); 69 | } 70 | for (let key of deploySchemaKeys) { 71 | if (!configKeys.includes(key)) { 72 | neededKeys.push(key); 73 | } 74 | if (configObj[key] === '') { 75 | unConfigedKeys.push(key); 76 | } 77 | } 78 | if (neededKeys.length > 0) { 79 | errorLog(`${configKey}缺少${neededKeys.join(',')}配置,请检查配置`); 80 | configValid = false; 81 | } 82 | if (unConfigedKeys.length > 0) { 83 | errorLog(`${configKey}中的${unConfigedKeys.join(', ')}暂未配置,请设置该配置项`); 84 | configValid = false; 85 | } 86 | return configValid; 87 | } 88 | 89 | // 检查deploy配置是否合理 90 | function checkDeployConfig(deployConfigPath) { 91 | if (fs.existsSync(deployConfigPath)) { 92 | const config = require(deployConfigPath); 93 | const { privateKey, passphrase, projectName } = config; 94 | const keys = Object.keys(config); 95 | const configs = []; 96 | for (let key of keys) { 97 | if (config[key] instanceof Object) { 98 | if (!checkConfigScheme(key, config[key], privateKey)) { 99 | return false; 100 | } 101 | config[key].command = key; 102 | config[key].privateKey = privateKey; 103 | config[key].passphrase = passphrase; 104 | config[key].projectName = projectName; 105 | configs.push(config[key]); 106 | } 107 | } 108 | return configs; 109 | } 110 | infoLog(`缺少部署相关的配置,请运行${underlineLog('deploy init')}下载部署配置`); 111 | return false; 112 | } 113 | 114 | module.exports = { 115 | startLog, 116 | infoLog, 117 | successLog, 118 | errorLog, 119 | underlineLog, 120 | checkNodeVersion, 121 | checkDeployConfig 122 | }; 123 | -------------------------------------------------------------------------------- /lib/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const childProcess = require('child_process'); 6 | const ora = require('ora'); 7 | const node_ssh = require('node-ssh'); 8 | const archiver = require('archiver'); 9 | const { successLog, errorLog, underlineLog } = require('../utils/index'); 10 | 11 | const projectDir = process.cwd(); 12 | 13 | let ssh = new node_ssh(); // 生成ssh实例 14 | 15 | // 部署流程入口 16 | async function deploy(config) { 17 | const { script, webDir, distPath, projectName, name } = config; 18 | try { 19 | execBuild(script); 20 | await startZip(distPath); 21 | await connectSSH(config); 22 | await uploadFile(webDir); 23 | await unzipFile(webDir); 24 | await deleteLocalZip(); 25 | successLog(`\n 恭喜您,${underlineLog(projectName)}项目${underlineLog(name)}部署成功了^_^\n`); 26 | process.exit(0); 27 | } catch (err) { 28 | errorLog(` 部署失败 ${err}`); 29 | process.exit(1); 30 | } 31 | } 32 | 33 | // 第一步,执行打包脚本 34 | function execBuild(script) { 35 | try { 36 | console.log(`\n(1)${script}`); 37 | const spinner = ora('正在打包中'); 38 | spinner.start(); 39 | console.log(); 40 | childProcess.execSync(script, { cwd: projectDir }); 41 | spinner.stop(); 42 | successLog(' 打包成功'); 43 | } catch (err) { 44 | errorLog(err); 45 | process.exit(1); 46 | } 47 | } 48 | 49 | // 第二部,打包zip 50 | function startZip(distPath) { 51 | return new Promise((resolve, reject) => { 52 | distPath = path.resolve(projectDir, distPath); 53 | console.log('(2)打包成zip'); 54 | const archive = archiver('zip', { 55 | zlib: { level: 9 }, 56 | }).on('error', err => { 57 | throw err; 58 | }); 59 | const output = fs.createWriteStream(`${projectDir}/dist.zip`); 60 | output.on('close', err => { 61 | if (err) { 62 | errorLog(` 关闭archiver异常 ${err}`); 63 | reject(err); 64 | process.exit(1); 65 | } 66 | successLog(' zip打包成功'); 67 | resolve(); 68 | }); 69 | archive.pipe(output); 70 | archive.directory(distPath, '/'); 71 | archive.finalize(); 72 | }); 73 | } 74 | 75 | // 第三步,连接SSH 76 | async function connectSSH(config) { 77 | const { host, port, username, password, privateKey, passphrase, distPath } = config; 78 | const sshConfig = { 79 | host, 80 | port, 81 | username, 82 | password, 83 | privateKey, 84 | passphrase 85 | }; 86 | try { 87 | console.log(`(3)连接${underlineLog(host)}`); 88 | await ssh.connect(sshConfig); 89 | successLog(' SSH连接成功'); 90 | } catch (err) { 91 | errorLog(` 连接失败 ${err}`); 92 | process.exit(1); 93 | } 94 | } 95 | 96 | // 第四部,上传zip包 97 | async function uploadFile(webDir) { 98 | try { 99 | console.log(`(4)上传zip至目录${underlineLog(webDir)}`); 100 | await ssh.putFile(`${projectDir}/dist.zip`, `${webDir}/dist.zip`); 101 | successLog(' zip包上传成功'); 102 | } catch (err) { 103 | errorLog(` zip包上传失败 ${err}`); 104 | process.exit(1); 105 | } 106 | } 107 | 108 | 109 | // 运行命令 110 | async function runCommand(command, webDir) { 111 | await ssh.execCommand(command, { cwd: webDir }); 112 | } 113 | 114 | // 第五步,解压zip包 115 | async function unzipFile(webDir) { 116 | try { 117 | console.log('(5)开始解压zip包'); 118 | await runCommand(`cd ${webDir}`, webDir); 119 | await runCommand('unzip -o dist.zip && rm -f dist.zip', webDir); 120 | successLog(' zip包解压成功'); 121 | } catch (err) { 122 | errorLog(` zip包解压失败 ${err}`); 123 | process.exit(1); 124 | } 125 | } 126 | 127 | // 第六步,删除本地dist.zip包 128 | async function deleteLocalZip() { 129 | return new Promise((resolve, reject) => { 130 | console.log('(6)开始删除本地zip包'); 131 | fs.unlink(`${projectDir}/dist.zip`, err => { 132 | if (err) { 133 | errorLog(` 本地zip包删除失败 ${err}`, err); 134 | reject(err); 135 | process.exit(1); 136 | } 137 | successLog(' 本地zip包删除成功\n'); 138 | resolve(); 139 | }); 140 | }); 141 | } 142 | 143 | 144 | module.exports = deploy; --------------------------------------------------------------------------------