Node.js用途很广,常用来开发服务、桌面应用等被开发者熟知,Node.js还有另外一个非常实用的场景 - 命令行应用(后文统称CLI)。本文将介绍CLI的开发流程、常用的功能,并以meet-cli为例实战演练,从零开始一步步开发一个可以在生产中使用(read world)的CLI工具。meet-cli现已开源,读者也可以直接下载查看。
Why CLI?
在深入开发之前,我们先了解几个问题。
CLI是什么?
Command Line Interface,顾名思义是一种通过命令行来交互的工具或者说应用。SPA应用中常用的如vue-cli, angular-cli, node.js开发搭建express-generator,orm框架sequelize-cli,还有我们最常用的webpack,npm等。他们是web开发者的辅助工具,旨在减少低级重复劳动,专注业务提高开发效率,规范develop workflow。
举比较典型的angular-cli 为例,读者可以查看它的npm说明文档,它可以让angular开发者快速创建最佳实践的angular应用,快速启动,快速创建component、directive、pipe、service、module等,用过的都说很好用,现在各个框架都有配套CLI。
CLI的根据不同业务场景有不同的功能,但万变不离其宗,本质都是通过命令行交互的方式在本地电脑运行代码,执行一些任务。
CLI有什么好处?
我们可以从工作中总结繁杂、有规律可循、或者简单重复劳动的工作用CLI来完成,只需一些命令,快速完成简单基础劳动。以下是我对现有工作中的可以用CLI工具实现的总结举例:
- 快速生成应用模板,如vue-cli等根据与开发者的一些交互式问答生成应用框架
- 创建module模板文件,如angular-cli,创建component,module;sequelize-cli 创建与mysql表映射的model等
- 服务启动,如ng serve
- eslint,代码校验,如vue,angular,基本都具备此功能
- 自动化测试 如vue,angular,基本都具备此功能
- 编译build,如vue,angular,基本都具备此功能
- *编译分析,利用webpack插件进行分析
- *git操作
- *生成的代码上传CDN
- *还可以是小工具用途的功能,如http请求api、图片压缩、生成雪碧图等等,只要你想做的都能做
总体而言就是一些快捷的操作替代人工重复劳动,提升开发效率。
与npm s的对比
npm s也可以实现开发工作流,通过在package.json 中的 s对象上配置相关npm 命令,执行相关js来达到相同的目的;
但是cli工具与npm s相比有什么优势呢?
- npm s是某个具体项目的,只能在该项目内使用,cli可以是全局安装的,多个项目使用;
- 使用npm s 在业务工程里面嵌入工作流,耦合太高;使用cli 可以让业务代码工作流相关代码剥离,业务代码专注业务
- cli工具可以不断迭代开发,演进,沉淀。
meet-cli 针对项目实际需求,贴合工作实际需求应运而生。接下来看看meet-cli的一些功能;
MEET-CLI
本文基于
美柚的web开发的工作主要是产品内hybrid应用的 h5部分,以及广告、营销互动类的h5、往往互相独立,工作中发现有以下一些问题:
- 每个h5的创建一些列目录和文件,每个h5都有公共的基础代码
- 每次新功能都需要配置相关的npm watch和build命令,我们需要一个创建模板的功能
- 各个工程之间都有一套自己的build代码,上传CDN的代码,各不相同,开发人员垮项目开发上手慢
- 每次创建新工程build的代码都需要重复做一次(或者通过复制粘贴的办法),我们需要一个公共的上传功能
基于工作中的问题,额外我再加了点小功能,meet-cli诞生了,下面展示下他的一些功能;
1、meet -help查看功能列表
一个cli工具都具有查看帮助的功能,图中可以看出meet-cli具备创建module、编译、发布(git提交与资源上传cdn)、单独指定文件上传cdn功能、分析生成文件功能
接下来快速演示上述几个主要功能
2、meet init
meet init 会在工程根目录下生成meet.config.js 文件,用以配置meet工具的使用
const path = require('path');module.exports = { // module 生成的目标目录 modulePath: path.resolve('public'), // module template 目录 moduleTemplatePath: path.resolve('meet/templates/module'), // project git url gitUrl:'http://gitlab.meiyou.com/advertise/ad-activity.git', // module build npm command npmBuildCommand:'npm run release:', // upload assets config upload:{ // CDN Server server:'alioss',// 阿里OSS - 'alioss', 七牛云 - 'qn' // alioss server config config:{ accessKeyId: "LTAIovxxxx0", accessKeySecret: "5xkXYxxxxf6wlzD8", bucket: "adstatic", region: "oss-cn-beijing", srcDir: path.resolve('public/assets'),// 要上传的dist文件夹 ignoreDir: false, deduplication: true, prefix: "ad-activity.meiyou.com", } }, // is publish after build? autoPublish: true, // 测试提交文字};3、meet new [module]
快速创建h5模块目录和基础文件,基础css,html,js,必要依赖,(还可以进行相关express路由配置,指定模块编译配置)
4、meet build [module]
build模块,生成代码,用webpack-bundle-analyzer 进行分析,可视化显示资源占比,可以一目了然的查看代码体积上是否存在问题,对于性能优化是一个好处
5、meet publish (git操作+upload CDN)
还有meet analysis 、meet upload 等功能都是上述功能的局部。meet upload 可以指定上传某个路径下的资源,作为上传工具单独而存在。
目前实用功能比较少,后面还会增加一些功能
上面这一波操作很酷, 那是怎么实现呢,我们的核心内容现在才登场,如何从零开始开发一个CLI呢?
从零开发CLI
我们将从零开始开发meet-cli来实战演示一个完整的cli的开发过程;(注:为了不影响我电脑的meet-cli,我将后文的cli demo命名为mei-cli ,大家见谅!)
基础模块
1、创建npm模块
执行命令,创建npm模块
npm init -y得到
上面这步骤大家都很熟悉;
2、bin 入口文件
在package.json 文件增加bin的对象
{ "name": "cli-demo", "version": "1.0.0", "de ion": "", "main": "index.js", "bin":{ "mei": "./bin/index.js" }, " s": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC"}js 中首行设置如下代码:
#!/usr/bin/env node上面这句话是用来告诉操作系统用node来运行这个文件
可以在js中console.log('hello mei')
3、全局安装
执行npm install -g ,将mei-cli安装到全局。so,最简单的cli诞生了,任意找个位置输入mei 命令,就执行了你的 ./bin/index.js文件,console.log了一句话,‘hello mei’。可以将mei-cli模块发布到npm上,这样就可以给社区使用了。如何发布npm模块,这里有一篇我的博客可以查看。
node.js 知识
node.js 具有filesystem模块,可以让开发者对文件进行读写、创建、删除等操作;
process、child_process、path、以及commonjs模块化知识等
基础掌握了,我们来认识一些常用组件
- commander CLI常用开发框架
- chalk 终端文字加颜色js组件
- clui spinners、sparklines、progress bars图样显示组件
- shelljs node.js运行shell命令组件
- blessed-contrib 命令行可视化组件
- lnquirer 命令行交互信息收集组件
- figlet FIGlet is a program that creates large characters out of ordinary screen characters
此外,还有游大神开发的meet-ali-oss上传模块
上述这些组件足以让你开发酷炫的cli,如果不够,这里还有50个组件 任你挑选;
我们要完成的cli主体结构图
文件结构要划分合理,index.js是主入口文件, commands专门放主要的命令功能逻辑,根据命令模块划分,比较细的功能实现可以抽成组件放在lib文件夹中,剩余的配置,以及模板等放meet文件夹中
主入口文件
#!/usr/bin/env nodeconst path = require('path');const fs = require('fs');const program = require('commander');const gmodule = require('../packages/commands/module');// const serve = require('../packages/commands/serve');const question = require('../packages/commands/question');const build = require('../packages/commands/build');const publish = require('../packages/commands/publish');const upload = require('../packages/commands/upload');const analysis = require('../packages/lib/analysis');const initial = require('../packages/commands/initial');let config = {};// 配置文件如果存在则读取if(fs.existsSync(path.resolve('meet.config.js'))){ config = require(path.resolve('meet.config.js'));}program .version('1.0.0','-v, --version') .command('init') .de ion('initialize your meet config') .action(initial);program .command('new [module]') .de ion('generator a new module') .action(function(module){ gmodule(config,module) });program .command('build [module]') .de ion('git build specify module and assets upload to CDN!') .action(function(module){ build(config,module) });program .command('publish') .de ion('upload assets to CDN and git commit && push') .action(function(){ publish(config) });program .command('upload') .de ion('upload your build dist files to CDN server') .action(function () { upload(config.upload); });program .command('analysis') .de ion('analysis dist files size and percent') .action(function () { analysis(config.upload.config.srcDir); });program .command('question') .de ion('analysis dist files size and percent') .action(function(){ question() });program.parse(process.argv);主入口文件利用commander监测终端输入命令时,触发相应的模块运行。commander会自动生成mei -help的命令,该命令用来显示支持的命令。命令命名尽可能短、见名知意,不支持的命令有相关提示,运行错误有正确的提示和响应,是cli的最佳实践。
这里在主入口文件中读取了meet.config.js,把相应的的配置信息传递给对应模块。如把CDN上传的配置信息传给上传模块,把
用了commander发现这cli也没有什么技术含量。
meet new [module] 触发运行的js
const path = require('path');const fs = require('fs');const chalk = require('chalk');const inquirer = require('inquirer');// 要拷贝的目标所在路径let templatePath;// 目标文件夹根路径let targetRootPath;function deleteFolderRecursive (path) { if (fs.existsSync(path)) { fs.readdirSync(path).forEach(function(file, index){ var curPath = path + "/" + file; if (fs.lstatSync(curPath).isDirectory()) { // recurse deleteFolderRecursive(curPath); } else { // delete file fs.un Sync(curPath); } }); fs.rmdirSync(path); }};function copyTemplates(name){ function readAndCopyFile(parentPath,tempPath){ let files = fs.readdirSync(parentPath); files.forEach((file)=>{ let curPath = `${parentPath}/${file}`; let stat = fs.statSync(curPath); let filePath = `${targetRootPath}/${tempPath}/${file}`; if(stat.isDirectory()){ fs.mkdirSync(filePath); readAndCopyFile(`${parentPath}/${file}`,`${tempPath}/${file}`); } else{ const contents = fs.readFileSync(curPath,'utf8'); fs.writeFileSync(filePath,contents, 'utf8'); } }); } readAndCopyFile(templatePath,name);}function generateModule(meetConfig,name){ templatePath = typeof meetConfig.moduleTemplatePath !== 'undefined'? path.resolve(meetConfig.moduleTemplatePath):path.join(__dirname,'..','meet/module'); targetRootPath = meetConfig.modulePath; let targetDir = path.join(targetRootPath,name); if(fs.existsSync(targetDir)){ // 如果已存在改模块,提问开发者是否覆盖该模块 inquirer.prompt([ { name:'module-overwrite', type:'confirm', message:`Module named ${name} is already existed, are you sure to overwrite?`, validate: function(input){ if(input.lowerCase !== 'y' && input.lowerCase !== 'n' ){ return 'Please input y/n !' } else{ return true; } } } ]) .then(answers=>{ console.log('answers',answers); // 如果确定覆盖 if(answers['module-overwrite']){ // 删除文件夹 deleteFolderRecursive(targetDir); console.log(chalk.yellow(`Module already existed , removing!`)); //创建新模块文件夹 fs.mkdirSync(targetDir); copyTemplates(name); console.log(chalk.green(`Generate new module "${name}" finished!`)); } }) .catch(err=>{ console.log(chalk.red(err)); }) } else{ //创建新模块文件夹 fs.mkdirSync(targetDir); copyTemplates(name); console.log(chalk.green(`Generate new module "${name}" finished!`)); }}module.exports = generateModule;主要逻辑是根据用户配置的templatePath 与targetRootPath,遍历templatePath路径下的所有文件夹与文件,copy文件到targetRootPath,如果已经存在则提示是否覆盖。
上面说明templatePath 是一个灵活的路径,模板可以在mei-cli中,也可以在任何一个位置,只要指定了正确的路径,就能实现相同的结果。此功能可以使用任何web框架,任何web框架都可以准备他的module模板,它的作用就是把模板文件copy到指定位置,也就是一键生成模板。
看到这里,发现确实没什么技术含量。
meet publish
const chalk = require('chalk');const inquirer = require('inquirer');const shellHelper = require('../lib/shellHelper');const upload = require('./upload');let config = { autoPublish: false};function gitCommit(){ // 发布,提示输入commit 信息 inquirer.prompt([ { name:'message', type:'input', message:`Enter your publish message
` } ]) .then(answers=>{ let message = answers.message; shellHelper.series([ 'git pull', 'git add .', `git commit -m "${message}"`, 'git push', ], function(err){ if(err){ console.log(chalk.red(err)); process.exit(0); } console.log(chalk.green('Git push finished!')); process.exit(0); }); }) .catch(err=>{ console.log(chalk.red(err)); });}function publish(meetConfig){ .assign(config,meetConfig); upload(config.upload) .then(res=>{ console.log(chalk.green('Upload To CDN finished!')); if(config.autoPublish === true){ gitCommit(); } }) .catch(err=>{ console.log(chalk.red(err)); })}module.exports = publish;meet publish 原理是利用node.js child_process顺序执行多个git命令进行git提交,利用meet-ali-oss进行资源文件上传。
剩下的还有build、initial、upload,analysis等功能,都是类似的,不再贴代码进行一一阐述了,读者可以下载meet-cli进行查看
git clone https://github.com/linweiwei123/meet-cli.git除此之外,还可以放飞眼界,在你的cli中加入更多功能,比如进行与服务器进行通信(用axios http模块请求)、实时通信、分享CLI命令界面等等(有些很鸡肋),只要是符合实际需要的,大胆设想,大胆实现,让你的CLI无比强大。
功能逻辑见仁见智,开发者可以发挥个人的智慧开发适合自己的CLI。
CLI开发中还有一些地方需要开发者注意。
注意事项
- .gitignore,.npmignore 跟npm模块一样CLI也需要注意提交文件内容限制
- package.json 中注意dependencies与devDependencies的区别
- 良好的文档描述
到此meet-cli就开发完成了,还可以发布到npm上给社区使用(适合的话)。
未来计划
todolist
- 增加图片处理命令 meet comp [path],用于压缩,生成webp
- 生成gitignore文件命令
- 生成eslint配置
- 与multipages-generator合并,形成完整的h5开发工作流
- 类似vue-cli 通过网页操作替代cli交互
总结
本文主要介绍了CLI的入门,常见的组件以及用法,常见的CLI功能。CLI是web开发的辅助工具,旨在提高web工作效率,希望本文能给大家的工作带来一些启迪与帮助!
原文发布时间:2018-07-03
本文来源掘金如需转载请紧急联系作者
继续阅读与本文标签相同的文章
-
前端的水平线,错误处理和调试
2026-06-02栏目: 教程
-
webpack结合reactjs、vuejs项目中图片处理
2026-06-02栏目: 教程
-
jQuery 前后端分离项目总结
2026-06-02栏目: 教程
-
JavaScript函数式编程之为什么要函数式编程(非严谨技术层面的扯淡)
2026-06-02栏目: 教程
-
44 道 JavaScript 难题(JavaScript Puzzlers!)
2026-06-02栏目: 教程
