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

PWA へ至る道 (Compressed Version) / WBNagoya 2018-07

PWA へ至る道 (Compressed Version) / WBNagoya 2018-07

More Decks by Toro_Unit (Hiroshi Urabe)

Other Decks in Programming

Transcript

  1. 2

  2. • ͜ͷ࿩͸ɺWordBenchେࡕ & ژ౎ & Ionic Japan 5݄ ϑϧε Πϯά

    Ͱ50෼ऑ࿩ͨ͠ωλΛ5෼ʹѹॖͨ͠ϞϊͰ͢ɻ • Uncompressed version: • https://speakerdeck.com/torounit/wordpress-de-pwa- hezhi-rudao • ͍Ζ͍Ζͬ͢ඈ͹͠·͢ɻ • ෆ໌఺ͳͲ͸ੋඇฉ͍ͯԼ͍͞ɻ • ๻͕ਲͬ෷͏લʹ͓ئ͍͠·͢ɻ 3
  3. Toro_Unit ઎෦ ߛ (͏Β΂ ͻΖ͠) • Frontend Engineer • WordPress

    Plugin and Theme Developer • Docker ͱ Haskell ͸͡Ί·ͨ͠ɻ Github: @torounit Twitter: @Toro_Unit 5
  4. 7

  5. Plugins and Themes • Custom Post Type Permalinks • Simple

    Post Type Permalinks • Powerful Posts Per Page (PPPP) • Responsive Slide • Vanilla • and more... 8
  6. ৴भ WordPress Meetup • WordBench ௕໺ΛҾ͖ܧ͙ܗͰɻ • WordPressΛࡘʹϐβ৯ͬͯΘʔ Θʔ஻Δձͩͱࢥͬͯ·͢ɻ •

    ࣮͸Ҋ֎ओ්཰ߴ͍ɻ • vol.1: 7/21 Sat. • vol.2: 8/18 Sat. • ʢদຊͰϏʔϧࡇ΍ͬͯ·͢ʣ 9
  7. 10

  8. 11

  9. 15

  10. WordPress ͰͲ͏΍Δʁ • HTML/CSS/JS Ͱͭͬͯ͘ɺREST API ͚ͩ࢖͏ɻ • ϑϩϯτΤϯυ͸ɺS3 ͱ͔

    Netlify ͱ͔ Github Pages ͱ͔ʹ ஔ͍ͯɺWordPress ͸ REST API ͚ͩ࢖͏ɻ • ϞμϯͳΞϓϩʔνɻ 19
  11. • ͦΕ WordPress ͡Όͳ͍΄͏ָ͕ͳ৔߹΋ɻ • Contentful ͱ͔ Strapi Έ͍ͨͳ Headless

    CMS ࢖͑͹ָ͔΋ɻ • طଘͷ WordPress αΠτͰ͸࢖͑ͳ͍͠ɺӡ༻ϑϩʔ΋ߟ͑௚͞ͳ͍ ͱɻ • ྫ͑͹ϓϨϏϡʔ͕ग़དྷͳ͍ͱ͔ɻ • طଘαΠτͷผͷ૭ޱͱͯ͠͸༗༻ɻ • WEBαΠτͱ͸ผʹΞϓϦΛ࡞ΔΑ͏ͳέʔεͱ͔ɻ • WordPress ͷςʔϚͱ͍͏ΑΓ͸γεςϜશମͱͯͪ͠ΌΜͱઃܭ͢Δ ඞཁɻ 20
  12. 23

  13. <?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
  14. 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
  15. • 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
  16. torounit/Aetherium • DEMO: https://aetherium-demo.torounit.com/ • Vue + Vuex + Workbox

    • SPA + PWA ͳWordPress ςʔϚɻ • REST API ͱͷ௨৴͸ɺWordPressʹಉࠝͷΫϥΠΞϯτɻ (Backbone.js !!!) 35
  17. URL ͷ ύʔεਏ͍໰୊ɻ • WordPress ͷURL͸ࣗ༝ʹϢʔβʔ͕ઃఆՄೳɻ • ͦΕʹԠͯ͡ɺΞʔΧΠϒͷURL͕มΘͬͨΓɻ • %postname%

    ͳ৔߹ɺݻఆϖʔδ(Page)Λ୳ͯ͠ɺଘࡏ͠ͳ ͔ͬͨΒɺ౤ߘ(Post)Λ୳͠ʹߦ͘ɻ • ΧελϜ౤ߘͳͲΛઃఆ͠ͳ͍ঢ়ଶͰɺ͍͍ͩͨ80ݸ͘Β͍ ͷϦϥΠτͷϧʔϧ͕ੜ੒͞ΕΔɻ 36
  18. function aetherium_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['page'] = $wp_rewrite->get_page_permastruct(); $permastructs['post'] = $wp_rewrite->permalink_structure; } else { $permastructs['post'] = $wp_rewrite->permalink_structure; $permastructs['page'] = $wp_rewrite->get_page_permastruct(); } $permastructs = array_merge( $extra_permastructs, $home_structs, $permastructs ); $structs = []; foreach ( $permastructs as $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 ); // for singular if ( in_array( $key, get_post_types( [ 'public' => true ] ) ) ) { $post_type = get_post_type_object( $key ); // for pages. if ( $post_type->hierarchical ) { $structs[] = [ 'name' => $key, 'path' => untrailingslashit( '/' . $struct ) . '(.+?)' . '/(\\d*)?' ]; continue; } //for posts. $structs[] = [ 'name' => $key, 'path' => untrailingslashit( '/' . $struct ) . '/(\\d*)?' ]; continue; } // for hierarchical taxonomies. if ( in_array( $key, get_taxonomies( [ 'public' => true ] ) ) ) { $taxonomy = get_taxonomy( $key ); if ( $taxonomy->hierarchical ) { $structs[] = [ 'name' => $key, 'path' => untrailingslashit( '/' . $struct ) . '(.+?)' . '/page/:page(\\d*)?' ]; $structs[] = [ 'name' => $key, 'path' => untrailingslashit( '/' . $struct ) . '(.+?)' ]; continue; } } //for archives. $structs[] = [ 'name' => $key, 'path' => untrailingslashit( '/' . $struct ) . '/page/:page(\\d*)?' ]; $structs[] = [ 'name' => $key, 'path' => untrailingslashit( '/' . $struct ) ]; } 38
  19. ؾ߹͍Ͱύʔε͢Ε͹·͊ͳΜͱ͔ͳΔΒ͍͠ɻ • WP_Rewrite ͔ΒύʔϚϦϯΫߏ଄Λऔಘ͠·ͬͯ͘ɺpath-to- regexp (vue-router Ͱ࢖ΘΕͯΔURLύʔαʔ)͕ղऍͰ͖Δܗࣜ ʹؤுͬͯม׵ͯ͠JSʹ౉͢ɻ • ఴ෇ϑΝΠϧͷϖʔδ/ίϝϯτճΓ/ͱ͔͸ߟ͑ͨ͘ͳ͍Ͱ͢...

    • ΧελϜ౤ߘ/λΫιϊϛʔͱ͔͸݁ߏͲ͏ʹ͔ͳΔɻ • Custom Post Type Permalinks ͱ͔ύʔϚϦϯΫվม͢ΔϓϥάΠ ϯͷ͜ͱ͸஌ͬͨ͜ͱͰ͸ͳ͍ɻʢͦͷ͏ͪࢼ͔͢΋͠Εͳ͍ʣ 39
  20. import moment from 'moment/moment'; export const date = async({ state

    }) => { let year = state.route.params.year; let monthnum = state.route.params.monthnum || '01'; let day = state.route.params.day || '01'; let first = `${year}-${monthnum}-${day}T00:00:00`; let last = moment( `${year}-${monthnum}-${day}` ).endOf( 'year' ).format( 'YYYY-MM-DDTHH:mm:ss' ); let posts = await getPosts( createPostsArguments( state, { after: first, before: last }) ); let queriedObject = {}; return { queriedObject, posts, hasMore: getCollectionInstance( 'Posts' ).hasMore() }; }; export const term = async({ state }) => { let posts = []; let queriedObject = {}; let taxonomy = state.taxonomies[state.route.name]; if ( taxonomy ) { let restBase = taxonomy.rest_base; let Collection = wp.api.getCollectionByRoute( `/wp/v2/${restBase}` ); let key = taxonomy.slug; let slugs = state.route.params[key].split( '/' ); let slug = slugs.pop(); let terms = await( new Collection() ).fetch({ data: { slug: slug } }); queriedObject = terms[0]; posts = await getPosts( createPostsArguments( state, { [restBase]: queriedObject.id }) ); return { queriedObject, posts,hasMore: getCollectionInstance( 'Posts' ).hasMore() }; } return { queriedObject: {}, posts: {}, hasMore: false }; }; export const home = async({ state }) => { let queriedObject = {}; if ( global.themeSettings.pageOnFront ) { queriedObject = await getPost( global.themeSettings.pageForPosts, 'Page' ); } let posts = await getPosts( createPostsArguments( state ) ); return { queriedObject, posts, hasMore: getCollectionInstance( 'Posts' ).hasMore() }; }; export const author = async({ state }) => { let queriedObject = await getUserbySlug( state.route.params.author ); let posts = await getPosts( createPostsArguments( state, { author: queriedObject.id }) ); return { queriedObject, posts, hasMore: getCollectionInstance( 'Posts' ).hasMore() }; }; export const singular = async({ state }) => { let posts = []; let queriedObject = {}; // for singular if ( state.route.query.preview ) { if ( state.route.query.preview_id ) { queriedObject = await getPost( state.route.query.preview_id ); posts = [ queriedObject ]; } else if ( state.route.query.p ) { queriedObject = await getPost( state.route.query.p ); posts = [ queriedObject ]; } else if ( state.route.query.page_id ) { queriedObject = await getPost( state.route.query.page_id, 'Page' ); posts = [ queriedObject ]; } } else { switch ( state.route.name ) { case 'front-page': { queriedObject = await getPost( global.themeSettings.pageOnFront, 'Page' ); posts = [ queriedObject ]; break; } case 'page': if ( state.route.params.pagename ) { let pagenames = state.route.params.pagename.split( '/' ); let pagename = pagenames.pop(); posts = await getPosts({ slug: pagename }, 'Pages' ); if ( 0 < posts.length ) { queriedObject = posts[0]; break; } if ( ! global.themeSettings.useVerbosePageRules ) { break; } } // if not found page, search post. case 'post': { if ( state.route.params.postname || state.route.params.pagename ) { posts = await getPosts({ slug: state.route.params.postname || state.route.params.pagename }); if ( 0 < posts.length ) { queriedObject = posts[0]; } } else { queriedObject = await getPost( state.route.params.post_id, 'Post' ); posts = [ queriedObject ]; } break; } } } return { queriedObject, posts, hasMore: false }; }; 41
  21. const getUserbySlug = async( slug ) => { let users

    = await( new wp.api.collections.Users() ).fetch({ data: { slug: slug } }); if ( users[0]) { return users[0]; } return {}; }; const getPost = async( id, type = 'Post' ) => { let Model = wp.api.models[type]; let model = new Model({ id: id }); return await model.fetch(); }; let instances = []; const getCollectionInstance = ( type ) => { let create = () => { if ( instances[type]) { return instances[type]; } let Collection = wp.api.collections[type]; instances[type] = new Collection(); return instances[type]; }; return create(); }; const getPosts = async( data, type = 'Posts' ) => { return await getCollectionInstance( type ).fetch({ data: data }); }; const createPostsArguments = ( state, param = {}) => { let page = state.route.params.page || 1; let perPage = global.themeSettings.postsPerPage let data = { page: page, per_page: perPage }; return Object.assign( data, param ); }; 42
  22. import types from '../mutation-types'; import * as query from './query.js';

    export const initialize = async({ commit, state }) => { await fetchSiteOption({ commit, state }); await fetchTypes({ commit, state }); await fetchTaxonomies({ commit, state }); await fetchPosts({ commit, state }); }; export const fetchSiteOption = async({ commit }) => { let response = await fetch( global.wpApiSettings.root ); let data = await response.json(); commit( types.SET_SITE_OPTION, data ); }; export const fetchTypes = async({ commit }) => { let postTypes = await( new wp.api.collections.Types() ).fetch(); commit( types.SET_POST_TYPES, postTypes ); }; export const fetchTaxonomies = async({ commit }) => { let taxnomies = await( new wp.api.collections.Taxonomies() ).fetch(); commit( types.SET_TAXONOMIES, taxnomies ); }; export const fetchPosts = async({ commit, state }) => { let result = { queriedObject: {}, hasMore: false, posts: [] }; let routeName = state.route.name; if ([ 'day', 'month', 'year' ].includes( routeName ) ) { result = await query.date({ state }); } else if ([ 'home' ].includes( routeName ) ) { result = await query.home({ state }); } else if ( Object.keys( state.taxonomies ).includes( routeName ) ) { result = await query.term({ state, taxonomyName: routeName }); } else if ([ 'author' ].includes( routeName ) ) { result = await query.author({ commit, state }); } else if ( state.route.query.preview || [ 'front-page', 'page', 'post' ].includes( routeName ) ) { result = await query.singular({ state }); } commit( types.SET_QUERIED_OBJECT, result.queriedObject ); commit( types.SET_HASMORE, result.hasMore ); commit( types.SET_POSTS, result.posts ); commit( types.SET_TEMPLATE_TYPE ); }; 43
  23. ؾ߹͍Ͱ WP_Query::parse_query ͷؾ࣋ͪʹͳΕ͹ͳΜͱ ͔ͳΔ͔΋͠Εͳ͍ɻ • ؾ߹͍ͰͳΜͱ͔͸ͳΔɻ͜ΕͰ΋શવ଍Γͳ͍͚Ͳɻɻɻ • get_post ͱ͔ɺget_posts Έ͍ͨͳͷ΋౰વ

    JS ଆͰ࣮૷͢Δඞཁɻ • wp.api.getCollectionByRoute, wp.api.getModelByRoute ͱ͔࢖͑͹ ΧελϜ౤ߘͱ͔ͱ΋ઓ͑Δ͸ͣɻ • λΫιϊϛʔ͸࣮૷ग़དྷͨؾ͕͢Δɻ • Backbone.jsϕʔεͩ͠jQuery΋ಡΈࠐΉ͚Ͳɺೝূͱ͔ߟ͑ͳͯ͘ྑ͍ ͷ͸ָɻ 44
  24. 46

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

    ͢Ε͹ॳճΞΫηε࣌ͷ ίϯςϯπ΋Ωϟογϡग़དྷΔɻ • ϑΥʔϧόοΫ࡞Γ΍͍͢ɻTOPϖʔδΛApp Shellʹಥͬࠐ ΜͲ͚͹ԿͱͰ΋ͳΔɻ • ͔͍͍ͬ͜ɻ 48
  26. FullSpec ͷ σϝϦοτ • ΊͬͪΌ͍ͨ΁Μɻؾ߹͍ ͕ඞཁɻ • ςϯϓϨʔτλάతͳϞϊ͸શ࣮ͯ૷͠ͳ͍ͱ͍͚ͳ͍ɻ͖΄Μඇಉظɻ • SQL࢖͑ͳ͍ɻ೥ผΞʔΧΠϒͷҰཡͱ͔͸JSͰ͸ਏ͍ɻ

    • ௨ৗͷREST APIͰऔΕͳ͍஋(Φϓγϣϯɺϝλσʔλ)͸APIΛ֦ு͢Δ ͔ɺPHP͔ΒJSʹ஋Λ౉͢ͳͲɺͱʹ͔͍͘Ζ͍Ζ΍Βͳ͍ͱ͍͚ͳ͍ɻ • JS ͷboilerplateతͳͷ͸࢖͍ʹ͍͘ɻWebpackྗඞཁ • jQuery Λ࢖ͬͯγϣʔτίʔυͰŋŋŋΈ͍ͨͳϓϥάΠϯ͸શ෦ࢮ͵ɻ 49
  27. ·ͱΊ • ϒϩάʹશ෦ॻ͍ͨɿ Vue.js ͱ REST API Ͱ WordPress ͷςʔϚΛ࡞ͬͯ·͢ɻ

    – Toro_Unit • WordPress ͬΆ͍ΞʔΩςΫνϟͰ PWA ΋·͊ग़དྷΔɻ • ςʔϚͰͷ SPA ͸ۀ຿ϨϕϧͳΒɺଟ෼ग़དྷΔɻʢػೳΛݶఆग़དྷΔͷͰ͋Ε͹ʣ • ഑෍͢ΔςʔϚͩͱ͠ΜͲͦ͏ɻ • WP API Ͱऔͬͯ͘ΔͷͱɺWP_Query Ͱऔͬͯ͘Δͷ͸ඍົʹҧ͏ͷͰɺશͯ αϙʔτͬͯͷ͸ΊͬͪΌ͠ΜͲͦ͏ɻ • ύʔϚϦϯΫͷਂ෵͸ਂ͍ɻ͍ͬͯ͏͔ WordPress ͷਂ෵͸ਂ͍ɻ 50