1. はじめに
表題の通りアプリを作ったので記事にしておこうと思います!
初めてHyperappで開発を始める方は
基本的な考え方を是非こちらでご確認ください。
https://qiita.com/hajime-nohara/items/888aae1c4e553f3cec86
実際にアプリを触りながら読んでもらえると良いと思います。
https://www.sharpen.tokyo/gantt.html
本記事はアプリの実装内容についてザックリ記し、開発環境設定については触れません。
※ 以降、本記事で紹介するアプリをsharpenと呼びます
※ 発展途上アプリです、ご指摘やアドバイスお願いします😉
1-1. sharpenはどんなアプリ?
シンプルなガントチャートUIをベースにした
タスク管理アプリです。
デザイン、UX、共にシンプルを追求。
1-2. 設計思想
・サンプルと呼べるほどのライトさ
・堅実な振る舞いの永続性
・mobile deviceにも対応
2. ソースコード解説
2-1. ディレクトリ構造
Hyperappの基本概念であるState, View, Action ごとに
ディレクトリを分けています。MVC風に言うとSVAですね。
その他、付随的なディレクトリがありますがここでは説明を省きます。
src/index.jsがエントリポイントになっていて、
そこでid="sharpen"
のDOMの中にHyperappで生成したDOMを展開します。
基本構造
▾ src/
▾ actions/
index.js
table.js
▾ state/
index.js
▾ views/
detailModal.js
ganttBar.js
index.js
...
index.js
index.pug
実際のディレクトリ構造
https://github.com/hajime-nohara/sharpen/tree/develop/src
2-2. State
sharpenで作成されるデータのひな形になる部分です。
sharpenのStateの中身は、大きく2つに分類できます。
分類 | 内容 |
---|---|
タスクを纏めるプロジェクトの共通データ | プロジェクト名、locale etc |
タスクに関するデータ | タスク名、開始/終了日付、todoリスト etc |
sharpenは、1つのプロジェクトのデータを1つのStateの変数で管理し、
ユーザが複数のState(プロジェクト)を管理できるように設計しています。
state/index.jsには初期データとしてStateが定義されています。
ユーザ操作によって変更されたStateは永続的に保存できる領域に
Storeし、以降は保存したStateをロードします。
sharpenの特徴は、
個々のデータ項目のためにDBカラムを定義せず、Stateをまるごと保存するところです。
その為、State内のレイアウトを変更したい場合は、jsでState内のレイアウト変更する処理を行います。
簡素化&スピーディな実装のための設計なんですが、
State(jsonオブジェクト)をベースに動くHyperappだから採用できたと言えます。
2-3. View
ganttBar.jsとdetailModal.jsの2つが中心的なViewです。
現在、タスクは全てGantt chart形式のBarで表現しています。
ganttBar.jsで一本のBarを表現します。
Barをタップした際にでモーダルダイアログを表示し、
詳細情報の表示/編集を行います。それはdetailModal.jsで実装されています。
その他のViewは、日付のヘッダ、ツールバー、モーダルダイアログ etcです。
これら付随機能も同じようにコンポーネント化して切り出し、別ファイルで書いています。
Viewでは以下の2つを実装しています
① JSX syntax をインターフェースとして VirtualDOMを生成
② onClickなどのイベント属性に指定する処理(関数化して実装。大抵の関数で何らかのActionを呼んでStateを更新しています)
JSXの詳細は割愛しますが、
一般的なMVCのテンプレートエンジンのようなノリでVirtualDOMを定義できる優れものです。
一見HTMLですが、
実態はh
関数で、あくまでもjavascript言語であり、具体的にはHashオブジェクトになります。
ganttBar.js
https://github.com/hajime-nohara/sharpen/blob/develop/src/views/ganttBar.js
detailModal.js
https://github.com/hajime-nohara/sharpen/blob/develop/src/views/detailModal.js
2-4. Action
現在、表(gantt chart大枠)のデータを編集するActionは、
action/table.jsと言う名前で切り出しています。
taskに関するActionもaction/task.jsと行った感じで切り出す予定です。
Actionは多数実装するので1ファイルだと見辛くなります。
カテゴリ分けして切り出した方が良いと思います。
action/table.js
https://github.com/hajime-nohara/sharpen/blob/develop/src/actions/table.js
sharpenではStateの直接更新をしています。
ReactではNG行為ですがHyperappではNGではないようです。
Hyperappにおいても、
Actionは新しいState(Hashオブジェクト)をreturnしてStateを更新するのが基本的な実装方法ですが、State内に、配列サイズが可変、階層が深い、といったオブジェクトがある為、
そのような方法を用いています。
Hyperappは、ActionがreturnしたObjectと現在のStateを比較し、両者が
異なる場合にDOMを更新しますので、Stateを直接更新する多くのActionで
DOMを更新するために、return {}
しています。
Stateの階層が深くなるとその更新が面倒になります。コンテンツ内容が充実している場合、
全てのDOMをHyperappで実装するのは
課題があると思います。これは後に触れます。
3. ガントチャートBarの動き
ここはjsの実装に一番時間を使った箇所です。
仕組みは単純ですが、DOMのイベントハンドラを結構使うし
クロスブラウザを意識しゴニョゴニョとトライ&エラーがありました。
基本的にはマウスポインタが移動したx軸の距離を取得し、
その値をBarのwidth
OR position left
に反映させるだけです。
https://github.com/hajime-nohara/sharpen/blob/develop/src/views/ganttBar.js
3-1. 例)Barの右端(終了日位置)を右側に伸ばす
順序 | ユーザ挙動 | プログラム挙動 |
---|---|---|
① | Barの右端をクリック |
onmousedown or ontouchstart した際の座標位置を変数に保持 |
② | 掴んだままマウスを右に移動 |
mousemove でマウス移動中にリアルタイムでBarのwidthを、「現在のwidth + (現在のマウスポインタのx軸の位置-①)」 の値に変更 |
③ | マウスアップ |
mouseup or touchend した際の座標位置を取得 |
④ | マウスアップ | 現在のwidth + (③ - ①) の値でStateを更新する(Action実行) |
⑤ | - | BarのDOMを更新(再描画) |
4. Semantic UI
このデザインフレームワークは素敵です。
デザインはオーソドックスでありながらオシャレ。凝った動きもしてくれます。
ドキュメントが見やすく、実はこのSemantic UIを用いた
デザイン実装が一番楽しい作業でした!
ダイナミックな動きをするSemantic UIコンポーネントは、
使う際に$('.classname').xxxx()
といった形で初期化するので、
oncreate={(e)=>$(e).xxxx()}
という風にHyperappのライフサイクルイベントで初期化を行なっています。
Semantic UIはjQueryベースのフレームワークですが今のところ問題なくHyperappと共存しております。
しかし、
一部のケースで注意が必要です。
4-1. Hyperappと一緒だとすんなり使えないコンポーネント
Multiple Search Selection
これは、Semantic UIのjsの処理で派手にDOMの追加削除を行なっているので、
使えないです。
State管理のHyperappは勝手にDOMの追加や削除はNGです。
例えば、Multiple Search Selection を使うと
Hyperappでリレンダーできなくなります。
Selection系の一部のSemantic UIコンポーネントは、初期化の時にinputタグなんかを追加するので、
そのまま使うとよろしくないですが、
初期化時に生成されるDOMを予め書いておいてから初期化すると使えます。
あと、
modal dialog
これも派手にDOMの追加削除を行うため、
涙し、Semantic UI採用を断念しかけたのですが、
detachable: false
このオプションで回避できました。
それでもlatest versionのSemantic UI だとデザインが崩れましたが、
諦めず頑張ってたら素敵なナイスガイのアドバイスに救われました。
ナイスガイのコメント
version 2.2.14 にしてみとの事
ダウングレードなので嫌でしたがそれでも私は semantic ui を使いたいのです!
そんなこんなで、Semantic UIをフル活用です。
5. 採用してしているその他の Library
名前 | 役割 | 公式page | 備考 |
---|---|---|---|
mobile-drag-drop | Mobile deviceでD&Dできる | https://github.com/timruffles/mobile-drag-drop | ガントチャートバーの編集をMobileで実現する為に採用 |
flatpickr | Calendar Library | https://github.com/flatpickr/flatpickr | 美しいデザインのCalendar UI |
dateformat | 日付データの書式を楽に設定できる | https://github.com/felixge/node-dateformat | 便利なフォーマッター |
clipboard | クリップボードコピーができる | https://github.com/zenorocha/clipboard.js | 発行したプロジェクト共有用のURLをコピーする際に使用 |
normalize.css | cssのnormalizeに | https://github.com/necolas/normalize.css |
6. 課題
前述でも少し触れましたが、いくつか課題を抱えています。
6-1. StateやViewをパーシャル化したい
ReactはViewコンポーネント毎にStateを持っていてコンポーネントが完全にパーシャルなんですが、
Hyperappの場合は、Stateは全体で1つしか持てずViewコンポーネント毎にStateを持つ事はできません。
(Reactの時は、Lift upやだな。Stateを共通で持ちたいなと思っていたものですが。。。)
理想としては、Stateは1つで、DOM更新の処理だけをパーシャル化できると完璧なんですが、今のところやり方がわかりません。
何が問題かと言うと、
コンテンツ内容が豊富なページでViewをたくさん実装すると、たった一箇所のDOM更新のためでも、結構な処理コストがかかると言う事です。Hyperappは、VirtualDOM(Hashオブジェクト)の中身をすべて見直して、どの部分が更新されてるかなとチェックするので、Hyperappで描画するDOMの数が多ければ多いほどCPUを使います。
USERの操作感をサクサクさせたいので、State更新を気軽に実装しちゃいますが、Stateがデカイ状態で
State更新をバンバン行うと、chromeのCPU使用率がすぐマックスになります。
なので、VirtualDOMの中身を全部チェックしないで、部分的にチェックしてDOM更新したいなと言う事です。
とりあえず、良さそうなテクニックがあるっぽいのでそれを試すつもりです。
hypercraft
6-2. middlewareを実装したいなぁ
やはり、Callback的な共通処理を実装したいケースはあるもので。。。
私の場合は、Stateを更新する際に共通処理を実行したいケースがありました。
middlewareのようにやろうとすると、reduce関数でActionを強引に作り直す、
みたいな事をしなければいけない感じかなと思っています。
Reactはどうやってるんだっけ?と完全に忘れてるので、
もう一度Reactのソースを確認してみようと思う今日この頃です。
とりあえず、最上位のviewの中で共通処理を実装してます。
Hyperappの生みの親である、jorgebucaranさんがslackで親切にその方法を教えてくれました。
ありがとうございます😊
6-3. 深くネストしているStateを扱う場合
これも少々厄介です。
階層深いStatenの奥深くの値の更新や削除は、pertial stateをnestしたアクションで更新するのもきついので強引に直接更新するしかないのか?
と言う状態です。
これらの課題に直面すると、1ページ全てをHyperappでやらずに
シンプルなSteteでミニマルに使いなさいという事かなと
思うのですが、私は、Hyperappでシングルページアプリケーションを作るために、
引き続き、色々勉強してみます。
hypeappで実装されたアプリを集めて紹介しているサイトなんかもあるので
この辺でお勉強してみます。
hyperapp.rocks
7. eslint
オレオレな見やすさを重視にコーディングしていましたが、
派手に怒られました。私が怒られた内容だけメモっておきます。
$ ./node_modules/.bin/eslint ./src/*
/Users/hanohara/Desktop/private/develop/sharpen/src/views/messageModal.js
1:19 error Strings must use singlequote quotes
2:8 warning 'utils' is defined but never used. Allowed unused vars must match /h/ no-unused-vars
19:22 error Unexpected usage of doublequote jsx-quotes
21:11 error Empty components are self-closing react/self-closing-comp
✖ 1537 problems (1436 errors, 101 warnings)
1397 errors, 16 warnings potentially fixable with the `--fix` option.
err message | 意味 |
---|---|
Empty components are self-closing | コンテンツがない空のタグはself closingして(例: <img src="" />) |
A space is required before closing bracket | self closing してるスラッシュの前にはスペースをいれて |
Block must not be padded by blank lines | 関数のブロック{}内の最初に改行を入れないで |
Multiple spaces found before 'from' | 'from'の前に複数にスペースがあるよ |
Strings must use singlequote | String型は シングルクゥオートを使って |
Extra semicolon | 余計なセミコロンがあるよ |
Do not use 'new' for side effects | 副作用狙いでnew使うな |
Unexpected usage of doublequote | ダブルクォートはなしで |
Missing space before => | =>の前にはスペースを |
Missing space after => | =>の後にはスペースを |
Expected indentation of 12 spaces but found 10 | スペース12個分のインデントかと思いますが、10個分しかないっすよ |
Expected '===' and instead saw '==' | '===' じゃなくて '==' になってますよ |
A space is required after ',' | カンマの後ろにはスペースは必須です |
Missing space before function parentheses | functionの括弧の前にはスペースが必要です |
There should be no spaces inside this paren | 括弧の中にスペースは記入すべきじゃない |
Trailing spaces not allowed | 行末のスペースはいけません |
More than 1 blank line not allowed | 改行が2行以上あるのはだめです |
Unexpected space between function name and paren | ファンクション名と括弧の間に不要なスペースがあります |
Expected space or tab after '//' in comment | コメントの//の後ろに不要な空白かタブがあります |
Infix operators must be spaced | 中置演算子の前後は空白が必要です。(a=bとかa+"b"とかで出る) |
Arrow function should not return assignment | アローファンクションは割り当てステートメントを返すべきではない |
Unary word operator 'delete' must be followed by whitespace | 単項演算子 'delete'の後に空白が続く必要があります |