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

PHP製のPodCast配信用WebアプリをReact+Next.jsなSSGで作り直してみた話

 PHP製のPodCast配信用WebアプリをReact+Next.jsなSSGで作り直してみた話

B51ca7a51ae1fd06bc536fe83e6113e2?s=128

Kaz Watanabe

March 26, 2021
Tweet

Transcript

  1. Θͨͳ΂!LB[@ 1)1੡ͷ1PE$BTU഑৴༻8FCΞϓϦ Λ3FBDU /FYUKTͳ44(Ͱ࡞Γ௚ͯ͠ Έͨ࿩ !1)1FS,BJHJ

  2. 8)0 ౉ลҰ޺ (Θͨͳ΂ ͔ͣͻΖ) @kaz_29 גࣜձࣾϋʔτϏʔπ ։ൃࣄۀ෦

  3. גࣜձࣾϋʔτϏʔπ ຊۀ͸MSPͷձࣾͰɺ ։ൃࣄۀΛ΍͍ͬͯ·͢ɻ IUUQTIFBSUCFBUTKQ

  4. Agenda • PodCastͷ࢓૊Έ • v1(چ൛)ͷߏ੒ • WHY? • v2Ͱ࢖༻͢Δٕज़ཁૉ •

    SPA / SSR / SSG ͱ͸? • v2ͷಈ࡞؀ڥ • ·ͱΊ
  5. IC4","#"

  6. Podcastͷ഑৴

  7. Podcastͷ഑৴ w 344'FFE w গͳ͘ͱ΋݅ͷΤϐιʔυ w ΞʔτϫʔΫ IUUQTIFMQBQQMFDPNJUDQPEDBTUT@DPOOFDUJUDDC

  8. Podcastͷ഑৴ IUUQTIFMQBQQMFDPNJUDQPEDBTUT@DPOOFDUJUDDC

  9. v1ͷಈ࡞؀ڥ • PHP / Slim3 • neon • Docker •

    Azure DevOps
  10. v1ͷಈ࡞؀ڥ

  11. v1ͷಈ࡞؀ڥ "[VSF8FC"QQGPS$POUBJOFST ίʔυΛQVTI ίϯςφΠϝʔδΛQVTI ίϯςφΠϝʔδΛQVMM 4UBHJOHTMPUΛߋ৽ຊ൪ʹEFQMPZ "[VSF%FW0QT 3FQPT1JQFMJOFT "[VSF$POUBJOFS3FHJTUPSZ αΠτΛ֬ೝ

    Ի੠σʔλ "[VSF#MPC4UPSBHF
  12. ಛʹ໰୊ͳ͘ ഑৴Ͱ͖͍ͯΔ͕…

  13. React SSGԽ͍ͨ͠ʂ

  14. WHY? • ӡӦͷखؒΛͳΔ΂͘ݮΒ͍ͨ͠ .neonฤू͢Δͷ໘౗😅 • ഑৴ͷίετ͸௿͍ํ͕ྑ͍ SSGԽ͢Δͱ੩తͳϑΝΠϧͷΈͷ഑৴ͳͷͰ͍ܰ • ࢓ࣄͷҊ݅Ͱ͸ɺ׬શͳSSG͸ͳͦ͞͏ͳͷͰ஌ݟΛಘ͍ͨ ં֯ͳͷͰҰൠʹެ։Ͱ͖ΔαΠτͰࢼ͍ͨ͠

  15. SPA / SSR / SSG

  16. SPA (Single Page Application / Client Side Rendering) • ϝϦοτ

    • JSͷϑϨʔϜϫʔΫͰϦονͳUXΛఏڙͰ͖Δ • ϖʔδભҠ͕ૣ͍(࡞Γʹ΋ΑΔ͕…) • σϝϦοτ • ॳظද͕ࣔ஗͍ • SEOతʹෆར(࠷ۙ͸ͦ͏Ͱ΋ͳ͍…?)
  17. SSR (Server Side Rendering) • ϝϦοτ • දࣔ·Ͱͷ͕࣌ؒ୹ॖͰ͖Δ αʔόͰϨϯμϦϯά͢ΔͨΊɻαʔόʔଆʹ͸ෛՙ͕͔͔Δɻ •

    SEOతʹ༗ར(࠷ۙ͸ͦ͏Ͱ΋ͳ͍…?) • σϝϦοτ • nodeͷαʔό͕ඞཁ • ؅ཧ͕໘౗
  18. SSG (Server Side Generator) • ϝϦοτ • ੩తͳαΠτͳͷͰ͍ܰ • Webαʔό͚ͩͰ഑৴Ͱ͖Δ

    • σϝϦοτ • ಈతͳϖʔδ͕ଟ͍αΠτʹ͸޲͔ͳ͍ • ߋ৽ස౓͕ߴ͍αΠτʹ͸޲͔ͳ͍ • ϖʔδ͕૿͑ΔͱϏϧυʹ͕͔͔࣌ؒΔ
  19. SSG + SPA • SSG ͱ SPAͷϋΠϒϦου(?) • SSRͳ͠ͰׂΓ੾Δ ͦ΋ͦ΋ϩάΠϯ͕ඞཁͩ͠SEOͱ͔…

    • LP͸ผͰ࡞੒ • ߋ৽΍ϖʔδ਺͕ଟͯ͘΋େৎ෉ • WebαʔόͷΈͰ഑৴Մೳ(mod_rewriteతͳػೳඞਢ)
  20. SSG͸ Podcastͷ഑৴ʹ࠷దʂ

  21. ࢖༻͢Δٕज़ཁૉ • docker / docker compose • PHP / CakePHP

    4.x • swagger-php • Next.js • Typescript • Storybook • PostgreSQL • Swagger UI • Github Actions • Azure Static Web Apps (PREVIEW)
  22. CakePHP IUUQTDBLFQIQPSH

  23. ࢖༻͢Δٕज़ཁૉ CakePHP • 4ܥ͕ग़ͯ1೥Ҏ্ܦաͯ͠ɺ࠷৽͸4.2.3 • ܧଓͯ͠৭ʑվળ͞Ε͍ͯΔ • ·ͩ·ͩݩؾͰ͢Αʂ

  24. Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ

  25. ࢖༻͢Δٕज़ཁૉ Swagger UI /** * Index method * * @OA\Get(

    * path="/api/articles.json", * tags={"COMMON"}, * summary="هࣄҰཡऔಘAPI", * description="هࣄҰཡऔಘAPI", * @OA\Parameter( * name="page", * in="query", * @OA\Schema( * type="integer", * ), * description="ϖʔδࢦఆ", * ), * @OA\Response( * response=200, * description="successful operation", * @OA\JsonContent( * @OA\Property( * property="success", * type="boolean", * default=true, * ), * @OA\Property( * property="data", * type="array", * @OA\Items(ref="#/components/schemas/Article"), * ), * ), * ), * ) IUUQTTXBHHFSJPUPPMTTXBHHFSVJ
  26. ࢖༻͢Δٕज़ཁૉ Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ

  27. ࢖༻͢Δٕज़ཁૉ Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ

  28. ٕज़ཁૉ React • Facebookۘ੡ͷJavaScriptϑϨʔϜϫʔΫ • Angularͱ͔Vueͱ͔ͱΑ͘ൺ΂ΒΕΔ • ࠃ಺Ͱ͸Vue͕ਓؾ? • ࠓճ͸࢖͍׳ΕͨReactͰ͢͢Ί·͢

  29. Storybook IUUQTTUPSZCPPLKTPSH

  30. ࢖༻͢Δٕज़ཁૉ Storybook import React from 'react' import { Meta }

    from '@storybook/react/types-6-0' import Presentational from '.' import { Article } from '~/types' import { articles } from '~/__fixture__/articles' export default { title: 'components/views/Admin/Articles' } as Meta const onSelect = (article: Article) => { console.info(article) } const onClick = () => console.info('onClick') const onSync = () => console.info('onSync') export const Default = () => <Presentational loading={false} articles={articles} onSelect={onSelect} onClick={onClick} /> IUUQTTUPSZCPPLKTPSH
  31. ࢖༻͢Δٕज़ཁૉ Storybook IUUQTTUPSZCPPLKTPSH

  32. Next.js IUUQTOFYUKTPSH

  33. ࢖༻͢Δٕज़ཁૉ Next.js • RectϑϩϯτΤϯυ։ൃ༻ͷWebϑϨʔϜϫʔΫ • ॳظঢ়ଶͰSSRʹରԠ͍ͯ͠Δ Universal Javascript (Isomorphic JavaScript)

    ରԠ • SSGʹ΋ରԠ͍ͯ͠Δ v9.3(2020/3)ͰSSGରԠͨ͠ • Կ΋ઃఆ͠ͳ͍Ͱ΋Α͠ͳʹಈ͘ ຊՈ͸ `No config needed.` Λᨳ͍ͬͯΔ ͕ɺ৭ʑઃఆ͸Ͱ͖·͢ IUUQTOFYUKTPSH
  34. Next.jsͷSSG

  35. SSGͷղઆͷલʹ… Next.jsͷDynamic Routing • pages/article/[id].tsx {ม਺໊}.[js or tsx]ͳϑΝΠϧΛ༻ҙ͢Δͱ… • http://example.com/article/1

    ͜Μͳײ͡ͷΞΫηεΛࣗಈͰroutingͯ͘͠ΕΔػೳ ctx.query.id ͰΞΫηεՄೳ • pages/article/[id]/edit.tsx σΟϨΫτϦΛಈతʹࢦఆ͢Δ͜ͱ΋Մೳ
  36. SPA / SSRͷ࣌ http://example.com/article/{id} import React from 'react' import {

    NextPage, NextPageContext } from 'next' type Props = { article: Article } const HogePage: NextPage<Props> = ({ article, }) => ( return <ArticleView article={article} /> ) ArticlePage.getInitialProps = async ({ query }: NextPageContext) => { const id = Number(query.id) const res = await fetch(`${process.env.NEXT_PUBLIC_API_ENDPOINT}/article/${id}.json`) const response = await res.json() return { article: response.data } } export default ArticlePage - ௚઀URLʹΞΫηε͞Εͨ৔߹ʹ͸αʔόʔαΠυͰಈ͘ - ϒϥ΢βͰભҠͨ͠৔߹ʹ͸ΫϥΠΞϯταΠυͰಈ͘
  37. SSG http://example.com/article/{id} type Props = { article: Article } const

    ArticlePage: NextPage<Props> = ({ article, }) => { return <ArticleView article={article} /> } export const getStaticPaths: GetStaticPaths = async () => { const res = await fetch(`${process.env.NEXT_PUBLIC_API_ENDPOINT}/articles.json`) const response = await res.json() const paths: any[] = [] for (const article of response.data) { paths.push({ params: { id: String(article.id) } }) } return { paths, fallback: false, } } export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => { const id = context.params && context.params.id if (!id) { return { notFound: true } } const res = await fetch(`${process.env.NEXT_PUBLIC_API_ENDPOINT}/articles/${id}.json`) const response = await res.json() return { props: { article: response.data } } } export default ArticlePage ଘࡏ͢ΔϖʔδͷҰཡΛऔಘ ݸผͷϖʔδͷ৘ใΛऔಘ (getInitialPropsʹ૬౰)
  38. SSG Ϗϧυ݁Ռ

  39. SSG http://example.com/article/{id} <IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule

    ^([0-9]+)$ article/$1.html [L] </IfModule> ഑৴؀ڥͰ͸͜Μͳײ͡ͷઃఆ͕ඞཁ http://example.com/1 => http://example.com/article/1.html
  40. ͓·͚ (SSG + SPAͷ৔߹) http://example.com/article/{id} import { NextPage } from

    'next' import React from 'react' import AdminLayout from '~/layouts/Admin/index' import AdminArticleView from '~/components/views/Admin/Article' import { useRouter } from 'next/router' type Props = React.ComponentProps<typeof AdminArticleView> const AdminArticlesPage: NextPage = () => { const router = useRouter() const props: Props = { article_id: Number(router.query.id), } return ( <AdminLayout> <AdminArticleView {...props} /> </AdminLayout> ) } export default AdminArticlesPage ApacheͳͲͷWebαʔόͰ഑৴͢Δ৔߹ɺ SSRͰ͸ͳ͍ͷͰαʔόαΠυͰgetInitialProps͸౰વಈ͔ͳ͍ɻ ࣗલͰDynamic RoutingͷύϥϝʔλΛऔಘ͢Δඞཁ͕͋Δ
  41. ࢖༻͢Δٕज़ཁૉ Azure Static Web Apps (PREVIEW) • ੩తαΠτͷϗεςΟϯάαʔϏε • GitHub

    ActionsʹΑΔ Build & Deploy (GitHub࿈ܞඞਢ) • ΧελϜυϝΠϯରԠɺSSLূ໌ॻ(ແྉ) ΧελϜυϝΠϯͷઃఆΛ͢ΔͱɺࣗಈతʹSSLূ໌ॻ΋࡞੒͞ΕΔ Zone Apex(Naked Domainʣ͸ݱঢ়ඇରԠ (Cloudflare࢖ͬͨΓ͢Ε͹ରԠ͸Ͱ͖Δ) • ϓϨϏϡʔػೳ PRΛ࡞੒͢ΔͱɺϓϨϏϡʔ൛͕ࣗಈσϓϩΠɻϚʔδ͢ΔͱϓϨϏϡʔ൛͸ࣗಈ࡟আ͞Εຊ൪൓өʹ΋൓өɻ • ΞϓϦΤϯδχΞʹ࢖͍΍͍͢ • ࠓͷͱ͜Ζແྉ • ϩʔΧϧ؀ڥͰ࣮ߦՄೳͳػೳ(static-web-apps-cli)͕ϦϦʔε͞Εͨ (2021/3/5 new!) • https://github.com/Azure/static-web-apps-cli IUUQTB[VSFNJDSPTPGUDPNKBKQTFSWJDFTBQQTFSWJDFTUBUJD
  42. v2ͷಈ࡞؀ڥ

  43. v2ͷಈ࡞؀ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT "[VSF4UBUJD8FC"QQT 13Λ࡞੒ (JUIVC"DUJPOT CVJMEΛ࣮ߦ σϓϩΠ ։ൃɾςετ 13

    هࣄσʔλ Λಉظ هࣄσʔλΛऔಘ Ի੠σʔλ "[VSF#MPC4UPSBHF
  44. ಈ͔Μ😱

  45. v2ͷಈ࡞؀ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT • ΤϯυϙΠϯτ͸͓ͦΒ͘App Service (Windows)? • ੈք֤஍ͷ஍ཧతʹ෼ࢄͨ͠ϙΠϯτ͔Βఏڙ͞ΕΔ https://docs.microsoft.com/ja-jp/azure/static-web-apps/overview

    • Azure Traffic managerܦ༝ͰΞΫηε͞ΕΔ EJH99999:::::;;;;;;;;;B[VSFTUBUJDBQQTOFU ʜ 99999:::::;;;;;;;;;B[VSFTUBUJDBQQTOFU*/" "/48&34&$5*0/ 99999:::::;;;;;;;;;B[VSFTUBUJDBQQTOFU*/$/".&B[VSFTUBUJDBQQTUSB⒏DNBOBHFSOFU B[VSFTUBUJDBQQTUSB⒏DNBOBHFSOFU*/$/".&NTIBILTUBUJDTJUFTQSPEFBTUBTJBQB[VSFXFCTJUFTOFU ʜ ͠͹΍Μࡶه"QQ4FSWJDF4UBUJD8FC"QQTͷ࢓૊ΈΛ୳Δʢඇެࣜʣ IUUQTCMPHTIJCBZBOKQFOUSZ ඇެࣜͳ৘ใͰ͢
  46. mod_rewrite࢖͑Μ😱 Կ͔ผͷํ๏ͳ͍ͷ͔ʜ

  47. v2ͷಈ࡞؀ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT • routes.jsonͰ੍ޚͰ͖Δ ͨͩ͠ਖ਼نදݱͱ͔Ͱࢦఆ͸Ͱ͖ͳ͍ͷͰॻ͖׵͍͑ͨϧʔτΛશͯهड़͢Δඞཁ͕͋Δ "[VSF4UBUJD8FC"QQTϓϨϏϡʔͰͷϧʔτ IUUQTEPDTNJDSPTPGUDPNKBKQB[VSFTUBUJDXFCBQQTSPVUFT \ SPVUFT<

    \ SPVUF  TFSWFBSUJDMFIUNM ^  > ^ ͜ΕΛΤϐιʔυ෼هࡌ͠ͳ͍ͱ͍͔Μ…
  48. v2ͷಈ࡞؀ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT • Ϗϧυ࣌ʹҰཡϖʔδͷgetStaticPropsͰࣗಈੜ੒ const exportRoutes = (articles: Article[])

    => { const routes = { routes: [] as any, platformErrorOverrides: [ { errorType: 'NotFound', serve: '/404.html', }, ], mimeTypes: { xml: 'application/xml; charset=UTF-8', }, } for (const article of articles) { routes.routes.push({ route: `/${article.id}`, serve: `/article/${article.id}.html`, }) } fs.writeFileSync('./public/routes.json', JSON.stringify(routes)) } export const getStaticProps: GetStaticProps = async (_) => { firebaseInit() const db = firebase.firestore() const ref = db.collection('articles') const articles: Article[] = [] try { const query = await ref.orderBy('date', 'desc').get() query.forEach(doc => { articles.push(doc.data() as Article) }) if (process.env.BUILD === '1') { exportRoutes(articles) } } catch (error) { console.error(error) } return { props: { articles, url: process.env.NEXT_PUBLIC_WEB_ENDPOINT } } }
  49. V2׬੒ https://heartbeats.jp/hbsakaba/ #hbsakaba

  50. ·ͱΊ

  51. ·ͱΊ • SSGศར • SSR / SSG / SSG+SPA దࡐదॴͰ࢖͍෼͚Δͱྑ͍

    • ϑϩϯτͷ։ൃ΋ָ͍͠Αʂ • ΋ͪΖΜۤ࿑΋͋Δ͚ͲɺόοΫΤϯυ΋ಉ͡😅 • ࣗ෼Ͱ੍ޚͰ͖ΔΞϓϦΛ࣋ͬͯΔͱ৭ʑ࣮ݧͰ͖ͯศར Enjoy!
  52. ͓͠·͍