$30 off During Our Annual Pro Sale. View Details »

Creating a Next.js-style Framework with Bun and...

jiko21
May 09, 2024
81

Creating a Next.js-style Framework with Bun and Hono

kansaits #6の登壇資料です。
https://kansaits.connpass.com/event/314470/

jiko21

May 09, 2024
Tweet

Transcript

  1. About jiko21… Name: Daiki Kojima (jiko21) Multistack Engineer @ AppBrew.inc

    Love: Guitar, TypeScript, baseball @jiko21 @jiko_21
  2. hono🔥 • ௒ߴ଎(ultrafast)&ܰྔ(lightweight)ͳWeb Application framework • ͲΜͳ؀ڥͰ΋(node, deno, bun, edge

    runtime)Ͱ΋͏͘͝ • TypeFirstͳϑϨʔϜϫʔΫ • ಋೖʹࡍͯ͠TypeScript༻ʹࡉ͘ઃఆ౳͸͍Βͳ͍ • ֤छศརͳmiddleware౳͋Γ • cache • logging • jsx
  3. honoʹ͸jsx renderer͕͋Δ im port { FC } from "hono/jsx"; export

    const Sample: FC<{color: string}> = ({color}) => { return <h1 style={{color}}>Hello Hono!</h1>; }
  4. @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"} } ] }
  5. ؤுͬͯ΍ͬͯΈͨΒ͜͏ͳͬͨɻ 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 (<Sample color={color}/>) } TUPSZCPPL޲͚ͷQBUIΛઃఆ TUPSZCPPL޲͚ͷ&WFOUIBOEMFSΛ༻ҙ sample.storycon fi g.tsx
  6. ؤுͬͯ΍ͬͯΈͨΒ͜͏ͳͬͨɻ 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Λ௥Ճ
  7. ཁ݅ 1. src/pagesʹϑΝΠϧ͕͋Ε͹ͦΕΛϖʔδͱͯ͠ղऍ 2. routing͸ҎԼͷΑ͏ͳײ͡ • /index.tsx 㱺 / •

    /hoge/[id]/fuga/[fugaId].tsx 㱺 /hoge/[id]/fuga/[fugaId] 3. getServerSideProps͕αʔόଆͰ࣮ߦ͞ΕΔ 4. page component͕default export͞ΕΔ
  8. ͜͏͢Δʂ(ಛʹେࣄͳ࿩͸ͳ͍ͷͰ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
  9. ඞཁͳ͜ͱ page.get(pathInfo.path, async (c) => { const App = await

    i m port(` $ {pathInfo. im portPath} `) as React.FC; const src = renderToString( <h t m l > <head></head> <body> <div id="root"> <App /> </div> </body> </h t ml > ); return c.h t m l (src) }) 1. pathʹget request͕͘Δ 2. ֘౰ϖʔδͷcomponentΛ hmlt textʹrendering͢Δ (renderToString) 3. htmlͱͯ͠ฦ͢
  10. දࣔͰ͖ͨ i m port { useState } from "react"; type

    PropsType = ReturnType<typeof getServersideProps>['props']; const App = (props: PropsType) => { console.log(props); const [value, setValue] = useState( 0 ); const onClick= () => { console.log('test') } return ( <div> <p>{props. m sg}</p> {value} <button onClick={() => setValue(value + 1)}>button1</button> <button onClick={onClick}>button2</button> </div> ) } export function getServersideProps() { return { props: { msg: 'hello' } } } export default App;
  11. ࣮ߦ࣌ʹpropsΛ஫ೖ͢Δ page.get(pathInfo.path, async (c) => { const {default: App, getServersideProps}

    = await im port(` $ {pathInfo. i m portPath}`) as { default: React.FC<GetServersidePropsResult<Object>['props']>, getServersideProps: GetServersideProps<Object> }; const result = getServersideProps ? await getServersideProps(c) : { props: {} }; const src = renderToString( <h tm l > <head></head> <body> <div id="root"> <App {...result.props} /> </div> </body> </h t m l > ); return c.h t m l (src) }) 1. pathʹget request͕͘Δ 2. getServersidePropsΛ࣮ߦ͢Δ 3. ֘౰ϖʔδͷcomponentΛ hmtl textʹrendering͢Δ (renderToString) 4. htmlͱͯ͠ฦ͢
  12. දࣔͰ͖ͨ i m port { useState } from "react"; type

    PropsType = ReturnType<typeof getServersideProps>['props']; const App = (props: PropsType) => { console.log(props); const [value, setValue] = useState( 0 ); const onClick= () => { console.log('test') } return ( <div> <p>{props. m sg}</p> {value} <button onClick={() => setValue(value + 1)}>button1</button> <button onClick={onClick}>button2</button> </div> ) } export function getServersideProps() { return { props: { msg: 'hello' } } } export default App;
  13. 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, <App />);`; 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. ฦ͢
  14. ࣮ࡍʹϖʔδʹຒΊࠐΉ page.get(pathInfo.path, async (c) => { const {default: App, getServersideProps}

    = await i m port(` $ {pathInfo. i m portPath}`) as { default: React.FC<GetServersidePropsResult<Object>['props']>, getServersideProps: GetServersideProps<Object> }; const result = getServersideProps ? await getServersideProps(c) : { props: {} }; const src = renderToString( <h tm l > <head></head> <body> <div id="root"> <App {...result.props} /> </div> </body> <script src={`/build/ $ {pathInfo.path}asset.js`} /> </h t m l > ); return c.h t m l (src) }) TDSJQUλάͰઌఔͷQBUIΛࢀর͢Δ
  15. clientଆͷscriptΛຒΊࠐΉ • Hydration failed because the initial UI does not

    match what was rendered on the server. ͱ͋ΔΑ͏ʹɺclientଆͰserverଆͷhtmlͱ Ұக͠ͳ͍Τϥʔ͕ൃੜ͍ͯ͠Δ… • ͦ͏͍͑͹getServersidePropsͰ౉ͨ͠props͕දࣔ͞Ε͍ͯͳ͍…
  16. propsΛຒΊࠐΉ page.get(pathInfo.path, async (c) => { const {default: App, getServersideProps}

    = await i m port(` $ {pathInfo. im portPath}`) as { default: React.FC<GetServersidePropsResult<Object>['props']>, getServersideProps: GetServersideProps<Object> }; const result = getServersideProps ? await getServersideProps(c) : { props: {} }; const src = renderToString( <h tm l > <head></head> <body> <div id="root"> <App {...result.props} /> </div> </body> <script type=“application/json" id=“getServersidePropsData" dangerouslySetInnerHT ML ={{ __h tm l : JSON.stringify(result.props) }} /> <script src={`/build/ $ {pathInfo.path}asset.js`} /> </h t m l > ); return c.h t m l (src) }) OFYU΍SFNJYͷΑ͏ʹIUNM಺ʹKTPOܗࣜͰ σʔλΛຒΊࠐΉ
  17. 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, <App {...props} />);`; 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ʹΘͨ͢