WebアプリからMonaca＆Onsen UIに移植して分かったアプリ実装のコツ

先にWeb版として開発していたツールを、ハイブリッドアプリとしてリリースするため、Monaca＆Onsen UIのプラットフォームに移植を行いました。

内部的なプログラム処理は、同じJavaScriptなので基本的に変わりませんが、Onsen UI特有の流儀に置き換える上で、色々と気づいた点があったので、書き残しておきます。

元のWeb版がシンプルなJavaScriptしか使っていなかったので、移植先のアプリもJavaScript Core版のOnsen UIを使用しています。

## まずはモック版を作ってみる

Onsen UIを使うのが初めての場合は、いきなりデータ登録などのプログラム部分を実装するのではなく、画面遷移やUI回りの動きだけをトレースしたモックを作ってみるのがいいと思います。

画面遷移については、基本的にページをスタックして積み重ねていく[ons-navigator](https://ja.onsen.io/v2/api/js/ons-navigator.html)と、メニューから別の画面へ切り替える[ons-splitter](https://ja.onsen.io/v2/api/js/ons-splitter.html)の2つを使えば、だいたいの動きが実現できます。

ただし、`ons-navigator`と`ons-splitter`を併用する場合は注意が必要なのですが、私の場合は`ons-splitter`の中に`ons-navigator`を入れ子にする形で、この2つの組み合わせを実現しました。

その逆のパターンだと、後々の処理でうまくいかなくなるケースが出てくるので、今のところこのやり方をオススメします。

```html:ons-splitterとons-navigatorの入れ子の例
<ons-splitter>
  <ons-splitter-side id="menu" side="left" width="220px" collapse>
    <ons-page>
      <ons-list>
        <ons-list-item onclick="fn.load('home.html')" tappable>
          Home
        </ons-list-item>
        <ons-list-item onclick="fn.load('page2.html')" tappable>
          Page2
        </ons-list-item>
        <ons-list-item onclick="fn.load('page3.html')" tappable>
          Page3
        </ons-list-item>
      </ons-list>
    </ons-page>
  </ons-splitter-side>
  <ons-splitter-content id="content" page="home.html">
  </ons-splitter-content>
</ons-splitter>

<ons-template id="home.html">
<ons-page id="home">
  <ons-navigator id="myNavigator" page="page1.html"></ons-navigator>
</ons-page>
</ons-template>

<ons-template id="page1.html">
<ons-page id="page1">
Page1
</ons-page>
</ons-template>

<ons-template id="page2.html">
<ons-page id="page2">
Page2
</ons-page>
</ons-template>

<ons-template id="page3.html">
<ons-page id="page3">
Page3
</ons-page>
</ons-template>
```

## Onsen UI リファレンスの読み方・使い方

Webからアプリ版に置き換えるには、要素を通常のHTMLタグからOnsen UIのコンポーネントに置き換える必要があります。

その時に参考になるのがOnsen UIの[公式サイト](https://ja.onsen.io/)にあるリファレンスですが、これには[CSS版](https://ja.onsen.io/v2/api/css.html)と[JS版](https://ja.onsen.io/v2/api/js/)があるので、どちらを見ればいいのか戸惑う人もいるでしょう。

特にWeb系のデザイナーやコーダー寄りの方だと、ついつい[CSS版](https://ja.onsen.io/v2/api/css.html)の方だけ読んで、コーディングを始めてしまう人も多いのではないでしょうか？

そうすると、見た目上はコンポーネントが再現できているのに、「フローティングボタンが追従しない」とか「固定ヘッダーの追従がもっさりしてる」といった予期せぬ不具合にたびたび遭遇します。

例えば、CSS版のリファレンスだと、Toolbarのコードサンプルは以下のように書かれていますが、実際このように記述してもヘッダーの固定化がうまく働きません。

```html:CSS版のコードサンプル
<div class="toolbar">
  <div class="toolbar__center">Navigation Bar</div>
</div>
```

JS版のリファレンスに書かれているように、`ons-toolbar`要素を使って記述すると、ヘッダーの固定化がスムーズに働きますが、これはOnsen UIがWeb Componentsとしての`ons-toolbar`要素に内部的に位置制御等をしてくれているからです。

```html:JS版のコードサンプル
<ons-toolbar>
  <div class="center">Navigation Bar</div>
</ons-toolbar>
```

では、CSS版のリファレンスは意味がないのかと言うと、そんなことはなくて、実際に生成されているOnsen UIのコンポーネントをChromeのデベロッパーツールなどで見ると、その意味がよく分かります。

「JS版のコードサンプル」のように記述すると、実際のDOM上は`class="toolbar"`や`class="toolbar__center"`といった属性が挿入されているので、CSS上は同じスタイルが適用されるのです。

```html:実際に生成されるDOM
<ons-toolbar class="toolbar">
  <div class="center toolbar__center">Navigation Bar</div>
</ons-toolbar>
```

なので、CSS版とJS版どちらの書き方で記述しても間違いだとは言えないのですが、位置の制御やUI的な動作が期待されるコンポーネントでは、`ons-*`といったWeb Components版の要素を使った方がいいでしょう。

逆に、Listのように単純に項目を並べて表示するようなコンポーネントの場合、CSS版とJS版どちらの書き方で記述しても影響がないと考えられます。

このような場合は、[CSS版](https://ja.onsen.io/v2/api/css.html)のリファレンスに掲載されているコンポーネントのスタイルを積極的に活用しましょう。

```html:CSS版のコードサンプル
<ul class="list">
  <li class="list-item">
    <div class="list-item__center">Item A</div>
  </li>
  <li class="list-item">
    <div class="list-item__center">Item B</div>
  </li>
  <li class="list-item">
    <div class="list-item__center">Item C</div>
  </li>
</ul>
```
```html:JS版のコードサンプル
<ons-list>
  <ons-list-item>
    <div class="center">Item A</div>
  </ons-list-item>
  <ons-list-item>
    <div class="center">Item B</div>
  </ons-list-item>
  <ons-list-item>
    <div class="center">Item C</div>
  </ons-list-item>
</ons-list>
```

## ページ遷移からスタックナビゲーションへの移行

今回、移植元となったWebアプリでは、昔ながらのサイトでお馴染みのページ遷移を行い、遷移したページ毎にJavaScriptをロードする形でプログラムを実装していました。

Onsen UIでは、前述の通り1ページ内に`ons-page`という要素を記述して、擬似的に画面を切り替えるスタックナビゲーションをベースにしているため、いわゆるSPA（Single Page Application）として書き直す必要があります。

といっても、それほど難しく考える必要はなく、今回のアプリ版のプログラムは、ざっくりと以下のような構成で、それぞれの処理を移植しました。

ページ毎にJSをロードさせる変わりに、[ons-navigator](https://ja.onsen.io/v2/api/js/ons-navigator.html)のサンプルにあるように、`init`のタイミングでページID毎に振り分けたプログラムを実行しています。

`onclick=""`で使用するような、アプリ全体で使える汎用的な関数などは、それらの外側に直接記述する形にしました。

```javascript:プログラム全体の構造
//メニュー切り替えのための処理
window.fn = {};
window.fn.open = function() {
  var menu = document.getElementById('menu');
  menu.open();
};
window.fn.load = function(page) {
  var content = document.getElementById('content');
  var menu = document.getElementById('menu');
  content
    .load(page)
    .then(menu.close.bind(menu));
};

//ページを切り替えた時の処理
document.addEventListener('init', function(event) {
  var page = event.target;

  if (page.id === 'page1') {

    //page1がロードされた時に実行されるプログラム

  } else if (page.id === 'page2') {

    //page2がロードされた時に実行されるプログラム

  }
});

//その他アプリ全体で使う関数
function allDataClear() {

}

function displayError() {

}
...

```

## パラメーターの受け渡し

Web版のプログラムでは、URLの末尾に`?live=0`のようなパラメーターを付与することで、localStorageから該当のデータを呼び出したりしていました。

Onsen UIでこのような画面遷移時のパラメーターの受け渡しを行うには、`ons-navigator`の`pushPage()`メソッドなどで用意されている`option`を利用します。

例えば以下のように記述すると、遷移先のpage2上のJS内で`page.data.live`という形で渡したデータを使用することができます。

```javascript:
document.querySelector('#myNavigator').pushPage('page2.html', {data: {live: 0}});
```
では、`?live=0&artist=0`のように、渡したいパラメーターが2つ以上ある場合はどうしたらいいでしょう？

実は`option`で渡せるものはオブジェクトで指定可能となっているので、値がいくつあっても問題ありません。以下のように記述することで、複数のパラメーターを引き回すことができます。

```javascript:
document.querySelector('#myNavigator').pushPage('page2.html', {data: {live: 0, artist: 1}});
```

ただし、リファレンス上は`popPage()`や`load()`でも`option`が指定できるように書いてあるのですが、上記のようなやり方ではうまくパラメーターを渡すことができませんでした。

このような処理を行いたい場合は、素直に`pushPage()`か`resetToPage()`メソッドを使った方が良さそうです。

## ダイアログは内部的に呼び出した方が便利

こちらもデータ登録系などでよく使うダイアログですが、[リファレンスのサンプル](https://ja.onsen.io/v2/api/js/ons-dialog.html)では、`ons-dialog`要素でダイアログを作ってから、それを表示させるやり方が紹介されています。

ですが、これだと`ons-dialog`が`ons-page`の外側になってしまうので、ページ毎に処理を書いている都合上、色々とやりにくい点が出てくるし、開いた先のダイアログに変数やパラメーターをどう渡すか？といった問題が出てきてしまいます。

それではどうするかと言うと、実は`ons-dialog`は`ons.notification`を使って以下のようにJS内から直接呼び出すことが可能です。この方が、ページ毎のプログラムに直接記述することができるので、今回のようなケースでは扱いやすいですね。

```javascript:確認ダイアログの呼び出し
ons.notification.confirm({
  message: '全てのデータを削除してよろしいですか？',
  title: '',
  primaryButtonIndex: 1,
  cancelable: true,
  modifier: 'material',
  callback: function(index) {
      switch(index) {
        case 1:
          //OKを押した時の処理
          localStorage.clear();
          fn.load('home.html');
          break;
        case 0:
          //Cancelを押した時の処理
          break;
      }
  }
});
```

## 便利なコンポーネントはどんどん使おう

今回のアプリでは、登録したデータを一覧表示する機能があるのですが、最終段階で実データを投入してみたら、思ったよりも表示に時間がかかってしまったということがありました。

Web版では気になるレベルではなかったのですが、やはりOnsen UIの場合はDOMの生成に多少時間がかかるのでしょうか…？

もちろん「スクロールに応じて何件ずつ読み込む」などの処理を、自力で対処することもできると思うのですが、Onsen UIではモバイルアプリでよく想定される問題に対応した、便利なコンポーネントや機能がたくさん用意されています。

[ons-lazy-repeat](https://ja.onsen.io/v2/api/js/ons-lazy-repeat.html)は、画面の表示領域に応じてリスト要素を生成してくれる便利なコンポーネントなのですが、非常に簡単に使える上に、汎用性が高いのが特長です。

```javascript:ons-lazy-repeatでオブジェクト内のデータを参照する例
infiniteList.delegate = {
  createItemContent: function(i) {

    return ons.createElement('<ons-list-item>item[i].title + item[i].date + item[i].area + item[i].place</ons-list-item>');

  },
  countItems: function() {
    return item.length;
  }
};

infiniteList.refresh();
```

`ons-lazy-repeat`では、`delegate.createItemContent`メソッドにindexとtemplateという引数を渡せますが、上記の例の場合は予め用意しておいた`item`というオブジェクトにインデックス`i`を使ってアクセスし、様々なデータを呼び出して内容を一覧表示しています。

templateの方は、どのように使うのかちょっと分からなかったのですが、indexだけでもこれだけ自由度の高い設計が可能なので、大抵のことはこれで対処できそうですね。

## まとめ

Onsen UIを使うのは初めてでしたが、既存のコンポーネントやメソッドをうまく活用することで、効率的にユーザビリティの高いアプリを構築できました。

Onsen UI特有のポイントだけ押さえておけば、Web版からの移行も特に問題なかったので、先にJSとHTMLでロジックだけ組んだり、Web版とアプリ版を並行して開発するなどのワークフローも導入できそうな気がします。

React版やAngular JS版などは、また機会があれば使ってみたいと思います。