Upgrade to Pro — share decks privately, control downloads, hide ads and more …

“モダン”ウェブアプリケーション 〜アメブロ5ヶ年計画〜

Kazunari Hara
September 24, 2017

“モダン”ウェブアプリケーション 〜アメブロ5ヶ年計画〜

HTML5 Conference 2017.9.24

1. “モダン”ウェブアプリケーション ∼アメブロ5ヶ年計画∼ HTML5 Conference 2017.9.24 原 一成 @herablog
2. アメーバブログ 2004年開始のブログサービス 芸能人の利用が多い 芸能人以外とそれ以外のPVは半々
3. 2015 バックエンドシステムの刷新 2017 2016 Isomorphic JavaScript AMP https Code-splitting Node.js v8 React 16 PWA
4. 5年かけて“モダン”な状態にする
5. “モダン”であること エコシステムとつながっていること
6. “モダン”であること エコシステムからの恩恵 エンジニアの流動性 ! "
7. エコシステムからの恩恵 最新技術を取り込みやすい 世界中のエンジニアと協力できる !
8. エンジニアの流動性 若者の参加を促しやすい 属人化を防ぎやすい "
9. SSRとSPA ServerClient Database Services Presentation Logic Data Services DAO Rendering
10. SSRとSPA ServerClient Database Services Presentation Logic Data Services DAO
11. SSRとSPA 初期表示がはやい ページ遷移は遅め ページ遷移がはやい 初期表示は遅め SSR SPA
12. https://goo.gl/KS3L5k Rendering on Google Search
13. Isomorphic JavaScript https://goo.gl/yLz93N
14. Isomorphic JavaScript https://goo.gl/yLz93N
15. Universal JavaScript https://goo.gl/xakXA1
16. Isomorphic JavaScript 2016年、アメブロのシステム刷新 ServerClient Database Services Presentation Logic Data Services DAO Browser
17. Isomorphic JavaScript Pros DataとUIの責務を分離できる UIは同じエンジニアがコントロールできる 初期・ランタイム両方の表示速度を改善できる
18. Isomorphic JavaScript 複雑さが少し増す (特にSSRとSPA切れ目) Node.jsの運用実績が必要 (あると良い) 全アプリに向かない (参照系に向く) Cons
19. Initial Speed with SSR and SPA 1.4x Faster
20. Runtime Speed with SSR and SPA 1.2x Faster
21. Isomorphic JavaScript 2016年、アメブロのシステム刷新 ServerClient Database Services Presentation Logic Data Services DAO Browser
22. React (Rendering) The Parts of Isomorphic JavaScript コンポーネント指向 サーバー、クライアント両方のレンダリング ライフサイクルメソッドでIsomorphicにしやすい
23. export class SampleComponent extends React.Component { componentWillMount() {} // サーバ用。constructorを使うのが推奨 componentDidMount() {} // クライアント用。初回の表示時 componentDidUpdate() {} // クライアント用。SPAの遷移時に使われることが多い componentWillUnmount() {} // クライアント用。タイマー処理を消すなど render() {} //サーバサイド、クライアントサイド両方 } React (Rendering) The Parts of Isomorphic JavaScript
24. Redux (State container) The Parts of Isomorphic JavaScript Fluxベースの状態管理のためのフレームワーク 関数ベースでIsomorphicしやすい 副作用など曖昧なところは公式サイトの推奨に合わせる
25. https://goo.gl/esQRsK How can I represent “side effects” such as AJAX calls?
26. Fetch API, Fetchr (Fetching) The Parts of Isomorphic JavaScript データの取得は Fetch API (bitinn/node-fetch, github/fetch) Fetchr (yahoo/fetchr) でIsomoprhic化
27. React Router (Routing) The Parts of Isomorphic JavaScript 各pathごとに対応するコンポーネントを呼び出し サーバーでは、エラー・リダイレクト処理 クライアントでは、ヒストリー・スクロール位置管理 などが必要
28. Node.js (Web server) The Parts of Isomorphic JavaScript IsomorphicはNode.jsの進化・安定の賜物 Docker利用しているため、バージョンアップは簡単 基本的に最新版を利用するようにしている
29. Renewal Policy できるだけ「スタンダード」なツールを使う 作っては壊すを繰り返す 共通化・最適化は最終段階でおこなう その代わり、パーツを簡単に取り除けるようにしておく
30. ❝ 早すぎる最適化は諸悪の根源である ❞ Donald Knuth
31. https://goo.gl/Q29qBX アメブロ 2016: React/Reduxでつくる Isomorphic web app https://goo.gl/GyoZh4 アメブロでReactやIsomorphic Web App を採用した理由――その成果と構成技術 https://goo.gl/xHdn83 アメブロの大規模システム刷新と それを支えるSpring
32. Caches 高負荷なサービスやハイパフォーマンスを求める場合、 サーバーサイドのキャッシュは必須 アメブロはブログサービスなのでキャッシュと相性が良い
33. Template Caches # $ %+ ' renderToString() Client Info HTML Cache
34. API Caches 最初はボトルネックになるHTMLのキャッシュのみ APIにも適切にキャッシュを設定
35. Caches 1.5x Faster
36. Code-Splitting JavaScriptの評価時間を短縮するため main.jsの誇大化を防ぐため いつかはフルSPAにするかもしれないため
37. Performance Metrics
38. Performance Metrics
39. https://goo.gl/WaULzs Leveraging the Performance Metrics that Most Affect User Experience
40. Webpack Dynamic Imports Dynamic Importsを利用 https://goo.gl/5Fw35x AmebaではAtomic Designの Organisms単位で分割 (今は) () => import(‘./organisms/Header.js’);
41. JavaScript評価時間 9x Faster
42. React 16 React Fiber 1.4x Faster React 16 (Beta 3) vs 15
43. https://goo.gl/4Cpff6 アメブロ: Isomprhicアプリケーションの パフォーマンス・チューニング https://goo.gl/W1fyTp アメブロ 2017: Isomorphic Web Appの進化編
44. Monitoring 継続的にモニタリングできる 必要に応じてフィードバック(アラート)を設定できる Amebaでは全てSlackに通知が来るようにしている
45. サーバーアプリケーションの レスポンスタイムをモニタリング ボトルネックを発見する
46. アプリケーションの状態を モニタリングする カスタマイズして様々な Metricsを作れる
47. SpeedCurve 継続的にパフォーマンス計測 - 毎日計測・Slack通知 - デプロイごとに計測・Slack通知
48. 備え付けのページスピードの他、 パフォーマンス値を送信して分析 - SpeedIndex (WPO-Foundation/RUM-SpeedIndex) - window.performance (first-paint, first-contentful- paint)
49. https://goo.gl/h4f8cz WebパフォーマンスとプロダクトKPIの相関を 可視化する話
50. HTTPS Hyper Text Transfer Protocol Secure 2017年夏、ついにアメブロ公開面のHTTPS配信が完了🎉 セキュリティ, SEO, Service Workerなどの新機能利用 観点からHTTPSでの配信は必須
51. CSP Report Content Security Policy ポリシーを定義し、違反リソースを 自動的にレポート レポート先はhttps://report-uri.io/ を利用
52. CSP Report Content Security Policy Content-Security-Policy-Report-Only: default-src 'self' https:; script-src 'self' https: 'unsafe-inline' ‘unsafe-eval'; img-src 'self' https: data:; style-src 'self' https: ‘unsafe-inline'; font-src 'self' https: data:; report-uri https://send-to-report-url
53. CSP Report Content Security Policy
54. HSTS HTTP Strict Transport Security HTTPの代わりにHTTPSを使うよう Webページからブラウザに伝達 徐々に期間を広げていった Strict-Transport-Security: max-age=expireTimeSeconds
55. 301 Redirect HSTSで問題がないか確認後、 httpへのアクセスをhttpsへ強制リダイレクト 301(恒久的)でリダイレクトする
56. Headers Mixed Contentsを防ぐためには、以下のヘッダーが有効 Upgrade-Insecure-Requests ブラウザはhttpのリソースを発見するとhttpsに変換してリクエスト。https 未対応の場合は、404になる。 Block-All-Mixed-Content ブラウザは、httpのリソースを常に読み込まないようにする。
57. Referrer Policy URLには機密情報が含まれている場合があるため、 セキュアでないページへのリクエスト時にリファラは無効 Amebaでは「origin」を選択中
58. https://goo.gl/NgSdg1 アメブロ 2017: 大規模サービスhttps化 ∼All Greenを目指して∼
59. AMP Accelerated Mobile Pages Project Web表示速度改善のプロジェクト ホワイトリスト化されたHTML, JS CSSは内に記述 検索結果とAMPカルーセルに載る
60. AMP at Ameba 7x Sessions from March 2017
61. AMP Spike AMPカルーセルからの突発的なアクセス増 普段の検索とは違った流入を得られる あるブログのAMPページのページビュー推移
62. AMP Performance 最新技術の利用により、安定したパフォーマンス 一部ページ、一部機能など段階的に導入するのもあり オリジナルサイトでAMPと同様のパフォーマンスを 担保できる場合は、利用しなくていいかも
63. LazyLoad with XHRしてデータを取得 mustacheでテンプレート記述 LazyLoadを簡単に実装できる 厳密なCORSルール
64. データが取得できませんでした。 {{#data}} {{entry_title}} {{/data}} LazyLoad with
65. https://goo.gl/1ZDj55 Actions and Events in AMP https://goo.gl/Lja76p CORS Requests in AMP
66. Auto Suggestion Search Bar with で初期状態を設定 input入力に応じてAMP.setState() で状態を更新 で推薦結果を再取得
67. Auto Suggestion Search Bar with { "autosuggest" : "" }
68. Auto Suggestion Search Bar with {{.}}
69. Like actions with and で初期状態を取得 ボタンタップで状態を反映 ポップアップ内のボタンタップは でAPIにデータ送信
70. いいね!する いいね!をキャンセルする Like actions with and
72. Install Service Workers with
73. Install Service Workers with
74. Install Service Workers with AMP to PWA 初期表示がはやい 静的 制限された機能 フル機能が使える 動的 初期表示は遅め AMP PWA
75. Install Service Workers with AMP to PWA AMP ( PWA )* * * Navigate to PWA
76. Install Service Workers with AMP with PWA 初期表示がはやい 静的 制限された機能 フル機能が使える 動的 初期表示は遅め AMP PWA 初期表示がはやい フル機能が使える 動的 Isomorphic
77. Install Service Workers with AMP with PWA AMP ( Isomorphic )* * * Link to Original Site
78. PWA Progressive Web App Webのユーザーエクスペリエンス NativeとWebのいいとこ取りを目指す Reliable, Fast, Engaging
79. Image Lightbox Native Like Image Lightbox ネイティブのような画像lightbox スムーズに画像を閲覧できる dimsemenov/PhotoSwipeを利用
80. Article Selector Native Like Article Selector 記事ソート機能 モーダル表示で様々なソートを実現
81. Service Worker Webページとは別にバックグラウンドで動作するスクリプト Fetch, Cache, Push, Background sync… ライフサイクルの理解が重要
82. Service Worker Lifecycle スコープ内のページが同じService Workerで制御 できるようにする Chromeのアップデートに似ている
83. Service Worker Lifecycle Install + * * ActivateWaiting
84. Service Worker Lifecycle Install * * Activate +Waiting
85. Service Worker Lifecycle Install * * Activate + * Fetch & Cache Waiting
86. Service Worker Lifecycle Install + * * Activate +Waiting
87. Service Worker Lifecycle Install * + * Activate +Waiting
88. Service Worker Lifecycle Install * Waiting * Activate + * Fetch & Cache
89. https://goo.gl/YKZmPK Service Workerのライフサイクル
90. Precache Service Workerでアセットをプレキャッシュ GoogleChrome/Workboxを利用 Webpackプロセスに組み込み 取得後は、再度ネットワークリクエストしない
91. self.AMEBLO_CDN = 'https://c.stat100.ameba.jp'; importScripts('https://c.stat100.ameba.jp/ameblo/assets/sp/ 20170914-3b91bd8/service-worker.js'); Precache クロスドメイン対応
92. const = fileManifest = [ { url: self.AMEBLO_CDN + '/ameblo/assets/sp/20170914-3b91bd8/0.js' }, { url: self.AMEBLO_CDN + '/ameblo/assets/sp/20170914-3b91bd8/main.css' }, { url: ‘/shell/20170914-3b91bd8‘ } ]; workboxSW.precache(fileManifest); Precache クロスドメイン対応
93. Precache
94. Precache
95. Runtime Cache 記事に関連するリソースをキャッシュ e.g. 画像, 記事データ GoogleChrome/Workboxを利用
96. Runtime Cache Strategy Cache First 一度キャッシュしたら、再度ネットワークリクエストしない。 バージョン付きのリソースに有効。 Network First 常にネットワークを優先する。取得できなかった場合のみキャッシュを利用。 APIデータなど、更新頻度が高いものに有効。
97. Runtime Cache Strategy StaleWhileRevalidate (Fastest) キャッシュがある場合はキャッシュを利用し、ネットワーク取得後、裏側で キャッシュを更新する。 更新度の低いリソース、アグレッシブなキャッシュに有効。
98. { runtimeCaching: [ { urlPattern: //_api/.*/, // Data from API handler: 'staleWhileRevalidate', }, { // Images in articles urlPattern: /^https://.*stat.ameba.jp/user_images/.*/, handler: ‘cacheFirst', options: { cache: { maxEntries: 100 }, }, } ] } Runtime Cache
99. Network Consumption ???x Cheaper ,
100. Go! Yakiniku🍖
101. Offline Notification オンライン/オフラインの変化時 ノティフィケーションを表示 online/offlineイベント navigator.onLineプロパティ
102. Offline Notification window.addEventListener('online', handleOnlineStatus, false); window.addEventListener('offline', handleOnlineStatus, false); const handleOnlineStatus = () => { const online = window.navigator.onLine; if (online) { hideFloatingNotification(); } else { showFloatingNotification(‘インターネットに接続できません。’) } };
103. Offline Availability オフラインでも記事が読める Service Workerで記事に必要な アセットをプレキャッシュ, ランタイムキャッシュ
104. Offline Availability 記事に必要なアセット: App Shell 記事データ (API) 記事内画像
105. App Shell ページに必要な最低限のアセット 現時点のAmebaでは、SPAモード と同等 Navigation Fallbackに設定
106. Web App Manifest Webアプリのメタデータを記述 ホーム画面からの起動時に利用される
107. { "short_name": "ca-seo", "name": "Ameba", "icons": [{ “src": “https://c.stat100.ameba.jp/img/sp/web-app-icon.png", "type": "image/png", "sizes": "192x192" }], "start_url": "/ca-seo?precache=1&utm_medium=direct&utm_source=homescreen", "background_color": "#efefef", "display": "standalone", "orientation": "portrait", "theme_color": "#efefef" } Web App Manifest
108. App Install Banners 必須条件 (変更の可能性あり): short_name, name, start_url, Service Worker, HTTPS, 144x144以上のpngアイコン
109. App Install Banners beforeinstallpromptイベントでインストールバナーを コントロールできる バナー表示直前/キャンセル/受け入れ 現在、Amebaではバナー非表示にしている (試験中のため)
110. App Install Banners var deferredPrompt; window.addEventListener(‘beforeinstallprompt', (e) => { e.preventDefault(); // プロンプトの表示を先送りする deferredPrompt = e; return false; }); btnSave.addEventListener('click', () => { if (deferredPrompt !== undefined) { deferredPrompt.prompt(); // プロンプトを表示 deferredPrompt.userChoice.then((choiceResult) => { console.log(choiceResult.outcome); // dismissed or accepted }); } });
111. https://goo.gl/H1CroZ ウェブアプリのインストール バナー
112. navigator.getInstalledRelatedApps() ブラウザからアプリのインストールを確認する 「アプリがインストールされていたら、App Install Banner 出さない」実装ができる アプリの実装も少し必要 ChromeでOrigin Trial中
113. [{ "relation": [”delegate_permission/common.handle_all_urls"], "target": { "namespace": ”web", "site": ”https://ameblo.jp" } }] navigator.getInstalledRelatedApps() Androidの実装
114. // manifest.json { ... "related_applications": [{ "platform": "play", "id": “jp.ameba” }], ... } // Your code if (navigator.getInstalledRelatedApps) { navigator.getInstalledRelatedApps().then(relatedApps => { console.log(relatedApps.length); // 0: not installed }); } navigator.getInstalledRelatedApps() Webの実装
115. https://goo.gl/C86Dha Detect if your Native app is installed from your web site
116. Ameba PWA (Beta) Add to Home screenから お試しいただけます
117. Next Milestones CDN Strategy Updates Image Optimization HTTP/2 More PWA iPhone X 😬 etc…
118. @herablog

Kazunari Hara

September 24, 2017
Tweet

More Decks by Kazunari Hara

Other Decks in Technology

Transcript

  1. export class SampleComponent extends React.Component { componentWillMount() {} // αʔό༻ɻconstructorΛ࢖͏ͷ͕ਪ঑

    componentDidMount() {} // ΫϥΠΞϯτ༻ɻॳճͷදࣔ࣌ componentDidUpdate() {} // ΫϥΠΞϯτ༻ɻSPAͷભҠ࣌ʹ࢖ΘΕΔ͜ͱ͕ଟ͍ componentWillUnmount() {} // ΫϥΠΞϯτ༻ɻλΠϚʔॲཧΛফ͢ͳͲ render() {} //αʔόαΠυɺΫϥΠΞϯταΠυ྆ํ } 3FBDU 3FOEFSJOH 5IF1BSUTPG*TPNPSQIJD+BWB4DSJQU
  2. $413FQPSU $POUFOU4FDVSJUZ1PMJDZ Content-Security-Policy-Report-Only: default-src 'self' https:; script-src 'self' https: 'unsafe-inline'

    ‘unsafe-eval'; img-src 'self' https: data:; style-src 'self' https: ‘unsafe-inline'; font-src 'self' https: data:; report-uri https://send-to-report-url
  3. <amp-list layout=“fixed-height" height=“105" width="auto" src=“https://gamp.ameblo.jp/_api/recent-images” > <div fallback>σʔλ͕औಘͰ͖·ͤΜͰͨ͠ɻ</div> <template type="amp-mustache">

    <amp-carousel height=“100" controls> {{#data}} <div> <p>{{entry_title}}</p> </div> {{/data}} </amp-carousel> </template> </amp-list> BNQMJTU -B[Z-PBEXJUIBNQMJTU
  4. BNQCJOE "VUP4VHHFTUJPO4FBSDI#BSXJUIBNQCJOE <amp-state id="searchState"> <script type="application/json"> { "autosuggest" : ""

    } </script> </amp-state> <input class="search-bar__input" type="search" name="q" placeholder="ϒϩάΛݕࡧ" tabindex="0" on=“input-debounced:AMP.setState( { searchState: { autosuggest: event.value }})” />
  5. <amp-state id=“iineState” credentials="include" src=“https://iine.ameba.jp/web/display_iine.json > </amp-state> <button on="tap:iine-lightbox,AMP.setState({iineState: iineState})” >͍͍Ͷʂ</button>

    <amp-lightbox id=“iine-lightbox” layout=“nodisplay”> <!— contents —> <button on=“tap:iine-lightbox.close”>Close</button> <amp-lightbox> BNQGPSN -JLFBDUJPOTXJUIBNQGPSNBOEBNQCJOE
  6. <amp-lightbox id=“iine-lightbox” layout=“nodisplay”> <form method=“post" target=“_top” action-xhr=“https://iine.ameba.jp/web/exec_iine.json” on="submit-success:AMP.setState({iineState: {exist: true}})"

    [class]="!iineState.exist ? 'visible' : 'hidden'" >͍͍Ͷʂ͢Δ</form> <form method=“post" target=“_top” action-xhr=“https://iine.ameba.jp/web/cancel_iine.json” on="submit-success:AMP.setState({iineState: {exist: false}})" [class]="iineState.exist ? 'visible' : 'hidden'" >͍͍ͶʂΛΩϟϯηϧ͢Δ</form> <amp-lightbox> BNQGPSN -JLFBDUJPOTXJUIBNQGPSNBOEBNQCJOE
  7. const = fileManifest = [ { url: self.AMEBLO_CDN + '/ameblo/assets/sp/20170914-3b91bd8/0.js'

    }, { url: self.AMEBLO_CDN + '/ameblo/assets/sp/20170914-3b91bd8/main.css' }, { url: ‘/shell/20170914-3b91bd8‘ } ]; workboxSW.precache(fileManifest); 1SFDBDIF ΫϩευϝΠϯରԠ
  8. { runtimeCaching: [ { urlPattern: /\/_api\/.*/, // Data from API

    handler: 'staleWhileRevalidate', }, { // Images in articles urlPattern: /^https:\/\/.*stat\.ameba\.jp\/user_images\/.*/, handler: ‘cacheFirst', options: { cache: { maxEntries: 100 }, }, } ] } 3VOUJNF$BDIF
  9. 0⒐JOF/PUJpDBUJPO window.addEventListener('online', handleOnlineStatus, false); window.addEventListener('offline', handleOnlineStatus, false); const handleOnlineStatus =

    () => { const online = window.navigator.onLine; if (online) { hideFloatingNotification(); } else { showFloatingNotification(‘Πϯλʔωοτʹ઀ଓͰ͖·ͤΜɻ’) } };
  10. { "short_name": "ca-seo", "name": "Ameba", "icons": [{ “src": “https://c.stat100.ameba.jp/img/sp/web-app-icon.png", "type":

    "image/png", "sizes": "192x192" }], "start_url": "/ca-seo?precache=1&utm_medium=direct&utm_source=homescreen", "background_color": "#efefef", "display": "standalone", "orientation": "portrait", "theme_color": "#efefef" } 8FC"QQ.BOJGFTU
  11. "QQ*OTUBMM#BOOFST var deferredPrompt; window.addEventListener(‘beforeinstallprompt', (e) => { e.preventDefault(); // ϓϩϯϓτͷදࣔΛઌૹΓ͢Δ

    deferredPrompt = e; return false; }); btnSave.addEventListener('click', () => { if (deferredPrompt !== undefined) { deferredPrompt.prompt(); // ϓϩϯϓτΛදࣔ deferredPrompt.userChoice.then((choiceResult) => { console.log(choiceResult.outcome); // dismissed or accepted }); } });
  12. <!— AndroidManifest.xml —> <meta-data android:name="asset_statements" android:resource="@string/ asset_statements" /> <!— strings.xml

    —> <string name=“asset_statements"> [{ \"relation\": [\”delegate_permission/common.handle_all_urls\"], \"target\": { \"namespace\": \”web\", \"site\": \”https://ameblo.jp" } }] </string> OBWJHBUPSHFU*OTUBMMFE3FMBUFE"QQT "OESPJEͷ࣮૷
  13. // manifest.json { ... "related_applications": [{ "platform": "play", "id": “jp.ameba”

    }], ... } // Your code if (navigator.getInstalledRelatedApps) { navigator.getInstalledRelatedApps().then(relatedApps => { console.log(relatedApps.length); // 0: not installed }); } OBWJHBUPSHFU*OTUBMMFE3FMBUFE"QQT 8FCͷ࣮૷