我们经常使用各种 Node Cli 工具(也就是Node命令行),例如 Hexo,vite/vue press,asar,等等等等。今天我们研究一下,cli 是怎么写出来的呢?
Node Cli,简单来说,就是用终端命令,调用 JS,产生相应的行为/输出。所以肯定还是用Node写的。
这方面我们来看看 Microsoft 的 asar 工具,他的命令行是怎么写的。这个是它有关于 cli 的源码,我已经加上了注释:
// 这一行应该是mac/linux环境自动加上的
#!/usr/bin/env node
// 导入package.json
var packageJSON = require('../package.json')
// 这行不是很懂,貌似不大重要
var splitVersion = function (version) { return version.split('.').map(function (part) { return Number(part) }) }
// 看变量名应该是node版本
var requiredNodeVersion = splitVersion(packageJSON.engines.node.slice(2))
// 这个貌似也是 node 版本
var actualNodeVersion = splitVersion(process.versions.node)
// 这应该是判断两个node版本的语句
if (actualNodeVersion[0] < requiredNodeVersion[0] || (actualNodeVersion[0] === requiredNodeVersion[0] && actualNodeVersion[1] < requiredNodeVersion[1])) {
console.error('CANNOT RUN WITH NODE ' + process.versions.node)
console.error('asar requires Node ' + packageJSON.engines.node + '.')
process.exit(1)
}
// Not consts so that this file can load in Node < 4.0
// 上面这段注释,是自带的,大概翻译为无法在版本小于4.0的node中使用那个文件,下面这两句是导入其他模块
var asar = require('../lib/asar')
// 这个commander貌似在下面都有出现,应该是cli的核心模块,语法貌似基于promise
var program = require('commander')
// 这应该是获取package中的asar版本
program.version('v' + packageJSON.version)
.description('Manipulate asar archive files')
// 这个根据 asar的cli输出,是规定一个命令
program.command('pack <dir> <output>')
// alias应该是简写
.alias('p')
// 命令介绍
.description('create asar archive')
// 其它一些选项
.option('--ordering <file path>', 'path to a text file for ordering contents')
.option('--unpack <expression>', 'do not pack files matching glob <expression>')
.option('--unpack-dir <expression>', 'do not pack dirs matching glob <expression> or starting with literal <expression>')
.option('--exclude-hidden', 'exclude hidden files')
// 应该是对输入行为的反馈,function里的参数是cli输入的参数
.action(function (dir, output, options) {
// 下面这些应该是asar的行为,跟cli没有太大关系
options = {
unpack: options.unpack,
unpackDir: options.unpackDir,
ordering: options.ordering,
version: options.sv,
arch: options.sa,
builddir: options.sb,
dot: !options.excludeHidden
}
asar.createPackageWithOptions(dir, output, options, function (error) {
if (error) {
console.error(error.stack)
process.exit(1)
}
})
})
// 再次规定一个命令
program.command('list <archive>')
// 大概是简写
.alias('l')
// 下面以前说过的就不说了
.description('list files of asar archive')
.option('-i, --is-pack', 'each file in the asar is pack or unpack')
.action(function (archive, options) {
options = {
isPack: options.isPack
}
var files = asar.listPackage(archive, options)
for (var i in files) {
console.log(files[i])
}
})
program.command('extract-file <archive> <filename>')
.alias('ef')
.description('extract one file from archive')
.action(function (archive, filename) {
require('fs').writeFileSync(require('path').basename(filename),
asar.extractFile(archive, filename))
})
program.command('extract <archive> <dest>')
.alias('e')
.description('extract archive')
.action(function (archive, dest) {
asar.extractAll(archive, dest)
})
// 如果用户输入的不是上面规定的任何一个,就运行下面这个action。
program.command('*')
// cmd参数应该是返回的用户输入
.action(function (cmd) {
console.log('asar: \'%s\' is not an asar command. See \'asar --help\'.', cmd)
})
// 这个我就不知道啥意思了,不过删掉他cli就无法运行
program.parse(process.argv)
// 不带参数默认为help
if (program.args.length === 0) {
program.help()
}
大家有了注释,但凡有点node/js功底,应该都能看个大差不差。
根据上面这个代码块,我们可以推测出来,这个commander应该是整个cli的核心。
当我们改掉名字、模块之后,我们发现它还是提示asar,所以我们知道了cli的名称来源于其引用commander的js文件名。还有就是,cli要想工作,就要有一个package.json。例如这个:
{
"name": "yue-css",
"version": "1.0.0",
"description": "generate yue.css html.",
// main不一定要指向cli的js,不过建议指向cli
"main": "yue-css.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
// 这句没啥大必要
"g": "node script.js"
},
"author": "Adkinsm",
"license": "MIT",
"dependencies": {
"commander": "5.0.0",
"markdown-it": "^12.0.6"
}
}
有了上面这个基础我们可以写出别的cli,例如这个yue.css的html生成器:
// yue-css.js,cli主文件
// 并不是非得var,const也可以的
const cli = require("commander");
// 引用主模块,注意花括号为部分引用
const { yue_css } = require("./script.js");
const packageJSON = require("./package.json");
// 尖括号里的是参数名,到时候如果用户漏输入每个参数,commander会提示参数名,所以通常用一些具有语义化的参数名
cli
.command("generate <MarkdownFilePath> <MarkdownFileName> <OutMarkdownFileName>")
// 整个cli只有这个命令和version
.alias("g")
.description("build Markdown to yue.css Html.")
.action(function (filepath,filename,outfile) {
// 三个参数,分别对应script中的三个参数,即markdown文件目录,markdown文件名,编译后的html文件名,html会输出到markdown的目录
// CommonJS 模块
yue_css(filepath,filename,outfile);
});
cli
.version("v" + packageJSON.version)
.description("build Markdown to yue.css Html.");
cli.parse(process.argv);
if (cli.args.length === 0) {
cli.help();
}
由于引用了别的模块(而且是自己写的,注意./),所以我们需要进行函数导出:
// script.js,功能主文件,指定cli不同参数的行为
// 注意exports,这是node的commonjs使用的模块导出语句
// 声明导出很像声明变量
exports.yue_css = function (path,file,outfile) {
// 三个参数,分别对应cli中的三个参数,即markdown文件目录,markdown文件名,编译后的html文件名,html会输出到markdown的目录
// 导入node自带的fs文件模块
let fs = require("fs");
// 涉及Markdown,这里采用比较规范严谨的markdown-it
// 注意这个连续声明使用逗号而非分号
let MarkdownIt = require("markdown-it"),
md = new MarkdownIt();//这里才是分号,标志声明结束
// 采用同步读取Api,将文件内容保存为变量
let mdData = fs.readFileSync(path + file, "utf-8");
// 虽然用了try/catch,不过貌似无法捕获异常,可能是因为我没有抛出err
try {
// 这些console其实完全是为了装13~~(当然也有让用户知道程序运行成功了以及让给我知道bug出在哪一步的功能)~~
console.log("Compiling File...");
// markdown-it的render函数,将编译后的html存在变量中
let html_marked = md.render(mdData);
//这是规定html的内容,把html后的markdown和yue.css的内容直接加进html里。
let data = `<style type="text/css">body{margin: 0;padding: 0.4em 1em 6em;background: #f9f9f7;}.yue {max-width: 650px;margin: 0 auto;}.yue{font:400 18px/1.62 -apple-system,BlinkMacSystemFont,"Segoe UI","Droid Sans","Helvetica Neue","PingFang SC","Hiragino Sans GB","Droid Sans Fallback","Microsoft YaHei",sans-serif;color:#444443}.yue ::-moz-selection{background-color:rgba(0,0,0,.2)}.yue ::selection{background-color:rgba(0,0,0,.2)}.yue h1,.yue h2,.yue h3,.yue h4,.yue h5,.yue h6{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Droid Sans","Helvetica Neue","PingFang SC","Hiragino Sans GB","Droid Sans Fallback","Microsoft YaHei",sans-serif;color:#222223}.yue h1{font-size:1.8em;margin:.67em 0}.yue>h1{margin-top:0;font-size:2em}.yue h2{font-size:1.5em;margin:.83em 0}.yue h3{font-size:1.17em;margin:1em 0}.yue h4,.yue h5,.yue h6{font-size:1em;margin:1.6em 0 1em 0}.yue h6{font-weight:500}.yue p{margin-top:0;margin-bottom:1.24em}.yue a{color:#111;word-wrap:break-word;-webkit-text-decoration-color:rgba(0,0,0,.4);text-decoration-color:rgba(0,0,0,.4)}.yue a:hover{color:#555;-webkit-text-decoration-color:rgba(0,0,0,.6);text-decoration-color:rgba(0,0,0,.6)}.yue h1 a,.yue h2 a,.yue h3 a{text-decoration:none}.yue b,.yue strong{font-weight:700;color:#222223}.yue em,.yue i{font-style:italic;color:#222223}.yue img{max-width:100%;height:auto;margin:.2em 0}.yue a img{border:none}.yue figure{position:relative;clear:both;outline:0;margin:10px 0 30px;padding:0;min-height:100px}.yue figure img{display:block;max-width:100%;margin:auto auto 4px;box-sizing:border-box}.yue figure figcaption{position:relative;width:100%;text-align:center;left:0;margin-top:10px;font-weight:400;font-size:14px;color:#666665}.yue figure figcaption a{text-decoration:none;color:#666665}.yue hr{display:block;width:14%;margin:40px auto 34px;border:0 none;border-top:3px solid #dededc}.yue blockquote{margin:0 0 1.64em 0;border-left:3px solid #dadada;padding-left:12px;color:#666664}.yue blockquote a{color:#666664}.yue ol,.yue ul{margin:0 0 24px 6px;padding-left:16px}.yue ul{list-style-type:square}.yue ol{list-style-type:decimal}.yue li{margin-bottom:.2em}.yue li ol,.yue li ul{margin-top:0;margin-bottom:0;margin-left:14px}.yue li ul{list-style-type:disc}.yue li ul ul{list-style-type:circle}.yue li p{margin:.4em 0 .6em}.yue .unstyled{list-style-type:none;margin:0;padding:0}.yue code,.yue tt{color:grey;font-size:.96em;background-color:#f9f9f7;padding:1px 2px;border:1px solid #eee;border-radius:3px;font-family:Menlo,Monaco,Consolas,"Courier New",monospace;word-wrap:break-word}.yue pre{margin:1.64em 0;padding:7px;border:none;border-left:3px solid #dadada;padding-left:10px;overflow:auto;line-height:1.5;font-size:.96em;font-family:Menlo,Monaco,Consolas,"Courier New",monospace;color:#4c4c4c;background-color:#f9f9f7}.yue pre code,.yue pre tt{color:#4c4c4c;border:none;background:0 0;padding:0}.yue table{width:100%;max-width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:1.5em;font-size:.96em;box-sizing:border-box}.yue td,.yue th{text-align:left;padding:4px 8px 4px 10px;border:1px solid #dadada}.yue td{vertical-align:top}.yue tr:nth-child(even){background-color:#efefee}.yue iframe{display:block;max-width:100%;margin-bottom:30px}.yue figure iframe{margin:auto}.yue table pre{margin:0;padding:0;border:none;background:0 0}@media (min-width:1100px){.yue blockquote{margin-left:-24px;padding-left:20px;border-width:4px}.yue blockquote blockquote{margin-left:0}}</style><link rel="stylesheet" href="./yue.css"><body><div class="yue">${html_marked}</div></body>`;
console.log("Done.");
console.log("Writing File...");
// 同步写出文件api
fs.writeFileSync(path + "/" + outfile, data, "utf-8");
// 提示用户文件存在了哪里
console.log("Writed File At " + path + outfile);
console.log("Done.");
} catch (e) {
// catch没啥用
console.log("[INFO] 出现 Error :");
console.log("[ERROR] " + e);
return;
}
};
如想体验实际效果,运行:
npm i yue-gen -g
使用方法:
如果你有一个Markdown文件,它的路径是 “D:\yue\readme.md”,你想把它编译成HTML,运行这个命令:
yue-css g D:\yue\ readme.md index.html
然后,yue.css 会将readme.md中的内容转译为HTML,再写入index.html。这个index.html会创建在 D:\yue\。
在转译的同时,yue.css中的样式文件会一并转移到index.html中的一个style标签,所以你只需安心写Markdown,关于HTML的一切交给yue-css。
I'm so cute. Please give me money.
- 本文链接:https://blog.adkimsm.asia/post/92f390ed68ab/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。