There are serious benefits to building small applications in the context of a complex, remote page. Vite makes it pretty simple to pull it off.
More and more frequently, I’ve been working with little small single-page applications that are built independently and then mounted into something more complex. Think of things like interactive calculators, multi-step forms, or other tools intended to plug into a “bigger” page, like one hosted by CMS.
Overall, it’s a helpful approach. Rather than being required to mess with a clunky application in order to work on just a small piece of it, you need to deal only with a little, self-contained application.
But there are some noteworthy challenges. The main one: you’re building a tool intended to be used in a specific context,totally outside that context. This inevitably brings about some gotchas & annoyances, often after you’ve gone through the work of publishing your latest version of the tool and mounting it to the (let’s assume from here on out it’s a) CMS. You’ll ship it and then find things like this:
Global styles loaded by the CMS are bleeding into your app.
Third-party analytics scripts aren’t tracking the tool’s activity as expected.
A sticky header on the page unexpectedly overlaps part of the tool as someone’s using it.
You didn’t realize a ton of library styles are already being loaded by the CMS, and so you double-shipped a ton of CSS.
Individually, none of these issues are difficult to solve. The problem is that the feedback loop to become aware of them is too big.
Thankfully, modern tooling can give us some of the best of both worlds — a snappy, lightweight development experience, and a local environment that mimics how the tool will actually exist in production. In particular, Vite makes it real easy.
To be fair, Vite isn’t even the first tool I’ve seen used to project a local SPA onto a remote page — my friend Lennart Kroneman showed me how to do it with Webpack a couple of years ago, and I know other tools are capable of proxying requests in a similar fashion as well. But the ease at which it’s possible with Vite is at a whole different level.
To prototype this, I’ve made an example “production” page published with Netlify. It’s got a bit of content and a little SPA mounted to it (”My Production App”). If you’d like to dabble, the repository lives here, and the published page is here. Just pretend it’s a published page hosted by an enterprise CMS.
Rendered between the page’s content is an empty DOM node used to mount the app, and a script tag to load the bundle:
<!-- app mounted here --><divid="app"></div><!-- app's bundle loaded here --><scripttype="module"src="/static/production-bundle.js"></script>
Even though I’m using Preact, the framework of choice is irrelevant. The basic assumption is that it’s something that mounts to #app once the bundle loads. Our application code looks like this:
// app.jsconstApp=()=>{return<h2>My DEVELOPMENT App!</h2>;};exportdefaultApp;
Which is then locally rendered in a local-dev.jsx file that’s directly referenced by our index.html file. This file is only intended to be used for local development. It’ll never be bundled up with the final product.
Now, let’s spin things up. To start, it doesn’t look like much. If all goes well, we’ll end with this same application being rendered locally, but as if it’s on that remote, production page.
It’s possible to pull all of this off with Vite’s built-in support for proxying requests. In fact, when I first started dabbling, this is the approach I took. But because we’re dealing with multiple request types with various sources, it gets cumbersome real quick, and so I don’t even recommend it anymore.
Instead, Vite has a simpler, yet equally effective approach we can take: the transformIndexHtml hook from its plugin API. When it’s wired up, we have full control over what’s rendered by the local development server with a relatively small amount of code.
We’ll start with a small Vite plugin, whose bones look like this:
// vite.config.jsconstHtmlRewriter=()=>({name:"html-rewriter",asynctransformIndexHtml(html){// Transformation happens here.return html;},});exportdefaultdefineConfig({// Plugin registered here:plugins:[preact(),HtmlRewriter()],// ... the rest of our config.});
With this in place, we have full power to do whatever we want with that HTML, like completely swap out all of the HTML for “your mom”:
constHtmlRewriter=()=>({name:"html-rewriter",asynctransformIndexHtml(html){// Request the remote page.const response =awaitfetch("https://vite-proxy-demo.netlify.app/some-page/");// Return the HTML.return response.text();},});
At this point, if you were to reload our local application, it’d look exactly like that remote page we’re targeting, even though we’re serving it on localhost.
The next step is to surgically rewrite the script tag to point to our development version of the application, rather than the one that’s currently rendering.
This’ll highly depend on how your production bundle is being loaded for a page you’re targeting. But for us, the application code is being loaded from a CloudFront URL. All we’ll do is target that remote script and swap it out for our local local-dev.jsx. Notice that since we’ll be serving it locally, we can even stick to using a relative path.
If you refresh the page, you’ll see nearly the same result as before. All code is coming in from a production page except the script we targeted. And because of this, it’s our local version of the application running on the page — no longer the remote one.
With that, we’re largely set. We can iterate on our application with a far less chance of interference from unexpected code running on the page, without running any other application locally.
Before we’re wrapped up, there are a couple more things to keep in mind as you implement any of this:
#1: Guarantee hot module reloading fires consistently.
I initially ran into some issues with changes not being propagated through Vite to the browser consistently. In order to guarantee that they do, include import.meta.hot in your entry point file:
// local-dev.jsx
+ import.meta.hot;
import { render } from "preact";
import App from "./app";
render(<App />, document.getElementById("app"));
That single line will force Vite to fully propagate an update, trigging a module reload, and keeping your development experience snappy:
#2: Yourscripttagmusthave atype="module"attribute on it.
Without it, Vite won’t be able to load and execute your local entry point. It’s a small thing that may also be easily remedied by HTML rewriting again. Nevertheless, it can trip you up.
I’ve quickly found myself implementing this setup in multiple projects, so I wrapped it up in a plugin: vite-plugin-proxy-page. It’s been tested in a variety of different projects, and you’re welcome to leverage it too. As an added benefit, there are a few other nice-to-haves handled by it too. Things like:
Locally caching the remote HTML so you’re not refetching it on every page load.
Being able to create a root node in the remote page’s HTML if it doesn’t already exist (and mount it in the correct spot).
Embracing these tactics has done some wonders for shrinking the formerly gigantic feedback loop we’d otherwise need to navigate when building in this sort of architecture. Hoping it does the same for you!
Alex MacArthur is a software engineer working for Dave Ramsey in
Nashville-ish, TN. Soli Deo gloria.
Get irregular emails about new posts or projects.
No spam. Unsubscribe whenever.
Leave a Free Comment
2
comments
Toms
Awesome implementation, i've been going down the exact rabbit hole and i just wish i had found this article faster haha.
I've been using this for development with back-end languages like Php (in my case laravel specifically) without using laravel-vite package (which basically forces you to run laravel locally as well). So i end up just running front-end only while having backend run off a domain.
1
reply
Toms
Only thing lacking for such implementation would be possibility to have dynamic routing - so the development server serves data from specific routes IE: If i write /product-item-1 , it will also serve data from the specific route