Going Buildless
Hi all š
Iām in a long distance relationship, and this means that every few weeks Iām on a plane to England. Everytime Iām on that plane, I think about how nice itād be to read some reddit posts. What I could do is find a reddit app that lets you cache posts for offline (im sure there is one out there), or I could take the opportunity to write something myself and use some of the latest and greatest technologies and web standards, and have some fun!
On top of that, there recently has been a lot of discussion around what I like to call āgoing buildlessā, which I think is a really fascinating and great recent development. And thats also exactly what this post is about; bringing fun to developing.
I also like to imagine this blogpost as somewhat of an homage to a couple of really awesome people in the community who are making some really awesome things possible, as well as a showcase of some exciting new technologies and standards, and Iāll be linking to all that good stuff as we move along.
Do note that this wonāt be a step-by-step tutorial, but if you want to check out the code, you can find the finished project on github. Our end result should look something like this:
So letās dive straight in and quickly install a few dependencies:
npm i @babel/core babel-loader @babel/preset-env @babel/preset-react webpack webpack-cli react react-dom redux react-redux html-webpack-plugin are-you-tired-yet html-loader webpack-dev-server
Iām kidding. Weāre not gonna use any of that. Weāre going to try and avoid as much tooling/dependencies as we can, and keep the entry barrier low.
What we will be using is:
- LitElement
For this project weāll be using LitElement as our component model. Itās easy to use, lightweight, close to the metal, and leverages web components. - @vaadin/router
Vaadin router is a really small (< 7kb) router that has an *awesome* developer experience, and I cannot recommend enough. - @pika/web
Pika is going to help us get our modules together for easy development. - es-dev-server
A simple dev server for modern web development workflows, made by us at open-wc. Although any http server will do; feel free to bring your own.
And thatās it. Weāll also be using a few browser standards, namely: es modules, web components, import-maps, kv-storage and service-worker.
So letās go ahead and install our dependencies:
npm i -S lit-element @vaadin/router
npm i -D @pika/web es-dev-server
Weāll also add a `postinstall` hook to our `package.json` thatās going to run Pika for us:
āscriptsā: {
āstartā: āes-dev-serverā,
āpostinstallā: āpika-webā
}
š Pika
Pika is a project by Fred K. Schott, that aims to bring that nostalgic, 2014 simplicity to 2019 web development. Fred is up to all sorts of awesome stuff, for one, he made pika.dev, which lets you easily search for modern JavaScript packages on npm. He also recently gave his talk Reimagining the Registry at DinosaurJS 2019, which I highly recommend you watch.
`@pika/web` takes things even one step further. If we run `pika-web`, itāll install our dependencies as single javascript files to a new `web_modules/` directory. If your dependency exports an ES āmoduleā entrypoint in its `package.json` manifest, Pika supports it. If you have any transitive dependencies, Pika will create separate chunks for any shared code among your dependencies. Easy peasy lemon squeezy.
What this means, is that in our case our output will look something like:
āā web_modules/
āā lit-element.js
āā @vaadin
āā router.js
Sweet! Thatās it. We have our dependencies ready to go as single javascript module files, and this is going to make things really convenient for us later on in this blogpost, just stay tuned!
š„ Import maps
Alright! Now weāve got our dependencies sorted out, letās get to work. Weāll make an `index.html` thatāll look something like this:
<html>
<! ā head etc ā
<body>
<reddit-pwa-app></reddit-pwa-app>
<script src=ā./src/reddit-pwa-app.jsā type=āmoduleā></script>
</body>
</html>
And `reddit-pwa-app.js`:
import { LitElement, html } from ālit-elementā;class RedditPwaApp extends LitElement {
// ā¦
render() {
return html`
<h1>Hello world!</h1>
`;
}
}customElements.define(āreddit-pwa-appā, RedditPwaApp);
Weāre off to a great start. Letās try and see how this looks in the browser so far, so lets start our server, open the browser andā¦ Whatās this? An error?
And weāve barely even started. Alright, letās take a look. The problem here is that our module specifiers are bare. They are bare module specifiers. What this means is that there are no paths specified, no file extensions, theyāre justā¦ pretty bare. Our browser has no idea on what to do with this, so itāll throw an error.
import { LitElement, html } from ālit-elementā; // ā bare module specifier
import { Router } from ā@vaadin/routerā; // ā bare module specifier
import { foo } from ā./bar.jsā; // ā not bare!
import { html } from āhttps://unpkg.com/lit-html'; // ā not bare!
Naturally, we could use some tools for this, like webpack, or rollup, or a dev server that rewrites the bare module specifiers to something meaningful to browsers, so we can load our imports. But that means we have to bring in a bunch of tooling, dive into configuration, and weāre trying to stay minimal here. We just want to write code! In order to solve this, weāre going to take a look at import maps.
Import maps is a new proposal that lets you control the behavior of JavaScript imports. Using an import map, we can control what URLs get fetched by JavaScript `import` statements and `import()` expressions, and allows this mapping to be reused in non-import contexts. This is great for several reasons:
- Allows our bare module specifiers to work
- Provides a fallback resolution so that `import $ from ājqueryā;` can try to go to a CDN first, but fall back to a local version if the CDN server is down
- Enables polyfilling of, or other control over, built-in modules (More on that later, hang on tight!)
- Solves the nested dependency problem (Go read that blog!)
Sounds pretty sweet, no? Import maps are currently available in Chrome 75+, behind a flag, and with that knowledge in mind, letās go to our `index.html`, and add an import map to our `<head>`:
<head>
<script type="importmap">
{
"imports": {
"@vaadin/router": "/web_modules/@vaadin/router.js",
"lit-element": "/web_modules/lit-element.js"
}
}
</script>
</head>
If we go back to our browser, and refresh our page, weāll have no more errors, and we should see our `<h1>Hello world!</h1>` on our screen.
Import maps is an incredibly interesting new standard, and definitely something you should be keeping your eyes on. If youāre interested in experimenting with them, and generate your own import map based on a `yarn.lock` file, you can try our open-wc import-maps-generate package and play around. Im really excited to see what people will develop in combination with import maps.
š” Service Worker
Alright, weāre going to skip ahead in time a little bit. Weāve got our dependencies working, we have our router set up, and weāve done some API calls to get the data from reddit, and display it on our screen. Going over all of the code is a bit out of scope for this blogpost, but remember that you can find all the code in the github repo if you want to read the implementation details.
Since weāre making this app so we can read reddit threads on the airplane it would be great if our application worked offline, and if we could somehow save some posts to read.
Service workers are a kind of JavaScript Worker that runs in the background. You can visualize it as sitting in between the web page, and the network. Whenever your web page makes a request, it goes through the service worker first. This means that we can intercept the request, and do stuff with it! For example; we can let the request go through to the network to get a response, and cache it when it returns so we can use that cached data later when we might be offline. We can also use a service worker to precache our assets. What this means is that we can precache any critical assets our application may need in order to work offline. If we have no network connection, we can simply fall back to the assets we cached, and still have a working (albeit offline) application.
If youāre interested in learning more about Progressive Web Apps and service worker, I highly recommend you read The Offline Cookbook by Jake Archibald. As well as this video tutorial series by Jad Joubran.
So letās go ahead and implement a service worker. In our `index.html`, weāll add the following snippet:
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js').then(() => {
console.log('ServiceWorker registered!');
}, (err) => {
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
Weāll also add a `sw.js` file to the root of our project. So weāre about to precache the assets of our app, and this is where Pika just made life really easy for us. If youāll take a look at the install handler in the service worker file:
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHENAME).then((cache) => {
return cache.addAll([
'/',
'./web_modules/lit-element.js',
'./web_modules/@vaadin/router.js',
'./src/reddit-pwa-app.js',
'./src/reddit-pwa-comment.js',
'./src/reddit-pwa-search.js',
'./src/reddit-pwa-subreddit.js',
'./src/reddit-pwa-thread.js',
'./src/utils.js',
]);
})
);
});
Youāll find that weāre totally in control of our assets, and we have a nice, clean list of files we need in order to work offline.
š“ Going offline
Right. Now that weāve cached our assets to work offline, it would be excellent if we could actually save some posts that we can read while offline. There are many ways that lead to Rome, but since weāre living on the edge a little bit, weāre going to go with: Kv-storage!
š¦ Built-in Modules
There are a few things to talk about here. Kv-storage is a built-in module. Built-in modules are very similar to regular JavaScript modules, except they ship with the browser. Itās good to note that while built-in modules ship with the browser, they are not exposed on the global scope, and are namespaced with `std:` (Yes, really.). This has a few advantages: they wonāt add any overhead to starting up a new JavaScript runtime context (e.g. a new tab, worker, or service worker), and they wonāt consume any memory or CPU unless theyāre actually imported, as well as avoid naming collisions with existing code.
Another interesting, if not somewhat controversial, proposal as a built-in module is the std-toast element, and the std-switch element.
š Kv-storage
Alright, with that out of the way, lets talk about kv-storage. Kv-storage (or
ākey value storageā) is fairly similar to localStorage, except for only a few major differences, and is layered on top of IndexedDB.
The motivation for kv-storage is that localStorage is synchronous, which can lead to bad performance and syncing issues. Itās also limited to exclusively String key/value pairs. The alternative, IndexedDb, isā¦ hard to use. The reason itās so hard to use is that it predates promises, and this leads to a, well, pretty bad developer experience. Not fun. Kv-storage, however, is a lot of fun, asynchronous, and easy to use! Consider the following example:
import { storage, /* StorageArea */ } from "std:kv-storage";(async () => {
await storage.set("mycat", "Tom");
console.log(await storage.get("mycat")); // Tom
})();
Notice how weāre importing from `std:kv-storage`? This import specifier is bare as well, but in this case itās okay because it actually ships with the browser.
Pretty neat. We can perfectly use this for adding a āsave for offlineā button, and simply store the JSON data for a reddit thread, and get it when we need it.
`reddit-pwa-thread.js:52`:
const savedPosts = new StorageArea("saved-posts");// ...async saveForOffline() {
await savedPosts.set(this.location.params.id, this.thread); // id of the post + thread as json
this.isPostSaved = true;
}
So now if we click the āsave for offlineā button, and we go to the developer tools āApplicationā tab, we can see a `kv-storage:saved-posts` that holds the JSON data for this post:
And if we go back to our search page, weāll have a list of saved posts with the post we just saved:
š® Polyfilling
Excellent. However, weāre about to run into another problem here. Living on the edge is fun, but also dangerous. The problem that weāre hitting here is that, at the time of writing, kv-storage is only implemented in Chrome behind a flag. Thatās obviously not great. Fortunately, thereās a polyfill available, and at the same time we get to show off yet another really useful feature of import-maps; polyfilling!
First things first, lets install the kv-storage-polyfill:
`npm i -S kv-storage-polyfill`
Note that our `postinstall` hook will run Pika for us again
And lets add the following to our import map in our `index.html`:
<script type="importmap">
{
"imports": {
"@vaadin/router": "/web_modules/@vaadin/router.js",
"lit-element": "/web_modules/lit-element.js",
"/web_modules/kv-storage-polyfill.js": [
"std:kv-storage",
"/web_modules/kv-storage-polyfill.js"
]
}
}
</script>
So what happens here is that whenever `/web_modules/kv-storage-polyfill.js` is requested or imported, the browser will first try to see if `std:kv-storage` is available; however, if that fails, itāll load `/web_modules/kv-storage-polyfill.js` instead.
So in code, if we import:
import { StorageArea } from '/web_modules/kv-storage-polyfill.js';
This is what will happen:
"/web_modules/kv-storage-polyfill.js": [ // when I'm requested
"std:kv-storage", // try me first!
"/web_modules/kv-storage-polyfill.js" // or fallback to me
]
š Conclusion
And we should now have a simple, functioning PWA, with minimal dependencies. There are a few nitpicks to this project that we could complain about, and theyād all likely be fair. For example; we probably couldāve gone without using Pika, but it does make life really easy for us. You could have made the same argument about adding a simple Webpack configuration, but youād have missed the point. The point here is to make a fun application, while using some of the latest features, drop some buzzwords, and have a low barrier for entry. As Fred Schott would say: āIn 2019, you should use a bundler because you want to, not because you need to.ā
If youāre interested in nitpicking, however, you can read this great discussion about using Webpack vs Pika vs buildless, and youāll get some great insights from Sean Larkinn of the Webpack core team himself, as well as Fred K. Schott, creator of Pika.
I hope you enjoyed this blog post, and I hope you learned something, or discovered some new interesting people to follow. There are lots of exciting developments happening in this space right now, and I hope I got you as excited about them as I am. If you have any questions, comments, feedback, or nitpicks, feel free to reach out to me on twitter at @passle_ or @openwc and donāt forget to check out open-wc.org š.
Honorable Mentions
To close this blog, Iād like to give a few shoutouts to some very interesting people that are doing some great stuff, and you may want to keep an eye on.
To start: Guy Bedford, who wrote es-module-shims, which, well, shims es modules, and import maps. Which if you ask me is quite an amazing feat, and allows me to actually use some of these new technologies that arenāt implemented on all browsers yet.
And if youāre interested in more of the same, you should definitely check out Luke Jackson's talk Donāt Build That App! No webpack, no worries š¤š¤, as Luke would say.
Iād also like to thank Benny Powers and Lars den Bakker for their helpful comments and feedback.