从0-1实现文件下载CLI工具(2):https://developer.aliyun.com/article/1395069?spm=a2c6h.13148508.setting.29.50c84f0e0ugg2G
参数转换传递
下面是defaultCommand
的逻辑,只需要将相关参数处理后透传给定义的download
方法即可,option
不支持 number 所以需要对数字字符串做一些转换
export default function defaultCommand(url: string, options: CLIOptions) { const { filename, location, timeout, proxy, override } = options downloadByUrl(url, { maxRedirects: +location, timeout: +timeout, proxy, override, filename }) .error((err) => { console.log('error url:', url) console.log('error msg:', redStr(err.message)) process.exit() }) .end((filepath) => { console.log('file save:', underlineStr(yellowStr(filepath))) }) }
下面是这块使用演示
下载进度展示
小文件还能无感等待,大文件咱就得整个进度条来显示了,方遍了解进度。
在npm
中检索,除了推荐了老牌库 progress,还看到了1个 cli-progress
咱们这里就用后者(最近更新时间看着近一些)
最简单的示例与结果如下
import cliProgress from 'cli-progress' const progressBar = new cliProgress.SingleBar({}) downloadByUrl(url) .progress((cur, rec, sum) => { // 初始化 if (progressBar.getProgress() === 0) { progressBar.start(sum, 0) } // 更新进度 progressBar.update(rec) // 结束 if (rec === sum) { progressBar.stop() } })
展示内容过于简单,可以自定义一下显示,展示文件大小和下载速度,参考文档,结合内置的一些值设定初始化如下
const format = '[{bar}] {percentage}% | ETA: {eta}s | {rec}/{sum} | Speed {speed}' const progressBar = new cliProgress.SingleBar( { format, barsize: 16 }, cliProgress.Presets.shades_classic )
紧接着是start
时设置sum
和speed
默认值
// 初始化的时候计算总大小 progressBar.start(sum, 0, { sum: formatSize(sum) }) // 过程中更新进度 progressBar.update(rec, { rec: formatSize(rec), speed: speed(cur) })
formatSize
方法实现如下(来源于谷歌推荐代码),短小精悍,将B转换为其它单位展示。
export function formatSize( size: number, pointLength?: number, units?: string[] ) { let unit units = units || ['B', 'K', 'M', 'G', 'TB'] // eslint-disable-next-line no-cond-assign while ((unit = units.shift()) && size > 1024) { size /= 1024 } return ( (unit === 'B' ? size : size.toFixed(pointLength === undefined ? 2 : pointLength)) + unit! ) } formatSize(1234) // 1.21K formatSize(10240) // 10.00K
计算下载速度
speed
方法实现如下
- 使用闭包
- 一段时间计算一次速度(1000ms / 时间周期 * 周期内下载量B)
/** * @param cycle 多久算一次(ms) */ function getSpeedCalculator(cycle = 500) { let startTime = 0 let endTime = 0 let speed = 'N/A' // 记录速度 let sum = 0 // 计算之前收到了多少B return (chunk: number) => { sum += chunk if (startTime === 0) { startTime = Date.now() } endTime = Date.now() // 计算一次 if (endTime - startTime >= cycle) { speed = `${formatSize((1000 / (endTime - startTime)) * sum)}/s` startTime = Date.now() sum = 0 } return speed } } // 获取到计算速度的方法 const speed = getSpeedCalculator() setTimeout(speed, 200, 4000) setTimeout(speed, 300, 5000) setTimeout(speed, 1000, 10240) setTimeout(() => { console.log(speed(0)) // 23.49K/s }, 1100)
优化后的下载效果如下
持久化配置存储
像proxy
,timeout
参数不希望每次都设置,就需要将这些配置存起来,下次直接读取。
通常的CLI工具都会在/Users/$username/.xxx
目录中存放自己的配置文件,即HOME
目录下。
同理我们可以开辟一个文件存放.efstrc
const configPath = path.join( process.env.HOME || process.env.USERPROFILE || process.cwd(), '.efstrc' )
读写配置实现如下,利用Array.prototype.reduce
方法在遍历的过程中做存取值操作
- 支持多级的key的读写
- 兼容异常场景,返回空或空对象
function getCLIConfig(key = '') { try { const value = JSON.parse(fs.readFileSync(configPath, 'utf-8')) return !key ? value : key.split('.').reduce((pre, k) => { return pre?.[key] }, value) } catch { return !key ? {} : '' } } function setCLIConfig(key: string, value: string) { if (!key) { return } const nowCfg = getCLIConfig() // 支持传入多级的key const keys = key.split('.') // 遍历设置的所有都配置都与nowCfg直接或间接的进行了引用关联 keys.reduce((pre, k, i) => { // 赋值 if (i === keys.length - 1) { pre[k] = value } else if (!(pre[k] instanceof Object)) { pre[k] = {} } return pre[k] }, nowCfg) // 输出到文件 fs.writeFileSync(configPath, JSON.stringify(nowCfg, null, 2)) } setCLIConfig('proxy', 'http://127.0.0.1:7890') setCLIConfig('timeout', '2000') setCLIConfig('github.name', 'ATQQ') setCLIConfig('github.info.url', 'https://github.com/ATQQ')
再添加一个移除配置的方法,与设置的的方法类似只是使用delete
操作符删除相关的key
function delCLIConfig(key: string) { if (!key) { return } const nowCfg = getCLIConfig() const keys = key.split('.') keys.reduce((pre, k, i) => { // 移除 if (i === keys.length - 1) { delete pre[k] } return pre[k] instanceof Object ? pre[k] : {} }, nowCfg) fs.writeFileSync(configPath, JSON.stringify(nowCfg, null, 2)) } delCLIConfig('timeout') delCLIConfig('github.info.name') delCLIConfig('github.name')
有了这3个方法支撑就可以封装成一个config
指令用于配置的CRUD
config指令实现
先是定义
program .command('config <type> <key> [value]') .alias('c') .description('crud config <type> in [del,get,set]') .action(configCommand)
configCommand
封装实现
export type ConfigType = 'set' | 'get' | 'del' function defaultCommand( type: ConfigType, key: string, value: string ) { if (type === 'set') { setCLIConfig(key, value) } if (type === 'del') { delCLIConfig(key) } if (type === 'get') { console.log(getCLIConfig(key) || '') } }
使用演示如下
config 指令这部分逻辑完全可以分离成一个通用的 commander
模块,在需要的CLI里直接注册即可,简化后大概如下
import { Command } from 'commander' const program = new Command() registerConfigCommand(program,'.efstrc')
最后
笔者对这个工具的想法还有很多,后续先把功能🐴出来再写续集,本文就先到这里。
内容有不妥的之处,还请评论区斧正。
CLI完整源码见GitHub