$30 off During Our Annual Pro Sale. View Details »

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

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

Kaz Watanabe

March 26, 2021
Tweet

More Decks by Kaz Watanabe

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. Agenda
    • PodCastͷ࢓૊Έ
    • v1(چ൛)ͷߏ੒
    • WHY?
    • v2Ͱ࢖༻͢Δٕज़ཁૉ
    • SPA / SSR / SSG ͱ͸?
    • v2ͷಈ࡞؀ڥ
    • ·ͱΊ

    View Slide

  5. IC4","#"

    View Slide

  6. Podcastͷ഑৴

    View Slide

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

    View Slide

  8. Podcastͷ഑৴
    IUUQTIFMQBQQMFDPNJUDQPEDBTUT@DPOOFDUJUDDC

    View Slide

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

    View Slide

  10. v1ͷಈ࡞؀ڥ

    View Slide

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

    View Slide

  12. ಛʹ໰୊ͳ͘
    ഑৴Ͱ͖͍ͯΔ͕…

    View Slide

  13. React SSGԽ͍ͨ͠ʂ

    View Slide

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

    View Slide

  15. SPA / SSR / SSG

    View Slide

  16. SPA
    (Single Page Application / Client Side Rendering)
    • ϝϦοτ
    • JSͷϑϨʔϜϫʔΫͰϦονͳUXΛఏڙͰ͖Δ
    • ϖʔδભҠ͕ૣ͍(࡞Γʹ΋ΑΔ͕…)
    • σϝϦοτ
    • ॳظද͕ࣔ஗͍
    • SEOతʹෆར(࠷ۙ͸ͦ͏Ͱ΋ͳ͍…?)

    View Slide

  17. SSR
    (Server Side Rendering)
    • ϝϦοτ
    • දࣔ·Ͱͷ͕࣌ؒ୹ॖͰ͖Δ
    αʔόͰϨϯμϦϯά͢ΔͨΊɻαʔόʔଆʹ͸ෛՙ͕͔͔Δɻ
    • SEOతʹ༗ར(࠷ۙ͸ͦ͏Ͱ΋ͳ͍…?)
    • σϝϦοτ
    • nodeͷαʔό͕ඞཁ
    • ؅ཧ͕໘౗

    View Slide

  18. SSG
    (Server Side Generator)
    • ϝϦοτ
    • ੩తͳαΠτͳͷͰ͍ܰ
    • Webαʔό͚ͩͰ഑৴Ͱ͖Δ
    • σϝϦοτ
    • ಈతͳϖʔδ͕ଟ͍αΠτʹ͸޲͔ͳ͍
    • ߋ৽ස౓͕ߴ͍αΠτʹ͸޲͔ͳ͍
    • ϖʔδ͕૿͑ΔͱϏϧυʹ͕͔͔࣌ؒΔ

    View Slide

  19. SSG + SPA
    • SSG ͱ SPAͷϋΠϒϦου(?)
    • SSRͳ͠ͰׂΓ੾Δ
    ͦ΋ͦ΋ϩάΠϯ͕ඞཁͩ͠SEOͱ͔…
    • LP͸ผͰ࡞੒
    • ߋ৽΍ϖʔδ਺͕ଟͯ͘΋େৎ෉
    • WebαʔόͷΈͰ഑৴Մೳ(mod_rewriteతͳػೳඞਢ)

    View Slide

  20. SSG͸
    Podcastͷ഑৴ʹ࠷దʂ

    View Slide

  21. ࢖༻͢Δٕज़ཁૉ
    • docker / docker compose
    • PHP / CakePHP 4.x
    • swagger-php
    • Next.js
    • Typescript
    • Storybook
    • PostgreSQL
    • Swagger UI
    • Github Actions
    • Azure Static Web Apps (PREVIEW)

    View Slide

  22. CakePHP
    IUUQTDBLFQIQPSH

    View Slide

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

    View Slide

  24. Swagger UI
    IUUQTTXBHHFSJPUPPMTTXBHHFSVJ

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  29. Storybook
    IUUQTTUPSZCPPLKTPSH

    View Slide

  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 = () => loading={false}
    articles={articles}
    onSelect={onSelect}
    onClick={onClick}
    />
    IUUQTTUPSZCPPLKTPSH

    View Slide

  31. ࢖༻͢Δٕज़ཁૉ
    Storybook
    IUUQTTUPSZCPPLKTPSH

    View Slide

  32. Next.js
    IUUQTOFYUKTPSH

    View Slide

  33. ࢖༻͢Δٕज़ཁૉ
    Next.js
    • RectϑϩϯτΤϯυ։ൃ༻ͷWebϑϨʔϜϫʔΫ
    • ॳظঢ়ଶͰSSRʹରԠ͍ͯ͠Δ
    Universal Javascript (Isomorphic JavaScript) ରԠ
    • SSGʹ΋ରԠ͍ͯ͠Δ
    v9.3(2020/3)ͰSSGରԠͨ͠
    • Կ΋ઃఆ͠ͳ͍Ͱ΋Α͠ͳʹಈ͘
    ຊՈ͸ `No config needed.` Λᨳ͍ͬͯΔ
    ͕ɺ৭ʑઃఆ͸Ͱ͖·͢
    IUUQTOFYUKTPSH

    View Slide

  34. Next.jsͷSSG

    View Slide

  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
    σΟϨΫτϦΛಈతʹࢦఆ͢Δ͜ͱ΋Մೳ

    View Slide

  36. SPA / SSRͷ࣌
    http://example.com/article/{id}
    import React from 'react'
    import { NextPage, NextPageContext } from 'next'
    type Props = {
    article: Article
    }
    const HogePage: NextPage = ({
    article,
    }) => (
    return
    )
    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ʹΞΫηε͞Εͨ৔߹ʹ͸αʔόʔαΠυͰಈ͘
    - ϒϥ΢βͰભҠͨ͠৔߹ʹ͸ΫϥΠΞϯταΠυͰಈ͘

    View Slide

  37. SSG
    http://example.com/article/{id}
    type Props = {
    article: Article
    }
    const ArticlePage: NextPage = ({
    article,
    }) => {
    return
    }
    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ʹ૬౰)

    View Slide

  38. SSG
    Ϗϧυ݁Ռ

    View Slide

  39. SSG
    http://example.com/article/{id}

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^([0-9]+)$ article/$1.html [L]

    ഑৴؀ڥͰ͸͜Μͳײ͡ͷઃఆ͕ඞཁ
    http://example.com/1 => http://example.com/article/1.html

    View Slide

  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
    const AdminArticlesPage: NextPage = () => {
    const router = useRouter()
    const props: Props = {
    article_id: Number(router.query.id),
    }
    return (



    )
    }
    export default AdminArticlesPage
    ApacheͳͲͷWebαʔόͰ഑৴͢Δ৔߹ɺ
    SSRͰ͸ͳ͍ͷͰαʔόαΠυͰgetInitialProps͸౰વಈ͔ͳ͍ɻ
    ࣗલͰDynamic RoutingͷύϥϝʔλΛऔಘ͢Δඞཁ͕͋Δ

    View Slide

  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

    View Slide

  42. v2ͷಈ࡞؀ڥ

    View Slide

  43. v2ͷಈ࡞؀ڥ
    "[VSF4UBUJD8FC"QQT(JUIVC"DUJPOT
    "[VSF4UBUJD8FC"QQT
    13Λ࡞੒
    (JUIVC"DUJPOT
    CVJMEΛ࣮ߦ
    σϓϩΠ
    ։ൃɾςετ
    13
    هࣄσʔλ
    Λಉظ
    هࣄσʔλΛऔಘ
    Ի੠σʔλ
    "[VSF#MPC4UPSBHF

    View Slide

  44. ಈ͔Μ😱

    View Slide

  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
    ඇެࣜͳ৘ใͰ͢

    View Slide

  46. mod_rewrite࢖͑Μ😱
    Կ͔ผͷํ๏ͳ͍ͷ͔ʜ

    View Slide

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

    View Slide

  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 } }
    }

    View Slide

  49. V2׬੒
    https://heartbeats.jp/hbsakaba/ #hbsakaba

    View Slide

  50. ·ͱΊ

    View Slide

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

    View Slide

  52. ͓͠·͍

    View Slide