I was doing my thing and then suddenly had an enlightenment. A new cool idea came to my mind.
But first a little bit of background. Last year I created a JavaScript library called Wayne that abstracts the idea of creating pure in-browser HTTP requests with the help of a Service Worker. The API is similar to the express.js NodeJS framework but works in the browser instead of the server.
So the cool idea that I had was to use a ReactJS application inside Service Worker and return a snapshot of that application from different HTTP requests. I've even been thinking about using HTTP requests to interact with applications.
I've started creating a PoC. My first try was searching for a library that was used for testing the React app. Since you can't use real DOM inside Service Woker. I was looking at react testing library documentation in the hope that they use some kind of fake DOM library, but after looking at the code it was using real ReactDOM and also it was not very intuitive. They lack getElementByID and were created just for making tests. So I've abandoned the idea of using a testing library.
But then I realized that I should probably need to use jsDOM. This is the library that can be used in nodejs to mock the DOM. This is what jest testing framework is using and this is what I was using to test jQuery Terminal library in Jasmine before jest was created.
One problem was to run jsDOM as UMD module. But luckily I was able to use browserify to compile jsDOM into UMD.
Below is the code I've used:
import * as React from 'react';
import { createRoot } from 'react-dom';
import { Wayne } from '@jcubic/wayne';
importScripts('https://cdn.jsdelivr.net/gh/jcubic/static/js/jsdom.min.js');
const { JSDOM } = jsdom;
const pathname = location.pathname.replace(/\/sw.js$/, '');
const Nav = () => {
return (
<ul>
<li><a href={`${pathname}/`}>home</a></li>
<li><a href={`${pathname}/__about__`}>about</a></li>
<li><a href={`${pathname}/__contact__`}>contact</a></li>
<li><a href={`${pathname}/source.jsx`}>Service Worker source</a></li>
</ul>
);
}
let globalSetPage;
const App = () => {
const [page, setPage] = React.useState('root');
React.useEffect(() => {
globalSetPage = setPage;
}, [setPage]);
return (
<>
<Nav/>
<p>Helo React + Wayne "{page}"</p>
</>
);
};
const dom = new JSDOM(`<!DOCTYPE html>
<html>
<head><title>React App</title></head>
<body>
<div id="root"></div>
</body>
</html>`);
self.window = dom.window;
self.document = self.window.document;
const root_node = document.getElementById('root');
const root = createRoot(root_node);
root.render(<App/>);
function html(res) {
setTimeout(() => {
const html = `<!DOCTYPE html>${document.documentElement.outerHTML}`;
res.html(html);
}, 0);
}
const app = new Wayne();
app.get('/__*__', (req, res) => {
globalSetPage(req.params[0]);
html(res);
});
app.get('/source.jsx', async (req, res) => {
const text = await fetch('../sw.jsx').then(res => res.text());
res.text(text);
});
To explain the code:
- It first loaded jsDOM from my private CDN using jsDelivr
- Then create an application with the useState hook
- It saves the setState from the hook as a global variable
- It adds Wayne routes and calls the setState function with data from the user
And that's it. To pass data to the app you just need to open URL __<SOMETHING>__
and the stuff between double underscores will be page variable inside React.
The code assigns the internal set state function into a global variable, but it would probably be better to use something like an EventEmitter for this. But this is just a hack and it works. This also makes the code easier to understand, even if it's not best practice.
You can give this hack a try here.
The source code can be found on GitHub in gh-pages branch.
And that's it. If you like this post, you can follow me on Twitter at @jcubic and visit my home page.
Top comments (0)