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
720
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
38
Greenは本当にGreenか? - B/GデプロイとAPI自動テストで安心デプロイ
kaz29
1
200
CI/CD/IaC 久々に0から環境を作ったらこうなりました
kaz29
1
490
開発エンジニアが実践するDevSecOps
kaz29
0
170
PHPCon福岡2024-Azureもなかなかいいですよ.pdf
kaz29
2
380
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.2k
Other Decks in Programming
See All in Programming
Laravel Boostに学ぶ、AIにPHPを書かせる技術 〜OSSの実装から蒸留するエージェント制御の王道〜
kentaroutakeda
3
710
人間の目はかわらない、だからJPEGは30年もつ
yuzneri
12
19k
仕様駆動開発へのトライを機に チームに適合する手法を模索し続けている話
freee
PRO
0
560
Built Our Own Background Agent at LayerX
layerx
PRO
10
5.7k
Laravelで学ぶ Webアプリケーションチューニング入門/web_application_tuning_101
hanhan1978
4
1.9k
AIを紡ぐPMのお話
swdtkuy
0
110
ソフトウェア設計に溶けるインフラ ― AWS CDK のインフラ認識論
konokenj
3
800
「寝てても仕事が進む」Claude Codeで組む第二の脳
tomoyafujita2016
0
340
自動化したのに回らないテスト運用の壁ーAI時代の品質責任と生産性
mfunaki
1
140
Foundation Models frameworkで画像分析
ryodeveloper
1
620
the container ship “Apple Silicon”@WWDC26 Recap -Japan-\(region).swift
shingangan
0
130
170k Jobs a Day on GKE: Scaling Mercari's CI Platform - and What's Next for AI-Native Development
junyaokabe
0
110
Featured
See All Featured
Winning Ecommerce Organic Search in an AI Era - #searchnstuff2025
aleyda
1
2.1k
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
My Coaching Mixtape
mlcsv
0
240
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
287
14k
Claude Code のすすめ
schroneko
67
230k
More Than Pixels: Becoming A User Experience Designer
marktimemedia
3
490
A Guide to Academic Writing Using Generative AI - A Workshop
ks91
PRO
1
370
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
254
22k
Game over? The fight for quality and originality in the time of robots
wayneb77
1
240
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
1
2.8k
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.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!
͓͠·͍