WordPress で PWA へ至る道

WordPress で PWA へ至る道

「WordBench大阪 & 京都 & Ionic Japan 5月 フルスイング」登壇資料です。

Transcript

  1. PWA ΁ࢸΔಓ Toro_Unit @2018.05.05 / WBOsaka 1

  2. $ whoami 2

  3. Toro_Unit ઎෦ ߛ (͏Β΂ ͻΖ͠) • Frontend Engineer • WordPress

    Plugin and Theme Developer • ͍͖͞Μ ͍͢͝Hຊ ͸͡Ί·ͨ͠ɻ Github: @torounit Twitter: @Toro_Unit 3
  4. ࡳຈͷձࣾͰϦϞʔτϫʔΫͯ͠·͢ 4

  5. 5

  6. Contribution • WordBench ௕໺ ϞσϨʔλʔ • WordCamp Kyoto 2017 /

    Osaka 2018 / Ogijima 2018 • WordCamp Tokyo 2017 Speaker • etc... 6
  7. Plugins and Themes • Custom Post Type Permalinks • Simple

    Post Type Permalinks • Powerful Posts Per Page (PPPP) • Responsive Slide • Vanilla • and more... 7
  8. WordBench Nagano ຖ݄΍ͬͯ·͢ɻ • ࣍ճ͸ 5/26 8

  9. 9

  10. 10

  11. ωοτΊͬͪΌ੾ΕΔɻ ͭΒ͍ɻ 11

  12. §1. Minimum PWA on WordPress 12

  13. 13

  14. ͩΕ͔͕ݴͬͨ ʮશͯͷαΠτ͸PWAʹͳΔ΂͖ɻʯ 14

  15. SEYANA Θ͔Δ 15

  16. 16

  17. 17

  18. 18

  19. HTML/CSS/JS + REST API • ҰൠతͳΞϓϩʔνͩͱࢥΘΕΔ • PHPॻ͔ͳͯ͘ྑ͍ • JSͰͷ஌ݟ͕͍Ζ͍Ζ࢖͑ΔɻNetlify

    ͱ͔ɺGithub Pages ͱ͔࢖ ͑͹Ϋοιָɻ • .com ΛόοΫΤϯυʹ͢Ε͹αʔόʔ୅ɺอकෆཁɻ • ςʔϚͷͭΒΈͱ͔WordPress ͕ jQuery ు͘ͱ͔ߟ͑ͳ͍ͯ͘ ͍ɻ 19
  20. ͜ΕΒΛ WordPress Ͱ PWA ͱ͍ͬͯྑ͍ͷͰ ͠ΐ͏͔ɻ • ผʹ REST API

    ͋Ε͹ͳΜͰ΋͑͑΍Μ͚ɻ • طଘͷαΠτͷผͷ഑৴ܗଶͱ͍͏ܗͳΒΘ͔Δɻ • ͍ͬͯ͏͔͜ͷΞϓϩʔνͰ WordPress ͍ͨ΁Μ͡Όͳ͍? • هࣄͷϓϨϏϡʔͱ͔ಈ͔ͳ͍͠ɻ 20
  21. ͜ͷΞϓϩʔνͷσϝϦοτɻ • WordPress ͷςʔϚ͸࢖͑ͳ͍ɻ • ͓Ε͸ςʔϚ࡞ऀɻ • ϓϥάΠϯ΋͍Ζ͍Ζ࢖͑ͳ͍ɻ 21

  22. ʮશͯͷαΠτ͸PWAʹͳΔ΂͖ɻʯ Ͱ͖ͳ͍΍Μ͚ʂʂ 22

  23. 23

  24. ͱ͍͏Θ͚ͰطଘͷWordPressαΠτΛPWAԽ͢ΔϓϥάΠ ϯ࡞Γ·ͨ͠ɻ • Smart PWA — WordPress Plugins • torounit/smart-pwa

    • GWͱ͸͍͍ͬͨɻ 24
  25. ΍ͬͨ͜ͱɻ 25

  26. <?php /** * Service Worker * * @package Smart_PWA */

    ?> 'use strict'; const APP_SHELL_CACHE_NAME = 'smart-pwa-<?php echo get_transient( 'smart_pwa_hash' ); ?>'; const RUNTIME_CACHE_NAME = 'smart-pwa-runtime-cache'; const NOT_AVAILABLE_KEY = '<?php echo '/' . user_trailingslashit( get_page_uri( get_option( 'smart_pwa_not_available_page', false ) ) ); ?>'; const PRE_CACHE_ASSETS = JSON.parse( '<?php echo json_encode( get_option( 'smart_pwa_assets_paths', [] ) ); ?>' ); const urlsToPreCache = [ '/', NOT_AVAILABLE_KEY, ].concat( PRE_CACHE_ASSETS ); const updateMessage = ( message ) => { self.clients.matchAll().then( clients => clients.forEach( client => client.postMessage( message ) ) ); } self.addEventListener( 'install', ( event ) => { console.log( '[ServiceWorker] Install' ); event.waitUntil( caches.open( APP_SHELL_CACHE_NAME ) .then( ( cache ) => { console.log( '[ServiceWorker] Caching app' ); return cache.addAll( urlsToPreCache ); } ) ); } ); self.addEventListener( 'activate', ( event ) => { console.log( '[ServiceWorker] Activate' ); const cacheWhitelist = [ APP_SHELL_CACHE_NAME ]; event.waitUntil( caches.keys().then( ( cacheNames ) => { return Promise.all( cacheNames.map( ( cacheName ) => { if (cacheWhitelist.indexOf( cacheName ) === - 1) { console.log( '[ServiceWorker] Removing old cache', cacheName ); return caches.delete( cacheName ); } } ) ); } ) ); } ); 26
  27. self.addEventListener( 'fetch', ( event ) => { if ( event.request.url.indexOf(

    'wp-admin' ) === - 1 && event.request.url.indexOf( 'wp-login' ) === - 1 && event.request.url.indexOf( 'customize_changeset_uuid' ) === - 1 && event.request.method === 'GET' ) { event.respondWith( caches.match( event.request ).then( ( responseFromCache ) => { //Return static content in cache. if (responseFromCache) { if ([ 'style', 'script', 'image' ].indexOf( event.request.destination ) > - 1) { return responseFromCache; } } //Fetch and save content to cache. return fetch( event.request ).then( ( response ) => { //Not 200. if (! response || response.status !== 200 || response.type !== 'basic') { return response; } let responseToCache = response.clone(); caches.open( RUNTIME_CACHE_NAME ).then( cache => { cache.put( event.request, responseToCache ); console.log( '[ServiceWorker] Fetched&Cached Data', event.request.url ); updateMessage( { key: 'updateContent', value: event.request.url } ); } ); return response; } ).catch( ( reason ) => { //when fetching failed, return content in cache. if (responseFromCache) { console.log( '[ServiceWorker] Cache Matched!', event.request.url, responseFromCache ); return responseFromCache; } //if not found in cache, return fallback page. if (event.request.mode === 'navigate') { return caches.match( NOT_AVAILABLE_KEY ).then( ( response ) => { return response; } ); } } ); } ) ); } } ); 27
  28. • global $wp_style, global $wp_scripts Λ୳ͯ͠ɺenqueue ͞Εͯ Δ CSS, JS

    ΛऔಘɻͦΕΛ PreCache ʹɻ • Ұఆ࣌ؒຖʹɺPreCache ͷ࠶औಘ & Worker ͷߋ৽ɻ • Transient API ࢖ͬͯΔ͚ͩͰ͕͢ɻ • style, script, image ͸ΩϟογϡΛฦ٫ɻhttps:// fetch.spec.whatwg.org/#requests • ͦΕҎ֎ͷ͸ɺfetch࣌ʹΩϟογϡͭͭ͠ɺݟ͔ͭΒͳ͚Ε͹Ωϟο γϡΛฦ٫ɻ • ͦΕ΋ͩΊͳΒɺݱࡏ͝ར༻௖͚·ͤΜϖʔδΛฦ٫ɻ 28
  29. DEMO. https://torounit.com 29

  30. ඍົϙΠϯτ • HTMLΛ·Δ͝ͱΩϟογϡ͍ͯ͠Δɻ • ςʔϚม͑ͨͱ͖ͭΒ͍ɻ ίϯςϯπͱݟͨ໨ͷ෼཭͕ ग़དྷແ͍ͷਏ͍ɻ • Ωϟογϡ͕ॏ͘ͳΓ΍͍͢ɻ •

    Ωϟογϡͷߋ৽Λड͚ͯಈతʹॻ͖׵͑Α͏ͱ͢Δͱ jQueryͰࢮ͵ɻ 30
  31. §2. FullSpec PWA on WordPress 31

  32. • App Shell ϞσϧΛͪΌΜͱ΍Γ͍ͨɻ 32

  33. ඞཁͳϞϊɻ • Service Worker • SPA ʹͳ͍ͬͯΔςʔϚ 33

  34. SPA on WordPress ςʔϚͷਏΈ • @see RESTࡇ • REST API

    ͳ WordPress ςʔϚʹ͍ͭͯߟ͑Δɻ • ϦϥΠτਏ͍ɻ • WordPressͷURLϑϦʔμϜ 34
  35. ͭͬͨ͘ɻ DEMO 35

  36. torounit/vue-spa-wp-theme • Vue + Vuex + Workbox • REST API

    ͱͷ௨৴͸ɺWordPressʹಉࠝͷΫϥΠΞϯτɻ (Backbone.js !!!) • ೝূपΓΛউखʹ΍ͬͯ͘ΕΔͷͰָͰ͸͋Δɻ 36
  37. URL ͷ ύʔεਏ͍໰୊ɻ 37

  38. function get_permastructs() { global $wp_rewrite; if ( get_option( 'page_for_posts' )

    ) { $home_structs = [ 'front-page' => '/', 'home' => get_page_uri( get_option( 'page_for_posts' ) ) ]; } else { $home_structs = [ 'home' => '/' ]; } $extra_permastructs = array_map( function ( $permastruct ) { return $permastruct['struct']; }, $wp_rewrite->extra_permastructs ); $permastructs = [ 'search' => $wp_rewrite->get_search_permastruct(), 'author' => $wp_rewrite->get_author_permastruct(), 'date' => $wp_rewrite->get_date_permastruct(), 'month' => $wp_rewrite->get_month_permastruct(), 'year' => $wp_rewrite->get_year_permastruct(), ]; if ( $wp_rewrite->use_verbose_page_rules ) { $permastructs['post'] = $wp_rewrite->permalink_structure; $permastructs['page'] = $wp_rewrite->get_page_permastruct(); } else { $permastructs['page'] = $wp_rewrite->get_page_permastruct(); $permastructs['post'] = $wp_rewrite->permalink_structure; } $permastructs = array_merge( $home_structs, $extra_permastructs, $permastructs ); return array_map( function ( $key, $value ) { $struct = trim( preg_replace( '/%([^\/]+)%/', ':$1', $value ), '/\\' ); $struct = str_replace( [':year',':monthnum',':day',':post_id'], [':year(\\d{4})',':monthnum(\\d{1,2})',':day(\\d{1,2})',':post_id(\\d+)'], $struct ); return [ 'name' => $key, 'path' => untrailingslashit( '/' . $struct ) . '/:endpoint(page)?/:page(\\d*)' ]; }, array_keys( $permastructs ), array_values( $permastructs ) ); } 38
  39. ؾ߹͍Ͱύʔε͢Ε͹·͊ͳΜͱ͔ͳΔΒ͍͠ɻ • WP_Rewrite ͔ΒύʔϚϦϯΫߏ଄Λऔಘ͠·ͬͯ͘ɺpath- to-regexp (vue-router Ͱ࢖ΘΕͯΔURLύʔαʔ)͕ղऍͰ ͖Δܗࣜʹؤுͬͯม׵͢Ε͹ͳΜͱ͔ͳΔɻ • ఴ෇ϑΝΠϧͷϖʔδ/ίϝϯτճΓ/ͱ͔͸஌Βͳ͍

    • ΧελϜ౤ߘͱ͔͸݁ߏͲ͏ʹ͔ͳΓͦ͏ɻ • Custom Post Type Permalinks ͱ͔஌Βͳ͍Ͱ͢ɻ 39
  40. export const fetchPosts = async ( { commit, state }

    ) => { let postsCollection = new wp.api.collections.Posts(); let posts = []; let queriedObject = {}; let hasMore = false; let page = state.route.params.page || 1; let data = { page: page }; switch (state.route.name) { case 'home': posts = await postsCollection.fetch( { data: data } ); hasMore = postsCollection.hasMore(); break; case 'category': { let categories = await (new wp.api.collections.Categories()).fetch( { data: { slug: state.route.params.category } } ); queriedObject = categories[ 0 ]; data = Object.assign( data, { categories: categories[ 0 ].id } ); posts = await postsCollection.fetch( { data: data } ); hasMore = postsCollection.hasMore(); break; } case 'post_tag': { let tags = await (new wp.api.collections.Tags()).fetch( { data: { slug: state.route.params.post_tag } } ); queriedObject = tags[ 0 ]; data = Object.assign( data, { tags: tags[ 0 ].id } ); posts = await postsCollection.fetch( { data: data } ); hasMore = postsCollection.hasMore(); break; } case 'author': { let users = await (new wp.api.collections.Users()).fetch( { data: { slug: state.route.params.author } } ); queriedObject = users[ 0 ]; data = Object.assign( data, { author: users[ 0 ].id } ); posts = await postsCollection.fetch( { data: data } ); hasMore = postsCollection.hasMore(); break; } case 'post': { if (state.route.params.postname) { posts = await postsCollection.fetch( { data: { slug: state.route.params.postname } } ); } else { let postModel = new wp.api.models.Post( { id: state.route.params.post_id } ); let post = await postModel.fetch(); posts = [ post ] } queriedObject = posts[ 0 ]; break; } case 'page': let pagesCollection = new wp.api.collections.Pages(); posts = await pagesCollection.fetch( { data: { slug: state.route.params.pagename } } ); queriedObject = posts[ 0 ]; break; case 'front-page': let pageModel = new wp.api.models.Page( { id: global.themeSettings.pageOnFront } ); let post = await pageModel.fetch(); posts = [ post ]; break; } commit( types.SET_QUERIED_OBJECT, queriedObject ); commit( types.SET_HASMORE, hasMore ); commit( types.SET_POSTS, posts ); }; 40
  41. ؾ߹͍Ͱ WP_Query::parse_query ͷؾ࣋ͪʹͳΕ͹ͳΜͱ ͔ͳΔ͔΋͠Εͳ͍ɻ • ؾ߹͍ͰͳΜͱ͔͸ͳΔɻGWͭͿΕ͚ͨͲɻ • ͜ΕͰ΋શવ଍Γͳ͍ɻɻɻ • wp.api.getCollectionByRoute,

    wp.api.getModelByRoute ͱ͔࢖͑͹ΧελϜ౤ߘͱ͔ͱ΋ઓ͑ͦ͏ɻ • Backbone.jsϕʔεͩ͠jQuery΋ಡΈࠐΉ͚Ͳɺೝূͱ͔ߟ ͑ͳͯ͘ྑ͍ͷ͸ָɻ 41
  42. Other tips • Service Worker ͸ / ༻ͱɺςʔϚ༻Ͱ̎छྨ༻ҙͨ͠ํ͕ଟ ෼ָɻ •

    Workbox ͕ศརͩͬͨɻ • άϩʔόϧͳঢ়ଶʹґଘ͢ΔϞϊ͕ଟ͍ͷͰɺVuex ͱ͔ Redux ͱ͔࢖ͬͨํָ͕ɻ 42
  43. 43

  44. FullSpec ͷ ͍͍ͱ͜Ζ • Service Worker ͷ install ޙʹ fetch

    ͢Ε͹ॳճΞΫηε࣌ͷ ίϯςϯπ΋Ωϟογϡग़དྷΔɻ • ϑΥʔϧόοΫ࡞Γ΍͍͢ɻTOPϖʔδΛApp Shellʹಥͬࠐ ΜͲ͚͹ԿͱͰ΋ͳΔɻ • ͔͍͍ͬ͜ɻ 44
  45. FullSpec ͷ σϝϦοτ • ΊͬͪΌ͍ͨ΁Μɻ • ςϯϓϨʔτλάతͳϞϊ͸શ࣮ͯ૷͠ͳ͍ͱ͍͚ͳ͍ɻ͖΄Μඇಉظɻ • <title> ͷ࣮૷ͱ͔΍͹͍ɻ

    • SQL࢖͑ͳ͍ɻ೥ผΞʔΧΠϒͷҰཡͱ͔͸JSͰ͸ਏ͍ɻ • ௨ৗͷREST APIͰऔΕͳ͍஋(Φϓγϣϯɺϝλσʔλ)͸APIΛ֦ு͢Δ͔ɺPHP͔ΒJSʹ஋ Λ౉͢ͳͲͱʹ͔͍͘Ζ͍Ζ΍Βͳ͍ͱ͍͚ͳ͍ɻ • JS ͷboilerplateతͳͷ͸࢖͍ʹ͍͘ɻ • webpack ॻ͖ͨ͘ͳ͍ • ࢖͑ͳ͘ͳΔϓϥάΠϯ΋ଟ͍ͷͰͦͷखͷ࣮૷΋΍Βͳ͍ͱɻɻ 45
  46. ·ͱΊ • WordPress ͬΆ͍ΞʔΩςΫνϟͰ PWA ΋·͊ग़དྷΔɻ • ςʔϚͰͷ SPA ͸ۀ຿ϨϕϧͳΒɺଟ෼ग़དྷΔɻ

    • ഑෍͢ΔςʔϚͩͱશͯͷػೳΛαϙʔτͰ͖ΔΘ͚Ͱ͸ ͳ͍ͷͰ͠ΜͲͦ͏ɻ 46
  47. ͓ΘΓɻ Thanks! Github: @torounit Twitter: @Toro_Unit Facebook: fb.me/torounit WEB Site:

    https://torounit.com 47