ローカルにプレーンなメモアプリを構築

プログラミングをしている人には、普段あちこちググったことを
メモっておきたい、という日常があると思います。twitterを
メモ代わりにつぶやいたり、wikiにまとめたり。

しかし、ツイッターにとってつぶやきは過ぎゆく過去であり、
そのためのインタフェースで設計されています。
一方、wikiにまとめたらそれはもう技術資料であり、メモではない、
等々こういったジレンマにかられたりすると思います。

というわけで僕は以下のようなメモアプリをローカルに構築しました。

![範囲を選択_047.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244766/5a0b5d39-05b1-07da-94c8-869af664d35d.png)


テキストエリアに入力し、「保存」ボタンを押すとDBに保存される
シンプルなメモです。自分はWebアプリエンジニアではないので、
画面例では素人ならではの紆余曲折が残っています。

#使っているアーキテクチャ
・HTML
・JavaScript
・mongodb
・nodejs(express)
・WebAPI

なるべくプレーンなHTML、JavaScriptで画面アプリを構築しています。
jqueryとか、vue.jsとか、そんな便利lib/frameworkでガンガン構築
するのは楽しそうですが、プレーンな部分を知らないと
結局よくわかりません。

#なんとなく会得したこと
・動的なデータ(今回は追加されていくメモ)を表示するためには、
　テーブル部品自体をJavaScriptで記述する。（当たり前のことかもですが)
・UIとは、**データが取得できたら再描画する**ことの繰り返しである。
　そのための非同期処理である。
　非同期処理として、Promise機構を利用している。
　WebAPIを叩くためのfetcherAPIがPromise実装しているので
　then句で非同期処理(データ取れたらUI再描画)を記述している。
・JavaScriptのモジュール化
　もっともモッサイ方法だと思いますが、単に複数ファイルに分けて、
　全部htmlでロードしています。しかし、簡単なんだ、これが・・
　ただ、本当にネットで公開したら一個のJavaScriptにまとめたほうが
　ダウンロード効率的には良いんですかね・・

#実装の順番
１．ローカルページの実装
　　HTMLとJavaScriptでゴリゴリ書きます。
２．Webサーバ構築
　　ローカルとはいえ、webサーバを立てています。nodejsのexpressです。
　　このwebサーバには以下の２つの役割を実装しています。
　　１．Webページのホスティング
　　２．WebAPIのサービサー
３．mongodbセットアップ
　　メモのようなゆるいデータを逐次貯めていくにはスキーマレスな
　　mongodbが良さげです。mongodbをインストールしてサーバ起動させます。
　　２のWebAPIは、nodejsのmongodbドライバを使って、このmongodbに
　　アクセスします。

#ソースコード

**index.html(メモブラウザ)**

```javascript

<html>
<head>
<title>メモ帳</title>
<link rel="stylesheet" type="text/css" href="main.css">
<style type="text/css">
  textarea{width:30%;height:20%;}
</style>
</head>

<body>
  <h1>メモ帳です</h1>
  <div>
    <ul id="todo-container"></ul>
  </div>
  <div>
  <textarea name="memo" id="id_memo"></textarea>
  <p> 
    <input type="button" value="保存" onclick=saveMemo();></input>
    <input type="button" value="消去" onclick=clear2();></input>
    <input type="button" value="reverse" onclick=sort_reverse();></input>
  </p>
  </div>
  <div>
    カテゴリ選択: 
  <select id="category" onchange="category_changed();">
    <option value="none">none</option>
    <option value="html/css">html/css</option>
    <option value="javascript">javascript</option>
    <option value="nodejs">nodejs</option>
    <option value="database">database</option>
    <option value="daily">daily</option>
    <option value="browser">browser</option>
    <option value="stm">stm</option>
  </select>
  　リバース
  <input type="button" value="昇順/降順" onclick=sort_reverse();></input>
  <table>
    <tbody id="memo_table">
    </tbody>
  </table>
  </div>
  
  <script type="text/javascript" src="./tool.js"></script>
  <script type="text/javascript" src="./memo_db.js"></script>
  <script type="text/javascript" src="./main.js"></script>

</body>
</html>
```
カテゴリ選択もJavaScriptで動的に捌けば、htmlページにあるのはただの
コンテナ宣言(div)だけにスッキリさせられそうです。

**main.js(UIのメイン処理)**

```javascript

function category_changed() {
  showMemoList(document.getElementById("category").value);
}

function sort_reverse() {
  sort_order = localStorage.getItem('sort_order');
  if (!sort_order) {
    localStorage.setItem('sort_order', 'asc');
  } else {
    if (sort_order == 'asc') {
      localStorage.setItem('sort_order', 'des');
    } else {
      localStorage.setItem('sort_order', 'asc');
    }   
  }
  showMemoList(document.getElementById("category").value);
}

function saveMemo() {
  // DBへの書き出しが完了したら、UIを再描画する。
  writeMemoDb({date: date_string(),
             memo:document.getElementById("id_memo").value,
             category: document.getElementById("category").value
            })
    .then(showMemoList('none'));
}

function deleteMemo(id) {
  deleteMemoDb(id)
    .then(showMemoList('none'));
}

function clear2() {
  document.getElementById("id_memo").value = ''; 
}

function showMemoList(category) {
  readMemoDbAll(category, localStorage.getItem('sort_order'))
    .then(res_json => {
      console.log('res json = ', res_json);
      let tb = document.getElementById("memo_table");
      while (tb.firstChild) {
        tb.removeChild(tb.firstChild);
      }
      for (let data_key in res_json) {
        console.log(res_json[data_key]['memo']);

        // 純粋なテキスト挿入ならcreateTextNode。
        // テキストにHTML構文を使いたいなら、innerHTMLセット。

        tb.appendChild(document.createElement("tr"));
        let td0 = document.createElement("td");
        let td1 = document.createElement("td");
        let td2 = document.createElement("td");
        let td3 = document.createElement("td");
        let td4 = document.createElement("td");
        td0.style.backgroundColor="green";
        td0.style.color="white";
        td0.appendChild(document.createTextNode(data_key));
        td1.style.backgroundColor="skyblue";
        td1.style.color="white";
        td1.appendChild(document.createTextNode(res_json[data_key]['date']));
        td2.innerHTML = res_json[data_key]['memo'];
        td3.appendChild(document.createTextNode(res_json[data_key]['category']));
        td4.innerHTML = '<input type="button" onclick="deleteMemo(\'' + res_json[data_key]['_id'] + '\')" value="削除">';

        tb.appendChild(td0);
        tb.appendChild(td1);
        tb.appendChild(td2);
        tb.appendChild(td3);
        tb.appendChild(td4);
      }
    })
}

```
ページロード時、各種ボタン押下時の機能を定義しています。
配列とfor文を使えばもう少しスッキリさせられそう。

**memo_db.js(mongodbの操作)**

```javascript

function deleteMemoDb(id) {
  return fetch('./api/memo/' + id, {method: "DELETE"})
    .then((response) => response.json())
}

function readMemoDbAll(category, sort_order) {
  return fetch('./api/memo?category=' + category + '&sort_order=' + sort_order)
    .then((response) => response.json())
}

function writeMemoDb(record) {
  return fetch('./api/memo',
        {method: "POST",
         headers: {
           "Content-TYpe": "application/json; charset=utf-8",
         },
         body: JSON.stringify(record)
        })
    .then(response => response.json())
    .then(data => console.log(JSON.stringify(data)))
    .catch(error => console.error(error));
}

```
WebAPIを叩いて、DBを操作しています。

**memo_app.js(サーバ側メモアプリ)**

```javascript

const express = require('express');
const bodyParser = require('body-parser')
const MongoClient = require('mongodb').MongoClient;
const mongo = require('mongodb');
const assert = require('assert');

const url = 'mongodb://localhost:27017';
const dbName = 'memo';
const option = {
  useNewUrlParser: true,
  useUnifiedTopology: true
}

const app = express();

app.use(express.static('web'));
app.use(bodyParser.json());

app.post('/api/memo/', (req, res) => {
  console.log(req.body);

  MongoClient.connect(url, option, (err, client) => {
    assert.equal(null, err);
    let obj = req.body;
    client.db('memo').collection('memo').insertOne(obj, (err, result) => {
      client.close();
      res.send(result);
    }); 
  }); 
});

app.get('/api/memo/:id', (req, res) => {
  console.log(req.params.id);
});

app.get('/api/memo/', (req, res) => {
  console.log('category = ', req.query.category);
  MongoClient.connect(url, option, (err, client) => {
    assert.equal(null, err);
    condition = {}
    if (req.query.category != 'none') {
      condition = {category: req.query.category}
    }   
    if (req.query.sort_order == 'asc') {
      client.db('memo').collection('memo').find(condition).sort({date:1}).toArray((err, result) => {client.close(); res.send(result);});
    } else {
      client.db('memo').collection('memo').find(condition).sort({date:-1}).toArray((err, result) => {client.close(); res.send(result);});
    }   
  }); 
});

app.delete('/api/memo/:id', (req, res) => {
  console.log(req.params.id);
  MongoClient.connect(url, option, (err, client) => {
    assert.equal(null, err);
    console.log('condition = ', {id:req.params.id});
    client.db('memo').collection('memo').deleteOne({_id: new mongo.ObjectId(req.params.id)}, (err, result) => {
      client.close();
      if (err) {
        res.send(JSON.stringify({result: 'ng'}));
      } else {
        res.send(JSON.stringify({result: 'ok'}));
      }   
    }); 
  }); 
});

app.listen(3009, () => console.log('listening on port 3009'));
```

#つくってみた所感
こんな感じです。別のPCに同じ環境を構築するにはちょっとポータブル性が
足りませんが、しょうがない。nodeパッケージ自体はポータブル性アリアリなので
それで良しとします。

最初はlocalStorageを使ってデータを貯めようかとも
思いましたが、やりたいことに対し機能不足でした。
localStorageにはあくまでページ設定を保存し（ソートオーダーとか）、
データ本体はサーバに置いてWebAPIでアクセスする形になりました。
このあたりがWebアーキテクチャの制約なのかなぁ。
ブラウザのHTMLでローカルに置いたmongodbに直接アクセスできては
いけないんですよね、たぶんセキュリティ的に。

ローカルデバイス内部で発生し、閉じた系でのデータ駆動をモニタしたり
制御するUIアプリでは、サーバサービス不要なんですが、
webアーキテクチャベースで構築するとこうならざるを得ないのかな。