Google先生が考えているモバイル体験2017(Web/ネイティブ)

Googleは「モバイルWebの読み込み負荷の問題」や「ネイティブアプリのユーザー体験の冗長さ」を解消するために、昨今いろんな施策を出している。
今年に入ってからもいろいろな実装をリリース/プレビューとして入れ込んできていて、今現在の状況が追いかけづらいので何が何なのか一旦整理しようと思う。

  • AMP (Accelerated Mobile Pages)
  • PWA (Progressive Web App)
  • WebAPK
  • Instant Apps

今回取り上げるのは以上の4つである。


AMP (Accelerated Mobile Pages)

AMPは、WebサイトをGoogleのキャッシュサーバーから配信することで高速な表示を目指そう、という技術の総称である。すでに日本でも実装済み。

たまにモバイル検索結果で出てくるこの⚡マークが目印。

 
 

 
 
基本的には記事メディアでの用途になる。現行ですでに実装されており、対応サイトも多い。

Pros

高速

これに尽きる。Googleがキャッシュしているのでムッチャ速い。
データ転送量が従来の1/10にもなるという。

あくまでGoogle検索上での実装なので、もちろんiOS/Android問わずその恩恵に預かることができる。

Cons

基本的には開発コスト/広告周りがデメリットとしてよく挙げられる。
AMPに対応したHTMLファイルを別途別ページとして用意する手間はがっつりかかってしまい、それなりの構造化作業を必要とする。

タグ制限/独自タグへの置き換え

script , frame,input , labelなど禁止されているタグもいっぱいある。
このへんはChromeでデバッグが可能。
MediaElements系は<amp-image />, <amp-video />などの独自タグに置き換えなければいけないなど、制限が多い。
ソーシャル連携のための<amp-twitter /> などもあって、対応には独自の知識が何かと必要になりそう。

AMP JS以外のスクリプトの制限

jsによる実行ブロックを防ぎ、ページの高速化を図るために amp HTMLでは規定のjavascriptファイル以外の実行を許されていない。
当ブログもAMPのプラグイン入れたけど、コードブロックの表示にhighlight.jsを使っているため、特にその辺はまともに動いていないので、この辺はがっつりCSSで描画をなおす必要がありそう。

広告

js/iframeなどの実行が許されていないので、当然既存の広告プラグインもそのままでは動作しない。
このへんはGoogleが <amp-ad /> などの専用タグを使った施策を紹介している。

 
 
 


PWA (Progressive Web App)


Webアプリ(サイト)をネイティブアプリみたいに使おうぜ、というGoogle先生の施策の一つである。
プログラムやデータをバックグラウンドプロセス上でキャッシュすることで、ページの高速化やオフラインでの動作が可能になる。
また付随技術としてWebアプリでもPush通知が使えるようになる。

ここにサンプルになりそうなサイトが溜まっている。
 
PWA Rocks
 
特にAndroidだと、対応サイトは「ホーム画面に追加」してネイティブアプリの感覚で扱うことができる。

Pros

ServiceWorker/ ホーム画面アプリモードによるスタンドアローン

W3Cで策定されている、ServiceWorker API というWebページとは別に実行されるスクリプトを使う。バックグラウンドプロセスみたいなものかな?
(WebWorkerは別スレッドみたいなもんだから間違えないように。)

Android Chromeの場合、サイトのルートに規定のjsonファイルを置けば、GoogleがPWA対応アプリとして認識し、ホーム画面への登録を促すToastを出してくれる。
タップすると、すぐにホームにアイコンが追加されるはず。

Push通知

Push API(WebPush)も基本このServiceWorkerのプロセス上に乗っているはずなので、PUSH通知ができる。
対応サイトでは、通知の認可を求めてくるようになる。
使える場面は結構多いはず。

Cons

iOSとかいうやつ

iOS Safariは ServiceWorker/WebPushなどのAPIが2017.1 現在非対応である。
要するにオフラインでの プログラム/APIからのデータのキャッシュや PUSH通知機能を使うことができない。

iOSは現状でもホーム画面アプリモードとしてWebアプリを登録自体は可能である。(Safariが立ち上がらず、ヘッダーとか隠せるようになるやつ。)
できないこともないが、もちろんオフラインで操作することはできない。 (あるいはAppCacheなどで頑張る)
 
URL遷移をやろうと思ったら、普通に遷移するとネイティブのSafariが立ち上がってしまうので、シングルページアプリケーション的なルーティングが必要になってくる。  
また、Webアプリの実行状態から抜けるたびにレンダリングやルーティングの状態がリセットされるので、状態を再度復元しておこうと思ったら、
localStorageなどを駆使して、がっつりアプリケーションの状態を保存するアーキテクチャを用意しないといけない。
ネイティブアプリ/SPA的な知識がかなり要求されてくる。
 
 
個人的には面白そうな分野なのでiOS SafariにはぜひServiceWorker,WebPUSH等の技術に対応してもらいたい。
だけど、Appleが考える(Appleが胴元の)ユーザーエクスペリエンスのあり方と微妙に違う気もするので、ホーム画面アプリとしてのPWAには及び腰になりそうな感じもする。

バックグラウンド動作でのバッテリー消費とかメモリ食いとか

PCのChromeアプリが裏で連れてくる、元気に跳ね回るプロセスのみなさんを彷彿とさせて若干不安になる。
Facebookアプリのバッテリー消費プロセスと同様のことがWebサイト開発者側の実装次第で起きるとしたら、携帯端末の状態が結構カオスになりそうな気もする。
このへんは技術要件をがっつり調べたわけではないので、確認しておきたい。
 
 
 


WebAPK(Android)


ここから先はAndroid独自の話となる。

WebAPK というのは先日実装が開始された機能である。   
Google、Android用ChromeにWEBサイトをアプリとして利用可能にする「WebAPK」を実装開始 -ガジェット通信-
 
 
これによると、先のPWAをパッケージングして、ネイティブアプリと同じようなchromium上で動作させる仕組みらしい。要するにWebView上のアプリである。
WebサイトをAndroidアプリのパッケージ形式であるAPKに変換して端末にインストールさせる。  
具体的にPWAのホーム画面登録時とユーザー体験としてどのように違うのかは、資料が足りないのでよくわかっていない。  

(規定のWebView上で動作させるようになるとすれば、Webアプリケーション側から専用のAPIとか叩けるようになったりするのかな?)
ただ、この方式のアプリの配布はどうやらGooglePlay側からできるようになるそうで、そこは面白いかな、と思う。 
(WebAPKの配布は既にGooglePlay側で機能しているそうである。QRコードからもインストールできるようになるとか。)
 
 
 


Instant Apps (Android)

Chrome Nightly Buildから実験的に実装が始まった機能。
これに関してはHTML5のWebアプリを動かそう!という文脈ではなく、コンパイル済みのAndroidのネイティブアプリをChrome上で一時的に呼び出して動作させようという施策のようである。

公式の先のVideoや、下のgifを見ると使用感がわかりやすい。
 
 
公式
事前のインストールなしでアプリを実行できる「Android Instant Apps」の実地試験が開始される -Gigazine
Android Instant Apps starts initial live testing
 
 

 
 

既存のAndroidネイティブのコードがそのまま利用可能で、Android4.2から対応できるようになるそうだ。(やめろ)
呼び出しの重さや対応できるAPIが気になるところ。SDKが提供されたら触ってみたい。
 
 
 


まとめ

AMPのようなGoogle独自の高速化施策を打つ一方、Webアプリとネイティブの垣根を取り払っていくようなアプローチが続々出てきて、大変だと思いつつものによっては面白いこともできそう。
全部の実装が果たして軌道に乗るのか、どれに乗っかるべきなのかは注視していきたい。

Read More

LWP::Simple::get() のハマりポイント

昔から動いているサイトがアクセスしている外部サイトのAPIが今夏に新バージョンになるということで、対応作業をしていました。私達のサーバサイド言語はPerlです。

新旧API、どちらもJSONを返すのですが、構造が若干異なっているという感じ。各アイテムのキー名にそれほど違いはなかったので、テンプレート側(デザイナー側)の負担を最小限にするために、新APIで叩いて返ってきたJSONの構造を少々変更して、テンプレート側には旧APIで叩いて今まで渡していたようなデータ構造をそのまま模してしまうことにしました。

この部分はJSON::XSでJSONテキストをハッシュリファレンスにしたものをひたすら組み換えるだけなので、APIのパターンが複数あるものの、それぞれ含めても一日二日あればできる範囲です。

コードを書いて手元の開発環境で動かしてみました。旧APIでは表示ができる。それを新APIにアクセスするようにして、データ組み換えコードを通るようにしてみたら文字化けが起こりました。組み換え部分では、枝葉のデータの文字コードは一切いじっていないのにです。

文字化けのパターンを見たら、UTF−8へのバイト列への変換を多重で行ってしまうパターンでした。ここまでわかると、根本的な原因は分からなくても対処法はわかります。

ざっくりと概要を抜き出すと、旧APIでは

JSON::XS->new->decode(...)

となっていた部分、新APIでは

 

JSON::XS->new->utf8->decode(...)

としたら文字化けしなくなりました。

対症療法ではこれでいいのですが、原因がわからないと気持ちが悪いです。調査してみました。

旧APIのコードは古かったので、リファクタリングをしたり新APIへのアクセスモジュールを書いて疎結合的に対応したかったのですが、納期が限られていたので新APIへの対応コードは旧APIへの対応コードをベースに書くことにしました。お仕事的な都合ですね。

外部サイトのAPIにアクセスしてJSONを取ってくる部分は、以下の様なコードとなっていました。

my $json = LWP::Simple::get($url);

その後のコードは文字コードにタッチしていないので、いろいろ調べた結果、名前の通りシンプルなことをしていないここが何かしているだろうという話になりました。

LWP::Simpleのgetのコードを見てみます。手元の最新版6.00でのお話です。

sub get ($)
{
 my $response = $ua->get(shift);
 return $response->decoded_content if $response->is_success;
 return undef;
}

$uaはLWP::UserAgentオブジェクトです。よって$responseはHTTP::Responseオブジェクト。

素直な感想としては $response->content じゃなくて $response->decoded_content なんだというところ。以前deflateされたコンテンツを扱う際に使ったことがある程度の知識でした。

HTTP::Responseのソースコードを読んでもdecoded_contentが無かったので、HTTP::ResponseのスーパークラスであるHTTP::Messageを読んでみたらdecoded_contentがありました。長いので抜粋しながら解説します。

sub decoded_content
{
 my($self, %opt) = @_;
 my $content_ref;
 my $content_ref_iscopy;

 eval {
 $content_ref = $self->content_ref;
 die "Can't decode ref content" if ref($content_ref) ne "SCALAR";

 if (my $h = $self->header("Content-Encoding")) {

想像通り、Content-Encodingのハンドリングを最初に始めています。

 

さらに読み進めていきます。

        if ($self->content_is_text || (my $is_xml = $self->content_is_xml)) {

Content-Encoding を見るところが終わったら、次はこんな条件節が出てきました。Content-Encoding以外のハンドリングをしているとは知らなかった。

 

HTTP::Messageは何も親クラスに持っていないし、content_is_textも見つからないなと思っていたのでこれって抽象クラスなのかなと思っていた(実際HTTP::Messageはベースクラスとして使われることを想定しています)のですが、AUTOLOADでハンドリングしていて、HTTP::Headersに処理を委譲しているようです。というわけでHTTP::Headersを見てみました。

sub content_is_text {
 my $self = shift;
 return $self->content_type =~ m,^text/,;
}

確かに明朗快活。

実際に新旧APIのヘッダどうなのと思ってwget -S で見てみたら、図星でした。

  • 旧API → Content-Type: text/html;charset=UTF-8
  • 新API → Content-Type: application/json;charset=UTF-8

というか旧APIはtext/htmlでJSONを返してきていたことが驚き。さすがAPI黎明期(?)に生まれたAPIだけある。

新APIになって、この $self->content_is_text にひっからなくなって挙動が変わったんだなーということが分かります。

HTTP::Message の decoded_content に戻ると、以下の様な条件節があります。

 if ($self->content_is_text || (my $is_xml = $self->content_is_xml)) {
 my $charset = lc(
 $opt{charset} ||
 $self->content_type_charset ||
 $opt{default_charset} ||
 $self->content_charset ||
 "ISO-8859-1"
 );

$self->content_type_charset は Content-Type ヘッダの中から charset= を探し出すもののようでした。これでtext/* の Content-Type であれば UTF-8 が拾われます。その後こうしています。

 else {
 require Encode;
 eval {
 $content_ref = \Encode::decode($charset, $$content_ref,
 ($opt{charset_strict} ? Encode::FB_CROAK() : 0) | Encod
e::LEAVE_SRC());
 };

Content-Type が text/*;charset=UTF-8 の場合は、ここでPerlの内部文字列にアップグレードされます。

旧APIのレスポンスヘッダはContent-Type: text/html;charset=UTF-8 だったのでここで知らずとアップグレードされていました。

新APIのレスポンスヘッダは、ここの条件節にはひっかからなかったので、ここでは処理されること無くバイト列で処理されることになります。

当初の部分に話を戻しますが、my $json = LWP::Simple::get($url) したものを JSON::XS->new->decode($json) していましたが、旧APIのほうはこれで内部文字列が渡るので内部文字列を持ったデータ構造となっており、テンプレートに渡す都合でこのJSONテキストからデコードしたハッシュリファレンスの構造をData::Recursive::EncodeでUTF-8のバイト列で構成されたものにしていました。

新APIだと、JSON::XS->new->decode($json) に渡される $json が前述の LWP::Simple の暗黙のお世話機能で内部文字列ではなくなってしまったので、Data::Recursive::Encode でUTF-8のバイト列を内部文字とみなしてさらにUTF-8のバイト列にしようとして文字化けを起こしていたのでした。この文字化けの起こり方は予想の範囲内でした。

解決策の一つとしては、新しいAPIでは JSON::XS->new->decode($json) ではなく JSON::XS->new->utf8->decode($json) とすることです。

そもそも、外部サイトの旧APIがJSONなのにContent-Typeとしてtext/htmlを返してくること、そしてLWP::Simple::get($url) が内部で HTTP::Response#content ではなく HTTP::Response#decoded_content を呼んでしまって余計な処理が入って、それを旧APIの処理プログラムを書いた人が受け取った文字列通りにData::Recursive::Encode->encode してしまったとか、色々なな部分に罠があったのでした。

JSONやHTTPに関わる部分は以下のようなことを守っていればハマりを回避できると思います。

  • LWP::Simple::get() は使わずにLWP::UserAgent#get を使ってHTTP::Response#contentを使ってレスポンスボディを取得する。HTTP::Response#is_success などでエラーハンドリングも書けるから。HTTP::Response#decoded_content を使いたい場合も、LWP::Simple::get() で暗黙で使うのではなく、明示的に使ったほうがよさそう
  • JSON::XS を使う際は、基本的にPerlの内部文字列としてdecodeすることを意識して JSON::XS->new->decode(…) ではなく JSON::XS->new->utf8->decode(…) と書く。このあたりはJSON関連モジュールでインターフェースは似たようなものの、挙動が微妙に違ったりするので、他のモジュールを使っている場合は注意する必要があるし、JSON::XSも意識して使う必要がありそう

まぁ、JSON APIの作成者が Content-Type: text/html でJSONを返すのはちょっと反則ですよね。Content-Type に text/json というものは無いっぽいので、application/json で返してきてくれると各種JSON処理モジュールや周辺関連でハマらず済むのかもしれません。逆にハマったら、JSON APIのContent-Typeに注目して、自分が使用しているHTTPクライアントモジュールを疑ってみるのもひとつの解決策だと思いました。

私は既存の旧APIへのアクセスモジュールを読みながら新APIへの対応を書いていてハマったという話で、新API自体が返す Content-Type は application/json となっていたので、API開発側のAPI設計も良い方向に向かっているのかもしれません。そんな新旧APIの差異でハマるとは思いもよりませんでしたけど、LWP::Simple::get() からの内部処理やハマリポイントなどが分かってちょっと勉強になりました。

Read More

Twitter Cards

twitter

 

OGPタグ同様にhead内のメタに仕込むと、ツイートされたページのタイトルや写真・本文の概要がわかりやすく表示される。

稼働させるにはタグを埋め込むだけではなく、Twitterのvalidatorページから承認を得る必要があるけど、申請後のレビューが登録メアドに届くまで「few weeks待ってね〜」とのことなので気長に待つしか無い。

 

 

Card Validator

Cards Markup Tag Reference

https://dev.twitter.com/docs/cards

Read More