bluwy / create-vite-extra

Extra Vite templates
Other
391 stars 62 forks source link

Vite + Vue + Streaming SSR #45

Closed phonzammi closed 9 months ago

phonzammi commented 9 months ago

Hi @bluwy, I want to ask for a favor. Could you create a template of Vite + Vue + Streaming SSR ?

FYI, I have created one, but I'm not sure if it is correct. Maybe if you have some spare time, I hope you could take a look and review it too. You can check it in my repo (vite-vue-streaming-ssr).

Hope you have a nice day and night

bluwy commented 9 months ago

Your implementation looks great 👍 I think it could be simplified a bit to use res.write() instead of generators, but I think it's also a matter of preference then.

I think it would be nice to add a streaming implementation. Perhaps as an option in the CLI with this flow: "Vue" > "Streaming or no-streaming" > "JS or TS". Which means we probably need to create a new template-ssr-vue-streaming and template-ssr-vue-streaming-ts templates which are largely duplicates of template-ssr-vue and template-ssr-vue-ts. Feel free to contribute them!

phonzammi commented 9 months ago

Your implementation looks great 👍

Thanks

phonzammi commented 9 months ago

I think it could be simplified a bit to use res.write() instead of generators

Yeah, maybe you're right, about using res.write(), I think you mean to write head & end of the html, right?

I've tried this way, but this way results with hydration mismatch :

const stream = renderToNodeStream(app, ctx)

stream.once('readable', () => {
  res.status(200).set({ 'Content-Type': 'text/html' })
  res.write(head)
})
stream.on('data', () => {
  stream.pipe(res, { end: false })
})
stream.on('end', () => {
  res.write(tail)
  res.end()
})
stream.on('error', (error) => {
  res.setHeader('Content-Type', 'text/json')
  res.end(JSON.stringify({ error }))
})
phonzammi commented 9 months ago

I've also tried this way & doesn't result with hydration mismatch : 💯

 const [head, tail] = template.split('<!--app-->')

const encoder = new TextEncoder()
// import { TransformStream } from 'stream/web'
const webStream = new TransformStream({ 
  start(controller) {
    controller.enqueue(encoder.encode(head))
  },
  flush(controller) {
    controller.enqueue(encoder.encode(tail))
  }
})

// import { Readable, Writable } from 'stream'
const readable = Readable.fromWeb(webStream.readable)
const writable = Writable.fromWeb(webStream.writable)

// import { pipeToNodeWritable } from 'vue/server-renderer'
pipeToNodeWritable(app, ctx, writable)

readable.pipe(res)

And I also want to try it (properly) with Node Transform

Maybe if you have any suggestions, I like to know them 🙏

phonzammi commented 9 months ago

I think it would be nice to add a streaming implementation. Perhaps as an option in the CLI with this flow: "Vue" > "Streaming or no-streaming" > "JS or TS".

Yes, that would very nice 👍

Feel free to contribute them!

Sure, I'll try

bluwy commented 9 months ago

I think your current implementation is perfectly fine too if you used that as the template, as long as the implementation is short and concise. Don't need to sweat on the ideal way to do it for now 😅

Initially though, I was thinking along the lines of (not tested yet):

const [head, tail] = template.split('<!--app-->')

const { stream } = render()

res.write(head)
for await (const chunk of stream) {
  res.write(chunk)
}
res.write(tail)
res.end()

If there's errors from the stream, I think it should propagate to the catch handler. Even with my implementation, we might also need to handle the case where a request could be closed before we finish streaming, which means we can stop calling res.write() thereafter to save on memory. Usually that is detected through the close event.

phonzammi commented 9 months ago

I think your current implementation is perfectly fine too if you used that as the template, as long as the implementation is short and concise. Don't need to sweat on the ideal way to do it for now 😅

Thanks. That's right, cause it's just an example template. Let me polish it a bit then

Initially though, I was thinking along the lines of (not tested yet):

So I've just try that and surprisingly it works & no hydration mismatch. Awesome 💯

If there's errors from the stream, I think it should propagate to the catch handler. Even with my implementation, we might also need to handle the case where a request could be closed before we finish streaming, which means we can stop calling res.write() thereafter to save on memory. Usually that is detected through the close event.

Nice, thanks for elaborating more