Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

IC4","#"

Slide 6

Slide 6 text

Podcastͷ഑৴

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Podcastͷ഑৴ IUUQTIFMQBQQMFDPNJUDQPEDBTUT@DPOOFDUJUDDC

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

v1ͷಈ࡞؀ڥ

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

React SSGԽ͍ͨ͠ʂ

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

SPA / SSR / SSG

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

SSG͸ Podcastͷ഑৴ʹ࠷దʂ

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

CakePHP IUUQTDBLFQIQPSH

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ

Slide 25

Slide 25 text

࢖༻͢Δٕज़ཁૉ 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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Storybook IUUQTTUPSZCPPLKTPSH

Slide 30

Slide 30 text

࢖༻͢Δٕज़ཁૉ 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 = () => IUUQTTUPSZCPPLKTPSH

Slide 31

Slide 31 text

࢖༻͢Δٕज़ཁૉ Storybook IUUQTTUPSZCPPLKTPSH

Slide 32

Slide 32 text

Next.js IUUQTOFYUKTPSH

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Next.jsͷSSG

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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ʹΞΫηε͞Εͨ৔߹ʹ͸αʔόʔαΠυͰಈ͘ - ϒϥ΢βͰભҠͨ͠৔߹ʹ͸ΫϥΠΞϯταΠυͰಈ͘

Slide 37

Slide 37 text

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ʹ૬౰)

Slide 38

Slide 38 text

SSG Ϗϧυ݁Ռ

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

͓·͚ (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ͷύϥϝʔλΛऔಘ͢Δඞཁ͕͋Δ

Slide 41

Slide 41 text

࢖༻͢Δٕज़ཁૉ 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

Slide 42

Slide 42 text

v2ͷಈ࡞؀ڥ

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

ಈ͔Μ😱

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

·ͱΊ

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

͓͠·͍