前言
cli即command line interface,其作用是处理以文本形式传递给程序的指令。中文译名也有命令行界面、字符用户界面等等。
A command-line interface (CLI) processes commands to a computer program in the form of lines of text.——Command-line interface-Wikipedia
在前端开发中,比较出名的cli主要有vue-cli、react的CRA等等。这些cli都非常棒,入门简单,配置标准,官方维护,可以快速开发一个前端应用。
但是,个人在自己项目中经常需要折腾一些东西:
- 希望一键上传静态资源到服务器
- 灵活地更改webpack配置(当然vue-cli支持配置扩展,CRA也有对应的解决方案)
- 等等...
虽然某些功能(比如上传静态资源到服务器)可以写脚本解决,但是每次新建一个项目都需要复制、粘贴、安装库,总归是麻烦的,所以可以把这些功能集成到我们的cli中。
本文记录了自己写的一个轻量cli的流程,包括调试、发布至npm等。由于配置是完全暴露的,所以取名为white-box-cli,适合对象主要为个人项目、小型前端团队、webpack折腾爱好者。
此cli生成的项目为ts+react技术栈,具体的功能点击链接就可以看了,就不在这占用篇幅。感觉方便可以star,感觉有新的功能点可以加入可以提issue讨论交流🎉。
接下来开始正文
前置知识:
- 对node有初步的了解
- 对webpack有初步的了解
- 对异步编程有初步的了解
本文主要阐述思路,具体的代码编写看个人技术栈情况。white-box-cli是用ts写的,通过tsc编译为js即可。本文不会涉及ts,为把重心放在思路上,假设使用js写。
文章结构
- bin设置
- cli项目结构及核心库
- 主程序编写
- cli init 功能
- cli dev 功能
- cli build 功能
- cli upload 功能
- 发布至npm
先创建项目,用npm init -y
初始化。这里我的项目名就叫white-box-cli了,下文也将用此名称。大家可以自行命名自己的cli,比如my-cli...
bin设置
首先在package.json中加入bin
字段配置
// package.json
{
...
bin:{
"white-box-cli": "bin/index.js"
}
}
接着创建目录bin
,新建文件index.js
,使得white-box-cli
命令指向index.js。当然你可以起任何自己想要的名字及目录,把执行文件放在bin目录下只是比较规范的做法。
在index.js先写入以下代码,再测试下能不能跑起来
#!/usr/bin/env node
console.log(process.argv);
其中,#!/usr/bin/env node
命令是必须加上去的,它用于指明这个脚本的解释器为node,否则无法执行js代码,详细可见此文:#!/usr/bin/env node 到底是什么?。
下一步使用npm link
,创建软链接到全局,这样我们就可以像调用全局包一样在任何地方调用此项目。如果npm包发布后不想使用全局安装调用,也可以直接使用npx。
这里我们先使用npm link及全局调用,方便调试。完成link后直接在命令行中输入white-box-cli param1 param2
(如果你在bin中叫my-cli就输入my-cli),可以看到如下输出,我们的参数param1与param2也在里面了。
[
'D:\\XXXX\\XXX\\node.exe', // 解释程序node路径
'C:\\Users\\XXXX\\AppData\\Roaming\\npm\\node_modules\\white-box-cli\\bin\\index.js', //脚本文件路径
'param1',
'param2'
]
如果成功的话就没问题了,接下来就开始写cli的代码。
cli项目结构及核心库
cli项目结构
- bin //上文提到的bin目录
- src // cli的主要源码目录
- cli-commands // 存放cli的命令
- init
- dev
- build
- upload
- constants // 常用变量
- util // 常用方法
- white-box-cli.js // cli主程序,在bin/index.js中require此文件
- template // init生成的模板项目。此处我直接放在cli里了,也可以像vue-cli一样去下载git仓库,就不需要template目录
核心库
- commander.js:方便开发命令行工具的库,可以解析各种命令
- Inquirer.js:方便命令行交互的库
- ssh2-sftp-client:sftp客户端,用于与ftp服务器建立连接,传输文件
- ora:各种命令行提示符
- chalk:带颜色的文字输出
主程序编写
主程序是cli的总入口,根据不同命令调用不同的模块。主要使用了commander库。此处先拿init功能作为示例。
#!/usr/bin/env node
import { program } from "commander";
import path from "path";
const version = require("../package.json").version;
program.version(version, "-v, --version");
program
.command("init <app-name>")
.description("使用 white-box-cli 初始化项目")
.action((name) => {
console.log('app-name: ', name);
});
//...
program.parse(process.argv); //解析输入命令
在bin/index.js中require主程序
#!/usr/bin/env node
require("../src/white-box-cli.js");
当输入white-box-cli init my-app
时,commander库会解析命令,触发对应的action方法。
输入:white-box-cli init my-app 输出:app-name: my-app
其他命令也大致如此,接下来只需编写各个功能的模块,如此可具有较好的维护性。
cli init 功能
init功能用于初始化项目,我们可以在主程序解析命令行,传参项目名,接下来生成模板项目即可。
// white-box-cli.js
// init action回调
program
.command("init <app-name>")
.description("使用 white-box-cli 初始化项目")
.action((name) => {
const { init } = require('./cli-commands/init');
init(name);
});
获取模板项目有两种方案,一种是拉git仓库,一种是内置在项目里。本文使用了后者。
// ./cli-commands/init/index.js
module.exports = function(name){
// process.cwd()获取工作区目录
const projectDir = path.join(process.cwd(), name); // 项目创建路径
const sourceDir = path.join(__dirname, "../../../template"); // 模板文件路径
// 使用mkdirp避免一级一级创建目录
mkdirp(projectDir).then(async (made) => {
if (made === undefined) {
tip.fail('"创建失败,存在同名目录"');
return;
}
// 解下来把模板文件拷贝到项目路径,做些适当的调整就ok了(比如修改项目package.json的name字段等等)
}
cli dev功能
dev功能为通过启动webpack-dev-server进入开发模式
// white-box-cli.js
...
program
.command("dev")
.description("进入开发模式")
.option("-p, --port [port]", "指定开发端口")
.action((cmd) => {
const optionObj = parseCmd(cmd); // 命令行option选项
const devWebpackPath = path.join(process.cwd(), "webpack.dev.js"); // 项目webpack.dev.js路径
const { dev } = require("./cli-commands/dev");
dev(optionObj, devWebpackPath);
});
...
// ./cli-commands/dev/index.js
import Webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
module.exports = function dev(cliOption, devWebpackPath) {
....some code
const config = require(devWebpackPath); // 获取devWebpack配置
const devServerPbj = Object.assign( // 整合各个配置,优先级为 默认 < dev.js配置 < cli输入
DEV_SERVER_DEFAULT,
config.devServer,
cliOption
);
config.devServer = devServerPbj;
// 端口
const port = devServerPbj["port"];
WebpackDevServer.addDevServerEntrypoints(config, devServerPbj); // 想要使用HMR必须做这一步
const compiler = Webpack(config);
compiler.hooks.done.tap("done", () => {
log();
tip.success(`构建成功! ${chalk.blueBright(
"http://localhost:" + port + "/"
)}`);
});
compiler.hooks.failed.tap('failed', (error) => {
tip.fail(error.message);
})
const server = new WebpackDevServer(compiler, devServerPbj);// 创建webpack dev server对象
server.listen(port, "127.0.0.1", () => {// 确定监听端口
console.log(`Starting server on http://localhost:${port}`);
});
}
以上是dev的核心部分,此部分需要干以下事情:
- 整合webpack.devServer,因为cli中会有webpack.dev.js配置、cli输入、cli默认数值造成冲突(比如端口)
- 支持HMR,即代码中的WebpackDevServer.addDevServerEntrypoints
- 创建编译器
- 创建webpack dev server对象,并启动服务
cli build功能
build功能用于打包代码
// white-box-cli.js
...some code
program
.command("build")
.description("打包应用")
.action(() => {
const prodWebpackPath = path.join(process.cwd(), "webpack.prod.js");
const { build } = require("./cli-commands/build");
build(prodWebpackPath);
});
...some code
打包的功能只需要传入webpack.prod.js配置,调用webpack
库即可
// ./cli-commands/build/index.js
import webpack from "webpack";
module.exports = function(path){
...some code
const config = require(path);
webpack(config, (err, stats) => {
if (err || stats.hasErrors()) {
console.log('打包出错')
...
}
console.log('打包完成')
...
})
...some code
}
upload功能
upload功能读取用户配置的upload.js,将静态资源传输到目标主机。可以借助ssh2-sftp-client
库,向FTP服务器传输文件。
在编写此功能时需要提醒使用者注意防止敏感信息(服务器用户名、密码、IP)的泄露!
// white-box-cli.js
...some code
program
.command("upload")
.description("上传至FTP服务器")
.option("-b, --build", "打包后再上传")
.action((cmd) => {
const optionObj = parseCmd(cmd);
const cwd = process.cwd();
const configPath = path.join(cwd, UPLOAD_NAME); // 开发者根目录下含FTP服务器信息的文件
const { upload } = require("./cli-commands/upload");
upload(optionObj, configPath);
});
...some code
./cli-commands/upload/index.js
import Client from "ssh2-sftp-client";
...
module.exports = function(optionObj, uploadConfigPath){
const sftp = new Client();
...some code
tip.loading("正在连接服务器...");
await sftp.connect(uploadConfig.targetServer);
tip.success("成功连接至服务器");
tip.success("开始上传...");
sftp.on("upload", (info) => {
console.log(` 上传成功: ${chalk.blueBright(info.source)}`);
});
await sftp.uploadDir(sourcePath, targetPath); //ssh2-sftp-client提供了上传目录的api,非常方便
tip.success(`${sourcePath} 内容成功上传至 ${targetPath}`);
sftp.end();
...some code
}
以上是upload的核心部分,省略了其他部分内容,比如保证敏感信息不泄露的交互、upload配置的验证、环境选择等等。
结语
以上便是编写一个轻量cli的主要思路,其实难度也不是特别大,多数时间也是基本花在查文档、看文档、想交互、了解相关库等等,但是写下来后确实对异步编程、webpack、node有了进一步的理解。
自己的cli完成后可以起一个自己喜欢的名字发布到npm上,以后就可以随时用了,至于如何发布到npm网上已经有很多很好的教程了,此处也不再赘述。
以上内容有需纠正之处欢迎大佬指正,也欢迎大家评论cli中能加什么常用功能。
GitHub: white-box-cli