
@popeindustries/lit-html-server
Efficiently render streaming lit-html templates on the server (or in a ServiceWorker!).
Although based on lit-html semantics, lit-html-server is a great general purpose HTML template streaming library. Tagged template literals are a native JavaScript feature, and the HTML rendered is 100% standard markup, with no special syntax or runtime required!
Features
- 6-7x faster than @lit-labs/ssr
- render full HTML pages (not just
body
) - stream responses in Node.js and ServiceWorker with first-class Promise/AsyncIterator support
- render optional hydration metadata with
hydratable
directive - render web components with light or shadow DOM
- default web component rendering with
innerHTML
support - customisable web component rendering with
ElementRenderer
- compatible with
lit-html/directives
Usage
Install with npm/yarn/pnpm
:
$ npm install --save @popeindustries/lit-html-server
...write your lit-html template:
import { html } from 'lit-html';
import { classMap } from 'lit-html/directives/class-map.js';
import { until } from '@popeindustries/lit-html-server/directives/until.js';
function Layout(data) {
return html`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>${data.title}</title>
</head>
<body>
${until(renderBody(data.api))}
</body>
</html>
`;
}
async function renderBody(api) {
const data = await fetchRemoteData(api);
return html`
<h1>${data.title}</h1>
<my-el ?enabled="${data.hasWidget}"></my-el>
<p class="${classMap({ negative: data.invertedText })}">${data.text}</p>
`;
}
...and render (plain HTTP server example, though similar for Express/Fastify/etc):
import http from 'http';
import { renderToNodeStream } from '@popeindustries/lit-html-server';
http.createServer((request, response) => {
const data = { title: 'Home', api: '/api/home' };
response.writeHead(200);
renderToNodeStream(Layout(data)).pipe(response);
});
Hydration
Server rendered HTML may be converted to live lit-html templates with the help of inline metadata. This process of reusing static HTML to seamlessly bootstrap dynamic templates is referred to as hydration.
lit-html-server does not output hydration metadata by default, but instead requires that a sub-tree is designated as hydratable via the rehydratable
directive:
import { hydratable } from '@popeindustries/lit-html-server/directives/hydratable.js';
function Layout(data) {
return html`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>${data.title}</title>
</head>
<body>
<h1>Some ${data.title}</h1>
${hydratable(renderMenu(data.api))}
<p>
Some paragraph of text to show that multiple<br />
hydration sub-trees can exist in the same container.
</p>
${hydratable(renderPage(data.api))}
<footer>Some footer</footer>
</body>
</html>
`;
}
...which generates output similar to:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<h1>Some Title</h1>
<nav negative>
<button>one</button
><button>two</button
><button>three</button
>
</nav>
<p>
Some paragraph of text to show that multiple<br />
hydration sub-trees can exist in the same container.
</p>
<main>This is the main page content.</main>
<footer>Some footer</footer>
</body>
</html>
In order to efficiently reuse templates on the client (renderMenu
and renderPage
in the example above), they should be hydrated and rendered with the help of @popeindustries/lit-html
.
Web Components
The rendering of web component content is largely handled by custom ElementRenderer
instances that adhere to the following interface:
declare class ElementRenderer {
static matchesClass(ceClass: typeof HTMLElement, tagName: string): boolean;
element: HTMLElement;
tagName: string;
constructor(tagName: string);
connectedCallback(): void;
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
setProperty(name: string, value: unknown): void;
setAttribute(name: string, value: string): void;
renderAttributes(): string;
render(): TemplateResult | string | null;
}
Custom ElementRenderer
instances should subclass the default renderer, and be passed along to the render function:
import { ElementRenderer, renderToNodeStream } from '@popeindustries/lit-html-server';
class MyElementRenderer extends ElementRenderer {
static matchesClass(ceClass, tagName) {
return '__myCompIdentifier__' in ceClass;
}
render() {
return this.element.myCompRenderFn();
}
}
const stream = renderToNodeStream(Layout(data), { elementRenderers: [MyElementRenderer] });
Note that the default ElementRenderer
will render innerHTML
strings or content returned by this.element.render()
.
Shadow DOM
If attachShadow
has been called by an element during construction/connection, lit-html-server will render the custom element content in a declarative Shadow DOM:
<my-el>
<template shadowroot="open">text </template>
</my-el>
DOM polyfills
In order to support importing and evaluating custom element code in Node, minimal DOM polyfills are attached to the Node global
when lit-html-server is imported. See dom-shim.js
for details.
Lazy (partial/deferred) hydration
When rendering web components, lit-html-server adds hydrate:defer
attributes to each custom element. This provides a mechanism to control and defer hydration order of nested web components that may be dependant on data passed from a parent. See @popeindustries/lit-html/lazy-hydration-mixin.js
for more on lazy hydration.
Directives
Most of the built-in lit-html/directives/*
already support server rendering, and work as expected in lit-html-server, the exception being those directives that are asynchronous. lit-html-server supports the rendering of Promises and AsyncInterators as first-class primitives, so special versions of async-append.js
, async-replace.js
, and until.js
should be imported from @popeindustries/lit-html-server/directives
.
API (Node.js)
RenderOptions
The following render methods accept an options
object with the following properties:
elementRenderers?: Array<ElementRendererConstructor>
- ElementRenderer subclasses for rendering of custom elements
renderToNodeStream(value: unknown, options?: RenderOptions): Readable
Returns the value
(generally the result of a template tagged by html
) as a Node.js Readable
stream of markup:
import { html, renderToNodeStream } from '@popeindustries/lit-html-server';
const name = 'Bob';
renderToNodeStream(html`<h1>Hello ${name}!</h1>`).pipe(response);
renderToWebStream(value: unknown, options?: RenderOptions): ReadableStream
Returns the value
(generally the result of a template tagged by html
) as a web ReadableStream
stream of markup:
import { html, renderToWebStream } from '@popeindustries/lit-html-server';
self.addEventListener('fetch', (event) => {
const name = 'Bob';
const stream = renderToWebStream(html`<h1>Hello ${name}!</h1>`);
const response = new Response(stream, {
headers: {
'content-type': 'text/html',
},
});
event.respondWith(response);
});
Note: due to the slight differences when running in Node or the browser, a separate version for running in a browser environment is exported as @popeindustries/lit-html-server/lit-html-service-worker.js
. For those dev servers/bundlers that support conditional package.json#exports
, exports are provided to enable importing directly from @popeindustries/lit-html-server
.
renderToString(value: unknown, options?: RenderOptions): Promise<string>
Returns the value
(generally the result of a template tagged by html
) as a Promise which resolves to a string of markup:
import { html, renderToString } from '@popeindustries/lit-html-server';
const name = 'Bob';
const markup = await renderToString(html` <h1>Hello ${name}!</h1> `);
response.end(markup);
renderToBuffer(value: unknown, options?: RenderOptions): Promise<Buffer>
Returns the value
(generally the result of a template tagged by html
) as a Promise which resolves to a Buffer
of markup:
import { html, renderToBuffer } from '@popeindustries/lit-html-server';
const name = 'Bob';
const markup = await renderToBuffer(html` <h1>Hello ${name}!</h1> `);
response.end(markup);