今回は、Node.jsでメモの追加・一覧表示・参照・削除ができるメモアプリ(CLI版)を作成しました。
データの保存先にはsqlite3、JavaScriptのクラス構文を使って作成しました。
要件
1. メモの追加
標準入力に入ってきたテキストを新しいメモとして追加する。
$ echo 'メモの内容' | app.js
2. メモの一覧を表示
それぞれのメモの最初の行のみを表示する。
$ memo.js -l
メモ1
今日の日記
晩ご飯のレシピ
3. メモの参照
選択したメモの全文が表示される。
(カーソルを合わせた時点もしくはメモを選択した時にメモの全文が表示される)
$ memo.js -r
Choose a note you want to see:
メモ1
今日の日記
> 晩ご飯のレシピ
晩ご飯のレシピ
カレー
豚肉
じゃがいも
人参
タマネギ
カレールー
4. メモの削除
選択したメモが削除される。
$ memo.js -d
Choose a note you want to delete:
メモ1
今日の日記
> 晩ご飯のレシピ
必要なパッケージをインストール
$ npm install --save enquirer minimist sqlite3
memoDB.js
- SQLite3データベースとのインタラクション(相互作用)を処理するクラス。
import pkg from "sqlite3";
const { Database } = pkg;
export default class MemoDB {
constructor() {
this.db = new Database("./db/memo.sqlite3");
this.tableName = "memos";
this.db.serialize(() => {
this.db.run(
`CREATE TABLE IF NOT EXISTS ${this.tableName} (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT)`,
(err) => {
if (err) {
console.error(err.message);
}
}
);
});
}
selectAll() {
return new Promise((resolve, reject) => {
this.db.all("SELECT * FROM " + this.tableName, (err, rows) => {
err ? reject(new Error(err)) : resolve(rows);
});
});
}
select(id) {
return new Promise((resolve, reject) => {
this.db.get(
"SELECT * FROM " + this.tableName + " WHERE id = ?",
id,
(err, row) => {
err ? reject(new Error(err)) : resolve(row);
}
);
});
}
insert(columnName, text) {
return new Promise((resolve, reject) => {
this.db.run(
"INSERT INTO " + this.tableName + "(" + columnName + ")" + " values(?)",
text,
(err) => {
err ? reject(new Error(err)) : resolve();
}
);
});
}
delete(id) {
return new Promise((resolve, reject) => {
this.db.run(
"DELETE FROM " + this.tableName + " WHERE id = ?",
id,
(err) => {
err ? reject(new Error(err)) : resolve();
}
);
});
}
close() {
this.db.close;
}
}
serialize()
- 指定したテーブルが存在しない場合は、新しいテーブルを作成。
テーブルは、自動的に増加する主キーのid
とテキストデータを保持するtext
の2つのカラムを持っている。
memo.js
- メモ操作に関連するクラス。
MemoDBクラスのインスタンスを引数として受け取り、Memoクラス内でデータベースの操作を行う。
import Menu from "./menu.js";
export default class Memo {
constructor(memoDB) {
this.memoDB = memoDB;
}
async create(lines) {
await this.memoDB.insert("text", lines.join("\n"));
return lines.join("\n");
}
async showAll() {
const allMemos = await this.memoDB.selectAll();
if (this.hasNoMemo(allMemos)) {
return [];
}
for (let memo of allMemos) {
console.log(memo.text.split("\n")[0]);
}
return allMemos;
}
async show() {
const allMemos = await this.memoDB.selectAll();
if (this.hasNoMemo(allMemos)) {
return null;
}
const selectedMemo = await this.select(allMemos, "see");
const selectedRow = await this.memoDB.select(selectedMemo.id);
return selectedRow.text;
}
async delete() {
const allMemos = await this.memoDB.selectAll();
if (this.hasNoMemo(allMemos)) {
return null;
}
const selectedMemo = await this.select(allMemos, "delete");
await this.memoDB.delete(selectedMemo.id);
return selectedMemo;
}
async select(memos, purpose) {
const menu = new Menu(memos);
return await menu.chooseMemoId("id", purpose);
}
hasNoMemo(memos) {
return memos.length === 0;
}
}
menu.js
import pkg from "enquirer";
const { prompt } = pkg;
export default class Menu {
constructor(choices) {
this.choices = choices.map((item) => ({
name: item.text.split("\n")[0],
value: item.id,
}));
}
async chooseMemoId(name, action) {
return await prompt({
type: "select",
name: name,
message: `Choose a memo you want to ${action}:`,
choices: this.choices,
result() {
return this.focused.value;
},
});
}
}
cli.js
#!/usr/bin/env node
import { createInterface } from "readline";
import MemoDB from "./memoDB.js";
import Memo from "./memo.js";
import minimist from "minimist";
async function main() {
const memoDB = new MemoDB();
const memo = new Memo(memoDB);
const argv = minimist(process.argv.slice(2));
if (!process.stdin.isTTY) {
const lines = await readStdin();
const createdMemo = await memo.create(lines);
console.log(
"\nYour memo, '" + createdMemo + "', has been successfully saved."
);
}
if (argv.l) {
const allMemos = await memo.showAll();
if (memo.hasNoMemo(allMemos)) {
console.log("No memos found. Please create a new one.");
}
}
if (argv.r) {
const memoContent = await memo.show();
if (memoContent) {
console.log("\n" + memoContent);
} else {
console.log("No memos found. Please create a new one.");
}
}
if (argv.d) {
const deletedMemo = await memo.delete();
if (deletedMemo) {
console.log("\nThe selected memo has been successfully deleted.");
} else {
console.log("No memos found. Please create a new one.");
}
}
}
function readStdin() {
return new Promise((resolve) => {
const lines = [];
const reader = createInterface({
input: process.stdin,
output: process.stdout,
});
reader.on("line", (line) => {
lines.push(line);
});
reader.on("close", () => {
resolve(lines);
});
});
}
main();
- main関数を定義
MemoDB
とMemo
クラスのインスタンスを作成し、minimist
でコマンドライン引数をパース。
process.argv.slice(2)
は最初の2つの引数(nodeのパスと実行ファイルのパス)を除いた残りの引数を取得。
process.stdin.isTTY
は、現在のプロセスがTTY(端末)からの入力を受け取っているかどうかをチェック。否定演算子(!)を前に付けているため、このif文はTTYからの入力がない場合、つまりパイプやリダイレクションからの入力がある場合に実行される。その入力を読み取り、新しいメモを作成。作成が完了したら、その内容をコンソールに表示。
- 最後に、標準入力からデータを非同期に読み込む
readStdin
関数と、メインの関数を実行するmain()
を定義。readStdin
関数はPromiseを返すため、データが全て読み込まれるまで待機し、その後処理を進めることができる。
1. そもそもクラスとは?
- プログラミングにおける「クラス」とは、オブジェクトを作るための設計図のようなもの。
オブジェクト指向プログラミングでは、クラスを使ってデータとそれを操作するメソッド(関数)を一緒にまとめる。
- JavaScriptにおいても、クラスはデータとメソッドを一緒にまとめる役割を果たす。
- JavaScriptのクラスは、他の言語のクラス構造と同じように見えるが、内部的には「プロトタイプベースのオブジェクト指向」を使用。
- 「プロトタイプベース」...オブジェクトが他のオブジェクトを元にして作られることを意味する。これはクラスベースの言語と異なり、クラスを必要としないのが特徴。クラス構文はこのプロトタイプベースのオブジェクト指向を扱いやすくするための糖衣構文。(シンタックスシュガー)
まとめ✏️
【参考】
クラス - JavaScript | MDN
クラス(Class) - とほほのWWW入門
Node.jsでのCLIの作り方と便利なライブラリまとめ - Qiita
mapbox/node-sqlite3: Asynchronous, non-blocking SQLite3 bindings for Node.js