1 | # Worker HTML
|
2 |
|
3 | [HTML templating](#html-templating) and [streaming response](#streaming-responses) library for [Worker Runtimes](https://workers.js.org) such as Cloudflare Workers.
|
4 |
|
5 |
|
6 | ## HTML Templating
|
7 |
|
8 | Templating is done purely in JavaScript using tagged template strings, inspired by [hyperHTML](https://github.com/WebReflection/hyperhtml) and [lit-html](https://github.com/polymer/lit-html).
|
9 |
|
10 | This library is using tagged template strings to create _streaming response bodies_ on the fly,
|
11 | using no special template syntax and giving you the full power of JS for composition.
|
12 |
|
13 | ### Examples
|
14 | String interpolation works just like regular template strings,
|
15 | but all content is sanitized by default:
|
16 |
|
17 | ```ts
|
18 | const helloWorld = 'Hello World!';
|
19 | const h1El = html`<h1>${helloWorld}</h1>`;
|
20 | ```
|
21 |
|
22 | What is known as "partials" in string-based templating libraries are just functions here:
|
23 |
|
24 | ```ts
|
25 | const timeEl = (ts = new Date()) => html`
|
26 | <time datetime="${ts.toISOString()}">${ts.toLocalString()}</time>
|
27 | `;
|
28 | ```
|
29 |
|
30 | What is known as "layouts" are just functions as well:
|
31 |
|
32 | ```ts
|
33 | const baseLayout = (title: string, content: HTMLContent) => html`
|
34 | <!DOCTYPE html>
|
35 | <html lang="en">
|
36 | <head>
|
37 | <meta charset="utf-8">
|
38 | <title>${title}</title>
|
39 | </head>
|
40 | <body>${content}</body>
|
41 | </html>
|
42 | `;
|
43 | ```
|
44 |
|
45 | Layouts can "inherit" from each other, again using just functions:
|
46 |
|
47 | ```ts
|
48 | const pageLayout = (title: string, content: HTMLContent) => baseLayout(title, html`
|
49 | <main>
|
50 | ${content}
|
51 | <footer>Powered by @worker-tools/html</footer>
|
52 | </main>
|
53 | `);
|
54 | ```
|
55 |
|
56 | Many more features of string-based templating libraries can be replicated using functions.
|
57 | Most satisfying should be the use of `map` to replace a whole host of custom looping syntax:
|
58 |
|
59 | ```ts
|
60 | html`<ul>${['Foo', 'Bar', 'Baz'].map(x => html`<li>${x}</li>`)}</ul>`;
|
61 | ```
|
62 |
|
63 | Putting it all together:
|
64 |
|
65 | ```ts
|
66 | function handleRequest(event: FetchEvent) {
|
67 | const page = pageLayout(helloWorld, html`
|
68 | ${h1El}
|
69 | <p>The current time is ${timeEl()}.</p>
|
70 | <ul>${['Foo', 'Bar', 'Baz'].map(x => html`<li>${x}</li>`)}</ul>
|
71 | `));
|
72 |
|
73 | return new HTMLResponse(page);
|
74 | }
|
75 |
|
76 | self.addEventListener('fetch', ev => ev.respondWith(handleRequest(ev)));
|
77 | ```
|
78 |
|
79 | Note that this works regardless of worker runtime: Cloudflare Workers, Service Workers in the browser, and hopefully other [Worker Runtimes](https://workers.js.org) that have yet to be implemented.
|
80 |
|
81 | ### Tooling
|
82 | Since the use of tagged string literals for HTML is not new (see hyperHTML, lit-html), there exists tooling for syntax highlighting, such as [`lit-html` in VSCode](https://marketplace.visualstudio.com/items?itemName=bierner.lit-html).
|
83 |
|
84 |
|
85 | ## Streaming Responses
|
86 |
|
87 | As a side effect of this approach, responses are streams by default. This means you can use async data, without delaying sending the headers and HTML content.
|
88 |
|
89 | In the example below, everything up to and including `<p>The current time is` will be sent immediately, with the reset sent after the API request completes:
|
90 |
|
91 | ```ts
|
92 | function handleRequest(event: FetchEvent) {
|
93 | // NOTE: No `await` here!
|
94 | const timeElPromise = fetch('https://time.api/now')
|
95 | .then(r => r.text())
|
96 | .then(t => timeEl(new Date(t)));
|
97 |
|
98 | return new HTMLResponse(pageLayout('Hello World!', html`
|
99 | <h1>Hello World!</h1>
|
100 | <p>The current time is ${timeElPromise}.</p>
|
101 | `));
|
102 | }
|
103 | ```
|
104 |
|
105 | While there's ways around the lack of async/await in the above example (namely IIAFEs), @worker-tools/html supports passing async functions as html content directly:
|
106 |
|
107 | ```ts
|
108 | function handleRequest(event: FetchEvent) {
|
109 | return new HTMLResponse(pageLayout('Hello World!', html`
|
110 | <h1>Hello World!</h1>
|
111 | ${async () => {
|
112 | const timeStamp = new Date(
|
113 | await fetch('https://time.api/now').then(r => r.text())
|
114 | );
|
115 | return html`<p>The current time is ${timeEl(timeStamp)}.</p>`
|
116 | }}
|
117 | `));
|
118 | }
|
119 | ```
|
120 |
|
121 | Note that there are some subtle differences compared to the earlier examples.
|
122 | - The initial response will include headers and html up to and including `<h1>Hello World!</h1>`
|
123 | - The time API request will not be sent until the headers and html up to and including `<h1>Hello World!</h1>` have hit the wire.
|
124 |
|
125 | These follow from the way async/await works, so shouldn't be too surprising to those already familiar with common async/await pitfalls.
|
126 |
|
127 | If for any reason you don't want to use streaming response bodies, you can use the `BufferedHTMLResponse` instead, which will buffer the entire body before releasing it to the network.
|
128 |
|
129 | ## See Other
|
130 | You can combine this library with tools from the [Worker Tools family](https://workers.tolls) such as `@worker-tools/response-creators`:
|
131 |
|
132 | ```ts
|
133 | import { internalServerError } from '@worker-tools/response-creators';
|
134 |
|
135 | function handleRequest(event: FetchEvent) {
|
136 | return new HTMLResponse(
|
137 | pageLayout('Ooops', html`<h1>Something went wrong</h1>`),
|
138 | internalServerError(),
|
139 | );
|
140 | }
|
141 | ```
|
142 |
|
143 | You can also see the [Worker News source code](https://github.com/worker-tools/worker-news) for an example of how to build an entire web app on the edge using Worker HTML.
|
144 |
|
145 | Finally, you can read [The Joys and Perils of Writing Plain Old Web Apps](https://qwtel.com/posts/software/the-joys-and-perils-of-writing-plain-old-web-apps/) for a personal account of building web apps in a Web 2.0 way.
|
146 |
|
147 | --------
|
148 |
|
149 | <p align="center"><a href="https://workers.tools"><img src="https://workers.tools/assets/img/logo.svg" width="100" height="100" /></a>
|
150 | <p align="center">This module is part of the Worker Tools collection<br/>β
|
151 |
|
152 | [Worker Tools](https://workers.tools) are a collection of TypeScript libraries for writing web servers in [Worker Runtimes](https://workers.js.org) such as Cloudflare Workers, Deno Deploy and Service Workers in the browser.
|
153 |
|
154 | If you liked this module, you might also like:
|
155 |
|
156 | - π§ [__Worker Router__][router] --- Complete routing solution that works across CF Workers, Deno and Service Workers
|
157 | - π [__Worker Middleware__][middleware] --- A suite of standalone HTTP server-side middleware with TypeScript support
|
158 | - π [__Worker HTML__][html] --- HTML templating and streaming response library
|
159 | - π¦ [__Storage Area__][kv-storage] --- Key-value store abstraction across [Cloudflare KV][cloudflare-kv-storage], [Deno][deno-kv-storage] and browsers.
|
160 | - π [__Response Creators__][response-creators] --- Factory functions for responses with pre-filled status and status text
|
161 | - π [__Stream Response__][stream-response] --- Use async generators to build streaming responses for SSE, etc...
|
162 | - π₯ [__JSON Fetch__][json-fetch] --- Drop-in replacements for Fetch API classes with first class support for JSON.
|
163 | - π¦ [__JSON Stream__][json-stream] --- Streaming JSON parser/stingifier with first class support for web streams.
|
164 |
|
165 | Worker Tools also includes a number of polyfills that help bridge the gap between Worker Runtimes:
|
166 | - βοΈ [__HTML Rewriter__][html-rewriter] --- Cloudflare's HTML Rewriter for use in Deno, browsers, etc...
|
167 | - π [__Location Polyfill__][location-polyfill] --- A `Location` polyfill for Cloudflare Workers.
|
168 | - π¦ [__Deno Fetch Event Adapter__][deno-fetch-event-adapter] --- Dispatches global `fetch` events using Denoβs native HTTP server.
|
169 |
|
170 | [router]: https://workers.tools/router
|
171 | [middleware]: https://workers.tools/middleware
|
172 | [html]: https://workers.tools/html
|
173 | [kv-storage]: https://workers.tools/kv-storage
|
174 | [cloudflare-kv-storage]: https://workers.tools/cloudflare-kv-storage
|
175 | [deno-kv-storage]: https://workers.tools/deno-kv-storage
|
176 | [kv-storage-polyfill]: https://workers.tools/kv-storage-polyfill
|
177 | [response-creators]: https://workers.tools/response-creators
|
178 | [stream-response]: https://workers.tools/stream-response
|
179 | [json-fetch]: https://workers.tools/json-fetch
|
180 | [json-stream]: https://workers.tools/json-stream
|
181 | [request-cookie-store]: https://workers.tools/request-cookie-store
|
182 | [extendable-promise]: https://workers.tools/extendable-promise
|
183 | [html-rewriter]: https://workers.tools/html-rewriter
|
184 | [location-polyfill]: https://workers.tools/location-polyfill
|
185 | [deno-fetch-event-adapter]: https://workers.tools/deno-fetch-event-adapter
|
186 |
|
187 | Fore more visit [workers.tools](https://workers.tools).
|