-
Notifications
You must be signed in to change notification settings - Fork 3
RFC: HTML streaming in Podium #507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
80b02c6
2c2b292
fa708b2
5b91cb7
4618d55
5a575a8
858a50f
ada6812
b75d0d4
ef3bcf9
651eee0
7a9a164
d954732
fa559f9
9d69703
4a7b87f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import Podlet from '@podium/podlet'; | ||
| import express from 'express'; | ||
|
|
||
| const podlet = new Podlet({ | ||
| name: 'content', | ||
| version: Date.now().toString(), | ||
| pathname: '/', | ||
| }); | ||
|
|
||
| podlet.css({ value: 'http://localhost:6103/css' }); | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.use(podlet.middleware()); | ||
|
|
||
| app.get('/manifest.json', (req, res) => { | ||
| res.send(podlet); | ||
| }); | ||
|
|
||
| app.get('/css', (req, res) => { | ||
| res.set('Content-Type', 'text/css'); | ||
| res.send(` | ||
| .content { | ||
| border: 1px solid black; | ||
| border-radius: 5px; | ||
| width: 100%; | ||
| padding: 20px; | ||
| margin: 0; | ||
| margin-bottom: 20px; | ||
| box-sizing: border-box; | ||
| } | ||
| `); | ||
| }); | ||
|
|
||
| app.get('/', (req, res) => { | ||
| res.send(` | ||
| <section class="content"> | ||
| main content goes here | ||
| </section> | ||
| `); | ||
| }); | ||
|
|
||
| app.listen(6103, () => { | ||
| console.log(`content podlet server running at http://localhost:6103`); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import Podlet from '@podium/podlet'; | ||
| import express from 'express'; | ||
|
|
||
| const podlet = new Podlet({ | ||
| name: 'footer', | ||
| version: Date.now().toString(), | ||
| pathname: '/', | ||
| }); | ||
|
|
||
| podlet.css({ value: 'http://localhost:6104/css' }); | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.use(podlet.middleware()); | ||
|
|
||
| app.get('/manifest.json', (req, res) => { | ||
| res.send(podlet); | ||
| }); | ||
|
|
||
| app.get('/css', (req, res) => { | ||
| res.set('Content-Type', 'text/css'); | ||
| res.send(` | ||
| footer { | ||
| border: 1px solid black; | ||
| border-radius: 5px; | ||
| width: 100%; | ||
| padding: 20px; | ||
| margin: 0; | ||
| margin-bottom: 20px; | ||
| box-sizing: border-box; | ||
| } | ||
| `); | ||
| }); | ||
|
|
||
| app.get('/', (req, res) => { | ||
| res.send(` | ||
| <footer> | ||
| footer content | ||
| </footer> | ||
| `); | ||
| }); | ||
|
|
||
| app.listen(6104, () => { | ||
| console.log(`footer podlet server running at http://localhost:6104`); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import Podlet from '@podium/podlet'; | ||
| import express from 'express'; | ||
|
|
||
| const podlet = new Podlet({ | ||
| name: 'header', | ||
| version: Date.now().toString(), | ||
| pathname: '/', | ||
| }); | ||
|
|
||
| podlet.css({ value: 'http://localhost:6101/css' }); | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.use(podlet.middleware()); | ||
|
|
||
| app.get('/manifest.json', (req, res) => { | ||
| res.send(podlet); | ||
| }); | ||
|
|
||
| app.get('/css', (req, res) => { | ||
| res.set('Content-Type', 'text/css'); | ||
| res.send(` | ||
| header { | ||
| border: 1px solid black; | ||
| border-radius: 5px; | ||
| width: 100%; | ||
| padding: 20px; | ||
| margin: 0; | ||
| margin-bottom: 20px; | ||
| box-sizing: border-box; | ||
| } | ||
| header h1 { | ||
| text-align: center; | ||
| margin: 0; | ||
| padding: 0; | ||
| } | ||
| `); | ||
| }); | ||
|
|
||
| app.get('/', (req, res) => { | ||
| res.send(` | ||
| <header> | ||
| <h1>Header</h1> | ||
| </header> | ||
| `); | ||
| }); | ||
|
|
||
| app.listen(6101, () => { | ||
| console.log(`header podlet server running at http://localhost:6101`); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import Podlet from '@podium/podlet'; | ||
| import express from 'express'; | ||
|
|
||
| const podlet = new Podlet({ | ||
| name: 'menu', | ||
| version: Date.now().toString(), | ||
| pathname: '/', | ||
| }); | ||
|
|
||
| podlet.css({ value: 'http://localhost:6102/css' }); | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.use(podlet.middleware()); | ||
|
|
||
| app.get('/manifest.json', (req, res) => { | ||
| res.send(podlet); | ||
| }); | ||
|
|
||
| app.get('/css', (req, res) => { | ||
| res.set('Content-Type', 'text/css'); | ||
| res.send(` | ||
| menu { | ||
| border: 1px solid black; | ||
| border-radius: 5px; | ||
| width: 100%; | ||
| padding: 10px; | ||
| margin: 0; | ||
| margin-bottom: 20px; | ||
| box-sizing: border-box; | ||
| } | ||
| menu ul { | ||
| list-style: none; | ||
| padding: 0; | ||
| margin: 0; | ||
| display: flex; | ||
| justify-content: space-evenly; | ||
| align-items: center; | ||
| } | ||
| menu ul li { | ||
| margin: 0; | ||
| padding: 0; | ||
| } | ||
| `); | ||
| }); | ||
|
|
||
| app.get('/', (req, res) => { | ||
| res.send(` | ||
| <menu> | ||
| <ul> | ||
| <li><a href="/">Home</a></li> | ||
| <li><a href="/about">About</a></li> | ||
| <li><a href="/things">Things</a></li> | ||
| <li><a href="/stuff">Stuff</a></li> | ||
| </ul> | ||
| </menu> | ||
| `); | ||
| }); | ||
|
|
||
| app.listen(6102, () => { | ||
| console.log(`menu podlet server running at http://localhost:6102`); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| import express from 'express'; | ||
| import Layout from '../../lib/layout.js'; | ||
| import { template } from './views/template.js'; | ||
|
|
||
| const layout = new Layout({ | ||
| pathname: '/foo', | ||
| logger: console, | ||
| name: 'demo', | ||
| }); | ||
|
|
||
| // use our custom streaming template | ||
| layout.view(template); | ||
|
|
||
| const content = layout.client.register({ | ||
| name: 'content', | ||
| uri: 'http://localhost:6103/manifest.json', | ||
| }); | ||
|
|
||
| const header = layout.client.register({ | ||
| name: 'header', | ||
| uri: 'http://localhost:6101/manifest.json', | ||
| }); | ||
|
|
||
| const menu = layout.client.register({ | ||
| name: 'menu', | ||
| uri: 'http://localhost:6102/manifest.json', | ||
| }); | ||
|
|
||
| const footer = layout.client.register({ | ||
| name: 'footer', | ||
| uri: 'http://localhost:6104/manifest.json', | ||
| }); | ||
|
|
||
| layout.css({ value: '/css' }); | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.use(layout.pathname(), layout.middleware()); | ||
|
|
||
| app.get('/foo/css', (req, res) => { | ||
| res.set('Content-Type', 'text/css'); | ||
| res.send(` | ||
| @keyframes pulse { | ||
| 0% { | ||
| background-color: #e0e0e0; | ||
| } | ||
| 50% { | ||
| background-color: #f0f0f0; | ||
| } | ||
| 100% { | ||
| background-color: #e0e0e0; | ||
| } | ||
| } | ||
| .skeleton { | ||
| width: 100%; | ||
| background-color: #e0e0e0; | ||
| border-radius: 5px; | ||
| animation: pulse 1.5s infinite ease-in-out; | ||
| margin: 0; | ||
| margin-bottom: 20px; | ||
| box-sizing: border-box; | ||
| } | ||
| .skeleton.header { | ||
| height:79px; | ||
| } | ||
| .skeleton.menu { | ||
| height:40px; | ||
| } | ||
| .skeleton.content { | ||
| height:60px; | ||
| } | ||
| .skeleton.footer { | ||
| height:60px; | ||
| } | ||
| `); | ||
| }); | ||
|
|
||
| app.get(layout.pathname(), async (req, res) => { | ||
| const incoming = res.locals.podium; | ||
|
|
||
| incoming.view = { | ||
| title: 'Example streaming application', | ||
| }; | ||
|
|
||
| const headerFetch = header.fetch(incoming); | ||
| const menuFetch = menu.fetch(incoming); | ||
| const contentFetch = content.fetch(incoming); | ||
| const footerFetch = footer.fetch(incoming); | ||
|
|
||
| incoming.hints.on('complete', async ({ js, css }) => { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. once we have hints in from the podlets, we can set up streaming |
||
| // set the assets on httpincoming so that they are available in the document template | ||
| incoming.js = [...incoming.js, ...js]; | ||
| incoming.css = [...incoming.css, ...css]; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is pretty awkward but works for now at least |
||
|
|
||
| // set up the stream which will send the document template head | ||
| const stream = res.podiumStream(); | ||
|
|
||
| // stream in the document body with slot placeholders for podlets | ||
| stream.send(` | ||
| <template shadowrootmode="open"> | ||
| <link href="/foo/css" type="text/css" rel="stylesheet"> | ||
| <div class="container"> | ||
| <div> | ||
| <div> | ||
| <slot name="header"><div class="skeleton header"></div></slot> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <div> | ||
| <slot name="menu"><div class="skeleton menu"></div></slot> | ||
| </div> | ||
| <div> | ||
| <slot name="content"><div class="skeleton content"></div></slot> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <div> | ||
| <slot name="footer"><div class="skeleton footer"></div></slot> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </template> | ||
| `); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case we are sending a DSD template with slots and placeholder skeleton html so that we can replace these by streaming in podlet content later. |
||
|
|
||
| // fake 1 second delay | ||
| await new Promise((res) => setTimeout(res, 1000)); | ||
|
|
||
| // stream in podlet content when available... | ||
| headerFetch.then((content) => { | ||
| stream.send(`<div slot="header">${content}</div>`); | ||
| }); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we decorate the podlet content in divs with slot attributes to indicate where they should be placed into the document. |
||
|
|
||
| await new Promise((res) => setTimeout(res, 1000)); | ||
|
|
||
| menuFetch.then((content) => { | ||
| stream.send(`<div slot="menu">${content}</div>`); | ||
| }); | ||
|
|
||
| await new Promise((res) => setTimeout(res, 1000)); | ||
|
|
||
| contentFetch.then((content) => { | ||
| stream.send(`<div slot="content">${content}</div>`); | ||
| }); | ||
|
|
||
| await new Promise((res) => setTimeout(res, 1000)); | ||
|
|
||
| footerFetch.then((content) => { | ||
| stream.send(`<div slot="footer">${content}</div>`); | ||
| }); | ||
|
|
||
| // close out the dom and the stream | ||
| await Promise.all([headerFetch, menuFetch, contentFetch, footerFetch]); | ||
| stream.done(); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indicate that we are finished streaming content so that the html template can finish things off |
||
| }); | ||
| }); | ||
|
|
||
| app.use(`${layout.pathname()}/assets`, express.static('assets')); | ||
|
|
||
| // eslint-disable-next-line no-unused-vars | ||
| app.use((error, req, res, next) => { | ||
| console.error(error); | ||
| res.status(500).send( | ||
| '<html><body><h1>Internal server error</h1></body></html>', | ||
| ); | ||
| }); | ||
|
|
||
| app.listen(6123, () => { | ||
| console.log(`layout server running at http://localhost:6123`); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The layout examples have some inline documentation explaining the different parts. It would be nice to have something similar for at least one of the podlets, or in a separate README.
If it's covered in the podlet-repo, perhaps link to docs there?