一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

Amazon Connect の導入と自社システムを連携した話

この記事は一休.comアドベントカレンダー2018の5日目です。

qiita.com


こんにちは。 CTO室の村石です。

一休ではAmazon Connect を導入し、カスタマーサービスの一部コールセンターを新しい体制へと変えました。

今回は導入した Amazon Connect に関して、概要から導入後の運用まで幅広く話をしようと思います。

Amazon Connect とは

AWSが提供するクラウド型のCTI*1システムのことで、コールセンターを簡単に作れることが魅力のサービスです。 また、Webサービスになっているため、ブラウザがあれば利用できます。

aws.amazon.com

コールセンターではよくある以下のようなことも、少しの設定で実現出来ることは大きな魅力だと思います。

  • 音声ガイダンスを流す
  • 案内で番号をプッシュさせる
  • 特定の担当部署に電話をつなげる

料金については、従量課金制となっており、通話が多いほど高くなります。 一方、オペレータ数などは料金の対象にならないため、柔軟に変更が可能です。

Amazon Connect は現在東京リージョンで提供されていませんが、近い将来提供されることが発表されていて今後が楽しみなサービスです。

(18/12/11 追記) 正式に東京リージョンでサービス開始されたようです。 ますます Amazon Connect の今後が楽しみなサービスですね。

aws.amazon.com

Amazon Connect の導入方法については割愛しますので、AWSの各種ドキュメントをご覧ください。

Amazon Connectの特徴

個人的に特に気になった特徴を紹介します。

短期間、初期費用不要で構築が可能

Amazon Connect インスタンスを作ってから、単純に電話できるようになるまでは、慣れれば数分で実現出来ます。 実際の運用環境を整えるには別途時間がかかりますが、時間のかかる電話線工事などがカットできるのは大きいです。

各種設定変更が楽

ここでいう各種設定とは以下のようなことです。(あくまで一部です。)

  • オペレータの追加
  • コールフローの編集
  • 営業時間の変更

Amazon Connect では、これらを管理サイトから変更することができます。 コールセンターの構築にベンダーを使うと、依頼が必要があったりと、オペレーションコストかかってしまいます。 時間がかかりがちの調整をせずとも、すぐに設定変更を行うことが出来るのは大きなポイントだと思います。

(現時点で)シドニーリージョン

前項でも述べたように、まだ日本で提供されていません。 今すぐ利用するためにはシドニーリージョンでの利用となります。 懸念として音声データの遅延が挙げられる思いますが、弊社が検証したところ、業務に差し支えるほどの遅延は確認できませんでした。

また、リージョンが海外のため、内部的には国際電話となります。 料金はそれほど気になりませんが、電話をかける際のダイヤルには気をつけないといけません。 しかし、東京リージョンでサービス開始されたとして、ダイヤルする際は国際電話でない保証はありません。

(18/12/11 追記) 「Amazon Connect とは」で追記した通り、東京リージョンにてサービス開始されましたので、削除させていただきました。

Amazon Connect の専用管理サイトが存在

AWS コンソール上の運用になるイメージを持つかもしれませんが、違います。 AWS コンソールでAmazon Connect のインスタンスを作成すると、専用の管理サイトが作成され、コールセンター業務は作成されたサイトで管理することになります。

電話を受けるオペレータのアカウントは、管理サイトから発行することになり、AWS アカウントとは別に存在します。

一通り構築が出来たらエンジニアは運用業務から離れることが出来る、というのも一つ特徴かと思います。

Amazon Connect のシステム

システム構成

電話のフローから見る構成図です。

システム構成-電話
システム構成-電話

オペレータはブラウザからWebサービスである Amazon Connect にログインして待機します。 Amazon Connect が着信を受け取り、実際にオペレータに繋がるまでに、いくつかの工程を通ります。 コールフロー / キュー / ルーティングプロファイル、この3点については、Amazon Connect を使う上で重要な項目のため少し詳しく説明します。

コールフロー

コールフローは、「着信した電話をどのように処理するか」を制御する重要な場所です。 電話がかかってきた時は、最初にコールフローに入ります。

コールフロー設定画面
コールフロー設定画面

上図のように、GUIを用いて設定することになっていて、シーケンスフローを組み立てる感覚で設定できます。

紹介しているのは、通話内容の録音設定を行い、後続のキューに転送するだけの単純な構成ですが、以下のように複雑な制御も設定可能です。

  • 営業時間内であるかチェックして、時間外なら音声ガイダンスを流して終了する
  • Lambda と連携し、任意の処理を走らせる
  • 発信者に番号プッシュの自動案内を行い、入力値によって応答するオペレータを変える

他にも、一般的なコールセンターで出来ることは一通り揃っていて、それらはすべてコールフローで設定することになります。

キュー

「着信した電話をため込む場所」です。 コールフローの終了後は基本的にキューに入り、オペレータと繋がるのを待つ状態になります。 待ちになった電話はFIFOのアルゴリズムに則り、順次オペレーターに繋がります。

ルーティングプロファイル

「キューとオペレータの橋渡し役」です。 キューに入った電話は、ルーティングプロファイルによって、電話可能のオペレータと結び付けられます。

ルーティングプロファイルとオペレータが「多対1」の関係であるのに対し、ルーティングプロファイルとキューは「多対多」の関係になっている、という点は混乱しやすいので注意が必要です。 一般的には、部署毎にルーティングプロファイルを作り、1対1の関係でキューを作るのが基本かと思いますので、気にならないかもしれません。

AWS の他サービスとの連携

Amazon Connect が取り巻く周辺のAWS事情です。 今後も更に連携できるものが増えていきそうです。

システム構成-AWS
システム構成-AWS

自社システムとの連携について

ここからは Amazon Connect と自社システムとの連携方法についてです。 連携の方法は大きく分けて2つの方法が考えられます。

  • コールフローの中で Lambda を使って連携
  • オペレータが電話を受けたタイミングで連携

ここでは、弊社で行った Lambda を使った連携を紹介します。 弊社が作成したのは、「Lambda を使って会員の情報を取得し、着信と同時にオペレータのブラウザに表示する」というものです。 簡単に処理フローを図示したものです。

Lambda連携-フロー図
Lambda連携-フロー図

これを実現するには、Lambda の他に amazon-connect-streams という JavaScript のライブラリも必要になります。 順を追って説明していきます。

Lambda 編

Lambda との連携はコールフローから行います。 まずはコールフローの中で Lambda と連携するための設定が必要です。 実際にコールフローを設定した内容です。

Lambda連携-コールフロー設定
Lambda連携-コールフロー設定

Amazon Connect と Lambda を繋げる

「AWS Lambda 関数を呼び出す」の設定項目には、呼び出したい Lambda のARNを設定します。

Lambda連携-関数を呼び出す
Lambda連携-関数を呼び出す

Lambda の返却値を Amazon Connect で設定する

「問い合わせ属性の設定」の設定項目には、Lambda 関数が返却した値を設定します。 この工程を行わないと、電話と共にデータが伝搬されません。

Lambda連携-問い合わせ属性の設定
Lambda連携-問い合わせ属性の設定

Lambda の処理

こちらは Lambda に記述する実際のソースコードになります。 python を用いています。 辞書型で渡される event 引数に発信者電話番号が入っています。 注意点としては、返却値のvalueは必ず文字列/数値/真偽値のいずれかである辞書型にする必要があります。

def lambda_handler(event, context):

    logger.info(event)
    
    end_point = event["Details"]["ContactData"]["CustomerEndpoint"]

    # ここで自社システムと連携して任意の処理を行う
    
    return {
        "response": json.dumps(end_point)
    }

引数 event 対するログ出力内容はこのようになっています。

{
    'Details': {
        'ContactData': {
            'Attributes': {},
            'Channel': 'VOICE',
            'ContactId': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
            'CustomerEndpoint': {
                'Address': '+81xxxxxxxxxx',
                'Type': 'TELEPHONE_NUMBER'
            },
            'InitialContactId': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
            'InitiationMethod': 'INBOUND',
            'InstanceARN': 'arn:aws:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
            'MediaStreams': {
                'Customer': {
                    'Audio': None
                }
            },
            'PreviousContactId': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
            'Queue': {
                'ARN': 'arn:aws:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
                'Name': 'test-queue',
                'OutboundCallerId': {
                    'Address': '+81xxxxxxxxxx',
                    'Type': 'TELEPHONE_NUMBER'
                }
            },
            'SystemEndpoint': {
                'Address': '+81xxxxxxxxxx',
                'Type': 'TELEPHONE_NUMBER'
            }
        },
        'Parameters': {}
    },
    'Name': 'ContactFlowEvent'
}

amazon-connect-streams編

Lambda で連携したデータを電話を受けた時に表示するために必要になります。

amazon-connect-streams とは

Amazon Connect のオペレータ画面で使用する JavaScript ライブラリです。 実際に電話を受ける、かける、などの操作ができます。簡単に言うと電話機です。 これを使うことで、電話機を自作のWebサイトで利用することができます。

github.com

実際に amazon-connect-streams を使うには、AWS でのIPホワイトリストの設定と、Node.jsのビルドが必要です。 導入方法については、ここでは割愛しますので、上記ドキュメントをご覧ください。

CCP (Contact Control Panel)の表示

CCP とは、amazon-connect-streams の中で使われる機能の一部で、電話機のUIを指します。 以下のソースコードが示すように表示したい場所にdivタグを用意し、DOMを渡すことでその場所にiframeが展開されます。

let containerDiv = document.getElementById("containerDiv");
connect.core.initCCP(containerDiv, {
   ccpUrl: "https://[インスタンスエイリアス].awsapps.com" // 実際の管理サイトのURL
});

表示されるCCPです。

Amazon Connect CCP
CCP

着信時のデータ受け取り

// initCCPの後

connect.contact((contact) => {
    contact.onConnecting((conectingContact) => {
        let attributes = conectingContact.getAttributes();
        console.log(attributes);
    });
});

出力した結果は以下となり、lambda で連携した内容と一致していることが確認できます。

amazon connect streams データ取得
amazon connect streams データ取得

(おまけ) Amazon Connect Service API編

Amazon Connect では、Web API が用意されていて、データ取得/操作が行えます。 これらを利用すれば、オペレータに表示する画面から直接 Amazon Connect の機能を使えます。

docs.aws.amazon.com

API を利用するには、IAM のポリシー設定も必要になります。

キューに存在する待ち呼*2の数を取得

例として、キューに存在する待ち呼数の取得を紹介します。 当該情報を取得するには、get_current_metric_data というAPIを利用します。 python を用いて説明します。

def getCurrentQueue():

    import boto3

    # API クライアントを生成します。(IAMのポリシー情報を渡します。)
    client = boto3.client(
        "connect",
        region_name="ap-southeast-2",
        aws_access_key_id="xxxxxxxxxxxxxxxxxx",
        aws_secret_access_key="xxxxxxxxxxxxxxxxxx"
    )

    # キューの現在の情報を取得します。
    current_metoric = client.get_current_metric_data(
        InstanceId="[Amazon Connect インスタンス ID]",
        Filters={
            'Queues': [
                "[キューID]"
            ]
        },
        CurrentMetrics=[
            {
                'Name': 'CONTACTS_IN_QUEUE',
                'Unit': 'COUNT'
            }
        ]
    )

    logger.info(current_metoric["MetricResults"])

get_current_metric_data APIには、引数の CurrentMetrics 配列に別の情報を追加することで様々なデータが取得できます。 詳しくは、API ドキュメントをご覧ください。

docs.aws.amazon.com

以下は出力内容になります。

[
    {
        'Collections': [
            {
                'Metric': {
                    'Name': 'CONTACTS_IN_QUEUE',
                    'Unit': 'COUNT'
                },
                'Value': 0
            }
        ]
    }
]

Amazon Connect を導入してみて

Amazon Connect の導入を行い、技術面、運用面で思うところをあげました。

良かったこと

構築がとても楽

とにかく電話が出来るまでが早くて驚きました。 電話線工事などにかかる時間がなくなり、代わりに自社システム連携などに時間を割くことが出来ました。

拡張性が優秀

AWS のサービスであるため、Lambda など他サービスの連携が楽という利点があります。

また、amazon-connect-streams では電話機の機能だけを切り出してくれているため、それ以外すべてを使いやすくカスタマイズ出来ます。

もはや自社システムに 電話サービスを追加する感覚とも言えるでしょう。

コールフローの設定で柔軟に運用が可能

自社システムの連携を随時アップデートしていく環境では、どうしても本番環境で動作確認したい場合があります。

そんな時もコールフローで特定の電話番号だけ別の処理をさせることも出来るので、動作確認が容易になります。 メンテナンスによる急な営業時間外の設定など、緊急性のある運用もカバーできるところも良い点だと思います。

オペレーター事情に左右されにくい

Amazon Connect ではユーザに対する権限の付与や、人員の増減、権限の設定変更、などが簡単に出来ます。

ベンダーによっては、権限の変更に依頼が必要だったり、料金変更があることもあります。

人員変更が滅多にない組織なら気にならないかもしれませんが、スタートアップや柔軟に変更を行う組織の場合はありがたいでしょう。

課題に感じること

学習コストが高い(技術)

東京リージョンで提供されることが発表されているとは言え、まだ日本で導入している企業は少ないため、ドキュメントは少ないです。 弊社もトライ・アンド・エラーを繰り返して習得しました。

月の無料利用額があるため、まずはその額内でいろいろ試して習得するのが良いかと思います。

(18/12/11 追記) 「Amazon Connect とは」で追記した通り、東京リージョンにてサービス開始されましたので、削除させていただきました。

学習コストが高い(運用)

技術視点だけではなく、運用面でも学習コストが高いと感じました。

運用に入ると、コールセンターの管理者に Amazon Connect の管理を任せたいものです。 コールフロー、エージェント管理、メトリクスの作成など、習得しなければ使いこなせないものが多い印象です。

技術視点と同様に、ドキュメントが少ないため、こちらもトライ・アンド・エラーで習得しなければならないのが現状です。

一部メトリクス情報に難あり

一部の内容で満足いかない箇所がありました。 例えば、弊社で感じている箇所として以下があります。

  • リアルタイムのキュー内容表示において、自動更新が約15秒間隔(待ち呼に気付くのが遅れる)
  • 問い合わせ検索(発着信の内容を調べられる)では、通話に関するタイムスタンプに秒単位の表示が無い(APIでは取得しているのであくまで表示だけのようです。)

細かい話と捉えることも出来ますが、組織が定めるコールセンター業務のKPIに影響することも有りえます。 特に大規模な組織では気になるところではないでしょうか。

オペレータのステータス管理に不安あり

Amazon Connect ではオペレータに「Available」「offline」など、状態を管理出来ます。

「Available」という状態が電話を受け付けている状態を表しますが、この状態は明示的に変更しない限り、そのまま維持されます。 変更を怠ると、物理的に電話が取れない状況であろうと、ルーティングプロファイルによって電話が割り当てられてしまう危険があります。 これを運用以外の方法で防ぐには、Web APIを利用して自動的に状態を変更する、コールフローとシフトを連動させる、など一工夫必要になります。 導入するにあたり、方針は決めておく必要がありそうです。

また、ACW*3の状態に手動で変更出来ない、ということもあります。 Amazon Connect では、任意のカスタムステータスの作成が可能であり、似たステータスを作ることは可能です。 しかしメトリクスに影響するなど不安は残ります。

オペレータへ繋がる仕組みが今ひとつ

Amazon Connect では、ルーティングプロファイル内のオペレータに優先度設定や、特定のオペレータへ繋ぐ機能はありません。

組織のやり方によりますが、他のオペレータが全て電話で埋まった時に管理者も電話を受ける、という方針があった場合、実現する方法は難しいです。 埋まった時点で状態を変更する運用回避や、ルーティングプロファイルや Lambda を駆使する方法もありますが、難易度は高い印象です。

(18/12/08 追記) 上記取消線の内容は、管理者用のルーティングプロファイルを別途作成し、キューの優先度及び遅延時間を設定することで実現できました。

まとめ

AWS の1サービスとして提供される Amazon Connect ですが、まだ日本のコールセンター業務に適応するには、運用面で課題が残りそうな印象があります。 ここは、日本企業の導入が進むのと同時に、Amazon Connect も進化し、適応していくことを期待しています。

一方、技術面の視点から見ると、AWS との連携やカスタマイズの容易さなど、エンジニアがとっかかりやすい環境が揃っていると感じました。 今回弊社が行った自社システムの連携もほんの一部に過ぎず、コールセンターにおける業務効率化の可能性はとても大きいと思います。

以上、Amazon Connect の導入を検討している会社の参考になれば幸いです。

muraishis

*1:Computer Telephony Integration の略。電話とシステムの連携を行う技術のこと。

*2:オペレータに繋がるのを待っている着信のこと

*3:After Call Workの略。電話を切った後、対応内容の記録など、オペレータが作業をする時間のことをいう。

サードパーティJavaScriptの最適化

本記事は、一休.com Advent Calendar 2018の4日目の記事です。

qiita.com


宇都宮です。宿泊事業本部でWebフロントエンドの開発をしています。

今日は、パフォーマンス改善に取り組むフロントエンドエンジニアの多くが頭を悩ませているであろう、サードパーティスクリプト(3rd Party JavaScript)について書きます。

サードパーティスクリプトとは

サードパーティスクリプトとは、外部のドメインから読み込むJavaScriptのことです。典型的には、Google Analytics等のスクリプトが、サードパーティスクリプトに該当します。

一休.comでは、サードパーティスクリプトを、アクセス解析・広告のリターゲティング・A/Bテスト等、様々な用途に使用しています。これらのスクリプトは、一つ一つは小さなものであるため、画面表示のスピードに対するインパクトは意識しづらいものです。しかし、細かいコストが積み重なることで、サイトの読み込み速度に大きな悪影響を与えることもあります。

一休.comにおけるサードパーティスクリプト

一休.comでも、様々なサードパーティスクリプトを利用しています。たとえば、スマホのホテルページ( https://www.ikyu.com/sd/00001290/ )では、以下の外部ドメインからスクリプトを読み込んでいました。

  • bam.nr-data.net
  • js-agent.newrelic.com
  • b92.yahoo.co.jp
  • www.google-analytics.com
  • www.googleadservices.com
  • s.btstatic.com
  • yjtag.yahoo.co.jp
  • s.yjtag.jp
  • www.googletagmanager.com
  • googleads.g.doubleclick.net
  • static.criteo.net
  • s.yimg.jp

これらを合計した転送量は、ページ内で読み込んでいるJSの3分の1を占めます(300KB中、100KB)。

サードパーティスクリプトがページ読み込み速度に与える影響

↓は、スマホホテルページのパフォーマンス計測結果です。

https://www.webpagetest.org/result/181017_SD_14a6718d17894b5d25bea6ff2c01713d/

f:id:ryo-utsunomiya:20181203112751p:plain
サードパーティスクリプトあり

画面の描画完了まで(Speed Index)が3.142s、ユーザが操作可能になるまで(First Interactive)には8.3秒かかっています。

次に、WebpagetestのBlock機能を利用して、サードパーティスクリプトの読み込みを行わない状態で計測してみます。

https://www.webpagetest.org/result/181017_9Z_9051ce3990f33aadaae363806147fada/

f:id:ryo-utsunomiya:20181203122317p:plain
サードパーティスクリプトなし

画面の描画完了まで(Speed Index)が3.142s => 2.787s、ユーザが操作可能になるまで(First Interactive)は8.3秒 => 5.6秒に改善しました。

サードパーティスクリプトの多くはasyncで読み込むため、レンダリングに与える影響は軽微です。しかし、あるjsが別のjsを連れてくる、という構成になっていることが多く、jsファイルの読み込みがダラダラと続きがちです。このことは計測結果のWaterfallを見るとわかりやすいと思います。これによって、画面の表示が終わってもブラウザはまだローディング中で、画面を操作するともたつく、という状況が生まれます。

このように、サードパーティスクリプトを使用する際は、それがユーザ体験に与える悪影響も考慮する必要があります。

他のサイトは?

一休.comでは、サードパーティスクリプトによって一定のパフォーマンス劣化が見られることがわかりました。

では、他のサイトではどうでしょうか?

一例として、A/B Testing, Ads and Other Third Party Tagsというトークでは、あるWebサイトから1つスクリプトを取り除くことで、ページのロード時間が4秒改善し、サイトの売り上げが26%改善したことが紹介されています(このトークは、他にも面白い事例が満載なので、サードパーティスクリプトの改善に興味のある方は視聴をおすすめします)。

日本の事例としては、日本経済新聞社の宍戸さんが公開されている、『PWA導入の成果と課題』というスライドが、身につまされる内容で参考になります。

PWA導入の成果と課題 / nikkei-pwa-html5conf2018 - Speaker Deck

このスライドでは、インターネット全体で、サードパーティスクリプトのサイズが自前のスクリプトのサイズよりも多くなってきていること(1st party 100KBに対して3rd party 250KB)、日経新聞でも、サイズは明言されていないものの、jsリクエストの半分がサードパーティスクリプトであることなどが紹介されています。

最適化されたサイトの事例

次に、サードパーティスクリプトの配信が最適化されたサイトの事例も紹介しておきます。

宿泊予約サイトのExpediaは、一休.comと同様、様々なサードパーティスクリプトを使用しています。Expediaでは、パフォーマンスを最適化したSPA版を開発しており、スマートフォン向けホテル検索ページはSPAになっています。

Expediaのスマートフォン向けホテル検索ページ(※PCから閲覧する場合はUAをスマホのものにしてください)を開いて、Chrome DevToolsのNetworkタブ等で読み込んでいるjsのhostを確認すると、全て同一ドメイン(https://c.travel-assets.com おそらくExpediaのCDN)から取得していることに気づきます。

推測ですが、サードパーティスクリプトをセルフホストするか、トラッキングやA/Bテストのためのスクリプトを内製しているものと思われます。外部からは一切読み込まないという決断ができるのがすごいところ。

サードパーティスクリプトのパフォーマンス改善

サードパーティスクリプトの最適化について書かれた包括的なドキュメントとしては、Google Web FundamentalsのLoading Third-Party JavaScriptがおすすめです。

抜粋して紹介すると、

  • サードパーティスクリプトはasyncまたはdeferで読み込むこと
  • サードパーティスクリプトの配信サーバが遅い場合、セルフホストを検討すること
  • サイトに明確な価値をもたらさないスクリプトは削除すること
  • preconnect/dns-prefetch等のresource hintsを活用すること

などです。

一休.comでも、サードパーティスクリプトは基本asyncで読み込み、さらに、サードパーティスクリプトのドメインにはpreconnectを行っています。

一番強力な方法は「削除」なのですが、サードパーティスクリプトは多くがビジネスサイド主導で導入されており、開発側の判断だけでは削除まで踏み切れません(宍戸さんのスライドでも、技術の問題だけではないと言及があります)。

サードパーティスクリプトの最適化には、エンジニアリングチームだけでなく、サービスを運営する組織全体が、UXに高い意識をもつ必要があります。

"User First and Challenge" を社是とする一休としては、積極的に取り組んでいきたいところです。

今後の方針

一休.comで、今後行えそうなサードパーティ最適化の施策としては、以下を検討しています。

  • サードパーティスクリプトのアセットバジェットの導入
    • 読み込んでよいJSはトータルでXXKB以内、等
  • Google Tag Manager(gtm.js)の削除
    • 他のスクリプトを連れてくるだけのJSがVue.js 2個分(44KB)は大きすぎ
    • 簡単にスクリプトを追加できる = 簡単にサイトを遅くできる

まとめ

サードパーティスクリプトには、それぞれに独自のメリットがあります。たとえば、Google Analytics等のアクセス解析は、サイトを運営するうえで欠かせない情報を提供してくれます。

一方、サードパーティスクリプトにはコストがかかります。サードパーティスクリプトを読み込むと、サイトは確実に遅くなります。

用法・容量を守って、価値あるWebサービスを提供していきたいですね 💪

参考文献

vimeo.com

speakerdeck.com

developers.google.com

VueコンポーネントのState管理を考える

この記事は一休.comアドベントカレンダー2018の3日目の記事です。

qiita.com


宇都宮です。宿泊事業本部でWebフロントエンドの開発をしています。

一休.comにVue.jsを導入して、約1年が経ちました。スマートフォン版の予約入力画面から始まり、PCとスマートフォン版のホテルページほか、さまざまなUIコンポーネントがVue.jsで実装されるようになってきています。

また、予約入力画面のような複雑な状態管理を伴う画面の実装のため、Vuexを導入しています。

ここ1年ほどVue.js + Vuexというスタックで開発を行ってきて、アプリケーションの設計について色々と思うところがあったので、今回は現状でどういう構成が最適と考えているか、紹介します。

Vue.jsアプリケーションのState

Vueコンポーネントには、親から受け取るpropsと、自分自身で保持するstate(data)という2つの概念があります。また、子コンポーネントから親コンポーネントに何かしらの値を戻したい場合、eventの仕組みを利用することができます。

propsとdataについては説明を省きますが、eventについてはサンプルコードを載せておきます。

KeywordSearch.vueというコンポーネントがあって、このコンポーネントは検索キーワードの入力とバリデーションを受け持つとします。一方、検索処理の実行はAPI経由なのか画面遷移なのかが画面によって変わるため、親コンポーネントに委譲します。

このような場合、以下のようにeventの仕組みを利用することで、子から親へ値を受け渡すことができます。

// KeywordSearch.vue
<template>
  <div>
    <label>
      検索キーワード <input type="text" name="query" v-model="keyword" />
    </label>
    <button type="button" @click="search">検索</button>
  </div>
</template>
<script>
export default {
  name: "keyword-search",
  data() {
    return {
      keyword: "",
    };
  },
  methods: {
    search() {
      // search イベントを発火し、keywordを引数として渡す
      this.$emit("search", this.keyword);
    }
  }
};
</script>

// KeywordSearch.vueの親コンポーネント
<template>
  <div><keyword-search @search="handleSearch" /></div>
</template>
<script>
import KeywordSearch from "./KeywordSearch.vue";
export default {
  name: "App",
  components: {
    KeywordSearch
  },
  methods: {
    // KeywordSearchのsearchイベントのハンドラ
    handleSearch(keyword) {
      // 検索APIに問い合わせたり、画面遷移したり
    }
  }
};
</script>

まとめると、Vueコンポーネントにおいて、データの流れは以下のようになっています。

  • 自分自身で保持: data
  • 親 => 子: props
  • 子 => 親: event

このように、コンポーネント間のデータの流れがpropsとeventによって行われることをとらえて、「Props Down, Event Up」と呼ぶことがあります(もともとはReact.jsのコミュニティ発祥のフレーズだと思います)。

この辺の話はVue.jsのドキュメントでも詳しく説明されています。

jp.vuejs.org

グローバルな状態管理の必要性

ここまで説明してきたのは親子関係のあるコンポーネントの話です。しかし、実際のアプリケーションでは、親子関係のないコンポーネント間でデータを共有したい場合があります。これについても、Vue.jsおよびVuexのドキュメントに、Vuexを使うべき場面についての解説が載っています。

jp.vuejs.org

vuex.vuejs.org

一休.comの画面の例としては、「今すぐポイント割引後料金を表示」の切り替えボタンは、直接の親子関係のないコンポーネントがオン・オフの状態を共有するため、Vuexを使っています。

f:id:ryo-utsunomiya:20181202164300p:plain
「今すぐポイント割引後料金を表示」の切り替えボタン

Vuexのバッドプラクティス

Vuexは便利なのですが、何でもかんでもVuexを使うのは良いやり方とはいえません。Vuexを使っていると、以下のようなコンポーネントを書いてしまうことがあります。

<template>
  <div>{{ someState }}</div>
</template>
<script>
import { mapState } from "vuex";

export default {
  computed: {
    ...mapState(["someState"])
  }
};
</script>

このコンポーネントの作りがイマイチなのは、単に値を受け取って表示するだけのコンポーネントなのに、propsではなくStoreに依存している点です。この設計が良くないことを説明するために、モジュール結合度という概念を紹介します。

モジュール結合度

モジュール結合度とは、あるモジュールと別のモジュールの結合度を表す度合いです。低いほど疎結合、高いほど密結合となります。見た目を制御するコンポーネントの場合、様々な場所での使用が想定されるため、結合度は低い方が望ましいです。

モジュール結合度 説明
1 データ結合 引数で単純なデータを渡すパターン
2 スタンプ結合 引数で構造体などのオブジェクトを渡すパターン
3 制御結合 引数の種類によって、メソッドの内の処理が変わるパターン
4 外部結合 単一のグローバルデータを参照しているパターン
5 共通結合 複数のグローバルデータを参照しているパターン
6 内容結合 他のオブジェクトの内部を参照しているパターン

qiita.com

Vueコンポーネントでいうと、propsは「データ結合」ないし「スタンプ結合」なので、結合度は1〜2です。これに対して、storeは概念としてはグローバルデータの参照に当たり、複数のデータを参照することが多いため、「共通結合」の5、gettersやactionsに依存している場合は「内容結合」の6になります。

つまり、storeを参照するコンポーネントはstoreと密結合し、storeなしでは利用できなくなってしまうのです。

このような密結合が常に問題になるわけではありませんが、コンポーネントツリーの末端に近いようなコンポーネントがstoreを参照しているのは、コンポーネントの設計が歪になっていることのシグナルかもしれません。

Vue + Vuexアプリケーションの状態管理の指針

Vuexがその設計の参考としているFluxアーキテクチャでは、状態が一カ所に集約されていて、Single Source of Truthになると安心できる、という考え方があります。この考え方を適用すると、データは基本的にVuexのstoreから引いてくるのが正しいように思えます。

こうしたSingle Source of Truthの考え方と、疎結合なコンポーネント設計は、対立する概念ではありません。ここで参考になるのは、Presentational ComponentとContainer Componentという考え方です。

medium.com

ざっくりいうと、Presentational Componentは、propsを受け取って、表示をするだけのコンポーネント、Container ComponentはPresentational Componentを管理し、値を受け渡すコンポーネントです。

  • Container Componentはstoreを参照し、必要に応じてPresentational Componentにデータを渡す
  • Presentational Componentはstoreは参照しない

このようにコンポーネントを分類することで、storeと密結合する部分をContainer Componentに限定し、Presentational Componentは疎結合に保つことができます。

また、もう1つの設計指針として、「Vuexはグローバルな状態のストアである」という点を強調しておきたいと思います。modulesに区切ることで細かいstoreを定義することは可能ですが、実装上どこからでも参照できるVuex storeは、概念的にはグローバル変数と同じです。コンポーネントローカルな状態(要素の表示・非表示等)は、Vuex Storeで管理する状態ではありません。一方、ローディングの表示等、画面全体に影響するような状態はVuexで管理すると便利です。

まとめ

以下のような指針で設計を行うことで、Vueコンポーネント設計の基本を守りつつ、Vuexを適材適所で活用できます。

  • 親子関係のあるコンポーネント間のデータの受け渡しは「Event Up, Props down」で
  • 親子関係のないコンポーネント間のデータの共有はVuexで
  • コンポーネントの固有のデータはコンポーネントのローカルステート(data)で
  • グローバルなデータはVuexで

参考文献

jp.vuejs.org jp.vuejs.org vuex.vuejs.org qiita.com medium.com

SVGスプライトアイコンの作り方・使い方

この記事は一休.com アドベントカレンダー 2018の2日目の記事です。

qiita.com


宇都宮です。宿泊事業本部でフロントエンドの開発を行っています。

今回は、最近一休.comに導入した、SVGスプライトによるアイコンの作り方・使い方について紹介します。

f:id:ryo-utsunomiya:20181201135110p:plain
StorybookのSVGスプライトアイコン一覧

アイコンの一般的な使い方

アイコンは、一般的に、以下のような方式で使用されると思います。

  1. ビットマップ画像(gif, png等)
  2. アイコンフォント(Font Awosome等)
  3. SVG

このうち、ビットマップ画像によるアイコンは拡大・縮小に弱いため、様々な解像度の画面に対応する必要のある現代には不向きです。アイコンフォントはベクター画像なので拡大・縮小に強く、豊富なアイコンがライブラリとして提供されているのが魅力です。SVGもアイコンフォントと同様のベクター画像ですが、フォントにはない柔軟性を備えています。

一休.comは歴史のあるサービスのため、これらのアイコンが混在していますが、最近はSVGアイコンを使うことが多いです。

SVGの柔軟性

SVGは、HTMLに直接埋め込んで使用可能です。そのため、CSSによるスタイリングが可能です。アイコンフォントも色の設定やサイズの調整が可能ですが、パーツ毎に色を塗り分けたりするような柔軟なスタイリングは、SVGでしかできません。また、JavaScriptから操作しやすいという特徴もあります。

一休.comにおけるSVGアイコン使用の問題点

一休.comでのSVGアイコンの使用方法は、いくつかの変遷をたどっています。

はじめはimgタグでsvgファイルを読み込む使い方でしたが、これではSVGの柔軟性で挙げた特徴はほとんど活用できません。

<img src="/path/to/icon.svg" />

インラインSVGにすると、柔軟性は得られますが、記述が煩雑になります。

<svg ..(アイコンにもよるが、200バイトくらい).. />

そこで、Vueコンポーネント化が試みられました。以下のように、1つのSVGアイコンに対して、1つのVueコンポーネントを作る設計です。

<template>
  <svg .../>
</template>
<script>
  export default {
    name: 'some-icon',
  };
</script>

これによってSVGのスタイリングの柔軟性は増しましたが、この方式には2つ問題がありました。

1つはパフォーマンスで、アイコン1個あたり1KB(minify+gzip)、アイコン20個で約20KBものJSサイズ増加が発生したことです。本来SVGアイコンは1個200~300byte程度で、gzipするとさらに縮みます。Vueコンポーネント化することで、本来のサイズの10倍ほどに膨らんでしまっています(これはVueコンポーネント設計のまずさに起因しているため、個別にコンポーネントを作るのではなく、汎用的なSVGアイコンコンポーネントを導入するようにしていれば、パフォーマンスへの悪影響は緩和できたと思います)。

もう1つの問題は、VueコンポーネントはVueのコンテキストの中でしか使えないことです。一休.comはフルSPAではないので、サーバサイド(aspx/cshtml)で出力している部分もあります。サーバサイドで出力している部分では、Vueコンポーネントは使えないため、imgタグなどを使う必要があります。

これらの問題を解消し、

  1. SVGの機能をフル活用できる
  2. パフォーマンス上のオーバーヘッドが最小限である
  3. JSフレームワークに依存せず、どこでも使える

という条件を満たすソリューションを検討しました。

SVGスプライトとuse要素

SVGについて調べたところ、use要素というものがあることがわかりました。ページ内の別のSVGに定義されている要素を呼び出して使うことができます。

<!-- アイコン定義 -->
<svg>
  <symbol id="someIcon" ... />
</svg>

<!-- アイコン使用 -->
<svg>
  <use xlink:href="someIcon" />
<svg>

use要素を使う場合、必要なアイコンをSVGスプライトにまとめて、それをインラインSVGとしてHTML内に書き出す必要があります。インラインSVGはネットワークリクエストが発生しないため、パフォーマンス面ではアイコンを個別に読み込むよりも有利です。

この方式を採用すると、

  1. SVGの機能をフル活用できる
  2. パフォーマンス上のオーバーヘッドが最小限である
  3. JSフレームワークに依存せず、どこでも使える

という要件を全て満たせることがわかりました。

残る課題は現行の開発環境への組み込みです。

gulpによるSVGスプライト

SVGスプライト化にはgulpを使いました。webpackオンリーの環境ならwebpackでやっても良いと思います。webpackの役割をあまり増やしたくないのと、SVGスプライト関係のサンプルコードはgulpを使っているものが多かったこともあり、gulpを使用しました。

SVGスプライトのためのgulpタスクは以下のようになっています。

const gulp = require('gulp');
const path = require('path');
const svgmin = require('gulp-svgmin');
const svgstore = require('gulp-svgstore');
const cheerio = require('gulp-cheerio');

const commonDir = 'path/to/common';

gulp.task('svg-sprite',() => {
  gulp
    .src(commonDir + 'icon/*.svg')
    .pipe(
      svgmin(file => {
        const prefix = path.basename(
          file.relative,
          path.extname(file.relative),
        );
        return {
          plugins: [
            {
              cleanupIDs: {
                prefix: prefix + '-',
                minify: true,
              },
            },
          ],
        };
      }),
    )
    .pipe(svgstore({ inlineSvg: true }))
    .pipe(
      cheerio({
        run: function($) {
          $('svg').attr('style', 'display:none');
        },
        parserOptions: { xmlMode: true },
      }),
    )
    .pipe(gulp.dest(commonDir + 'icon-dist'));
});

このタスクは以下の流れで処理を行います。

  1. common/icon 配下にあるsvgファイルを取得
  2. gulp-svgminを使ってSVGを圧縮
  3. gulp-svgstoreを使ってSVGを結合
  4. gulp-cheerioを使って、結合したSVGファイルを非表示に
  5. common/icon-dist にファイル(icon.svg)を書き出し

このようにして、 https://www.ikyu.com/common/icon-dist/icon.svg (実際にサイトで使用しているSVGスプライト)は作成されています。また、このSVGスプライトは、サーバサイドでHTMLのbodyの開始直後に書き出されています。

f:id:ryo-utsunomiya:20181201133705p:plain

利用側では、以下のように、svg要素の中にuse要素を配置し、use要素のxlink:href属性にSVGのid(#ファイル名)を指定することで、アイコンを参照できます。スタイリングはCSSで行います。

<svg class="l-header-search-icon"><use xlink:href="#search"></use></svg>

まとめ

以上、SVGスプライトを使用したアイコンの作り方について紹介しました。

参考文献

GulpでSVGスプライトとアイコン一覧を一発生成 - Bit Journey's Tech Blog

Chrome Dev Summit 2018に参加しました!

この記事は一休.comアドベントカレンダー2018の1日目です。

こんにちは。レストラン事業本部の西村です。

11月12、13日にサンフランシスコで開催されたChrome Dev Summit 2018に参加しました。

f:id:nishimurae:20181130175843p:plain:w560

今年はChromeが10周年ということで、この10年で変わったこと、これからについての話で始まりました。 2日に渡って行われた22のセッションの中で、特に注目した点について深掘りしていきます。

1日目のセッション

1日目は現在提供している技術について、具体的な事例を交えながら紹介されました。

VisBug

VisBugは、hoveringとKeyboard shortcutsでブラウザ上でサイトの画像を差し替えたり、一部のコンテンツの内容やスタイルを変更できるChrome extensionです。

ブラウザ上でちょっとしたスタイル修正や画像の入れ替えをしてデザイン決め、といったことが簡単にできます!

f:id:nishimurae:20181129160030g:plain:w560

こちらはDay 1 Keynoteの最後に紹介されました。

Performance

例年以上にWebサイトのパフォーマンスに関する話が多く、ケーススタディも豊富でした。 ネイティブアプリがメインだったけれど、Webでも利用できるようにしたらアプリのダウンロード数も上がった!という話がSpotifyの事例で紹介されていました。

f:id:nishimurae:20181129172315p:plain:w500

こちらはGet Down to Business: Why the Web Mattersの中で紹介されています。

改善戦略の立て方

セッションの中で、プロジェクトの進め方に関する重要な教訓がありました。

  • 長期的なビジョンを持つ
  • 短期的な目標を計画する
  • 目標の照準を長期的なビジョンに合わせていく

パフォーマンスを改善してもビジネス的なKPIに直結するわけではなく、すぐに成果が出ないことに悩む時もあると思います。 長期的なビジョンを持ちながら短期的な目標を設定し着実にクリアしていくことを意識した方が達成感も持てると感じました。

Walmartの事例では、長期的な目標としてTime to Interactiveを70%削減、それに対して短期的な目標としてJavaScriptを500KB/CSSを40KB削減と設定しています。

f:id:nishimurae:20181129173452p:plain:w560

こちらはECの事例を用いて、Modern Websites for E-commerce in the Real Worldの中で語られています。

Performance Budgetの設定

Performance Budgetとは、Webサイトのパフォーマンスに影響する要素における数値的な限界値です。 計測ツールを使って様々な視点から分析し、適切なPerformance Budgetを設定することが重要です。

こちらはCarousellが設定しているPerformance Budgetです。

f:id:nishimurae:20181129172900p:plain:w560

Pinterestの事例では、JavaScriptのバンドルサイズ、そしてPinner Wait Time(PWT)という独自の指標を設定していました。 PWTはTime to Interactive + Image Loadingです。サービスにとって重要な画像を意識した指標ですね。

このように、それぞれの事業に沿ったPerformance Budgetを設定することはパフォーマンス改善の戦略を立てる上でとても重要です。

継続的な測定、改善

You can't improve what you don't measure. - Peter Drucker

計測しないものは改善できない、ということですね。セッションの中でも計測ツールが複数紹介されました。

Lighthouse

Lighthouseとは、Webサイトの品質向上に役立つ、オープンソースの自動化されたツールです。 Chrome Dev ToolsのAuditsタブに組み込まれているので、すでに利用している方も多いと思います。 今回は最新の変更点を紹介していました。

  • PWAのチェック項目が追加
  • Scoreの判定が厳しくなった
  • 回線の呼称を変更(Fast 3G => Slow 4G)

Scoreの判定が厳しくなった結果、緑と黄の比率が大きく変わっています。

f:id:nishimurae:20181129182943p:plain:w560

Lighthouseはlighthouse-ciのように、CIにも活用しやすくなってきています。 また、他のツールとの統合も積極的に進めていて、CalibretreoSpeedCurveSquareSpaceといったサードパーティ製のツールが紹介されました。

Chrome User Experience Report

Chrome User Experience Report(CrUX)は、ChromeユーザーによるWeb体験の実データを集計したレポートです。 BigQueryやPageSpeed Insightsで利用することができます。

最近の更新では、国ごとにデータセットが用意され地域別の分析ができるそうです。また、CrUX Dashboardではレポートをカスタマイズし、共有できます。

f:id:nishimurae:20181130110514p:plain:w560

PageSpeed Insights

PageSpeed Insightsとは、Webサイトのパフォーマンスを数値化して具体的な改善点を提案してくれる、統一されたツールです。 今回は、新しくリリースしたv5での変更点が紹介されていました。

  • 分析にLighthouseを使用
  • Lighthouseのパフォーマンスカテゴリのスコアを提供
  • フィールドデータにChrome User Experience Report(CrUX)を活用

これまでPageSpeed Insightsは独自の仕組みで測定をしていましたが、Lighthouseを利用することで以前とは違う検証結果になっているようです。

web.dev

web.devは、Lighthouseを使用した統合ツールの1つです。計測ツールとドキュメントが一体化されているため、測定後の次のステップが分かりやすくなっています。 現在beta版として提供されているので、少し紹介しようと思います。

f:id:nishimurae:20181129161228p:plain:w560

Lighthouseを使って結果をレポートしてくれます。その下にTodoがあり、優先度も表示されています。 ドキュメントがリンクされているのも特徴的です。

いくつか計測ツールを紹介しましたが、Googleはこれまでバラバラだった計測ツールをLighthouseに一本化していくことを進めていくようです。 Google製のツールだけでなく、サードパーティ製のツールも奨励していました。

計測するツールに関する基本的な部分は、State of the Union for Speed Toolingで語られています。

画像やフォント、JavaScriptに関するパフォーマンス改善の具体的なTipsについては、Speed Essentials: Key Techniques for Fast Websitesを見ると参考になります。

2日目のセッション

2日目は現在開発している新しい技術について、これからの展望が語られました。
すでにトライアルできるものから開発初期段階のものまで様々です。

virtual-scroller

virtual-scrollerは、バーチャルスクロール(画面内の必要なコンテンツのみレンダリングし、ユーザーのスクロールに応じて更新していく)を実現します。
Layered APIプロジェクトの1つとして進められていて、開発初期段階です。

Virtual Scrollとは

大量のコンテンツを表示するケースではレンダリングに時間がかかってしまいます。 例としてAddress BookやSNSのフィードなどが挙げられていました。

Virtual Scrollでは、画面外の不要なDOMを削除し必要な部分のみレンダリングするため、このようなケースのパフォーマンス改善に有効です。

f:id:nishimurae:20181128164519g:plain:w350

Virtual Scrollの技術自体は以前から存在し、特にネイティブアプリではお馴染みの技術です。 しかしWebに関してはまだ最善とは言えず、解決すべき問題が存在します。例えば、

  • 同じコンテンツ内でのリンクが動かない(全てのDOMが揃っていないため)
  • ページ内検索できない
  • 検索にインデックスさせられない

SEO観点での問題が大きいですね。画面外のDOMが存在しないため、実用面においてある程度犠牲を払わなければならないのが現実です。

どのように解決するのか

virtual-scrollerでは、リンクやページ内検索などの問題をInvisible DOMで解決しようとしています。 Invisible DOMとは、見えないけれども検索ができるDOMです。 検索などはできますが、レイアウトやスタイリング等のコストが不要なためパフォーマンスが上がります。

f:id:nishimurae:20181128164700g:plain:w350

通常のVirtual Scrollでは、画面外の不要な部分はDOMが存在しませんでした。Invisible DOMを利用したvirtual-scrollerでは、画面外の部分は見えませんがDOM自体はすべて存在します。

virtual-scollerではInvisible DOMをサポートする他にも、新たな問題を探しその解決にも取り組むそうです。 virtual-scroller: Let there be less (DOM)で詳細が語られています。

バーチャルレンダリングでは主にSEO観点での懸念があり、メリット/デメリットを理解した上で使用する必要があるというのが現状です。 今後Invisible DOMをサポートできるようになればより実用的になりそうです。

Web Packaging

Web Packagingは、端的に言うとWebサイトをパッケージ化する技術のことで、今回はSigned ExchangesBundled Exchangesが紹介されました。

1つ目のSigned Exchangesは、Exchange(HTTPリクエスト/レスポンスのペア)に署名して、任意のキャッシュサーバーから配信できるようにする仕組みです。

活用例として、AMPページのURLの最適化が紹介されていました。 Signed Exchangesにより署名されたAMPページを公開することで、Google検索は、AMPのキャッシュから配信されるAMPページではなく、Googleのキャッシュから配信される署名されたAMPページにリンクするようになります。

これがユーザーにとって何が嬉しいのか、もう少し具体的な話をしましょう。

f:id:nishimurae:20181126181634p:plain:w350

こちらは一休レストランのAMPページです。Googleの管理するAMPのキャッシュサーバーから配信されているので、URLを見るとドメインはwww.google.co.jpとなっています。 また、AMPページ用のヘッダーがアドレスバーの下に表示されています(上図赤枠)。

Signed Exchangesを利用することで、AMPページを通常のページと同様のドメインで、AMP用のヘッダーの表示なしで提供することができます。

2つ目のBundled Exchangesは、複数のExchange(HTTPリクエスト/レスポンスのペア)を1つにまとめる仕組みです。 現在プロトタイプの段階だそうですが、オフラインでのPWAのインストールなどに活用できるとのことでした。

Portals

Portalsは、SPAのようにサイト・ページ間のスムーズな遷移をブラウザで実現してくれる、現在開発中の技術です。 セッションではNavigationからTransitionへと説明されていましたが、異なるドメインであっても遷移間の摩擦を感じないページの移動が可能になります。

f:id:nishimurae:20181122173534p:plain

メリットとしては、

  • サイトを再構築することなく、
  • 異なるドメインであっても、

seamlessなページ遷移を可能にするという点です。

<portal>タグを埋め込むことでiframeのように利用することができます。iframeとの具体的な違いは以下の通りです。

f:id:nishimurae:20181128172647p:plain:w560

Portalが作動すると、documentがportalに置き換えられるため、常にトップレベルのブラウジングコンテキストとして作られます。 iframeは入れ子構造になるため、その点でも違いがあります。

まだ仕様策定の初期段階ではありますが、実用化が楽しみです。 デモでは「となりのヤングジャンプ」の事例が紹介されていましたが、ページ数の多い電子書籍にはかなり活用できそうですね。

Web PackagingとPortalsの話は、From Low Friction to Zero Friction with Web Packaging and Portalsで語られました。

フォーラム

朝から夕方まで休憩を挟みながらセッションが続きますが、その間隣の会場ではフォーラムが開催されています。 ここでは各ブースでデモを見たり、Googleのエンジニアと直接交流することができます。 セッションで語られたweb.dev、Lighthouse、VisBugなどのデモはもちろん、WebのパフォーマンスやUI/UXなど何でも相談できるReview Clinicというブースもあります。

今回はReview Clinicで一休.com レストランのスマホサイトのUI/UXについて聞いてみました。

最近のUI変更について聞いてみました!

10月にリリースした、プラン一覧のフィルターのUIについても聞いてみました。 プランを平日限定、飲み放題などの属性や時間帯で絞ったり、並べ替えを切り替えたりする機能です。

f:id:nishimurae:20181126193118j:plain:w350

上からこだわりフィルター(横スクロール)、時間帯タブ、並べ替えトグルの順番で並んでいます。 便利ですが多機能なため、どのように分かりやすく見せるかが難しい点です。

全部違うデザインなので少し分かりにくい。揃えたほうが良いと思う。
例えばフィルターの部分に関しては、スクロールできることが明らかに分かる方が良い。
スクロールバーを常に表示するとか、文字が分かりやすく途中で切れるようにするなど、何がベストか探してみてほしい。

やはり懸念していたように、複雑に見えたようです。

明らかなデザインをどのように実現していくかは、実際のユーザーの行動を数値的に見て判断する必要があります。 A/BテストをしたりしてどのようなUIが好まれるのか深掘りしてみると良いかもしれません。

学んだこと

Be Obvious: 明らかなデザインを意識すべき

会話の中で何度も "Be Obvious" と語られていました。 Googleでも様々なUIをユーザーの行動の結果を見て比較しているけれど、「明らかなデザイン」が良いという結果が出ているそうです。

機能としてはまず使ってもらえないと意味がなく、使ってもらうためには明らかに分かりやすいデザインでなければならないと感じました。

具体的に言うと以下のようなアプローチがあります。

  • カルーセルは画像がスクロールする方向に必ず半分くらいはみ出るようにして明らかに続きがあるように見せる
  • クリックできるボタンのスタイルを揃えてクリックできないものと区別する
  • アイコンにテキストで機能的な説明をつける(例: トレンドのアイコンに「トレンド」と説明をつける、など)

アクセシビリティを意識すべき

アクセシビリティとは、簡単に言うと、いかなるユーザーが、いかなるデバイスを使い、いかなる環境の下であっても利用できるようにする、ということです。 Webの制作者としては、具体的に以下の3つを意識すると良い、と話されていました。

  • タップターゲット
  • alt属性
  • 色のコントラスト比

ちなみにアクセシビリティはLighthouseで測定できます。 1日目のセッションで紹介したVisBugでも簡単にチェックすることができるので、そちらを利用してもいいかもしれません。

定量/定性的なユーザーフィードバックを元に意思決定すべき

1つ目の明らかなデザインの中でも触れていますが、UIに関する意思決定は定量/定性的なユーザーフィードバックを元に判断するべき、という話もされていました。

当然のことですが、一概に「このデザインが良い・悪い」ということはなく、ユーザーの動向を見ながら改善戦略を考えることの大切さを改めて感じました。

まとめ

初のChrome Dev Summit参加でしたが、総じて興味深い内容が多かったです。 特にセッション内でもケーススタディが多く、PWAの導入事例が国内外問わず増えたと感じました。 ケーススタディでは、実際に各社が設定したPerformance Budgetの話もされていて参考になりました。

技術的な観点で言うと、最も興味深かったのはやはりWeb PackagingPortalsです。 アプリケーションを再構築することなく利用できる、というのが嬉しいですね。

また、フォーラムやアフターパーティーのように交流できる場もあり、Googleのエンジニアやデザイナーとフランクに話ができるのも魅力でした。

おまけ:Women & Allies Receptionに参加しました!

実はChrome Dev Summit 2018の前日の夜にWomen & Allies Receptionというイベントにも参加しました。

Women TechMakersが主催していて、Chrome Dev Summit 2018参加者の女性全員が招待されていたようです。 実際参加していたのは50名くらいだったと覚えています。

イベント名の通り女性同盟なので参加者同士の交流がメインで、Googleの女性エンジニア、プロダクトマネジャー数人によるKeynote sessionもありました。 また、Googleからは、男性も含めChrome Dev Summitでセッションを担当している方など他に数名参加されていました。

Keynote sessionでは、ハラスメント問題や女性スピーカーが少ない問題などが話されていました。 つい最近起こったGoogle社でのストライキも、ハラスメント問題が関係していましたね。

女性スピーカーが少ない問題については、こちらの記事が取り上げられていました。

インポスター症候群やハラスメントに対する恐れなどが挙げられています。 Keynote session後のQ&Aでもこのあたりに関する議論が多かったように感じます。

明日は@ryo511さんによる「SVGスプライトアイコンの作り方・使い方」です。お楽しみに!

イベント開催のお知らせ ~12/12(水) Ikyu Frontend Meetup~

こんにちは。今日はイベント開催のご案内です。

12/12(水) に一休.com / 一休レストランの開発事例についてのミートアップイベントを開催いたします。

Ikyu Frontend Meetup

今回は「フロントエンド開発」をテーマとして

  • 一休レストラン スマートフォン検索ページのSPA化
  • 一休.com スマートフォンホテルページのパフォーマンス改善

を軸にNuxt.jsの導入、コンポーネント設計、CSS設計、画像最適化によるパフォーマンス改善などの事例をご紹介いたします。

セッションのほかにも、パネルディスカッションを予定していますので、参加者の皆さまと交流しながら、日々の学びを交換できればと思っています。

お申込みはこちらから。

ikyu.connpass.com

イベント実施に至ったきっかけ

user-first.ikyu.co.jp

この記事を公開したところ、「情報交換しましょう!」というお問合せを多数いただいたので、内容を膨らませてフロントエンド開発の取組みについてお話したら面白いのではないか、ということでイベントを開催することにしました。

お問合せをいただいた会社の方には、よいきっかけを与えてくださり、とても感謝しています。ありがとうございます。


まだ、募集枠には余裕がありますので、皆さまのご応募をお待ちしております!

インフラエンジニアからSREへ ~クラウドとSaaS活用が変えるサービス運用のお仕事~

2018年4月、データセンター完全クローズ

一休は、今年の4月にデータセンターを完全にクローズしました。現在、すべてのサービスをAWSを使って提供しています。
この過程で各種運用ツールやビルド/デプロイのパイプラインなどをすべて外部サービスを使うように変更しました。
これによって、インフラエンジニアやサービス運用担当者の役割や業務が大きく変わりました。本稿では、その背景を簡単に紹介したいと思います。

ざっくり言えば、

  • 物理サーバのセットアップ&データセンターへの搬入のような仕事はなくなった。
  • アプライアンスの保守契約、パッチ適用、運用ツールのバックアップのような仕事もなくなった。
  • 各種メトリクスを見ながら、Infrastructure as Codeでクラウドリソースの管理や調整をする仕事がメインになった。
  • 必要に応じて、プロダクトのソースコードに踏み込んで必要な改修を行い、サービスの安定化を支援する仕事も増えている。

結果として、SREとしての役割が求められるようになっています。

クラウド移行

一休のクラウド移行は、2016年末にキックオフ、2017年夏のアプリケーションサーバの移行を経て、2018年2月のデータベースの移行で、完全に完了しました。
概要は、過去の@shibayanid:ninjinkunの記事でも紹介しています。

user-first.ikyu.co.jp

user-first.ikyu.co.jp

user-first.ikyu.co.jp

これに伴って、以下のようなツールやミドルウェアも外部サービスに移行しました。

  • ソースコード管理はGitHub Enterprise から GitHub.comへ
  • CI/CDはJenkins + ファイル同期ツールからAppVeyor & CircleCIへ
  • リバースプロキシはBIG-IPからFastlyへ
  • メールサーバはアプライアンス製品からSendGridへ
  • etc

それぞれに、

  • 移行の動機
  • 移行のタイミングで一緒に改善できたこと
  • 移行によってなくなった作業

があります。

GitHub Enterprise(GHE) から GitHub.comへ

  • [移行の動機] GHEを使い続ける必然性がない。
  • [一緒に改善] 不要ファイルを削除してリポジトリのサイズダウン。
  • [なくなった作業] GHEのバックアップとバージョンアップ。

もともと、社内にGitHub Enterprise(GHE)を立て、ソースコードを管理していました。しかし、社内環境で自前で管理する必然性がありませんでした。
そこで、運用環境をクラウドに移行するのをきっかけにして、GitHub.comへコードを移行しました。
このとき、宿泊予約サービスのソースコードリポジトリに大量に残っていた不要な画像をGitHubへの移行対象外にしました。これによってリポジトリが小さくなり、CI/CDの速度が向上し、開発時のリポジトリ操作の快適になりました。
そして、GHEのバックアップやバージョンアップという作業がなくなりました。

※ 画像の改善については、以下のid:kentana20のスライドに詳しいです。

speakerdeck.com

Jenkins + ファイル同期ツールからAppVeyor & CircleCIへ

  • [移行の動機] 複雑になってしまったJenkinsをクラウドに持っていく必然性がなかった。
  • [一緒に改善] ビルドパイプラインの安定化 &リリース回数アップ。
  • [なくなった作業] Jenkinsのジョブのメンテナンス、不安定なファイル同期ソフトの管理。

CI/CDは社内に立てたJenkinsが担っていました。自前でブルーグリーンデプロイを実現するために複雑なジョブを組んだ結果、メンテナンスしにくくなっていました。またJenkins はバージョン1系を使っていたのでジョブの定義をGitHubで管理できていませんでした。
そして大きな問題だったのがビルド結果を配布するのに使っていたファイル同期ツールです。このツール、有償の製品だったのですが、動作が安定しない。。。
結果、途中でデプロイが失敗して手動でリカバリする、という辛い作業を繰り返していました。
運用環境をクラウドに移行するに伴い、このデプロイの仕組みをそのまま持っていく必然性はありませんでした。
また、アプリケーションはAWS Elastic Beanstalk(EB)を使うことに決まっていたため、デプロイは完全にEBが提供仕組みに任せることができました。
あとは、ビルドをどうやるか、ですが、AppVeyorとCircleCIで実現することにしました。CircleCIだけではWindowsプラットフォームで動作するアプリケーションのビルドに対応できないため、AppVeyorも併用しています。
AppVeyorもCircleCIもymlファイルで制御できるので、アプリケーションのリポジトリで一緒に管理することでメンテナンスしやすくなりました。
この移行によって複雑なJenkinsジョブや不安定なファイル同期ソフトのメンテナンスをする必要がなくなり、CI/CDパイプライン自体が大幅に安定しました。
その結果、リリースに手がかからなくなったため、本番リリースの回数を増やすことができました。

BIG-IPからFastlyへ

  • [移行の動機] クラウドに持っていくのは高コスト。
  • [一緒に改善] FastlyのCDN積極活用によるサイトのUX改善と安定化。
  • [なくなった作業] アプライアンスの監視、メンテナンスや保守契約更新。

データセンターでは、ロードバランサ/リバースプロキシとしてBIG-IPを使っていました。
BIG-IPにはiRuleと呼ばれる独自のリライト、ルーティングの機構が搭載されており、一休でもこれを使っていましたが、管理が大変でした。
また、BIG-IPをクラウドに持っていくのも、移行後の運用コストを考えると難しいと判断しました。
そこで、クラウド移行に伴い、BIG-IPの代わりにFastlyを使うことにました。
FastlyをマネージドなVarnishが搭載されたリバースプロキシととらえ、VCLファイルをGitHubで管理し、TerraformとCircleCIでCI/CDを構築しました。結果、インフラエンジニアでなくても、ルーティングのルールを修正できるようになりました。変更に対する心理的な負担も大幅に軽減されました。
また、負荷対策やUXの改善のためにCDNとしての機能も積極的に活用しています。
さらに、にFastlyの進化の恩恵を自動的に受けられるのも大きいです。例えば、ほとんど何もせずにHTTP/2を導入することができました。

アプライアンス製品のメールサーバからSendGridへ

  • [移行の動機] クラウドにアプライアンスを持って行くわけにいかない。
  • [一緒に改善] メール関連の処理をリファクタリングして合理的に。メルマガ配信も簡単にSendGridに移行。
  • [なくなった作業] アプライアンスの監視、メンテナンスや保守契約更新。

データセンターではアプライアンス製品のSMTPサーバを使っていましたが、これもクラウドに持っていけません。そこですべてのメールをSendGridを使って送信するようにしました。日本の代理店である構造計画研究所様にもしっかりとサポートしていただきました。

移行の詳細は、過去の@shibayanの記事や、@minato128のスライドが詳しいです。

user-first.ikyu.co.jp

https://speakerdeck.com/minato128/ikyu-mail-platform

移行に伴い、積極的にリファクタリングしました。
例えば、メール送信の必要性を棚卸しして、不要なメールは廃止しました。また、開発者向けのエラー通知メールなどメールである必要のないものはSlackへの通知に切り替えました。
この移行でアプライアンスの保守更新やパッチ適用などの各種メンテナンス作業から解放されました。
また、別の外部サービスで実現していたマーケティングのメール配信も比較的簡単にSendGridに移行できました。これによってメールマガジンの配信コストが大幅に削減できました。

仕事が減った(^^) じゃあ何する?

ほかにも、アプリケーションログの管理や本番データベースの定期運用などにも同様の変化が起きました。この結果、インフラエンジニアの管理作業が大幅に削減できました。
では、今はどんなことをしているかというと、、、

Infrastructure as Codeでクラウドリソースの管理

もちろん、クラウドに移行したからといって、キャパシティプランニングや配置設計のような仕事がなくなるわけではありません。
むしろ、リソースを柔軟かつ素早く確保できるというクラウドのメリットを積極的に活用してサービスの安定化に貢献する必要があります。
一休ではクラウドリソースの管理をTerraformを使ってAWSのリソースを管理しています。
モニタリングはDataDogを使っています。DataDogのメトリクスを見ながら、リソースの消費具合の推移をにらみつつ、必要な調整を行うのは重要な仕事の一つです。
また、新規サービス構築ではプロダクトを開発しているエンジニアと協力して、Terraformを使って必要なクラウドインフラをセットアップし、モニタリングの設定を行います。Terraform自体のバージョンアップやメンテナンスも重要な仕事です。

プロダクトのソースコードに踏み込んで必要な改修を行う。

プロダクトのコードを修正する機会も増えています。特に、サービスの安定化に貢献できるような修正をしています。
例えば、キャッシュの導入や高負荷なSQLの分割 & 非同期化などです。

つまり、サービスの安定化がミッションになっています。

というわけで、 インフラエンジニア改めSREになりました。 一休ではインフラエンジニア改めSREとして活躍してくれる仲間を募集しています。

hrmos.co

当社については以下の紹介記事をご覧ください。

user-first.ikyu.co.jp

この記事の筆者について

  • システム本部CTO室所属の 徳武 です。
  • サービスの技術基盤の開発運用、宿泊サービスの開発支援を行なっています。

一休.comレストランのスマートフォン検索ページがSPAになりました

一休.com レストランは今年の 7 月 18 日、スマートフォン向け検索ページのリニューアルを行いました。このエントリーでは、その中身について少し紹介させていただきます。

検索ページの課題

一休.com レストランではスマートフォン向け検索ページに対して「遅い」という課題意識がありました。これは技術面で少しブレイクダウンすると;

  • パーソナライズドを含む複雑な処理を行っているため、サーバーサイド処理が重い。
  • UI 上無駄な遅延処理を行っているため、クライアントサイドの描画が遅い。

というサーバー側とクライアント側両方の課題がありました。クライアントサイドの「無駄な遅延処理」というのは;

  • 検索結果取得が REST API 化されているにも関わず、再検索の度にページリロードを行い、サーバーサイドの描画からやり直している。

という実装に問題がありました。下図がリニューアル前のページ描画の様子です:

f:id:supercalifragilisticexpiali:20180924215006p:plain
画面描画後に動的な検索結果が遅延描画されている図

社内では UI 上のこの問題点に対して課題意識が大きく;

「検索と再検索の回遊性を改善したい」

という要求が強くなっていました。

Web フロントエンドのコンポーネント化

話は変わり、一休.com レストランは古い技術セットの上に構築されています。Microsoft の Classic ASP と VBScript を主体としたアプリケーションで永らく運営されてきました。その一方でユーザー向け web ページの表現力を向上させて行きたいという要求から、Vue.js によるフロントエンド実装も徐々に始まっていました。

Vue.js は SPA のような高い表現力を簡単な記述で実現できるのが魅力です。従来の web 開発では;

  • HTML
  • CSS
  • JavaScript

という ファイルタイプによる縦割り開発 が主流でした。しかし;

  • BEM や ECSS のようなCSS フレームワークの提唱する思想、
  • Vue.js のような JavaScript フレームワークのメカニズム、
  • Web Components による標準化の流れ

から明らかなように、UI 改善において コンポーネントを中心に串刺し設計する事の重要性 が認知されてきています。

一休.com レストランでは、あらゆるフロントエンド実装を Vue.js に移行する事で Smart UI パターンのような開発形態から、コンポーネント化された web アプリケーションへ移行する事を技術的な目標としています。

実現すればコンポーネント指向の持つユーザーメリットを享受できるようになり、一休の掲げる「ユーザーファースト」を後押しする開発体制が実現できると考えています。

SEO とサーバーサイドレンダリング

しかしここで問題がありました。SEO の考慮です。一休.com レストランの高い集客力は SEO に真摯に取り組んできた結果でもあります。

この SEO の観点から;

  • ページ上のあらゆる重要なコンテンツは、サーバーサイドレンダリングされていなければならない。

という要求がありました。これは、クライアントサイドレンダリングを基本とする Vue.js でコンポーネント化された web アプリケーションへ移行する目論見と衝突しました。

  • コンポーネント指向開発を実現する事。
  • 重要なコンテンツはサーバーサイドレンダリングされる事。

この 2 つが「ユーザーファースト」を目指す上で必要な技術要件でした。

尚、今年の Google I/O 2018 の Google Webmasters からのアナウンスで状況は変わりつつありますが、リニューアルの意思決定から開発の段階では、クライアントサイドレンダリングで良しと言える状況ではありませんでした。

業務課題と技術課題の合致

このような背景から、コンポーネント指向とサーバーサイドレンダリングという 2 つ の技術課題の解決手段として、サーバーサイド JavaScript の導入が現実味を帯びてきました。Vue.js においてはユニバーサル JavaScript を実現するフレームワーク Nuxt.js が存在しており、これによるプロトタイプを社内で進めるようになります。

結果として、一休.com レストランの技術スタックの理想像は下図のようになって行きました:

f:id:supercalifragilisticexpiali:20180830215511p:plain
フロントエンド構成のヴィジョン

これなら業務課題である検索ページの UI 改善と、技術課題を解決できます。最終的に、一休.com レストランの検索ページリニューアルで、Nuxt.js の導入を決断しました。

次の項では Nuxt.js で実装が開始された検索ページの設計を紹介します。


コンポーネント指向設計

検索ページリニューアルでは、全面的にコンポーネント指向設計を導入しました。

コンポーネントの定義

一休.com レストランの web フロントエンドでは、コンポーネントを下図のように捉え定義しました:

f:id:supercalifragilisticexpiali:20180924222349p:plain
コンポーネントの定義

  • データ、テンプレート、ロジック、スタイル、それぞれ関連性が深いもの同士をモジュール化
  • ファイルタイプによる縦割りではなく、関連性によるファイルタイプ横断の串刺しでグループ化
  • フロントエンド実装のあらゆるアセットをコンポーネントと捉えて管理

上記を基本とし、CSS、JavaScript、画像、Vue 単一ファイルコンポーネントなど全ての分割粒度として、共通のコンポーネントという概念を前提としました。

ITCSS によるレイヤードアーキテクチャ

これらコンポーネントを共通のレイヤードアーキテクチャで管理するため、一休.com レストランでは CSS アーキテクチャの 1 つである ITCSS を採用しました。

f:id:supercalifragilisticexpiali:20180924220842p:plain
ITCSS レイヤードアーキテクチャ

ITCSS 自体は CSS エンジニアである Harry Roberts 氏 が提唱する CSS の詳細度を管理するためのレイヤードアーキテクチャです。

  • CSS でコンポーネント指向設計を実践する事を前提にしたアーキテクチャである点、
  • 抽象度を管理するレイヤードアーキテクチャとして柔軟である点、

これらの点から web フロントエンドのコンポーネント抽象度化レイヤーとして自然に捉え直す事ができます。このアーキテクチャを利用し、以下のようにレイヤーごとの責務を定義しました:

レイヤー 定義
Settings CSS 変数や、定数などのデータを扱う。
Tools CSS ミックスイン、フィルター、またはバリューオブジェクト、DTO のようなアプリケーション上の型となる定義を扱う。
Generic CSS 要素型セレクターによるグローバルスタイル定義、アプリケーション全体で共通化された処理、グローバルな副作用を持つビジネスロジックを扱う。
Elements Atoms のようなプリミティヴなコンポーネントを扱う。これ以下のレイヤーで Vue.js SFC を扱う。
Objects Molecules のようなアプリケーション上のコンテキストを含むコンポーネントを扱う。
Components Organisms のようなアプリケーション上意味のある機能単位のコンポーネントを扱う。

上図のように Elements レイヤーより Atomic Design のレイヤー概念も取り入れています。そもそもとして、下図のような Atomic Design によるレイヤードアーキテクチャも検討しました。

f:id:supercalifragilisticexpiali:20180924220846p:plain
Atomic Design レイヤードアーキテクチャ

しかし;

  • Atomic Design における Atoms レイヤーはグローバルなデータやビジネスロジックなどを表現するレイヤーとしてはスコープが広くなり過ぎる。
  • 一方でこれらの表現に向いていそうなクリーンアーキテクチャでは、UI コンポーネントの抽象化を表現するには表現力が低い。
  • ITCSS はその点で、グローバルなデータやビジネスロジックを表現するレイヤーと UI コンポーネントを表現するレイヤーが最初から定義されておりバランスが良い。

と考え ITCSS を採用しました。

パフォーマンスの観点

ITCSS は CSS 詳細度を管理するアーキテクチャであり CSS のクライアントパフォーマンスを最大化する目的があります。また web フロントエンドはコンパイラより実装者にパフォーマンス最適化の責務があると考えます。

この観点から Tools と Generic レイヤーでデータ定義とビジネスロジックを扱うレイヤーを分ける事が、オブジェクトにメソッドを生やしてビジネスロジックを実装するのを避ける事につながり、webpack の tree-shaking による最適化を享受しやすい実装を導出するメカニズムになると考えています。

コンポーネント指向の置き換え可能という特性を、パフォーマンスの観点を持ったレイヤードアーキテクチャで管理する事で DRY を実践しやすく、結果としてハイパフォーマンスなフロントエンドが実現可能なメカニズムとなる事を期待しています。


つづいて Nuxt.js による BFF 実装を進めていく上で難しかった点をいくつか紹介します。

ユニバーサル JavaScript

まず Nuxt.js の最大の特徴は、Vue.js による実装で、サーバーサイドもクライアントサイドも透過的に記述できる事でしょう。これによりコンポーネント指向設計において、サーバーとクライアントという動作環境の違いを、ファイルシステムの違い同様、串刺しにコンポーネントとしてカプセル化できるようになります。これが Nuxt.js の大きなメリットです。

サーバーサイドとクライアントサイド API の違い

しかし同じ JavaScript とは言え、Node.js と web ブラウザの API には違いがあります。Nuxt.js が用意していないインターフェースで、サーバー/クライアントを透過的に扱いたかったのが次の 2 つです:

  • サーバー/クライアントでの Cookie インターフェースの違いを吸収する
  • バグレポード Bugsnag のサーバー/クライアントサイドのクライアントを透過的に扱う

これらの実現に、Nuxt.js の modules と plugins 機能を用いました。

ユニバーサル Cookie

Nuxt.js で Cookie を透過的に扱うにあたって UniversalCookie コンポーネントを作成し、サーバー/クライアントサイドでの CRUD 処理を透過的に記述できるインターフェースを用意しました。そしてこれを Nuxt.js の plugins として登録しました。

plugins/cookie.ts:

import * as http from 'http';
import { createUniversalCookie } from '@/components/generic/UniversalCookie';

export default function ({ req, res }: { req: http.IncomingMessage, res: http.ServerResponse }, inject): void {
  const cookie = createUniversalCookie(req, res);
  inject('cookie', cookie);
}

上記のような plugin を登録する事で、Nuxt.js のコンテキスト上では app.$cookie.set(key, value)this.$cookie.get(key) と言った記述で、サーバー/クライント関係なく cookie の読み書きができるようになりました。

ユニバーサル Bugsnag

一休.com レストランではクライアントサイドのバグ検知に Bugsnag を利用しており、ユニバーサル JavaScript 化に当たって、サーバーサイドの JavaScript 処理エラーも Bugsnag へレポートするインターフェースを用意しました。

modules/bugsnag/index.js:

const path = require('path');
const bugsnag = require('bugsnag');

module.exports = function BugsnagModule(moduleOptions) {
  bugsnag.register(moduleOptions.SERVER_API_KEY, {
    appVersion: (process.env.VERSION_SHA1).slice(0, 7),
    autoCaptureSessions: false,
    autoNotify: process.env.NODE_ENV !== 'development',
    releaseStage: process.env.NODE_ENV || 'development',
  });
  this.nuxt.hook('render:setupMiddleware', app => app.use(bugsnag.requestHandler));
  this.nuxt.hook('render:errorMiddleware', app => app.use(bugsnag.errorHandler));
  this.addPlugin({
    src: path.resolve(__dirname, 'plugin.js'),
    options: moduleOptions,
  });
};

modules/bugsnag/plugin.js:

import Vue from 'vue';

export default function (context, inject) {
  const VERSION = (process.env.VERSION_SHA1).slice(0, 7);

  // サーバーサイド Bugsnag
  if (process.server) {
    const bugsnag = require('bugsnag');

    bugsnag.register('<%= options.SERVER_API_KEY %>', {
      appVersion: VERSION,
      autoCaptureSessions: false,
      autoNotify: process.env.NODE_ENV !== 'development',
      releaseStage: process.env.NODE_ENV,
    });

    inject('bugsnag', bugsnag);
  }

  // クライアントサイド Bugsnag
  if (process.client) {
    const bugsnagJs = require('bugsnag-js');
    const bugsnagVue = require('bugsnag-vue');

    const client = bugsnagJs({
      apiKey: '<%= options.CLIENT_API_KEY %>',
      appVersion: VERSION,
      autoCaptureSessions: false,
      autoNotify: process.env.NODE_ENV !== 'development',
      releaseStage: process.env.NODE_ENV,
    });

    client.use(bugsnagVue(Vue));
    inject('bugsnag', client);
  }
}

上記のような module を登録する事で、app.$bugsnag.notify(new Error('...')) ないし this.$bugsnag.notify(new Error('...')) でハンドリングされたエラー処理のレポートを透過的に記述できるようになり、例外もサーバー/クライアントサイド両方を検知できるようにしました。

ただしこれらは Nuxt.js のコンテキストを通じてインターフェースを初期化する必要があるため、 this.$cookiethis.$bugsnag への参照を持つコンポーネントは Nuxt.js 実装に密結合となります。なのでこれら plugins へのアクセスは layouts/pages を通じてのみ行うルールとし、コンポーネントの責務をコントロールしています。

副作用の考慮

クライアント JavaScript をユニバーサル JavaScript に対応する過程にも注意する事がありました。グローバルな初期処理による副作用です。

例えば都度参照ではコストが大きい window.innerWidth のようなグローバルプロパティ値をキャッシュする次のようなモジュールがあります。

windowsize.js:

let windowWidth;
let windowHeight;

function resize() {
  windowWidth = window.innerWidth;
  windowHeight = window.innerHeight;
}

window.addEventListener('resize', resize, false);
window.addEventListener('orientationchange', resize, false);

resize();

export {
  windowWidth,
  windowHeight,
};

このようなモジュールが Nuxt.js の pages コンポーネントで import されるとサーバーエラーとなり、ユーザーには 500 エラーが返る事になります。しかしクライアントサイドに限った JavaScript 実装であれば、ありがちな実装であり、グローバルな副作用を局所化する手段としても合理的です。しかし window オブジェクトのようにクライアント JavaScript にしか存在しない API の暗黙的な参照が発生する上記のようなモジュールをうっかり Nuxt.js で import すればアプリケーションが起動しなくなります。こういった点はユニバーサル JavaScript において煩わしさを感じる点でもあり、副作用の影響を考える上で面白い点でもあると思います。

これを解決する方法としては、あらゆるグローバルな処理を Nuxt.js (Vue.js) のライフサイクルに載せるという方法を取りました。上記の windowsize.js は次のように変更しました。

windowsize.js:

import Vue from 'vue';

export const WindowSize = Vue.extend({
  data() {
    return {
      height: undefined,
      width: undefined,
    };
  },
  created() {
    if (this.$isServer) {
      return;
    }
    window.addEventListener('resize', this.resize, false);
    window.addEventListener('orientationchange', this.resize, false);
    this.resize();
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resize);
    window.removeEventListener('orientationchange', this.resize);
  },
  methods: {
    resize(): void {
      this.height = window.innerHeight;
      this.width = window.innerWidth;
    },
  },
});

export const windowSize = new WindowSize();

こうする事でクライアントサイドの API に依存するグローバルな処理が記述されたモジュールでも透過的に import できるようになりました。

クライアントサイドのみまたはサーバーサイドのみの実装とは違い、いくつか考慮するべき事はありますが、結果的により堅牢な実装を求められる点がユニバーサル JavaScript の面白さでもあると思います。


リニューアルの成果

リリースされた検索ページでは下図のように不要なリロードを必要としない SPA にリニューアルされました。これによってスマートフォンでの再検索のストレスが軽減されたと考えています。

f:id:supercalifragilisticexpiali:20181004235229g:plain
SPA 化による不要なリロードの無くなった検索ページ

またクライアントサイドのパフォーマンス指標である Speed Index の RUM 値も、リリースを境に改善する事ができました。

f:id:supercalifragilisticexpiali:20181005103148p:plain
RUM-SpeedIndex トラッキング値の変化

このリニューアルを契機に Classic ASP による密結合なアプリケーション実装から、Python をバックエンド、API とし、BFF と Nuxt.js をフロントエンドとする疎結合な開発体制が確立しました。今後はこの開発体制のメリットを最大化しユーザー体験の向上へと繋げていくのが、新フロントエンドの課題です。

モダン・フロントエンドで提供する価値とは

変化の激しい web フロントエンドですが、昨年は PWA の推進や AMP の登場、パフォーマンス指標の定量化など、めまぐるしい年だったように思います。一休.com レストランの web サイトは、これら技術を最大限活用し、ユーザーにとってより良い web サービスを提供してゆきたいと考えています。

今後の web フロントエンドの取り組みとしては;

  • 継続的なパフォーマンス改善
  • PWA 化による web 体験のエンハンスメント
  • ユーザー体験の向上につながるドラスティックな UI 改善

などを考えています。

そんなわけで 一休.com レストランでは、ユニバーサル JavaScript が得意なフルスタックエンジニア、BFF 設計や GraphQL を得意とされる Node.js エンジニア、コンポーネント指向を実践でき Web Components のような標準仕様にも敏感な web フロントエンドエンジニア、デザインシステムや Brad Frost 氏の提唱する Atomic Design へ高い関心をお持ちのデザイナーなど、プロフェッショナリズムにあふれたメンバーを募集しています。ラグジュアリーなサービスを最高のクラフトマンシップで支えてくれる方からのご応募お待ちしております!

www.ikyu.co.jp

www.wantedly.com

以上、CTO 室レストラン担当エンジニアの稲尾 id:supercalifragilisticexpiali がお伝えしました。