皆さま、こんにちは
ブレイブソフトのシローです。
仕事としてサーバサイドの保守がメインになりつつ、「あれ?最近、 開発やってなくね?」ということで、
開発力を取り戻すことを目的として、とりあえずTodoアプリを作ることにしました。
その際に、色々試行錯誤したよ、という内容です。
Todoアプリとは?
Todoアプリとは、簡単にいうと「タスク管理を行うツール」です。
例として、Trelloみたいものがあります。
なぜTodoアプリ?
実際のサービスの開発では、「要件定義」や「画面設計」だの開発以外に考えることが多いです。
その点、Todoアプリの開発は機能や画面設計がとてもシンプルです。
Todoアプリの主な機能は
- タスクを追加する
- タスクを編集する
- タスクを削除する
- タスク一覧を表示する
くらいですし、
画面も基本的に一つで済みます。
なので、サービスの考案などに多くの時間をかけず、開発に注力するには向いていると思います。
また同時にフロントエンド、バックエンド、デザインについて満遍なく触れますので、総合的な開発力が身につくと思っています。
作ったもの
先に、今回作ったものを見せようと思います。
一応プロジェクトのソースコードはこちらです。
機能はごく普通のTodoアプリ・・・にするのはちょっとシンプルすぎるので、
時間になると、「そろそろ期限切れだよ!!」みたいな通知をしてくれる機能をつけてみました。
要件定義
シンプルです。
- タスクを追加する
- タスクを編集する
- タスクを削除する
- タスク一覧を表示する
- 期限切れになる前に通知する時間(30分前とか)を設定する
- タスクが期限切れになりそうなら通知する
- タスクの期限が切れると再通知する
画面設計
1ページだからすごく楽です。
draw.ioでも使ってささっと、ドラフトを作りました。
実際のアプリとは色々と違いますが、大体のイメージさえ出来ていればいいんです。
実装方法
まず、
Web系にするか、ネイティブ系にするか
Webで作成すると誰からもアクセスされちゃうので、認証機能が必要になる。そして、セキュアにするほど認証は面倒・・・
また、オンラインでなくても動作したいという考えのもと、ネイティブ系になりました。
次
デスクトップにするか、スマホアプリにするか
僕は使える言語がWeb系に特化しているので、スマホアプリならCordova、
デスクトップアプリならElectron
になるんですが、「スマホでタスクチェックはやらないかな」という結論のもと、Electronでデスクトップアプリを作成することにしました。
Vue.jsも使ってみた
今までフロントはjQueryばかり使ってきたのですが、
フレームワークを利用した開発を積んで置けば、
可能性広がりそうだなと思い、Vue.jsを利用してみようと思いました。
また、jsでの開発を快適に行うために、babelやwebpackも利用しました。
データベースをどうするか
アプリケーションから外部のデータベースにアクセスするよりも、
アプリ内部に組み込めるような形式でデータベースを利用したい。
また、複雑な機能を持たないため、RDBにするよりも、json形式でデータを更新できるNoSQLが良いと思い。
NeDBを使うことにしました。(使い方はここ見ると良いカモです。)
以上をまとめると、
- ネイティブのデスクトップアプリ
- ソフトウェアフレームワークでElectronを使用
- フロントサイドのフレームワークでVue.jsを使用
- データベースはNeDBを使用
となりました。
プロジェクト構成
結果としてこうなっています。
|--data.db #NeDB用ファイル
|--database.js #NeDBのコントローラ
|--index.html #アプリケーション表示用HTML
|--js
| |--bundle.js #アプリケーションが利用するwebpack圧縮後のjsファイル
|--main.js #Electron起動ファイル
|--node_modules #モジュール群
|--notifier.js #通知用コントローラ
|--package.json #npm 設定
|--parametor.json #アプリ内変数
|--setting.js #アプリ内変数のコントローラ
|--src #フロント用の開発ソースファイル群(webpackで圧縮されてbundle.jsになる)
| |--app.js
| |--vue
| | |--app.vue
| | |--css
| | | |--header.css
| | | |--style.css
| | | |--task.css
| | |--js
| | | |--app.js
|--webpack.config.js #webpack設定ファイル
実装
アプリケーションの実装でコアとなった部分についてです。
Electronの起動
何はともあれ、Electron実行しないと始まりません、起動用スクリプト: main.js,レンダー用ファイル: index.htmlに以下を記述します。
main.js
const electron = require('electron') const path = require('path') const app = electron.app const BrowserWindow = electron.BrowserWindow let mainWindow = null app.on('ready', () => { mainWindow = new BrowserWindow( { width: 1200, height: 800 } ) mainWindow.loadURL(path.join('file://', __dirname, 'index.html')) mainWindow.isMinimized() }) app.on('window-all-closed', () => { app.quit() })
index.html
<!doctype html> <html> <head> <meta charset ="utf-8" /> <title>TODO Notifire</title> <link rel="stylesheet" href="./css/style.css" /> </head> <body> <div id="app"> </div> <script src="./js/bundle.js"></script> </body> </html>
これらのファイルを同じ階層に置いて”electron .”と実行すると実際にアプリケーションが立ち上がる用になります。
フロントの実装
index.htmlからはbundle.jsを読み込んでいます。
このファイルはsrc以下のファイルをwebpackで圧縮して出力したものです。
フロントの実装はVue.jsで行いましたが、一からデザインを作るのも面倒なので、bootstrapみたいなElement-uiという、
コンポーネントを利用して実装しました。
/src/vue/app.vue
<template> <div> <section id="header" style="background: 'gray'"> <h1>TODO Notifire</h1> <el-button id="add-new-task" v-on:click="addNewTask()">New</el-button> <span>Notify Time: </span> <el-time-select v-model="notifyInterval" v-bind:picker-options="{ start: '00:00', step: '00:15', end: '23:45' }" v-on:change="changeNotifyInterval()" placeholder="Select notify time"> </el-time-select> </section> <section id="content"> <ul id="task-list"> <li v-for="(task, index) in taskList"> <span v-bind:style="{ color: task.statusColor}">{{ task.statusText }}</span> <el-input class="task-name" type="text" v-model="task.name" v-on:change="changeTaskName(index)"/> <el-date-picker v-model="task.limitDateTime" v-on:change="changeLimitDateTime(index)" type="datetime" placeholder="Select limit datetime"> </el-date-picker> <el-button type="primary" icon="el-icon-delete" class="remove-task" v-on:click="removeTask(index)"></el-button> </li> </ul> </section> </div> </template> <script src="./js/app.js"></script>
通知の実装
“node-notifier”という、node.jsによる通知機能のモジュールを使いました。(モジュールについて詳しくはここ)
これを使って、期限切れそうになると通知する用にします。
notifier.js
const NN = require('node-notifier') module.exports = { notification: (title = 'Todo Notifier', message = 'Sample Notification') => { NN.notify({ title, message, wait: true }) } }
これをタスクが切れそうなタイミングで発火するようにします。
タスクの締め切り時刻を監視して、締め切り前、締め切り後になったらnotifier
/src/vue/js/app.js
import { remote } from 'electron' const notify = remote.require('./notifier') const notification = (title, message) => { notify.notification(title, message) } const database = remote.require('./database') const setting = remote.require('./setting') const taskStatus = { todo: { text: 'Todo', color: '#39ff64' }, notified: { text: 'Notified', color: '#ffec2a' }, expired: { text: 'Expired', color: '#ff2a2a' } } export default { mounted: function () { // 締め切り直前と締め切り後のタスクを通知する setInterval(() => { let notifyInterval = (() => { let strs = this.notifyInterval.split(':') let hours = parseInt(strs[0], 10) * 60 * 60 * 1000 let minutes = parseInt(strs[1], 10) * 60 * 1000 return hours + minutes })() for (let task of this.taskList) { if (!task.name || !task.limitDateTime) continue let now = Date.parse((new Date())) let limitDateTime = Date.parse(task.limitDateTime) let diff = limitDateTime - now if (diff < notifyInterval && diff > 0) { if (task.notified) continue let title = 'そろそろタスクの締め切り前です!' let message = task.name let doc = { expired: true, statusText: taskStatus.notified.text, statusColor: taskStatus.notified.color } database.updateData(doc, { _id: task._id }, false, (res) => { if (!res) { console.log('Failed to change mode') } else { task.notified = true task.statusText = taskStatus.notified.text task.statusColor = taskStatus.notified.color notification(title, message) } }) } else if (diff < 0) { if (task.expired) continue let title = '締め切りです!' let message = task.name let doc = { expired: true, statusText: taskStatus.expired.text, statusColor: taskStatus.expired.color } database.updateData(doc, { _id: task._id }, false, (res) => { if (!res) { console.log('Failed to change mode') } else { task.expired = true task.statusText = taskStatus.expired.text task.statusColor = taskStatus.expired.color notification(title, message) } }) } else { let doc = { notified: false, expired: false, statusText: taskStatus.todo.text, statusColor: taskStatus.todo.color } database.updateData(doc, { _id: task._id }, false, (res) => { if (!res) { console.log('Failed to change mode') } else { task.expired = false task.notified = false task.statusText = taskStatus.todo.text task.statusColor = taskStatus.todo.color } }) } } }, this.taskCheckInterval) . . .
通知の様子
期限切れそうになタスク様子
アプリの表示
最後に
新しい技術を調べて実装経験を積むためにも、Todoアプリの作成はおすすめです。
投稿者プロフィール
- eventosの開発をやっているエンジニアです。