Slide 1

Slide 1 text

Creating a Next.js-style Framework with Bun and Hono KansaiTS#6 @jiko21

Slide 2

Slide 2 text

About jiko21… Name: Daiki Kojima (jiko21) Multistack Engineer @ AppBrew.inc Love: Guitar, TypeScript, baseball @jiko21 @jiko_21

Slide 3

Slide 3 text

hono🔥

Slide 4

Slide 4 text

hono🔥 • ௒ߴ଎(ultrafast)&ܰྔ(lightweight)ͳWeb Application framework • ͲΜͳ؀ڥͰ΋(node, deno, bun, edge runtime)Ͱ΋͏͘͝ • TypeFirstͳϑϨʔϜϫʔΫ • ಋೖʹࡍͯ͠TypeScript༻ʹࡉ͘ઃఆ౳͸͍Βͳ͍ • ֤छศརͳmiddleware౳͋Γ • cache • logging • jsx

Slide 5

Slide 5 text

ViewΛjsxͰॻ͖͍ͨ • Reactʹ͍ͩͿ׳ΕͯΔͷͰͦͷsyntaxͰॻ͖͍ͨʂ • template engineͰ΋͍͍͕ɺ஋౉͕ͦ͠΋ͦ΋ͳΕͳ͍… • ίϯϙʔωϯτͷࢀরͳͲɺ࢖͍ճ͠΋ྑ͍…

Slide 6

Slide 6 text

honoʹ͸jsx renderer͕͋Δ im port { FC } from "hono/jsx"; export const Sample: FC<{color: string}> = ({color}) => { return

Hello Hono!

; }

Slide 7

Slide 7 text

ͪΐͬͱڽͬͨ͜ͱ͍ͨ͠ͳΝ • interactionΛ͚͍ͭͨ • honojs/honoxͰՄೳ(ޙड़) • StorybookΛ࢖͍͍ͨͳ͊

Slide 8

Slide 8 text

Storybook࢖͍͍ͨͳΝ

Slide 9

Slide 9 text

Storybook࢖͍͍ͨͳΝ • ͦ΋ͦ΋hono/jsxࣗମ͕αϙʔτ͞Εͯͳ͍ • React componentͱͯ͠ಡΈࠐ·ͤΔͷ΋೉ͦ͠͏…

Slide 10

Slide 10 text

@storybook/server • React΍VueͷΑ͏ͳUIϑϨʔϜϫʔΫҎ֎ͷ ΋ͷʹstorybookΛ࢖͏ͨΊͷlibrary • ΍ͬͯΔ͜ͱͱͯ͠͸.stories.jsonϑΝΠϧΛݩʹˠ requestΛ౤͛ͯhtmlΛදࣔ͢Δ͚ͩ • storybook͕౤͛Δrequestͷॲཧ͸ ࣗલͰॻ͔ͳ͍ͱ͍͚ͳ͍ { "title": "Example/Sample", "parameters": { "server": { "url": "http://localhost:3000/stories", "id": "sample" } }, "args": { "color": "red" }, "tags": ["autodocs"], "stories": [ { "name": "Pr im ary" }, { "name": "Secondary", "args": { "color": "blue"} }, { "name": " S m all", "args": { "color": "green"} } ] }

Slide 11

Slide 11 text

ؤுͬͯ΍ͬͯΈͨΒ͜͏ͳͬͨɻ im port { Context } from 'hono'; im port { Sample } from './Sample'; export const forPath = '/sample' export const forStory = (ctx: Context) => { const color = ctx.req.query('color') ?? 'red' return ctx.h t ml () } TUPSZCPPL޲͚ͷQBUIΛઃఆ TUPSZCPPL޲͚ͷ&WFOUIBOEMFSΛ༻ҙ sample.storycon fi g.tsx

Slide 12

Slide 12 text

ؤுͬͯ΍ͬͯΈͨΒ͜͏ͳͬͨɻ const glob = new Glob('**/*.storyconfig.tsx'); for await (const file of glob.scan(path.join(process.cwd(), 'src/components'))) { const {forPath, forStory} = await im port(path.join(process.cwd(), 'src/components', file)); app.get(path.join('/stories', forPath), forStory); } TUPSZCPPL޲͚DPO fi HϑΝΠϧΛݺͼग़ͯ͠ SPVUJOHΛ௥Ճ

Slide 13

Slide 13 text

ؤுͬͯ΍ͬͯΈͨΒ͜͏ͳͬͨɻ https://github.com/jiko21/hono-storybook-sample

Slide 14

Slide 14 text

΍ͬͺReactͰॻ͖͍ͨʂ

Slide 15

Slide 15 text

honoͰreact࢖͍͍ͨ! • honoͰreactΛΨϯΨϯॻ͖͍ͨ! • ׳Ε͍ͯΔ • ॾʑReact޲͚ͷπʔϧ͕࢖͑Δ • ͳ͓͔ͭ(͔ͤͬ͘ͳͷͰ)SSR͍ͨ͠

Slide 16

Slide 16 text

΍Δ΂͖͜ͱ • page routingΛؤுΔ • ద੾ʹσΟϨΫτϦ഑ஔΛͨ͠Βrouting͞ΕΔ • ReactͷDOMΛҰ౓αʔόʔଆͰhydrateͯ͠htmlͱͯ͠ฦ͢ • clientଆͷscriptΛੜ੒ͯ͠ฦ͢

Slide 17

Slide 17 text

ཁ݅ 1. src/pagesʹϑΝΠϧ͕͋Ε͹ͦΕΛϖʔδͱͯ͠ղऍ 2. routing͸ҎԼͷΑ͏ͳײ͡ • /index.tsx 㱺 / • /hoge/[id]/fuga/[fugaId].tsx 㱺 /hoge/[id]/fuga/[fugaId] 3. getServerSideProps͕αʔόଆͰ࣮ߦ͞ΕΔ 4. page component͕default export͞ΕΔ

Slide 18

Slide 18 text

page routing

Slide 19

Slide 19 text

͜͏͢Δʂ(ಛʹେࣄͳ࿩͸ͳ͍ͷͰskip) export function _genPathInfo(source: string[], basePath: string) { const outputPath = [] const im portPath = [] const slugs = [] let depth = 0; const basePaths = source.slice( 0 , source.length - 1) for (const basePath of basePaths) { depth += 1; const slugPattern = basePath. m atch(SLUG_PARA M_ PAT TE RN) if (slugPattern) { slugs.push(slugPattern.at(1)); outputPath.push(`: $ {slugPattern.at(1)}`); } else {depth outputPath.push(basePath); } im portPath.push(basePath) } const action = source.at(-1); if (action !== 'index.tsx') { depth += 1; const slugPattern = action?. m atch(SLUG_FILE_ARA M _ PAT TE RN) if (slugPattern) { outputPath.push(`: $ {slugPattern.at(1)}`); slugs.push(slugPattern.at(1)); } else { outputPath.push(action?.replace('.tsx', '')); } im portPath.push(action?.replace('.tsx', '') ?? ''); } return { path: outputPath.join('/'), im portPath: path.join(basePath, i m portPath.join('/')), filePath: path.join(basePath, source.join('/')), depth, slugs, } } https://github.com/jiko21/nextjs-like-framework

Slide 20

Slide 20 text

HTMLΛฦ͢

Slide 21

Slide 21 text

ඞཁͳ͜ͱ page.get(pathInfo.path, async (c) => { const App = await i m port(` $ {pathInfo. im portPath} `) as React.FC; const src = renderToString(
); return c.h t m l (src) }) 1. pathʹget request͕͘Δ 2. ֘౰ϖʔδͷcomponentΛ hmlt textʹrendering͢Δ (renderToString) 3. htmlͱͯ͠ฦ͢

Slide 22

Slide 22 text

දࣔͰ͖ͨ i m port { useState } from "react"; type PropsType = ReturnType['props']; const App = (props: PropsType) => { console.log(props); const [value, setValue] = useState( 0 ); const onClick= () => { console.log('test') } return (

{props. m sg}

{value} setValue(value + 1)}>button1 button2
) } export function getServersideProps() { return { props: { msg: 'hello' } } } export default App;

Slide 23

Slide 23 text

ΜɺgetSeversideProps͕൓ө͞Εͯͳ͍…

Slide 24

Slide 24 text

࣮ߦ࣌ʹpropsΛ஫ೖ͢Δ page.get(pathInfo.path, async (c) => { const {default: App, getServersideProps} = await im port(` $ {pathInfo. i m portPath}`) as { default: React.FC['props']>, getServersideProps: GetServersideProps }; const result = getServersideProps ? await getServersideProps(c) : { props: {} }; const src = renderToString(
); return c.h t m l (src) }) 1. pathʹget request͕͘Δ 2. getServersidePropsΛ࣮ߦ͢Δ 3. ֘౰ϖʔδͷcomponentΛ hmtl textʹrendering͢Δ (renderToString) 4. htmlͱͯ͠ฦ͢

Slide 25

Slide 25 text

දࣔͰ͖ͨ i m port { useState } from "react"; type PropsType = ReturnType['props']; const App = (props: PropsType) => { console.log(props); const [value, setValue] = useState( 0 ); const onClick= () => { console.log('test') } return (

{props. m sg}

{value} setValue(value + 1)}>button1 button2
) } export function getServersideProps() { return { props: { msg: 'hello' } } } export default App;

Slide 26

Slide 26 text

͚Ͳಈ͔ͳ͍…

Slide 27

Slide 27 text

ScriptΛຒΊࠐΉ

Slide 28

Slide 28 text

clientଆͷscriptΛຒΊࠐΉ • ͜ͷ··ͩͱϘλϯΛԡͯ͠΋Կ΋ൃੜ͠ͳ͍ɻ • renderToXXXܥͷؔ਺͸React ComponentࣗମΛhydrate͢Δ΋ͷ ͷɺclientଆͰ࣮ߦ͞ΕΔscriptࣗମ͸ੜ੒͠ͳ͍͠ɺbundle͠ͳ ͍ɻ(ͦΒͦ͏Α) • ࠓճ͸Bun.buildΛ࢖͏(bunΛ࢖ͬͯΔͷͰ)

Slide 29

Slide 29 text

Bun.build • Bun͕ඪ४Ͱ༻ҙ͍ͯ͠Δbundler • ҰԠesbuildͷ1.76ഒɺwebpack5ͷ224ഒૣ͍Β͍͠ • ͨͩɺੜ੒ͨ͠ fi leͷmodule formatͱ͔͸”esm”ͷΈ (cjsͱ͔બ΂Δ͕·ͩରԠͯ͠ͳ͍) https://bun.sh/docs/bundler

Slide 30

Slide 30 text

ScriptΛੜ੒͢Δ page.get(`/build/ $ {pathInfo.path}asset.js`, async (c) => { const data = ` i m port App from ' $ {pathInfo.filePath}'; i m port { hydrateRoot } from ‘react-dom/client'; const root = document.getElementById('root'); hydrateRoot(root, );`; await Bun. w rite(`./out/ $ {pathInfo.path}asset.tsx`, data); const result = await Bun.build({ entrypoints: [`./out/ $ {pathInfo.path}asset.tsx`], }); return c.body((await result.outputs[ 0 ].text()), 20 0 , { 'Content-Type': 'text/javascript', }); }) 1. ྗٕͰentrypointͱͳΔϑΝ ΠϧΛੜ੒ 2. Bun.buildͰbuild͢Δ 3. ฦ͢

Slide 31

Slide 31 text

࣮ࡍʹϖʔδʹຒΊࠐΉ page.get(pathInfo.path, async (c) => { const {default: App, getServersideProps} = await i m port(` $ {pathInfo. i m portPath}`) as { default: React.FC['props']>, getServersideProps: GetServersideProps }; const result = getServersideProps ? await getServersideProps(c) : { props: {} }; const src = renderToString(
); return c.h t m l (src) }) TDSJQUλάͰઌఔͷQBUIΛࢀর͢Δ

Slide 32

Slide 32 text

ಈ͔ͳ͘ͳͬͨ…

Slide 33

Slide 33 text

SSRͰ͖͍ͯΔ͔ʁ

Slide 34

Slide 34 text

clientଆͷscriptΛຒΊࠐΉ • Hydration failed because the initial UI does not match what was rendered on the server. ͱ͋ΔΑ͏ʹɺclientଆͰserverଆͷhtmlͱ Ұக͠ͳ͍Τϥʔ͕ൃੜ͍ͯ͠Δ… • ͦ͏͍͑͹getServersidePropsͰ౉ͨ͠props͕දࣔ͞Ε͍ͯͳ͍…

Slide 35

Slide 35 text

propsΛຒΊࠐΉ page.get(pathInfo.path, async (c) => { const {default: App, getServersideProps} = await i m port(` $ {pathInfo. im portPath}`) as { default: React.FC['props']>, getServersideProps: GetServersideProps }; const result = getServersideProps ? await getServersideProps(c) : { props: {} }; const src = renderToString(
); return c.h t m l (src) }) OFYU΍SFNJYͷΑ͏ʹIUNM಺ʹKTPOܗࣜͰ σʔλΛຒΊࠐΉ

Slide 36

Slide 36 text

propsΛࢀর͢Δ page.get(`/build/ $ {pathInfo.path}asset.js`, async (c) => { const data = ` i m port App from ' $ {pathInfo.filePath}'; i m port { hydrateRoot } from 'react-dom/client'; const root = document.getElementById('root'); const propsStr = document.getElementById("getServersidePropsData"); const props = JSON.parse(propsStr.textContent); hydrateRoot(root, );`; await Bun. w rite(`./out/ $ {pathInfo.path}asset.tsx`, data); const result = await Bun.build({ entrypoints: [`./out/ $ {pathInfo.path}asset.tsx`], }); return c.body((await result.outputs[ 0 ].text()), 20 0 , { 'Content-Type': 'text/javascript', }); }), DMJFOUଆͰ͸IUNM্ͷKTPOσʔλΛऔΓग़ͯ͠ DPNQPOFOUʹΘͨ͢

Slide 37

Slide 37 text

ಈ͍ͨ

Slide 38

Slide 38 text

͜Μͳ͜ͱΛ͠ͳͯ͘΋…

Slide 39

Slide 39 text

honox΍hono/react-renderer͕͋ΔΑʂ • honox΍hono/react-rendererΛ༻͍Δ͜ͱͰ ΋ͬͱ؆୯ʹReact+honoߏ੒͕࣮ݱՄೳ • ྗٕͷ࣮૷͢ΔΑΓͬͪ͜࢖͍·͠ΐ͏ʂ https://github.com/honojs/honox

Slide 40

Slide 40 text

·ͱΊ

Slide 41

Slide 41 text

·ͱΊ • honoࣗମ͔ͳΓബ͘࡞ΒΕ͍ͯΔͷͰࣗ༝౓ߴͯ͘ΧελϜੑ͕ ߴ͍ • Bun.buildΈ͍ͨͳ΋ͷ΋ੜ͑ͯΔͷͰେମͷ΋ͷ͸Hono+BunͰ ࡞Εͦ͏ • ͱ͸͍͑͜Μͳʹखͷ͜Μͩ͜ͱΛ͠ͳͯ͘΋React on Hono͸࣮ݱ Ͱ͖·͢mm

Slide 42

Slide 42 text

ࢀߟ • https://github.com/honojs/honox • https://hono.dev/ • https://bun.sh/docs/bundler