Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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. SPA (Single Page Application / Client Side Rendering) • ϝϦοτ

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

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

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

    • LP͸ผͰ࡞੒ • ߋ৽΍ϖʔδ਺͕ଟͯ͘΋େৎ෉ • WebαʔόͷΈͰ഑৴Մೳ(mod_rewriteతͳػೳඞਢ)
  5. ࢖༻͢Δٕज़ཁૉ • docker / docker compose • PHP / CakePHP

    4.x • swagger-php • Next.js • Typescript • Storybook • PostgreSQL • Swagger UI • Github Actions • Azure Static Web Apps (PREVIEW)
  6. ࢖༻͢Δٕज़ཁૉ 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
  7. ࢖༻͢Δٕज़ཁૉ 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
  8. ࢖༻͢Δٕज़ཁૉ Next.js • RectϑϩϯτΤϯυ։ൃ༻ͷWebϑϨʔϜϫʔΫ • ॳظঢ়ଶͰSSRʹରԠ͍ͯ͠Δ Universal Javascript (Isomorphic JavaScript)

    ରԠ • SSGʹ΋ରԠ͍ͯ͠Δ v9.3(2020/3)ͰSSGରԠͨ͠ • Կ΋ઃఆ͠ͳ͍Ͱ΋Α͠ͳʹಈ͘ ຊՈ͸ `No config needed.` Λᨳ͍ͬͯΔ ͕ɺ৭ʑઃఆ͸Ͱ͖·͢ IUUQTOFYUKTPSH
  9. 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 σΟϨΫτϦΛಈతʹࢦఆ͢Δ͜ͱ΋Մೳ
  10. 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ʹΞΫηε͞Εͨ৔߹ʹ͸αʔόʔαΠυͰಈ͘ - ϒϥ΢βͰભҠͨ͠৔߹ʹ͸ΫϥΠΞϯταΠυͰಈ͘
  11. 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ʹ૬౰)
  12. 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
  13. ͓·͚ (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ͷύϥϝʔλΛऔಘ͢Δඞཁ͕͋Δ
  14. ࢖༻͢Δٕज़ཁૉ 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
  15. 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 ඇެࣜͳ৘ใͰ͢
  16. 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 } } }
  17. ·ͱΊ • SSGศར • SSR / SSG / SSG+SPA దࡐదॴͰ࢖͍෼͚Δͱྑ͍

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