スタッフブログ Staff Blog

MTのData APIを活用してオフライン対応のiOSアプリを作成する

現在ネットワーク接続がオフラインの場合でも利用できるiOSのネイティブアプリを試作しています。コンテンツはMovable Typeに格納されているので、Data APIを利用してデータのやりとりをすることを検討中です。目下ある程度データをやりとりする仕組みができあがったので、Data APIの利用例としてご紹介してみたいと思います。

iOSアプリでデータを蓄積する方法

前提知識として少しご紹介しますが、iOSアプリでオフライン対応を行うには、何らかのデータベースを利用してコンテンツを蓄積する方法が考えられます。

データベースの仕組みにはCore Data、SQLite、Realmなどがありますが、今回はRealmを使用することにしました。

データの取得

iOSアプリをオフラインでも利用可能にするために、まずアプリ起動時にすべてのコンテンツをダウンロードする必要があります。これは、標準で用意されている記事を取得するエンドポイントを利用することで実現できます。

https://yourdomain/path/to/mt/mt-data-api.cgi/v3/sites/[blog_id]/entries?limit=10000

MTEntriesタグでlastn="0"を指定して全ての記事を取得するといったことができませんので、limitに大きな値を指定する必要がありました。(limit=0にすると50件しか取得できません。全記事を取得するパラメーターの要望をFogBugzに書こうかなとも...。)

エンドポイントにアクセスして返ってきた結果をRealmに保存します。Realm Browserでデータベースを確認すると、以下のように記事データが入っていることが確認できます。
Realm Browserでデータベースの内容を表示した画面

全記事の取得は負荷が高い

今回のブログには1,000件強の記事が入っていたため、先のエンドポイントにアクセスしても結果が返ってくるまでにしばらく時間を要しました。このままではアプリの起動時間が長くなりますし(ちなみに、予めデータベースをアプリに入れておく手法もあります)、アプリのユーザー数が増えアクセス数が増えた場合が不安です。

そこで、アルファサード株式会社が提供している「ResourceObjectプラグイン」で、予め全記事のデータが入ったJSONをサーバーに出力しておきます。以下のような簡単なテンプレートを書くだけで、mt-data-api.cgi/v3/sites/[blog_id]/entriesにアクセスした時と同じ内容のJSONが出力されます。

<mt:setVars>
    fields=title,id,body,more,keywords,basename,tags,categories,date,permalink
</mt:setVars>
<mt:Entries lastn="0"><mt:If name="__first__"><$mt:EntriesCount setvar="total_results"$></mt:If></mt:Entries>

{"totalResults":<$mt:Var name="total_results"$>,"items": [<MTEntries glue="," lastn="0"><mt:ResourceObject stash="entry" fields="$fields"></mt:Entries>]}

静的なJSONを取得するようにした結果、アプリの起動時間を大幅に短縮することができました。

記事データの更新

前項でアプリ内にブログのコンテンツを全て保存しましたが、その後もブログが更新されていくため、定期的にアプリのデータベースを更新する必要があります。そこで、記事を取得した日を記録しておき、以下のようなパラメーターで記事を取得するエンドポイントにアクセスすることで、2017年7月30日以降に編集が行われた記事が取得できます。これで得たレスポンスを元にデータベースを更新すれば完了です。

mt-data-api.cgi/v3/sites/[blog_id]/entries?limit=10000&dateFrom=2017-07-30&dateField=modified_on

記事データの削除

頻度は少ないかもしれませんが、記事の新規追加・既存記事の編集の他に記事のステータスが「非公開」に変更される、また記事が削除されることも予想されます。

ステータスが「非公開」に変更された記事データの取得

記事を取得するエンドポイントを利用してステータスが「非公開」の記事も取得することはできるのですが、Movable Typeの認証を通過させる必要があります。認証情報をアプリ内に隠し持つこともできるかもしれませんが、Data APIのエンドポイントを拡張し、認証なしで指定日以降にステータスが「非公開」になった記事IDの配列を取得できるように開発を行いました。

ステータスが「非公開」の記事を取得する部分のコードは以下のようになりました。(余談ですが、MT::Objectのloadメソッドを覚えると様々なことができるようなります。)


package DataAPIExtendEntries::DataAPI;
use strict;

use MT::DataAPI::Endpoint::Common;
use MT::DataAPI::Resource;
use MT::Entry;

sub get_unpublished_entries {
    my ($app, $endpoint) = @_;
    my ($blog) = context_objects(@_) or return;
    my $terms;
    my @ret;

    $terms->{'blog_id'} = $blog->id;
    $terms->{'class'} = 'entry';
    $terms->{'status'} = 1;

    if (my $dateFrom = $app->param('dateFrom')) {
        if ($dateFrom =~ m/\-/) {
            $dateFrom =~ s/\-//g;
        }
        $terms->{'modified_on'} = {'>=' => $dateFrom . '000000'};
    }

    my @entries = MT::Entry->load($terms);
    foreach my $entry (@entries) {
        my $id = $entry->id;
        push(@ret, $id);
    }

    my $length = @ret;

    return {
        totalResults => $length,
        ids => \@ret
    };
}

Data APIのエンドポイントの拡張は、上野様の「Movable Type DataAPI拡張プラグイン作成の第一歩 - uehatsu's tech blog」が参考になりました。ありがとうございます。

削除された記事データの取得

削除された記事の情報を取得するのは厄介です。なぜならMovable Typeでは記事を削除すると、記事を保存しているmt_entryテーブルから記事情報が削除されてしまうためです。標準ではどのようにしても削除した記事情報を得ることはできません。

そのため、公式の「Movable Type オブジェクト・リファレンス - MT::Object」を参考にMT::ObjectのサブクラスであるDeletedObjectクラスを作成し、記事を削除した際にDeletedObjectクラスに記事IDと削除日時を記録することを考えました。DeletedObjectクラスのプロパティは以下のように定義し、データベースにmt_deletedobjectテーブルが作成されました。


package RecordDeletedObject::DeletedObject;
use strict;

use base qw( MT::Object );
__PACKAGE__->install_properties( {
    column_defs => {
        'blog_id' => {
            'label' => 'BlogID',
            'type' => 'integer'
        },
        'class' => {
            'label' => 'Class',
            'size' => 64,
            'type' => 'string'
        },
        'deleted_object_id' => {
            'label' => 'DeletedObjectID',
            'type' => 'integer'
        },
        'id' => 'integer not null auto_increment',
    },
    indexes => {
        'blog_id' => 1,
        'class' => 1,
    },
    datasource => 'deletedobject',
    primary_key => 'id',
    class_type  => 'deletedobject',
    audit => 1,
} );

その後MT::App::CMS::cms_post_delete.entryフックポイントを利用して記事情報の登録する関数、また先に紹介した『ステータスが「非公開」に変更された記事データの取得』と同じようにMT::Objectのloadメソッドを利用してテーブルのデータを取得する関数を作成しました。結果、mt-data-api.cgi/v3/sites/[blog_id]/deletedentriesにアクセスすると以下のキャプチャ画像のように指定日以降に削除した記事のIDが取得できるようになりました。
自作のdeletedentriesエンドポイントにアクセスし、指定日以降に削除した記事のIDを取得した画面

まとめ

標準で用意されているData APIのエンドポイントに加え、削除した記事などを取得できるエンドポイントを追加することで、Data APIを活用してオフラインに対応したiOSのネイティブアプリが作成できることが分かりました。コンテンツを一覧表示したり記事を表示したりする際、サーバーへのアクセスがないため非常に高速に表示されます。(画像がある場合は別になってしまいますが...。)

本稿がMovable TypeのData API活用例、Data APIを活用したネイティブアプリの作成例として参考になりましたら幸いです。


お知らせ(PR)

当社関連会社であるファサード株式会社では、Movable Type / PowerCMSで運用中のウェブサイトを10分でアプリ化できるパッケージ「Apliko」(モバイルアプリパック)を販売しております。詳しくはAplikoのウェブサイトをご覧ください。

スタッフの誕生日会。(回しそうめん)

こんにちは、谷貞です。

今回の話題は6月30日生まれのスタッフの誕生日会です〜!

(前回のブログの話題はなんだったかな、と振り返ったらスタッフの誕生日会でした。)

うちのスタッフは3・6・9月生まれと程よく分散しているのでちょうど良い間隔ですね。

前回は会社でホットプレート焼肉でしたが、今回はなんと「回しそうめん」!

主役の英樹くんは最近お昼に麺類ばかり食べていると言っていたので、優弥くんのアイデアで流しそうめんに決定。

機械を買っても1回しか使わなさそうだったので知り合いからお借りしました。

個人のFacebookに動画をあげたら「回転そうめんじゃね〜(о´∀`о)」と突っ込みをいただいたので「流しそうめん」ならぬ「回しそうめん」です。

近くで流れるそうめんの写真を撮っていなかったので画像で遠目にご確認ください。。。

いつもはスマホで恒例の集合写真を撮っていたのに、今回は初めて三脚を持って来て一眼レフで撮ったせいか、集合写真を撮るのを忘れていましたが、ちょうど良いタイミングでセルフタイマーを起動していたので乾杯のシーンが撮れていました^^(若干ボケボケですが...

最近はレンジでそうめんを茹でられるので便利ですね〜

ちょうど真夏日で、冷たい素麺がぴったりでした。美味しかった^^

ケーキはいちごがたくさん乗った可愛いハート型。

おめでとうございました♪

表示パフォーマンスの改善事例紹介(JavaScript編)

オフタイムに趣味で閲覧しているとあるサイトにおいて、表示完了までに約8秒(Firefoxを使用)の時間を要するサイトがありました。仕事ではないのであまり気にかけてはいなかったのですが、他の閲覧者の方から「IEで見ると40秒以上かかるのですが...」という話が上がり、フロントエンド・エンジニアとして少し調べてみるとにしました。

時間がかかっているのはJavaScript

実際にIEで閲覧してみると確かに非常に長い時間待たされます。ただ画面には「読み込み中」のテキストが表示されているので、「もしかするとブラウザでの処理に時間がかかっているのか」と考えました。

そこでIEの開発者向けツールでプロファイリングをしてみると、長い時間待っているはスクリプトの処理が続いていることが分かりました。(赤い棒が長時間にわたり伸びています。)

プロファイリング結果からコードの問題の特定

ブラウザを仕事の際に普段使用しているChromeに戻し、再度プロファイリングを取ってみました。結果、下記のようなデータを得ることができました。
Chromeの開発者ツールで取得したプロファイリング結果の表示

画像では長時間処理に時間がかかっている部分にフォーカスしています。XMLHTTPRequestの処理に時間がかかっているのですが、さらに探っていくとsuccess内の処理に問題がありそうなことが分かります。JavaScriptのファイル名と行数も表示されているので、示された部分(下記サンプルコード)を中心に見ていくことにします。

$.ajax({
  url: "/xml/entries.xml",
  dataType: "xml",
  success: function(xml) {
    var layer = $('<div id="news" />');
    var ul = $('<ul />');
    $(xml).find("item").each(function(i) {
      var tag = $('<li class="item"><span class="date">' + $(this).find("entryDate").text() + '</span><spa class="title">' + $(this).find("entryTitle").text() + '</span></li>');
      layer.append(ul.append(tag));
    });
    layer.append(ul);
  }
});

先の画像には表示されていないのですが、開発者ツール内のBottom Upタブの先頭には「appendChild」や「Recalculate Style」が表示されていました。以上のことから問題はlayer.append(ul.append(tag));にあると推測します。もっともループ内で.append()を実行するとパフォーマンスに影響を与えることは知っていたので、ここが問題だと確信していました。

※jQueryのコードを読めば分かるのですが、.append()の中でJavaScriptネイティブの.appendChild()をコールしています。

.append()処理の改善

そこでループ内で.append()を実行することをやめ、下記サンプルコードのようにループ内ではHTMLの文字列を連結しループ外で.append()を実行するようにしたところ、パフォーマンスは大幅に改善。Chromeで約7.5秒要していた処理が約2秒に、またIEでは40秒以上要していた処理が約3秒で完了するようになりました。

$.ajax({
  url: "/xml/entries.xml",
  dataType: "xml",
  success: function(xml) {
    var layer = $('<div id="news" />');
    var ul = $('<ul />');
    var lists = '';
    $(xml).find("item").each(function(i) {
      var tag = $('<li class="item"><span class="date">' + $(this).find("entryDate").text() + '</span><spa class="title">' + $(this).find("entryTitle").text() + '</span></li>');
      lists += html;
    });
    ul.append($(lists));
    layer.append(ul);
  }
});

ただ、これでもBottom Upタブの先頭には「Recalculate Style」が出てきます。ふと開発者ツールのElementsタブでul要素を見てみると、非常に多くのli要素が追加されていることに気付きました。XMLには必要以上の記事が出力されていたようです。

そこで明らかに表示されない記事をDOMに追加しないように改善したところ、パフォーマンスはさらに改善しChromeでは一瞬で表示が完了するようになりました。まだ「Forced reflow」の警告が出ていたりコードに気になる点がありますが、本記事ではここまでの紹介とさせて頂きます。

長期間の運用に伴って落ちた表示パフォーマンス

この事例では、サイト運用開始当初は表示パフォーマンスの問題はなかったと推測します。恐らく新規構築で記事数が少ないために、表示パフォーマンス上良くないコードであったとしても処理がすぐに終わっていたのだろうと考えられます。

最初に負荷テストを実行して問題がないか確かめることもできますしコーディングのベストプラクティスを適用することで避けることも可能ですが、運用開始後定期的に表示パフォーマンスに問題はないかチェックすることも重要ではないかと考えます。

Vue.jsに触れてみて

半年程前から『Vue.js(ビュージェイエス)』を扱うようになりました。Vueは、公式サイトでは「ユーザーインターフェイスを構築するためのプログレッシブフレームワーク」と紹介されています。リアクティブなデータバインディングとコンポーネントシステム等が特徴と言われますが、その辺りは「はじめに - Vue.js」から順にお読み頂いた方が理解しやすいでしょう。

本記事では、私がVueを触って感じたメリットなどをいくつかご紹介したいと思います。

低い学習コスト

以前Angularを使用してアプリケーションを組んだことがありますが、機能が多く学習に時間がかかりそうだなという印象を持ちました。しかし、Vueは数時間手を動かしながら学習することでコアな機能は習得できるかと思います。株式会社ピクセルグリッドさまが毎週配信されている『CodeGrid』でもちょうど初心者向けの連載が始まっていたところで、私はそれを読みながら学習を進め、あっという間に一つのアプリケーションを作成することができました。

現在は公式ドキュメント(日本語版)を読み、記事にはなかった情報などを習得しています。公式ドキュメントも親しみやすく、Vueの知識を付けるには最適でしょう。

見通しの良いコードの実現

Movable Typeを使用して何かの情報...例えば病院のリストや金融機関のリスト、ニュースなどの記事をJSONやXMLで出力し、一覧表示するケースはよくあるのではないかと思います。Vueではテンプレートとロジックを容易に分離でき、見通しの良いコードに仕上げることができます。

例えば、最近私が見たとあるサイトでは次のようにしてXMLを読み込みHTMLを組み立てていました。
(元のコードは実行速度に難があったため、チューニングしたものを掲載します。)

$.ajax({
  url: "news.xml",
  dataType: "xml",
  success: function(xml) {
    var ul = document.createElement('ul');
    var lists = [];
    var items = xml.getElementsByTagName('item');

    for (var i = 0, nItems = items.length; i < nItems; i += 1) {
        var date = items[i].getElementsByTagName('entryDate')[0].textContent;
        var text = items[i].getElementsByTagName('entryTitle')[0].textContent;
        var html = '<li class="item"><a href="#" data-itemindex="' + i + '">' +
                    '<span class="date">' + date + '</span><span class="title">' +
                    text + '</a></li>';
        lists.push(html);
    }

    ul.innerHTML = lists.join('');
  }
});

Vueを使うと、以下のようにテンプレートとXMLを読み込むロジックを分離することができます。(ここでは.vue拡張子の単一ファイルコンポーネントの仕組みを利用しています。)記事データをdataオブジェクトのentriesプロパティ入れるとビューの更新が行われ、即座に記事リストが表示されます。

<template>
  <ul>
    <li v-for="entry of entries" :key="entry.id" class="item">
      <a href="#"><span class="date">{{entry.date}}</span><span class="title">{{entry.title}}</span></a>
    </li>
  </ul>
</template>

<script>
export default {
  data () {
    return {
      entries: []
    }
  },
  created () {
    $.ajax({
      url: 'news.xml',
      dataType: 'xml'
    }).done((xml) => {
      const items = xml.getElementsByTagName('item');
      let entries = [];

      for (let i = 0, nItems = items.length; i < nItems; i += 1) {
        const id = items[i].getElementsByTagName('entryID')[0].textContent;
        const title = items[i].getElementsByTagName('entryTitle')[0].textContent;
        const date = items[i].getElementsByTagName('entryDate')[0].textContent;
        entries.push({
          index: i,
          id: id,
          title: title,
          date: date
        });
      }

      this.entries = entries;
    });
  },
};
</script>

アプリケーションにはリスト表示以外のUIやメソッドも必要でしょうから、どちらがコードを記述しやすいか、またどちらがメンテナンスしやすいか、は自ずと分かるのではないかと思います。

リアクティブシステムでロジックの複雑さを回避

記事一覧にページネーションを実装する例について考えてみましょう。

まず、dataオブジェクトへ現在のページ・1ページの表示記事数・最大ページを追加します。そして「次のページへ」「前のページへ」がクリックされた時は、dataオブジェクトの現在のページ数のみ変更します。現在のページ数が変更されると自動的にdispEntriesメソッドが呼ばれ、指定したページを表示するために必要な記事を抜き出してビューに反映できるようになりました。

ボタンをクリックした時のメソッド内でビューを更新するメソッドを呼ぶ必要がないため、メソッドの呼び出しがあちこちに点在し蜘蛛の巣のように絡み合うような事態を避けることができるのが大きなメリットではないかと感じました。

<template>
  <div id="news">
    <ul>
      <li v-for="entry of dispEntries" :key="entry.id" class="item">
        <a href="#"><span class="date">{{entry.date}}</span><span class="title">{{entry.title}}</span></a>
      </li>
    </ul>
    <button @click="loadPrevPage" v-if="page !== 0">前のページ</button>
    <button @click="loadNextPage" v-if="page !== maxPages">次のページ</button>
  </div><!-- /#news -->
</template>

<script>
export default {
  data () {
    return {
      page: 0,
      nPages: null,
      itemsPerPage: 8,
      maxPages: 50,
      entries: []
    }
  },
  created () {
    // 「見通しの良いコードの実現」で紹介したコード
  },
   methods: {
    loadPrevPage () {
      this.page -= 1;
    },
    loadNextPage () {
      this.page += 1;
    }
  },
  computed: {
    dispEntries () {
      var startItemIndex = this.page * this.itemsPerPage
      return this.entries.slice(startItemIndex, startItemIndex + this.itemsPerPage)
    }
  }
}
</script>

また、現在表示されているページに応じて「前のページ」を消すことも容易です。button要素にv-if="page !== 0"としておけば、dataオブジェクトの現在のページ数が0になった時、ボタンは自動的に非表示になります。

vue-webpack-boilerplateで開発が便利に

vue-cli」を使用することで、Vueを使用したプロジェクトに必要な各種ファイルが用意され、スピーディーに制作を開始することができました。

プロジェクトのテンプレートはいくつかあるようですが、私は公式に配布されている「vue-webpack-boilerplate」を利用しました。(コマンドラインでvue init webpackを実行するとダウンロードされます。)

vue-webpack-boilerplateを利用して大変便利だったのは、画面の差分更新(Hot Reloading)を実現する「webpack-hot-middleware」が組み込まれていた点です。例えば何かのリストをフィルタ表示している時のビューを作成する場合、Vueのテンプレートを編集して画面が自動リロードされるとまた一から条件を設定してフィルタをしなければなりません。しかし、vue-webpack-boilerplateを利用していれば変更があったところのみを上手く画面に反映してくれるので、一から条件を設定しなおす必要がありませんでした。この差分更新(Hot Reloading)は、開発の大きな助けになるのではないかと感じました。

PostCSSを用いた作業効率化の事例紹介

案件概要

下記のような現状のサイトをスマートフォン対応(マルチスクリーン対応)したいとのご要望で、コーディングのみのご依頼です。

  • 画面上部にグローバルナビ、画面中央の右側にローカルナビを配置したオーソドックスな企業サイト
    • ページ数は100ページ以上
    • ナビゲーションはテキストとCSSでスタイリングされている
  • コンテンツ内容(グループ)毎にCSSが準備されスタイリングされている
    • コンテンツ内容(グループ)内ではモジュールが整備され、ページ間で統一した見栄えを提供している
  • .scssファイル等の提供はなし

対応

サイトを拝見したところ、最近ではレスポンシブWebデザインを採用してマルチスクリーン対応するところをPCのみ対応で制作したという状況のようです。モジュール(コンポーネント)のデザインは今でもよく見かけるものばかりで、マルチスクリーン対応のために全面リニューアルする必要は全くなさそうです。

よって、既存のCSSに手を入れる、具体的にはメディアクエリを利用してスタイル付けを行っていくことにより、さまざまなスクリーンサイズに対応することになります。(もちろん、モジュールのデザインによっては部分的に新たなデザインを検討する必要はあるでしょう。)その際の課題として、以下の2点が考えられます。

  • .cssファイル数が多い
  • .scssファイル等の提供がない

このまま.cssファイルを編集していくか、それとも.scssファイルに変換して編集していくか検討しましたが、どちらも負荷のかかる作業です。そこで、今回はCSSパーサーである「PostCSS」を利用して、既存の.cssを活かしつつ作業の効率化を図ることにしました。

具体的には、以下のPostCSSプラグインを導入しました。

これにより以下の利点がもたらされます。

  • 変数(CSS Custom Properties)がどのファイルでも利用可能になる
  • custom media queriesを利用することでメディアクエリが管理しやすくなる
  • CSSセレクタ毎にメディアクエリを利用してスタイルを記述することができるようになり(既存のセレクタ内にネストして記述できる)、また最終的に「CSS MQPacker」がクエリ毎に設定を集約してくれる
  • 最新のブラウザ環境に合わせてベンダープレフィックスの整理が行われる

まとめ

PostCSSを利用して新しいCSSの仕様や有益な処理を取り入れることにより、容易に作業の効率化を図ることができました。現在のCSS構成を変えることなく利用するので、あるページに設定したスタイルが他のページに悪影響を及ぼし始めた、ということを防ぐことができます。

案件に合わせて最適なツールを選択し利用していくことで、提供する作業の品質を向上させていきたいと考えています。

お問い合わせ Contact

制作のご依頼、ご相談などは下記のフォームからご連絡ください。