Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
PHP製のPodCast配信用WebアプリをReact+Next.jsなSSGで作り直してみた話
Search
Kaz Watanabe
March 26, 2021
Programming
710
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
PHP製のPodCast配信用WebアプリをReact+Next.jsなSSGで作り直してみた話
Kaz Watanabe
March 26, 2021
More Decks by Kaz Watanabe
See All by Kaz Watanabe
開発エンジニアが取り組む DevSecOps ~ GitHub Enterprise × Azure での実践~
kaz29
0
32
Greenは本当にGreenか? - B/GデプロイとAPI自動テストで安心デプロイ
kaz29
1
190
CI/CD/IaC 久々に0から環境を作ったらこうなりました
kaz29
1
480
開発エンジニアが実践するDevSecOps
kaz29
0
150
PHPCon福岡2024-Azureもなかなかいいですよ.pdf
kaz29
2
370
Azure Container Apps + Bicep 〜 こんな感じで運用しています
kaz29
3
1.3k
20220908_フロントエンドパフォーマンス改善.pdf
kaz29
2
200
バックエンドエンジニアの私がお勧めする SPAフロントエンド開発環境
kaz29
6
6.3k
201909-PHPCon北海道-PHPでCI_CD.pdf
kaz29
0
4.1k
Other Decks in Programming
See All in Programming
正しくソフトウェアを作る、前提を疑うための認知の視点 / doubt-premise
minodriven
17
6.2k
生成AI時代にこそ効くGo | Why Go Works in the Age of Generative AI
mom0tomo
8
3.2k
メソッドのジェネリクスでGoの夢は広がるか? / Kyoto.go #65
utgwkk
3
620
New "Type" system on PicoRuby
pocke
1
710
さぁV100、メモリをお食べ・・・
nilpe
0
130
Hunting Vulnerabilities in Symfony with LLMs
vinceamstoutz
0
430
柔軟なPDFレイアウトエディタを支える型システム設計 — Discriminated UnionとConditional Typeの実践
minako__ph
4
1.6k
運用エージェントは "作る" から "育てる" へ - 記憶と自己進化の3層設計パターン / self-evolving-agents-three-layer-agent-design
gawa
12
3.6k
oxlintはeslint/typescript-eslintを置き換えられるのか
shomafujita
2
330
These Five Tricks Can Make Your Apps Greener, Cheaper, & Nicer
hollycummins
0
280
脅威をエンジニアリングの糧にして――現場編 / Turning Threats into Engineering Fuel — Field Edition
nrslib
0
260
Why Laravel apps break—Mastering the fundamentals to keep them maintainable
kentaroutakeda
1
340
Featured
See All Featured
The Invisible Side of Design
smashingmag
302
52k
The Art of Programming - Codeland 2020
erikaheidi
57
14k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.2k
First, design no harm
axbom
PRO
2
1.2k
Statistics for Hackers
jakevdp
799
230k
For a Future-Friendly Web
brad_frost
183
10k
Principles of Awesome APIs and How to Build Them.
keavy
128
17k
The browser strikes back
jonoalderson
0
1.2k
The SEO identity crisis: Don't let AI make you average
varn
0
480
Speed Design
sergeychernyshev
33
1.8k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.5k
Darren the Foodie - Storyboard
khoart
PRO
3
3.4k
Transcript
Θͨͳ!LB[@ 1)1ͷ1PE$BTU৴༻8FCΞϓϦ Λ3FBDU /FYUKTͳ44(Ͱ࡞Γͯ͠ Έͨ !1)1FS,BJHJ
8)0 ลҰ (Θͨͳ ͔ͣͻΖ) @kaz_29 גࣜձࣾϋʔτϏʔπ ։ൃࣄۀ෦
גࣜձࣾϋʔτϏʔπ ຊۀMSPͷձࣾͰɺ ։ൃࣄۀΛ͍ͬͯ·͢ɻ IUUQTIFBSUCFBUTKQ
Agenda • PodCastͷΈ • v1(چ൛)ͷߏ • WHY? • v2Ͱ༻͢Δٕज़ཁૉ •
SPA / SSR / SSG ͱ? • v2ͷಈ࡞ڥ • ·ͱΊ
IC4","#"
Podcastͷ৴
Podcastͷ৴ w 344'FFE w গͳ͘ͱ݅ͷΤϐιʔυ w ΞʔτϫʔΫ IUUQTIFMQBQQMFDPNJUDQPEDBTUT@DPOOFDUJUDDC
Podcastͷ৴ IUUQTIFMQBQQMFDPNJUDQPEDBTUT@DPOOFDUJUDDC
v1ͷಈ࡞ڥ • PHP / Slim3 • neon • Docker •
Azure DevOps
v1ͷಈ࡞ڥ
v1ͷಈ࡞ڥ "[VSF8FC"QQGPS$POUBJOFST ίʔυΛQVTI ίϯςφΠϝʔδΛQVTI ίϯςφΠϝʔδΛQVMM 4UBHJOHTMPUΛߋ৽ຊ൪ʹEFQMPZ "[VSF%FW0QT 3FQPT1JQFMJOFT "[VSF$POUBJOFS3FHJTUPSZ αΠτΛ֬ೝ
Իσʔλ "[VSF#MPC4UPSBHF
ಛʹͳ͘ ৴Ͱ͖͍ͯΔ͕…
React SSGԽ͍ͨ͠ʂ
WHY? • ӡӦͷखؒΛͳΔ͘ݮΒ͍ͨ͠ .neonฤू͢Δͷ໘😅 • ৴ͷίετ͍ํ͕ྑ͍ SSGԽ͢Δͱ੩తͳϑΝΠϧͷΈͷ৴ͳͷͰ͍ܰ • ࣄͷҊ݅ͰɺશͳSSGͳͦ͞͏ͳͷͰݟΛಘ͍ͨ ં֯ͳͷͰҰൠʹެ։Ͱ͖ΔαΠτͰࢼ͍ͨ͠
SPA / SSR / SSG
SPA (Single Page Application / Client Side Rendering) • ϝϦοτ
• JSͷϑϨʔϜϫʔΫͰϦονͳUXΛఏڙͰ͖Δ • ϖʔδભҠ͕ૣ͍(࡞ΓʹΑΔ͕…) • σϝϦοτ • ॳظද͕͍ࣔ • SEOతʹෆར(࠷ۙͦ͏Ͱͳ͍…?)
SSR (Server Side Rendering) • ϝϦοτ • දࣔ·Ͱͷ͕࣌ؒॖͰ͖Δ αʔόͰϨϯμϦϯά͢ΔͨΊɻαʔόʔଆʹෛՙ͕͔͔Δɻ •
SEOతʹ༗ར(࠷ۙͦ͏Ͱͳ͍…?) • σϝϦοτ • nodeͷαʔό͕ඞཁ • ཧ͕໘
SSG (Server Side Generator) • ϝϦοτ • ੩తͳαΠτͳͷͰ͍ܰ • Webαʔό͚ͩͰ৴Ͱ͖Δ
• σϝϦοτ • ಈతͳϖʔδ͕ଟ͍αΠτʹ͔ͳ͍ • ߋ৽ස͕ߴ͍αΠτʹ͔ͳ͍ • ϖʔδ͕૿͑ΔͱϏϧυʹ͕͔͔࣌ؒΔ
SSG + SPA • SSG ͱ SPAͷϋΠϒϦου(?) • SSRͳ͠ͰׂΓΔ ͦͦϩάΠϯ͕ඞཁͩ͠SEOͱ͔…
• LPผͰ࡞ • ߋ৽ϖʔδ͕ଟͯ͘େৎ • WebαʔόͷΈͰ৴Մೳ(mod_rewriteతͳػೳඞਢ)
SSG Podcastͷ৴ʹ࠷దʂ
༻͢Δٕज़ཁૉ • docker / docker compose • PHP / CakePHP
4.x • swagger-php • Next.js • Typescript • Storybook • PostgreSQL • Swagger UI • Github Actions • Azure Static Web Apps (PREVIEW)
CakePHP IUUQTDBLFQIQPSH
༻͢Δٕज़ཁૉ CakePHP • 4ܥ͕ग़ͯ1Ҏ্ܦաͯ͠ɺ࠷৽4.2.3 • ܧଓͯ͠৭ʑվળ͞Ε͍ͯΔ • ·ͩ·ͩݩؾͰ͢Αʂ
Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ
༻͢Δٕज़ཁૉ 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
༻͢Δٕज़ཁૉ Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ
༻͢Δٕज़ཁૉ Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ
ٕज़ཁૉ React • FacebookۘͷJavaScriptϑϨʔϜϫʔΫ • Angularͱ͔Vueͱ͔ͱΑ͘ൺΒΕΔ • ࠃͰVue͕ਓؾ? • ࠓճ͍׳ΕͨReactͰ͢͢Ί·͢
Storybook IUUQTTUPSZCPPLKTPSH
༻͢Δٕज़ཁૉ 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
༻͢Δٕज़ཁૉ Storybook IUUQTTUPSZCPPLKTPSH
Next.js IUUQTOFYUKTPSH
༻͢Δٕज़ཁૉ Next.js • RectϑϩϯτΤϯυ։ൃ༻ͷWebϑϨʔϜϫʔΫ • ॳظঢ়ଶͰSSRʹରԠ͍ͯ͠Δ Universal Javascript (Isomorphic JavaScript)
ରԠ • SSGʹରԠ͍ͯ͠Δ v9.3(2020/3)ͰSSGରԠͨ͠ • Կઃఆ͠ͳ͍ͰΑ͠ͳʹಈ͘ ຊՈ `No config needed.` Λᨳ͍ͬͯΔ ͕ɺ৭ʑઃఆͰ͖·͢ IUUQTOFYUKTPSH
Next.jsͷSSG
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 σΟϨΫτϦΛಈతʹࢦఆ͢Δ͜ͱՄೳ
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ʹΞΫηε͞Εͨ߹ʹαʔόʔαΠυͰಈ͘ - ϒϥβͰભҠͨ͠߹ʹΫϥΠΞϯταΠυͰಈ͘
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ʹ૬)
SSG Ϗϧυ݁Ռ
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
͓·͚ (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ͷύϥϝʔλΛऔಘ͢Δඞཁ͕͋Δ
༻͢Δٕज़ཁૉ 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
v2ͷಈ࡞ڥ
v2ͷಈ࡞ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT "[VSF4UBUJD8FC"QQT 13Λ࡞ (JUIVC"DUJPOT CVJMEΛ࣮ߦ σϓϩΠ ։ൃɾςετ 13
هࣄσʔλ Λಉظ هࣄσʔλΛऔಘ Իσʔλ "[VSF#MPC4UPSBHF
ಈ͔Μ😱
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 ඇެࣜͳใͰ͢
mod_rewrite͑Μ😱 Կ͔ผͷํ๏ͳ͍ͷ͔ʜ
v2ͷಈ࡞ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT • routes.jsonͰ੍ޚͰ͖Δ ͨͩ͠ਖ਼نදݱͱ͔ͰࢦఆͰ͖ͳ͍ͷͰॻ͖͍͑ͨϧʔτΛશͯهड़͢Δඞཁ͕͋Δ "[VSF4UBUJD8FC"QQTϓϨϏϡʔͰͷϧʔτ IUUQTEPDTNJDSPTPGUDPNKBKQB[VSFTUBUJDXFCBQQTSPVUFT \ SPVUFT<
\ SPVUF TFSWFBSUJDMFIUNM ^ > ^ ͜ΕΛΤϐιʔυهࡌ͠ͳ͍ͱ͍͔Μ…
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 } } }
V2 https://heartbeats.jp/hbsakaba/ #hbsakaba
·ͱΊ
·ͱΊ • SSGศར • SSR / SSG / SSG+SPA దࡐదॴͰ͍͚Δͱྑ͍
• ϑϩϯτͷ։ൃָ͍͠Αʂ • ͪΖΜۤ࿑͋Δ͚ͲɺόοΫΤϯυಉ͡😅 • ࣗͰ੍ޚͰ͖ΔΞϓϦΛ࣋ͬͯΔͱ৭ʑ࣮ݧͰ͖ͯศར Enjoy!
͓͠·͍