Streaming template literals
Template literals are pretty cool right?
const areThey = 'Yes';
console.log(`${areThey}, they are`);
// Logs: Yes, they are
You can also assign a function to process the template, known as "tagged" templates:
function strongValues(strings, ...values) {
return strings.reduce((totalStr, str, i) => {
totalStr += str;
if (i in values) totalStr += `<strong>${values[i]}</strong>`;
return totalStr;
}, '');
}
const areThey = 'Yes';
console.log(strongValues`${areThey}, they are`);
// Logs: <strong>Yes</strong>, they are
The syntax for tagging a template seems really un-JavaScripty to me, and I haven't been able to figure out why strings
is an array yet each of the values
is a separate argument, but meh, it's a cool feature. You don't even have to return a string:
function daftTag() {
return [1, 2, 3];
}
console.log(daftTag`WHY ARE YOU IGNORING ME?`);
// Logs: [1, 2, 3]
"But how can we involve streams in this?" I hear me cry
Generating streams in a service worker allows you to serve a mixture of cached & network content in a single response. This is amazing for performance, but manually combining the streams feels a bit, well, manual.
Say we wanted to output:
<h1>Title</h1>
…content…
…where the title and content come from different sources. That would look like this:
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
// Promise for the title
const titlePromise = fetch('/get-metadata')
.then((r) => r.json())
.then((data) => data.title);
// Promise for the content stream
const contentPromise = fetch('/get-content').then((r) => r.body);
// Tie them all together
pushString('<h1>');
titlePromise
.then(pushString)
.then(() => pushString('</h1>\n'))
.then(() => contentPromise)
.then(pushStream)
.then(() => controller.close());
// Helper functions
function pushString(str) {
controller.enqueue(encoder.encode(str));
}
function pushStream(stream) {
// Get a lock on the stream
var reader = stream.getReader();
return reader.read().then(function process(result) {
if (result.done) return;
// Push the value to the combined stream
controller.enqueue(result.value);
// Read more & process
return reader.read().then(process);
});
}
},
});
Ew. Imagine we could just do this:
// Promise for the title
const title = fetch('/get-metadata')
.then((r) => r.json())
.then((data) => data.title);
// Promise for the content stream
const content = fetch('/get-content').then((r) => r.body);
const stream = templateStream`
<h1>${title}</h1>
${content}
`;
Well, you can!
View demo Note: You'll need Chrome Canary with chrome://flags/#enable-experimental-web-platform-features
enabled.
This means the title can be displayed as soon as it arrives, without waiting for the main content. The main content stream is piped, meaning it can render as it downloads. The implementation of templateStream
is pretty light too, making it a cheap and easy way of building streams from multiple sources.
If you're wanting something with a few more features (eg conditionals & iteration), DustJS is the only template engine I'm aware of that supports this, but I'm not a fan of the syntax. Hopefully we'll see other template engines such as handlebars adopt a similar model, although it's not something they're interested in right now.