Skip to content
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

RFC: HTML streaming in Podium #507

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,44 @@ app.get(layout.pathname(), (req, res) => {
});
```

### res.podiumStream(...templateArguments)

Method on the `http.ServerResponse` object for streaming HTML to the browser. This function returns a `ResponseStream` object that can then be used to push out HTML to the browser in chunks using its .send() function. Once streaming is finished, the .done() function must be called to close the stream.

```js
const stream = res.podiumStream();
stream.send(`<div>HTML chunk 1</div>`);
stream.send(`<div>HTML chunk 2</div>`);
stream.done();
```

The Podium document template will still be used. When you call res.podiumStream(), the document head will be sent to the browser immediately. Once the .done() function is called, the closing part of the template will be sent before the stream is closed out.

Note that any arguments passed to .podiumStream(...args) will be passed on to the layout's document template.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be an options object so we can add to it without causing a breaking change.

res.podiumStream({
  view: {
    templateArgs: [...args],
  },
});

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there are any breaking changes to worry about here. Did you spot something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not now, I'm thinking if we want to add stuff in the future.


**Working with assets**

When working with assets, its important to wait for podlets to have sent their assets to the layout via 103 early hints. Use the incoming.hints `complete` event for this. Wait for assets to be ready, set assets on the incoming object and then call res.podiumStream.

```js
const incoming = res.locals.podium;
const headerFetch = p1Client.fetch(incoming);
const footerFetch = p2Client.fetch(incoming);

incoming.hints.on('complete', async ({ js, css }) => {
incoming.js = js;
incoming.css = css;

const stream = res.podiumStream();
const [header, footer] = await Promise.all([
headerFetch,
footerFetch,
]);
stream.send(`<header>${header}</header>...<footer>${footer}</footer>`);
stream.done();
});
```

### .client

A property that exposes an instance of the [@podium/client] for fetching content
Expand Down
61 changes: 61 additions & 0 deletions example/streaming/podlets/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Podlet from '@podium/podlet';
import express from 'express';

const podlet = new Podlet({
name: 'content-podlet',
version: Date.now().toString(),
pathname: '/',
useShadowDOM: true,
});

podlet.css({ value: 'http://localhost:6103/css', strategy: 'shadow-dom' });

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(`
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.content {
width: 100%;
display: flex;
flex-direction: column;
gap: 1em;
font-family: Verdana, serif;
font-weight: 400;
font-style: normal;
}
h1 {
color: #136C72;
}
`);
});

app.get('/', async (req, res) => {
res.set('Content-Type', 'text/html');
res.sendHeaders();

await new Promise((res) => setTimeout(res, 2200));

res.podiumSend(`
<section class="content">
<h1>Podlets fetched and composed, on demand, just for you</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.</p>
</section>
`);
Comment on lines +45 to +56
Copy link
Contributor

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?

});

app.listen(6103, () => {
console.log(`content podlet server running at http://localhost:6103`);
});
83 changes: 83 additions & 0 deletions example/streaming/podlets/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Podlet from '@podium/podlet';
import express from 'express';

const podlet = new Podlet({
name: 'footer-podlet',
version: Date.now().toString(),
pathname: '/',
useShadowDOM: true,
});

podlet.css({ value: 'http://localhost:6104/css', strategy: 'shadow-dom' });

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(`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
footer {
width: 100%;
background-color: #23424A;
color: white;
padding: 1em 0 6em 0;
font-family: Verdana, serif;
font-weight: 400;
font-style: normal;
}
.container {
width: 75%;
max-width: 1000px;
margin: 0 auto;
}
ul {
list-style: none;
display: flex;
justify-content: space-evenly;
align-items: center;
}
a {
text-transform: upper-case;
color: white;
text-decoration: none;
}
a:hover, a:active {
text-decoration: underline;
}
`);
});

app.get('/', async (req, res) => {
res.set('Content-Type', 'text/html');
res.sendHeaders();

await new Promise((res) => setTimeout(res, 100));

res.podiumSend(`
<footer>
<div class="container">
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
<li><a href="#">Sign in</a></li>
<li><a href="#">Sign up</a></li>
</ul>
</div>
</footer>
`);
});

app.listen(6104, () => {
console.log(`footer podlet server running at http://localhost:6104`);
});
97 changes: 97 additions & 0 deletions example/streaming/podlets/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Podlet from '@podium/podlet';
import express from 'express';

const podlet = new Podlet({
name: 'header-podlet',
version: Date.now().toString(),
pathname: '/',
useShadowDOM: true,
});

podlet.css({ value: 'http://localhost:6101/css', strategy: 'shadow-dom' });

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(`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
header {
width: 100%;
padding: 6em 0;
background-color: #23424A;
font-size: 1.5rem;
font-family: "Verdana", sans-serif;
font-weight: 400;
font-style: normal;
color: white;
}
h1 {
font-size: 3rem;
color: white;
font-family: "Verdana", sans-serif;
font-weight: 900;
font-style: normal;
}
.container {
width: 75%;
max-width: 1000px;
margin: 0 auto;
}
.inner-container {
display: flex;
flex-direction: column;
gap: 1em;
}
.button {
color: black;
background-color: #38CFD9;
padding: 0.5em 1.25em;
border-radius: 1em;
text-decoration: none;
width: fit-content;
}
.button:hover, .button:active {
text-decoration: underline;
width: fit-content;
}
@media (min-width: 800px) {
.inner-container {
width: 70%;
}
}
`);
});

app.get('/', async (req, res) => {
res.set('Content-Type', 'text/html');
res.sendHeaders();

await new Promise((res) => setTimeout(res, 100));

res.podiumSend(`
<header>
<div class="container">
<div class="inner-container">
<h1>Podium layouts can be composed using streaming</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.</p>
<a href="#" class="button">I want to learn</a>
</div>
</div>
</header>
`);
});

app.listen(6101, () => {
console.log(`header podlet server running at http://localhost:6101`);
});
76 changes: 76 additions & 0 deletions example/streaming/podlets/menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Podlet from '@podium/podlet';
import express from 'express';

const podlet = new Podlet({
name: 'menu-podlet',
version: Date.now().toString(),
pathname: '/',
useShadowDOM: true,
});

podlet.css({ value: 'http://localhost:6102/css', strategy: 'shadow-dom' });

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(`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.menu {
width: 100%;
background-color: #136C72;
padding: 1em 0;
font-family: Verdana, serif;
font-weight: 400;
font-style: normal;
}
.menu ul {
list-style: none;
display: flex;
justify-content: space-evenly;
align-items: center;
}
a {
text-transform: upper-case;
color: white;
text-decoration: none;
}
a:hover, a:active {
text-decoration: underline;
}
`);
});

app.get('/', async (req, res) => {
res.set('Content-Type', 'text/html');
res.sendHeaders();

// imagine this is your slow database call
await new Promise((res) => setTimeout(res, 100));

res.podiumSend(`
<nav class="menu">
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
<li><a href="#">Sign in</a></li>
<li><a href="#">Sign up</a></li>
</ul>
</nav>
`);
});

app.listen(6102, () => {
console.log(`menu podlet server running at http://localhost:6102`);
});
Loading