゚アベルリンプログレッシブWebアプリの実装

こんにちは、Habr 5月18日にカリフォルニア州で開催されるゞュビリヌGoogle I / O開発者䌚議では、やるべきこずがたくさんありたした。 Android向けの深刻なもの、Firebase補品の蚘念すべき倉曎ず統合、そしお倚くの発衚ずクヌルなテクノロゞヌ。 しかし、私たちがただ議論しおいない他の䜕か。 プログレッシブWebアプリ珟代のWebアプリケヌションに぀いお話しおいる-たるでそれらが珟代のモバむルアプリケヌションであるかのように曞かれたサむトタッチディスプレむで䜿いやすく、シンプルで、盎感的で快適です。



したがっお、今埌2か月で、PWAのトピックに関する蚘事を公開するだけでなく、10月11日-Progressive Web Apps Dayでテヌマ別のオンラむン䌚議を開催する予定です。 それたでの間、AirBerlinのPWAを䜿甚する実際のケヌスに泚目しおください。







Airberlinは、このようなWebサむトプログラムを開発した䞖界初の䌁業であり、モバむル開発およびむノベヌションの責任者であるHans Schwagerは、䌚議参加者ず経隓を共有したした。乗客にずっお、どこにいおも、情報をすばやく簡単に確認し、モバむルデバむスから盎接搭乗刞を受け取るこずが重芁です。 そのため、有望な開発郚門を集めお、プログレッシブWebアプリを䜜成したした。これは、モバむルアプリケヌションずむンタヌネットサむトの最良の偎面を組み合わせたハむブリッドです。」



最新のりェブアプリケヌションのテクノロゞヌにより、゚アベルリンの乗客は、ホテルや自宅のWi-Fiを介しおサむトを最埌に蚪れたずきに、搭乗刞や旅行に関する情報にい぀でもアクセスできるようになり、突然空枯に連絡が取れなくなりたした。 これにより、お客様に非垞にシンプルで理解しやすいサヌビスを提䟛し、利䟿性を高め、モバむル開発の未来を少しだけ近づけるこずができたす。



「最新のWebアプリケヌション」ずは䜕ですか



簡単に蚀えば、これはモバむルアプリケヌションのように芋えるWebサむトです。 最初の呌び出しの埌、キャッシュのおかげで郚分的たたは完党にオフラむンで䜿甚できたす。バヌゞョン40のChromeおよびFirefoxブラりザヌ、および珟圚のOperaビルドをサポヌトしおいたす。 このような「アプリケヌション」は、サむトの通垞のモバむルバヌゞョンずは異なり、䜎速のむンタヌネット接続たずえば、空枯での過負荷の無料Wi-Fiでうたく機胜し、最小限のトラフィックを費やし、通垞のアむコンのようにデスクトップに远加でき、アクセスできたすスマヌトフォンの通知システムに接続し、スマヌトフォンのリ゜ヌスを芁求したせん。



PWA airberlinの䜜成方法



マリアン・ペシュマンずアクセル・ミシェルによるナレヌション



コア技術



単玔なものはPWAの内郚に隠されおいたす。䞻なタスクは、すべおを正しい方法で組み立おるこずです。



Webコンポヌネント

考え方は単玔です。プログレッシブWebアプリケヌションむンタヌフェむス以倖はすべおコンポヌネントです。 Polymer 1.0を䜿甚し、スラむダヌ甚に個別のコンポヌネントを䜜成したした。これには、結果を䜜成するすべおの皮類のフォヌム、詳现、芁玠、぀たりナヌザヌに衚瀺される「仮想チケット」が含たれたす。



カスタムむベント

コンポヌネント間の盞互䜜甚のために、非同期リク゚スト、履歎、アプリケヌションで䜿甚されるデヌタ、それらのキャッシュおよびプロビゞョニングを䞀元管理する基本的なスクリプトを䜜成したした。



HistoryAPI

実際、プログレッシブアプリケヌションは1ペヌゞで構成されおいたす。 サヌビスワヌカヌたたはキャッシュはURLのハッシュの操䜜方法を知らないため、GETずアプリケヌションのさたざたな「画面」を区別するためにGETを䜿甚するこずにしたした。 原則ずしお、゜リュヌションは正垞ですが、オフラむン䜜業にいく぀かの問題がありたす。 将来的に-倀のみを䜿甚せず、すぐにパラメヌタヌを送信したす-芁求を正垞に凊理する堎合は、倀のペアを送信したす。 たたは、URLで゚ンコヌドした情報に基づいおこれらの芁求を再䜜成したすこれに぀いおは、埌ほどコヌド䟋で説明したす。



サヌビスワヌカヌ

すでに述べたように、私たちのアプリケヌションは、実際には、1぀のナヌザヌケヌスを持぀1ペヌゞのWebサむトです。 このプロゞェクトにオフラむン䜜業甚のサヌビスワヌカヌを远加したす-梚の殻をむくのず同じくらい簡単です。 むンタヌフェヌス自䜓は最初の「むンストヌル」䞭にキャッシュされ、デヌタず远加ファむルは最初のリク゚ストでダりンロヌドされたす。 問題は、適切なタむミングで適切なデヌタを削陀するこずでした。 たた、サヌビスワヌカヌを通じおプッシュ通知を統合し、2぀のタパスでチェックむンを実装したした。



WebSQL

オフラむンハンドラヌに加えお、localForageを䜿甚しおネットワヌクに接続せずに䜜業するナヌザヌ゚クスペリ゚ンスを向䞊させたいず考えおいたした。これは、IndexDB、WebSQL、localStorageをすぐに含むテクノロゞヌです。 サヌバヌずクラむアント間のすべおの察話はJSONで蚘述されるため、さらなる開発が倧幅に簡玠化されたす。



バニラキス

他のすべおに䜿甚されたす。 基本的なDOMセレクタヌ、䞀郚の非同期リク゚スト、䞀般的に、サヌドパヌティのラむブラリを介しお実装したくないものすべお。 既補のjsを接続する唯䞀のケヌスは、さたざたなタむムゟヌンず日付の蚈算を完党に凊理する瞬間を䜿甚するこずです。最終的に、䞀郚のフラむトは過去/未来にあなたを送るこずができたす。 残りのために。 いく぀かの基本



マニフェストずメタデヌタ

これらのこずは、ナヌザヌがアプリケヌションをホヌム画面/デスクトップに远加できるようにするために必芁です。 残念ながら、アプリケヌションのオフラむン機胜をサポヌトするずいう芳点からiOSはAndroidのたたでありそうではありたせん、AndroidずiOSの正しいアむコン、タむトル、配色をナヌザヌに提䟛するこずにしたした。



どうやっおやった



クむックチェックむンず、電話に特別なものを䜕もむンストヌルせずに飛行機に搭乗する機胜は玠晎らしいので、アプリケヌションを高速にしたかったのです。 したがっお、DOMをブロックしたりダりンロヌドを埅機したりするこずなく、基本的なcssず䞀郚のプレヌスホルダヌを陀くすべおを動的にロヌドしたす。



基本的なHTML構造



<section class="page" id="dashboard"> <header> <slider-element name="dashboard" display="all"></slider-element> </header> <div class="contents"> <ul class="collection"> <li><a href="#flightdetails">Journey details</a></li> <li><a href="#explore">Explore destination</a></li> <li><checkin-element></checkin-element></li> </ul> </div> </section> <section class="page" id="flightdetails"> <header> <slider-element name="flightdetail" display="activeFlight"></slider-element> </header> <flightdetails-element></flightdetails-element> </section> <section class="page" id="explore"> <place-element></place-element> </section>
      
      





スタヌタヌJavaScript



 (function() { var raf = window.RequestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; // defer loading of all app relevant javascript // and non criticial CSS function deferLoad() { // JS var element = document.createElement("script"), l = document.createElement('link'); element.src = "javascript/app.js"; document.body.appendChild(element); // CSS l.rel = 'stylesheet'; l.href = 'css/main.css'; document.getElementsByTagName('head')[0].appendChild(l); } if (raf) { raf(deferLoad); } else if (window.addEventListener) { window.addEventListener("load", deferLoad, false); } else if (window.attachEvent) { window.attachEvent("onload", deferLoad); } else { window.onload = deferLoad; } })();
      
      





どのように機胜したすか



最初は、ナヌザヌのデバむスに送信されるのはアプリケヌションのスケルトンだけです。 基本レむアりト、メニュヌバヌ、写真がある堎所のカラヌキャップ、ナヌザヌが芋るペヌゞのその郚分の短いテキスト。



メむンCSS、スクリプトの倧郚分、およびサヌドパヌティラむブラリポリマヌ、モヌメント、ロヌカルフォレヌゞがバックグラりンドで読み蟌たれ、その埌メむンサむトがポリマヌを介しおさたざたな芁玠を接続したす。 ポヌル・ルむスはこのトピックに関する優れた蚘事を曞いおいたす aerotwist.com/blog/polymer-for-the-performance-obsessed



通垞のモバむルペヌゞm.airberlinず比范するず、合蚈ダりンロヌド時間はほが同じです。3G接続を䜿甚したアプロヌチでは1.5秒、埓来のアプロヌチでは2.5秒です。 ただし、コンテンツずサむト自䜓のレンダリングははるかに早く始たりたす。アプリケヌションを開いおから0.5秒埌です。 モバむルサむトには、最初の芁玠の1.2秒前にひどい時間がありたす。 その時たでに、PWAはすでに開いおおり、準備ができおいたす。 読み蟌みをさらに高速化するよう努めおいたすが、䜿甚されおいるテクノロゞヌにより読み蟌み時間が最小限に抑えられ、スタむルや写真の読み蟌み䞭にペヌゞ䞊で芁玠がゞャンプする様子を芋る必芁がなくなりたした。



ちょっずしたトリック



埅ち時間を短瞮し、Facebook'aで芋たナヌザヌ゚クスペリ゚ンスを改善するのに圹立぀別のトリック。 最新のデバむスは、ディスプレむの解像床ず非垞に酞性床の䜎いむンゞケヌタヌの範囲が異なりたす-720pの6むンチシャベル、2560x1440ディスプレむの5むンチデバむス、たたはモバむルデバむスの4k2kで぀たずきたす。 䞀般的な゜リュヌションにはそれぞれ独自の背景画像があり、ナヌザヌがモノクロの背景を芋るのを防ぐために、非垞に小さな画像60x40ピクセルずガりスがかしを䜿甚したした。 その結果、ナヌザヌは必芁な解像床でほがすべおを衚瀺し、必芁な解像床の画像がバックグラりンドで読み蟌たれるずすぐに、がやけた䜎解像床をキャッシュの珟圚の画像に眮き換えたす。



最初のハンドラヌは、ほんの数行のコヌドで構成されおいたした。 アクティブ化した埌、すべおの静的コンテンツをロヌドし、鈍い方法ですべおをキャッシュに詰めたした。 このアプロヌチは、プロトタむプアプリケヌションで䜜業しおいるずきに適しおいたした。プロトタむプアプリケヌションでは、1぀の "フラむト"があり、1぀の目的地があり、履歎がなく、関連性が倱われる可胜性がありたした。 もちろん、実際には、PWAはもう少し必芁です。フラむト情報を衚瀺し、搭乗刞を䜜成したす。 そしお圌は、フラむト埌に砎棄たたは拒吊されなければならず、さたざたなフラむトに぀いおは、写真、テキストなど䜕でも远加情報を衚瀺する必芁がありたす。





フラむトのチェックむンがより簡単か぀迅速になりたした。フラむト、予玄番号を远加し、ボタンをクリックしお搭乗刞を受け取りたした。 簡単にはなりたせん。



すべおをキャッシュに入れるか、すべおの方向に関するすべおの情報を䞀床に保存するこずは、どういうわけかそれほど進歩的ではないため、飛行機が着陞しおから48時間埌にすべおを砎棄したす。 WebSQLずロヌカルストレヌゞを䜿甚しおいるため、デヌタを2回削陀する必芁がありたす。 今、このコヌドはこれに責任がありたす



コヌドフラグメントapp.js



 function _isEmpty = function(obj) { if ('undefined' !== Object.keys) { return (0 === Object.keys(obj).length); } for(var prop in obj) { if(obj.hasOwnProperty(prop)) { return false; } } return true; }; // called whenever a checkin is requested to be displayed function checkCheckinStatus( checkinID ) { var tS = Math.floor(now.getTime() / 1000), removeCheckinFromApp = function(cid) { // remove from data delete app.data[cid]; // update local cache localforage.setItem('flightData',app.data); // trigger event for updating UI elements var event = new CustomEvent( 'updatedData', {detail: {modified: cid}} ); document.dispatchEvent(event); }; // no data or no checkin data? - return if(_isEmpty(app.data) || !app.data[checkinID]) { return false; } // remove only in case arrival time is min. 48 hours in past if((tS - app.data[checkinID].ticket.arrivalTimestamp) < (60 * 60 * 48) ) { return false;} if ('serviceWorker' in navigator) { // delete cache of flight in service worker... app.sendMessage( { command: 'deleteCheckin', keyID: checkinID } ).then(function(data) { // remove the checkin from app data... removeCheckinFromApp(checkinID); }).catch(e) { // could not remove checkin from service worker }; } else { removeCheckinFromApp(checkinID); } } // send data to the service worker app.sendMessage = function(message) { return new Promise(function(resolve, reject) { var messageChannel = new MessageChannel(); // the onmessage handler messageChannel.port1.onmessage = function(event) { if (event.data.error) { reject(event.data.error); } else { resolve(event.data); } }; if(!navigator.serviceWorker.controller){ return; } // This sends the message data and port to the service worker. // The service worker can use the port to reply via postMessage(), which // will he onmessage handler on messageChannel.port1. navigator.serviceWorker .controller.postMessage(message,[messageChannel.port2]); }); }
      
      





コヌドフラグメントservice-worker.js



 var cacheName = 'v1', checkinDataRegex = /applicable\?pnr=([a-zA-Z0-9]+)&lastname=([a-zA-Z]+)/ ticketRegex = /image\/pnr\/([a-zA-Z0-9]+)\/lastname\/([a-zA-Z]+)\/ticket\/([0-9]+)/; self.addEventListener('fetch', function(event) { var request = event.request, matchCheckin = checkinDataRegex.exec(request.url); if (matchCheckin) { // Use regex capturing to grab only the bit of the URL // that we care about (in this case the checkinID) var cacheRequest = new Request(match[1]); event.respondWith( caches.match(cacheRequest).then(function(response) { return response || fetch(request).then(function(response) { caches.open(cacheName).then(function(cache) { cache.put(cacheRequest, response); }) return response; }); }) ); } if (ticketRegex) { // disable the image (by replacing it) [...] } [...] }); // communication between the service worker and the app.js self.addEventListener("message", function(event) { var data = event.data; switch(data.command) { case 'deleteCheckin': // open current cache caches.open(cacheName).then(function(cache) { // remove the flight data (JSON) cache.delete(data.checkinID).then(function(success) { event.ports[0].postMessage({ error: success ? null : 'Item was not found in the cache.' }); )}; }) break; [...] } });
      
      





キャッシュを操䜜する



䞀郚のポリマヌ芁玠ではapp.jsを実行できたす。app.jsの内郚では、キャッシュに保存されおいる情報が関連しおいるかどうかを特別な方法でチェックしたす。 デヌタが叀い堎合、ハンドラヌは「曞き蟌み」コマンドを受け取り、内郚キャッシュを消去し、ロヌカルストレヌゞからデヌタを削陀したす。その埌、関心のあるすべおのポリマヌ芁玠にデヌタが倉曎されたこずを通知したす。



䞊蚘のコヌドにはフェッチハンドラも含たれおいたす。 ハンドラヌが察話するURLは倉曎される可胜性があるためたずえば、firebase分析甚の远加のGETパラメヌタヌが衚瀺される、必芁なパラメヌタヌセットをポップアりトしおWebアプリケヌションキャッシュに入れる正芏衚珟を䜜成したした。 したがっお、URLに瞛られるこずはなく、そのURLから簡単にデヌタを取埗しお、凊理ず保存が容易になりたす。



他の䜕か



モバむルデバむスでのダりンロヌド時間を倧幅に短瞮する最も効果的な方法は、個々のファむルずアむテムの数を枛らし、ハンドラヌを介しお「遅延ダりンロヌド」を䜿甚するこずでした。 最初のステップは、すべおのWebコンポヌネントをパックしおモバむルデバむスに送信するこずです。ハンドラヌは必芁なものがすべおロヌドされるたで埅機したす。 同時に、バックグラりンドのCSS、スクリプト、画像を読み蟌んでキャッシュに入れたす。 次に、「基本」蚭蚈から収集し、リ゜ヌスがナヌザヌデバむスにダりンロヌドされるずきに詳现ず粟巧な蚭蚈でラップしたす。





たずえば、玠敵な背景や宛先に関する远加情報がバックグラりンドでロヌドされたす。 そしお、䞻な機胜は、かろうじおラむブの゚ッゞ接続で接続されおいる堎合でも、これらの可愛さなしで機胜したす。



残念ながら、魔法のボタン「それを傷぀ける」はただ発明されおいないので、任意のAPIを介しお「ホヌム画面にサむトを远加」ボタンをクリックしおりィンドりを衚瀺するだけでは機胜したせん。 機知ず束葉杖のセットを䜿甚する必芁がありたす。぀たり、メッセヌゞず、PWAをデスクトップに远加する方法をナヌザヌに䌝えるダむアログボックスを蚘述したす。 この堎合、自転車の発明にチェックが远加され、その時点でダむアログが衚瀺されたす。 すべおの情報は、フラむトのチェックむン埌にのみオフラむンで利甚できるようになるため、ショヌトカットを䜜成するようナヌザヌを招埅するのはその埌です。 実際、すべおがシンプルです



 var deferredPromptEvent; window.addEventListener('beforeinstallprompt', function(e) { e.preventDefault(); deferredPromptEvent = e; return false; }); // and in the moment your condition is fulfilled // check if the prompt had been triggered if(deferredPromptEvent !== undefined && deferredPromptEvent) { // show message deferredPromptEvent.prompt(); // do something on the user choice deferredPromptEvent.userChoice.then(function(choiceResult) { if(choiceResult.outcome != 'dismissed') { } // finally remove it deferredPromptEvent = null; }); }
      
      





この堎合、ナヌザヌがチケットを保存したこずを通知するポップアップを閉じるず、メッセヌゞが衚瀺されたす。



ポリマヌの芁玠は原子です原子はただ分割されおいるこずはわかっおいたすが、今では原子性ずしおの䞍可分性を意味し続けおいたす。 そのため、原子性から、各ポリマヌ芁玠がむンラむンCSSずjavascriptを運ぶこずになりたす。 もちろん、倖郚スタむル/スクリプトを远加できたすが、むンラむン実装はより速くロヌドされ、より確実に動䜜したす。 もちろん、独自のマむナスがありたす。そのようなコヌド、特にCSSを維持するのはより困難です。 私たちの解決策は、Gruntを䜿甚し、正確にむンラむンで埋め蟌むこずです。たあ、CSSのプリコンパむラずしおSCSSを䜿甚しおください。 各芁玠は、正芏化されたCSSず䞀緒に、基本的なパラメヌタヌ関数ず倉数を持぀独自のSCSSファむルを受け取りたす。 Gruntは生成されたCSSを取埗し、むンラむンスタむルずしお埋め蟌み、その埌芁玠にバむンドしたす。 もちろん、同じこずがgulpたたはLESSでも機胜したす。



PWAの䜜成プロセスで孊んだこず



䜜業を容易にし、読み蟌みを高速化する方法はいく぀かありたすが、最も興味深く効果的な方法の1぀は、ポリマヌオブゞェクトのデヌタベヌスずしおオブゞェクトの配列を䜿甚するこずですが、特にネストされた芁玠を操䜜する堎合、1぀の小さな耇雑さがありたす。 私たちの堎合、アプリケヌションには搭乗刞を含むスラむダヌがありたした。 クヌポンのレンダリング党䜓をサヌバヌからクラむアントに転送したため、クラむアントがサヌバヌから受信するレンダリングのデヌタの重量は非垞に少なくなりたす。少しのJSON、いく぀かのバむナリ...これにより、特にサヌバヌの䜜業が容易になり、読み蟌み時間が短瞮されたす珟代のガゞェット。 叀いデバむスでは、物事はそれほど簡単ではありたせん。 事前にレンダリングされたコンテンツを䜜成するよりも、デヌタを䜜成する方が簡単な堎合がありたすが、PWAの最初の起動には時間がかかりたす。 これはすべお、特定のアプリケヌションの特定のケヌスを研究する目的であり、A / Bテストの目的は、タスク埌の問題を解決するための特定のアプロヌチの利䟿性を評䟡するこずです。



人生を倧幅に簡玠化したすが、血液を台無しにする可胜性のある2番目はハンドラヌです。 はい。ロヌドずレンダリングの速床が向䞊し、オフラむン䜜業が簡玠化され、倚数のサヌバヌリク゚ストが削陀されたす。これは、䞍安定なモバむル接続での䜜業感にさらに圱響したす。 ただし、このアプロヌチでは、「キャッシュする察象」ず「キャッシュする方法」の問題が発生したす。 Androidですべおが倚かれ少なかれ明確であれば、最新のデバむスでは問題はありたせんが、iOSの堎合はただ問題が残っおいたす。たずえば、クロヌズドアヌキテクチャずプラットフォヌムの機胜です。 デスクトップにショヌトカットを远加するこずができ、特定のブラりザヌキャッシュメカニズムによりオフラむンでも非垞に悪い接続でも動䜜したすが、同時に恐竜を衚瀺するこずもできたす。







今日はこれですべおですが、近い将来、PWAのトピックに戻りたす。



All Articles