创建Node.js命令行项目
备注:package.json要一样,即所有依赖的版本要一致,github中的文档也查阅对应版本
初始化项目
- 新建一个空项目
- 进入这个项目,运行
yarn init -y
初始化项目,它会创建一个package.json文件,将版本号改为0.0.1 - 创建一个index.js在里面写代码,并运行它
console.log('Hello!Node.js')
node index
就可以运行- 接下来要做一个命令行程序,类似:
Commander.js
- 我们需要借助一个库Commander.js,安装这个库:
yarn add commander
- 如何使用这个库呢? CRM学习法,直接复制文档中可运行的代码
const program = require('commander');
const pkg = require('./package.json')
// `.version()`方法可以设置版本
program
.version(pkg.version)
program
.option('-d, --debug', 'output extra debugging')
.option('-s, --small', 'small pizza size')
.option('-p, --pizza-type <type>', 'flavour of pizza');
program.parse(process.argv);
当我们试着运行node index -h
,结果显示:
-h 是哪里来的?
- CRM学习法,接着我们去尝试修改代码
const program = require('commander');
const pkg = require('./package.json')
program
.version(pkg.version)
program
.option('-x, --xxx', 'what the x ???')
program.parse(process.argv);
运行结果:
说明option
就是用来声明命令行有哪些选项,-h
显而易见是使用commander默认自带的,只要使用-h
就会输出命令行有哪些选项
- 接着观察文档中的代码,我们不妨在index.js中
console.log(program.opts().xxx)
运行node index
,结果显示:
运行node index -x
或 node index --xxx
,结果显示:
说明这个选项就像一个开关,只要运行命令行的时候只要写了-x
对应的--xxx
就有了,到这里node就可以运行index.js 后面可以跟-x 或 --xxx
选项,即能接收用户传的参数了
- 如何让命令支持子命令?即
node index add
?不妨去看下文档,文档中在标题为Commands的地方就有有关子命令的代码,CRM学习法,粘贴过来,运行它
const program = require('commander');
const pkg = require('./package.json')
program
.version(pkg.version)
program
.option('-x, --xxx', 'what the x ???')
program
.command('clone <source> [destination]')
.description('clone a repository into a newly created directory')
.action((source, destination) => {
console.log('clone command called');
});
program.parse(process.argv);
program有一个选项叫-x
;它还有一个子命令叫clone
,它的描述是.description
,它的作用是.action
命令行中运行代码
这次除了显示options之外,还多了一个Commands
下的clone子命令
打印add命令提示
接着用CRM学习法,修改代码,把clone
改为add
试一试!
备注: 通过读文档知道,如何给命令添加多个参数
program
.option('-c, --compress [percentage]') // 0 或 1 个参数
.option('--preprocess <file...>') // 至少 1 个参数
.option('--test [name...]') // 0 或多个参数
const program = require('commander');
const pkg = require('./package.json')
program
.version(pkg.version)
program
.option('-x, --xxx', 'what the x ???')
program
.command('add <taskName> [other...]')
.description('add a task')
.action(() => {
console.log("Hi!!!")
});
program.parse(process.argv);
尝试用一下吧!!
如何输出用户使用add子命令添加的多个任务名呢?
const program = require('commander');
const pkg = require('./package.json')
program
.version(pkg.version)
program
.option('-x, --xxx', 'what the x ???')
program
.command('add <taskName> [other...]')
.description('add a task')
.action((x,y) => {
const words = x + " " + y.join(" ")
console.log(words)
});
program.parse(process.argv);
备注: 如果真的遇到报错,也不用慌,观察报错信息和查阅github中的文档去试着修改代码,打log,去解决!!!
打印clear 命令提示
- 再添加一个clear的子命令吧!
const program = require('commander');
const pkg = require('./package.json')
program
.version(pkg.version)
program
.option('-x, --xxx', 'what the x ???')
program
.command('add <taskName> [other...]')
.description('add a task')
.action((x,y) => {
const words = x + " " + y.join(" ")
console.log(words)
});
program
.command('clear [all]')
.description('clear all tasks')
.action(() => {
console.log('this is clear')
});
program.parse(process.argv);
实现创建任务功能add
如果是命令行程序,它的入口文件不应该叫index.js,把它改为cli.js(command line)
再新建一个index.js,这个index.js就是我们提供的所有功能,如add、clear功能
看下拆分后的文件
// cli.js
const program = require('commander');
const pkg = require('./package.json')
const api = require("./index.js")
program
.version(pkg.version)
program
.option('-x, --xxx', 'what the x ???')
program
.command('add <taskName> [other...]')
.description('add a task')
.action((x,y) => {
const words = x + " " + y.join(" ")
api.add(words)
});
program
.command('clear [all]')
.description('clear all tasks')
.action(() => {
console.log('this is clear')
});
program.parse(process.argv);
// index.js
module.exports.add = (title) => {
console.log('add')
}
现在要想一下如何实现add的功能呢?
我们应该将add的内容放到数据库中,但是没有数据库,推荐将add的内容放到用户的Home目录~
Node.js如何获取到用户的Home目录呢?Google一下:node.js get home directory,CRM学习法,先抄代码运行一下
const homedir = require('os').homedir();
但是home目录用于是可以在环境变量中配置的,因此我们最好获取用户的home变量,Google一下:node.js get home variable,CRM学习法,先抄代码运行一下
const HOME = process.env.HOME;
- 接着我们要在Home目录中创建文件,存储add的内容todo(查API:devdocs.io/)
在window中用\
表示路径,在mac和os中用/
表示路径,还要使用//、/\
转义符转义
那么怎么兼容多个系统的写法呢?
node.js提供了一个方法,专门用来拼路径,home
和.todo
之间不用加任何斜杠,它会根据不同的系统帮我们加不同的斜杠
const p = require("path")
const dbPath = p.join(home,'.todo')
fs.readFile
的文档:
options中的encoding一般不写,默认是utf-8
flag是什么呢?点击下devdocs中的链接
读文件如果用a+
的模式读,读文件内容并可以往里面追加内容,如果没有文件,默认会帮我们创建文件,这个好像更加符合我们的需求
得到的结果放到回调的参数中,一个是error,另外一个是data,data有可能是string也有可能是buffer,不如统一转成string
const homedir = require('os').homedir();
const home = process.env.HOME || homedir;
const p = require("path")
const dbPath = p.join(home,'.todo')
const fs = require('fs')
module.exports.add = (title) => {
// 读取之前的任务
fs.readFile(dbPath,{flag: 'a+'},(err,data) => {
console.log(data.toString())
})
// 往里面添加一个 title任务
// 存储任务文件
}
执行node cli add xxx
,得到的内容是一个回车(空文件默认内容是回车)
执行start ~
打开根目录,home目录下创建了文件
这样我们已经可以通过readFile,读取到之前的任务了
- 如果我们发现读取文件的内容是空的,我们需要创建一个空数组用来存储任务todo
const homedir = require('os').homedir();
const home = process.env.HOME || homedir;
const p = require("path")
const dbPath = p.join(home, '.todo')
const fs = require('fs')
module.exports.add = (title) => {
// 读取之前的任务
fs.readFile(dbPath, {flag: 'a+'}, (error, data) => {
if (error) {
console.log(error)
} else {
let list
try {
list = JSON.parse(data.toString())
}catch (error2){
list = []
}
console.log(list)
}
})
// 往里面添加一个 title任务
// 存储任务文件
}
- 接着,我们就可以往里面放todo了
const homedir = require('os').homedir();
const home = process.env.HOME || homedir;
const p = require("path")
const dbPath = p.join(home, '.todo')
const fs = require('fs')
module.exports.add = (title) => {
// 读取之前的任务
fs.readFile(dbPath, {flag: 'a+'}, (error, data) => {
if (error) {
console.log(error)
} else {
let list
try {
list = JSON.parse(data.toString())
} catch (error2) {
list = []
}
const task = {
title: title,
done: false
}
list.push(task)
console.log(list)
}
})
// 往里面添加一个 title任务
// 存储任务文件
}
- 接着把它存到todo里面,当然要把list变成字符串,因为文件里只能存字符串
查 devdocs 文档fs.writeFile()
,'[]’表示参数可以不传
const homedir = require('os').homedir();
const home = process.env.HOME || homedir;
const p = require("path")
const dbPath = p.join(home, '.todo')
const fs = require('fs')
module.exports.add = (title) => {
// 读取之前的任务
fs.readFile(dbPath, {flag: 'a+'}, (error, data) => {
if (error) {
console.log(error)
} else {
let list
try {
list = JSON.parse(data.toString())
} catch (error2) {
list = []
}
// 往里面添加一个 title任务
const task = {
title: title,
done: false
}
list.push(task)
// 存储任务文件
const string = JSON.stringify(list)
fs.writeFile(dbPath,string,(error3) => {
if(error3){
console.log(error3)
}
})
console.log(list)
}
})
}
我们可以通过cat ~/.todo
看下文件中的内容,当前是空的
我们使用add子命令添加一个todo试一下
到这里说明我们的add命令已经可以使用了!!!
封装优化代码
代码如果可以优化成下面的代码就完美了!!
module.exports.add = (title) => {
// 读取之前的任务
const list = db.read()
// 往里面添加一个 title任务
list.push({title, done: false})
// 存储任务文件
db.write(list)
下面正式优化代码吧
新建一个文件db.js
-
接下来,删除todo文件
rm ~/.todo
-
测试下add命令是否可用
node cli add 买水
完整代码:
// cli.js
const program = require('commander');
const api = require("./index.js")
const pkg = require('./package.json')
program
.version(pkg.version)
program
.option('-x, --xxx', 'what the x ???')
program
.command('add <taskName> [other...]')
.description('add a task')
.action((x,y) => {
const words = x + " " + y.join(" ")
api.add(words)
});
program
.command('clear [all]')
.description('clear all tasks')
.action(() => {
console.log('this is clear')
});
program.parse(process.argv);
// index.js
const homedir = require('os').homedir();
const home = process.env.HOME || homedir;
const p = require("path");
const dbPath = p.join(home, '.todo');
const fs = require('fs');
const db = require("./db.js");
// 优化后的代码
module.exports.add = async (title) => {
// 读取之前的任务
const list = await db.read()
// 往里面添加一个 title任务
list.push({title, done: false})
// 存储任务文件
await db.write(list)
}
// db.js
const homedir = require('os').homedir();
const home = process.env.HOME || homedir;
const p = require("path")
const fs = require('fs')
const dbPath = p.join(home, '.todo')
const db = {
read(path = dbPath) {
return new Promise((resolve, reject) => {
fs.readFile(path, {flag: 'a+'}, (error, data) => {
if (error) {
// 如果失败就返回error
reject(error)
} else {
let list
try {
list = JSON.parse(data.toString())
} catch (error2) {
list = []
}
// 如果成功就将异步的结果返回到外面
resolve(list)
}
})
})
},
write(list,path = dbPath) {
return new Promise((resolve, reject)=>{
const string = JSON.stringify(list)
fs.writeFile(dbPath,string,(error) => {
if(error){
reject(error)
}else{
resolve()
}
})
})
}
}
module.exports = db
注意:
- 在异步的代码中不能直接
return
,我们需要使用Promise
,成功到的时候使用resolve(list)
将结果传告诉外面,失败的时候使用reject(error)
,将error告诉外面 - 由于调用
db.read()
返回的是Promise,于是我们可以使用await
,await
可以拿到Promise的结果,由于这里使用了await
,因此它外面的函数必须标记为async - 拆分的文件的思路:将想要的优化后的效果写在index.js中
module.exports.add = (title) => {
// 1.读取之前的任务
const list = db.read()
// 2.往里面添加一个 title任务
list.push({title, done: false})
// 3.存储任务文件
await db.write(list)
- 添加版本号时需要从
package.json
中读版本号,这样在node cli.js --version
时就可以知道当前命令的版本号了
const pkg = require('./package.json')
program
.version(pkg.version)
然后把之前未优化的代码一部分一部分的拆开,这种技巧就是面向接口编程,即先把接口想好,再把代码变成这些接口,代码再怎么复杂别人不想看,只想看上面代码中接口的部分逻辑
消除WebStorm的警告
如何消除webstorm中可恶的下划波浪线?
为什么会有下划波浪线?
因为被标记为下划线的代码,对于webstorm来说根本不知道是从哪里来的,这些被划线的部分,只有在Node.js中才有,但是webstorm并不知道你用的是Node.js
因此我们需要做一些配置
如果没有相应的Library就点击Download去下载相应的Library
另外还需要删除代码中暗灰色的代码,这些代码是声明了但是没有使用的变量
我们一定要保证IDEA在不该报错的时候一定不要报错,如果有错误,一定要马上改掉,否则如果报错积累多了,就没办法发现哪里是真正的错误
完成所有功能
实现clear清除任务功能
add功能已经实现了,接下来要实现以下clear
我们应该先想一下接口应该是怎样的
program
.command('clear [all]')
.description('clear all tasks')
.action(() => {
api.clear()
});
- 接下来实现以下clear
module.exports.clear = async() => {
await db.write([])
}
备注:node.js中的代码都是异步的
运行node cli clear
就会将~/.todo
文件中的内容替换为空数组
实现展示全部任务功能
接下来要实现一个效果:
当用户在命令行输入node cli.js
的时候,应该展示所有的任务todo
我们怎么知道用户没有传命令add和clear呢?
我们尝试打印console.log(process.argv)
也就是说,如果process.argv
的长度为2,就说明用户只传了node.exe 和 cli.js 这两个路径作为参数,而没有传子命令,此时就让它展示所有的任务todo
先把接口写好:
if(process.argv.length === 2){
void api.showAll()
}
备注: void
的作用仅仅是为了强制消除webstorm的警告
再写一下showAll的具体逻辑:
module.exports.showAll = async () => {
// 读取之前的任务
const list = await db.read()
// 打印之前的任务
list.forEach((task,index) => {
console.log(`${task.title ? '[✘]': '[✔]'}${index + 1} - ${task.title}`)
})
}
尝试添加任务,并使用node cli.js
来展示当前的全部任务todo
实现用户对任务打✔ 和 打 ✘
我们应该给用户一个可选择的列表,让用户可以自己选择选项,做了就打✔,没做就打✘
我们需要借助一个库来做列表:github.com/SBoudrias/I…
使用CRM学习法,copy能运行的代码
先安装这个库
yarn add @inquirer/prompts
copy 能运行的例子
如下图所示中的example
看下运行的效果:
CRM学习法第二步: 改代码
module.exports.showAll = async () => {
// 读取之前的任务
const list = await db.read()
inquirer.prompt(
{
type: "list",
name: "index",
message: "请选择你想操作的任务",
choices: list.map((task, index) => {
return `${task.title ? '[✘]' : '[✔]'}${index + 1} - ${task.title}`
})
})
.then(answer => {
console.log(answer.index)
})
}
选择之后得到的结果不是我们想要的,我们只想要下标index,choices
是不是有其他写法?
选择的内容是[✘]1 - 买水
,但是得到的值是下标index?
去文档中搜关键字choices
从文档中可以看出choices
的可以是数组,数组的值可以是name,value形式的对象
我们尝试让name是内容,value是index
module.exports.showAll = async () => {
// 读取之前的任务
const list = await db.read()
inquirer.prompt({
type: "list",
name: "index",
message: "请选择你想操作的任务",
choices: list.map((task, index) => {
return {name:`${task.title ? '[✘]' : '[✔]'}${index + 1} - ${task.title}`,value:index.toString()}
})
}).then(answer => {
console.log(answer.index)
})
}
那么如果用户不想选择怎么办?
加个退出功能
给用户加个退出
module.exports.showAll = async () => {
// 读取之前的任务
const list = await db.read()
inquirer.prompt({
type: "list",
name: "index",
message: "请选择你想操作的任务",
choices: [{name: '退出',value: '-1'},...list.map((task, index) => {
return {name:`${task.title ? '[✘]' : '[✔]'}${index + 1} - ${task.title}`,value:index.toString()}
})]
}).then(answer => {
console.log(answer.index)
})
}
支持对话状态时创建、修改、删除任务
如果用户既不想选择又不想退出,而是想添加todo,每次还要退出运行node cli add xxx
,太麻烦
选择列表中再加一个可以添加任务的选项
module.exports.showAll = async () => {
// 读取之前的任务
const list = await db.read()
inquirer.prompt({
type: "list",
name: "index",
message: "请选择你想操作的任务",
choices: [{name: '退出', value: '-1'}, ...list.map((task, index) => {
return {name: `${task.title ? '[✘]' : '[✔]'}${index + 1} - ${task.title}`, value: index.toString()}
}), {name: '+ 新增任务', value: '-2'}]
}).then(answer => {
console.log(answer.index)
})
}
先将问答列表做好,接下来根据用户选择index的值,执行后续的逻辑
完整代码:
const db = require("./db.js");
const inquirer = require("inquirer");
// 优化后的代码
module.exports.add = async (title) => {
// 读取之前的任务
const list = await db.read()
// 往里面添加一个 title任务
list.push({title, done: false})
// 存储任务文件
await db.write(list)
}
module.exports.clear = async () => {
await db.write([])
}
module.exports.showAll = async () => {
// 读取之前的任务
const list = await db.read()
inquirer.prompt({
type: "list",
name: "index",
message: "请选择你想操作的任务",
// 用户可以选择退出、对选择的todo任务打✔✘,新增任务
choices: [{name: '退出', value: '-1'}, ...list.map((task, index) => {
return {name: `${task.done ? '[✔]' : '[✘]'}${index + 1} - ${task.title}`, value: index.toString()}
}), {name: '+ 新增任务', value: '-2'}]
}).then(answer => {
const index = parseInt(answer.index)
if (index >= 0) {
// 选择了一个todo任务
inquirer.prompt({
type: "list",
name: "action",
message: "选择操作",
// 如果选择了一个todo任务,用户可以选择退出、标记todo任务已完成、未完成、删除、修改标题和退出
choices: [
{name: '退出', value: 'quit'},
{name: '已完成', value: 'markAsDone'},
{name: '未完成', value: 'markAsUndone'},
{name: '改标题', value: 'updateTitle'},
{name: '删除', value: 'remove'},
]
}).then(answer2 => {
// 对用户的选择执行步同的业务处理
switch (answer2.action) {
case 'markAsDone':
list[index].done = true
db.write(list)
break;
case 'markAsUndone':
list[index].done = false
db.write(list)
break;
case 'updateTitle':
inquirer.prompt({
type: 'input',
name: 'title',
message: "新的标题",
}).then((answer) => {
list[index].title = answer.title
db.write(list)
});
break;
case 'remove':
// 从下标index删除一个
list.splice(index, 1)
db.write(list)
break;
}
})
} else if (index === -1) {
// 选择了退出
} else if (index === -2) {
// 创建任务
inquirer.prompt({
type: 'input',
name: 'title',
message: "输入任务标题",
}).then((answer) => {
list.push({
title: answer.title,
done: false
})
db.write(list)
});
}
})
}
备注:修改标题时,要让用户输入要修改的标题,因此要去文档中查input
相关的example
再次封装优化代码
把一堆代码取一个恰当的名字(函数),然后调用它
const db = require("./db.js");
const inquirer = require("inquirer");
// 创建任务
module.exports.add = async (title) => {
// 读取之前的任务
const list = await db.read()
// 往里面添加一个 title任务
list.push({title, done: false})
// 存储任务文件
await db.write(list)
}
// 清除任务
module.exports.clear = async () => {
await db.write([])
}
// 打印全部的任务
module.exports.showAll = async () => {
// 读取之前的任务
const list = await db.read()
printTasks(list)
}
// 打印全部任务todoList
function printTasks(list) {
inquirer.prompt({
type: "list",
name: "index",
message: "请选择你想操作的任务",
// 用户可以选择退出、对选择的todo任务打✔✘,新增任务
choices: [{name: '退出', value: '-1'}, ...list.map((task, index) => {
return {name: `${task.done ? '[✔]' : '[✘]'}${index + 1} - ${task.title}`, value: index.toString()}
}), {name: '+ 新增任务', value: '-2'}]
}).then(answer => {
const index = parseInt(answer.index)
if (index >= 0) {
askForAction(index, list)
} else if (index === -1) {
// 选择了退出
} else if (index === -2) {
askForCreateTask(list)
}
})
}
// 选择了一个todo任务
function askForAction(index, list) {
const actions = {markAsDone, markAsUndone, updateTitle, remove}
inquirer.prompt({
type: "list",
name: "action",
message: "选择操作",
// 如果选择了一个todo任务,用户可以选择退出、标记todo任务已完成、未完成、删除、修改标题和退出
choices: [
{name: '退出', value: 'quit'},
{name: '已完成', value: 'markAsDone'},
{name: '未完成', value: 'markAsUndone'},
{name: '改标题', value: 'updateTitle'},
{name: '删除', value: 'remove'},
]
}).then(answer2 => {
// 对用户的选择执行步同的业务处理
const action = actions[answer2.action]
action && action(list,index)
})
}
// 询问创建任务
function askForCreateTask(list){
inquirer.prompt({
type: 'input',
name: 'title',
message: "输入任务标题",
}).then((answer) => {
list.push({
title: answer.title,
done: false
})
void db.write(list)
});
}
// 任务标记已完成
function markAsDone(list,index){
list[index].done = true
void db.write(list)
}
// 任务标记未完成
function markAsUndone(list,index){
list[index].done = false
void db.write(list)
}
// 更新任务标题
function updateTitle(list,index){
inquirer.prompt({
type: 'input',
name: 'title',
message: "新的标题",
}).then((answer) => {
list[index].title = answer.title
void db.write(list)
});
}
// 移出任务
function remove(list,index){
// 从下标index删除一个
list.splice(index, 1)
void db.write(list)
}
【温馨提示】
Webstorm还有一个好用的功能sructure
,可以清晰的看清楚当前代码的组织结构,声明了哪些函数,导出了什么函数等
最终效果
如何发布到npm
接下来要将代码发布出去,让所有人都可以使用我们的代码
修改package.json
- 发布的时候name必须是唯一的
cli.js
是给命令行使用的代码,main.js
才是todo工具的逻辑部分代码- 让
cli.js
变成note命令
-
cli.js
要加一个shebang,它的作用是告诉命令行要用node来执行代码
#!/usr/bin/env node
-
cli.js
必须是可执行文件,方法是在当前项目命令行中执行下面的代码:chmod +x cli.js
- 发布之前要告诉npm哪些文件是要上传的,因此要修改
"files:["*js"]"
运行命令行
- 可以使用
npm
发布也可以使用yarn
发布 - 发布之前一定要将淘宝源切回到原始源,因为taobao源是不接受你publish的文件的,只有原始源才接受
nrm use npm
- 使用
npm adduser
登录npm ,提示one-time-password
的时候意思是要输入一次性的密码,会发到注册时的邮箱,输入即可
-
接着我们就可以使用
npm publish
发布我们的包文件了 -
如果报下面的错,可能是我们的包名已经存在了
得到下面的提示,就表示包发布成功了!
如下图所示,在npm账号里已经有发布的包了!
任何人都可以使用发布的工具了
使用
yarn global add node-notebook-reagen
遇到的问题
但是运行 note
时,发现找不到命令note
使用绝对路径可以调到
或者配置路径
运行npm -g bin
,打印npm将要安装可执行文件的文件夹,将结果中的路径配置到系统环境变量中
全局安装
当在命令行中运行npm i node-notebook-reagen -g
全局安装时,就会将下载的包文件放到E:\Node\node_global\node_modules
中(路径E:\Node\node_global
是配置的环境变量path)
当运行note
时, 实际上运行的是node cli.js
Node就会到E:\Node\node_global\node_modules\node-notebook-reagen
中找到对应的cli.js执行
运行结果:
局部安装
当在命令行中运行npm i node-notebook-reagen
局部安装时,会就会将下载的包文件放到F:\cs\node_modules
中
当运行note
时, 实际上运行的是node cli.js
Node就会到F:\cs\node_modules\node-notebook-reagen
中找到对应的cli.js执行
运行结果:
项目地址
改版
修改完代码后,记得修改package.json
中的版本号,同时重新发布新的版本npm publish
,绝对不可以把同一个版本publish两次
用户如果要更新版本运行
npm i node-notebook-reagen@lastest
选择自己想安装的版本即可
常见错误
- 发布的包名跟线上包名重复,不允许发布,请改名
- 用户名和密码不匹配,请重置密码
- 没有改版本号,不能重复发布同一版,请升级版本
- 其他问题自行Google