Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Service Worker Routing library for in browser HTTP requests

License

NotificationsYou must be signed in to change notification settings

vijeet-shah/wayne

 
 

Repository files navigation

Logo of Wayne library - it represents construction worker helmet and text with the name of the library

npmPRs Welcome

Service Worker Routing library for in-browser HTTP requests

It's like an Express inside Service Worker.

Most of the time Service Worker is used for caching HTTP requests and making the app work when thereis no internet (mostly forPWA), but infact, you can create completely new responses to requests that never leave the browser. This librarymakes that easier by adding a simple API similar to Express.

Usage

Installation from npm:

npm install @jcubic/wayne
yarn add @jcubic/wayne

The standard way of installing the service worker

if('serviceWorker'innavigator){constscope=location.pathname.replace(/\/[^\/]+$/,'/');navigator.serviceWorker.register('sw.js',{ scope,type:'module'}).then(function(reg){reg.addEventListener('updatefound',function(){constinstallingWorker=reg.installing;console.log('A new service worker is being installed:',installingWorker);});// registration workedconsole.log('Registration succeeded. Scope is '+reg.scope);}).catch(function(error){// registration failedconsole.log('Registration failed with '+error);});}

If you want to support browsers that don't support ES Modules in Service Worker use this instead:

if('serviceWorker'innavigator){constscope=location.pathname.replace(/\/[^\/]+$/,'/');navigator.serviceWorker.register('sw.js',{ scope}).then(function(reg){reg.addEventListener('updatefound',function(){constinstallingWorker=reg.installing;console.log('A new service worker is being installed:',installingWorker);});// registration workedconsole.log('Registration succeeded. Scope is '+reg.scope);}).catch(function(error){// registration failedconsole.log('Registration failed with '+error);});}

Inside the same file you can sendAJAX requests with standardfetch API.

functionget(url){fetch(url).then(res=>res.text()).then(text=>output.innerHTML=text);}input.addEventListener('click',()=>{get(`./user/${user_id.value}`);});error.addEventListener('click',()=>{get(`./error`);});

Service worker -sw.js file

Importing Wayne module:

  • when worker created as ES Module
import{Wayne}from'https://cdn.jsdelivr.net/npm/@jcubic/wayne';constapp=newWayne();
  • When the Service Worker created as normal script
importScripts('https://cdn.jsdelivr.net/npm/@jcubic/wayne/index.umd.min.js');constapp=newwayne.Wayne();

Using the library

constusers={1:'Jakub T. Jankiewicz',2:'John Doe',3:'Jane Doe'};app.get('/user/{id}',function(req,res){constuser=users[req.params.id];if(user){res.json({result:user});}else{res.json({error:'User Not Found'});}});app.get('/error',function(req,res){nonExisting();});app.get('/redirect',function(req,res){res.redirect(301,'/message');});app.get('/message',function(req,res){res.text('Lorem Ipsum');});app.get('/external',function(req,res){// lorem ipsum APIres.redirect('https://api.buildable.dev/@62d55492951509001abc363e/live/lorem-ipsum');});

Handle the same extension for all requests

importScripts('https://cdn.jsdelivr.net/npm/@jcubic/wayne/index.umd.min.js','https://cdn.jsdelivr.net/gh/jcubic/static@master/js/path.js');constapp=newWayne();app.get('*',function(req,res){consturl=newURL(req.url);constextension=path.extname(url.pathname);constaccept=req.headers.get('Accept');if(extension==='.js'&&accept.match(/text\/html/)){res.text('// Sorry no source code for you');}else{res.fetch(req);}});

This code will show the comment// Sorry no source code for you for every request to JavaScriptfiles from the browser (if open in a new tab). When you want to view the file the browser sendsAccept: text/html HTTP header.

File system middleware

import{Wayne,FileSystem}from'https://cdn.jsdelivr.net/npm/@jcubic/wayne';importFSfrom"https://cdn.skypack.dev/@isomorphic-git/lightning-fs";importmimefrom"https://cdn.skypack.dev/mime";importpathfrom"https://cdn.skypack.dev/path-browserify";const{promises:fs}=newFS("__wayne__");constapp=newWayne();app.use(FileSystem({ path, fs, mime,prefix:'__fs__'}));

When not using a module the code will be similar. When you access URLs withthe prefix__fs__ like./__fs__/foo it will read files from the indexedDB filesystem named__wayne__. SeeLightning-FS repo for details about the library.

From version 0.12 you can usetest callback option to check if the file should serve from the filesystem. Note that it will receive URLs from all domains.

From version 0.13.0 you can usedir callback function that allow to dynamically change directory of served files.

consttest=url=>{constpath=url.pathname;// return true if pathname should go to filesystemreturnpath.match(/__fs__/);};constdir=()=>'/';app.use(wayne.FileSystem({ path, fs, mime, test, dir}));

From version 0.14.0 both functionsdir andtest can be async. So you can use data from IndexedDBe.g. usingidb-keyval by Jake Archibald.

A patch in 0.14.3 allow putting interceptors to inject something into output HTML from FileSystemmiddleware. You do this by adding middleware before FileSystem and patchres.send method:

functionfs_interecept(callback){returnfunction(req,res,next){constsend=res.send.bind(res);res.send=function(data, ...rest){consturl=newURL(req.url);if(test(url)){data=callback(data);}returnsend(data, ...rest);};next();};}app.use(fs_interecept(function(html){returnhtml.replace(/<\/body>/,`<script>console.log('intercepted')</script></body>`);}));

You should use the sametest function to make sure that you patch only those requests that camefrom FS.

RPC mechanism

In Service Worker, you create a generic route that sends data to the BroadcastChannel:

import{send}from'https://cdn.jsdelivr.net/npm/@jcubic/wayne';constchannel=newBroadcastChannel('__rpc__');app.get('/rpc/{name}/*',async(req,res)=>{constargs=req.params[0].split('/');constmethod=req.params.name;try{constdata=awaitsend(channel,method,args);res.json(data);}catch(e){res.text(e.message);}});

and in the main thread, you create the other side of the channel and the remote methods:

import{rpc}from'https://cdn.jsdelivr.net/npm/@jcubic/wayne';constchannel=newBroadcastChannel('__rpc__');rpc(channel,{ping:function(){return'pong';},sin:function(x){returnMath.sin(x);},random:function(){returnMath.random();},json:function(){returnfetch('https://api.npoint.io/8c7cc24b3fd405b775ce').then(res=>res.json());}});

When you send a request/rpc/ping you will get a response frommethods.ping function.

fetch('./rpc/ping').then(res=>res.text()).then(text=>{console.log({ text});});

With this setup, you can create new functions/methods that will map to HTTP requests.

The demo below uses random requests:

letindex=0;constrequests=['./rpc/ping/','./rpc/json/','./rpc/random/','./rpc/sin/10'];rpc.addEventListener('click',()=>{get(random_request());});functionrandom_request(){constnext_index=index++%requests.length;returnrequests[next_index];}

Server-Sent Events

Server-Sent Eventsis the way to stream data in the browser. It's a native browser implementation of LongPolling. Here is an example of how to use SSE with Wayne:

Service Worker

app.get('/sse',function(req,res){conststream=res.sse({onClose(){clearInterval(timerId);}});vartimerId=setInterval(function(){constnow=(newDate()).toString();stream.send({data:now});},1000);});

Main tread

letsee_source;sse_start.addEventListener('click',()=>{see_source=newEventSource("./sse");see_source.onmessage=event=>{console.log(event.data);};});sse_stop.addEventListener('click',()=>{if(see_source){see_source.close();see_source=null;}});

3rd party URL

Service Worker allows intercepting everything that origineted from the page that has service workerinclding URLs from different origin. From version 0.15.0 Wayne allow to inrecept such URLs. You just usefull URL instead of just path as a route:

app.get('https://github.com/{user}/{repo}',(req,res)=>{res.text(`Sorry, you can't fetch${req.params.user} repo named${req.params.repo}`);});

If you run fetch in browser:

awaitfetch('https://github.com/jcubic/wayne').then(res=>res.text());

you will get the string:

"Sorry, you can't fetch jcubic repo named wayne"

If you want to restrict the request to only same origin you can do this with filter option:

constapp=newWayne({filter:req=>{consturl=newURL(req.url);returnurl.host===self.location.host;}});

Using with ES Modules

You can intercept the import of ES Module with Wayne. Here is example:

Main tread

<script>window.ready=navigator.serviceWorker.register('./sw.js',{scope:location.pathname})</script><scripttype="module">// wait for Service Wokerawaitwindow.ready;// next tick delay is require for the worker to intitialize properlyawaitnewPromise(resolve=>setTimeout(resolve,0));// static imports works only when you install and refresh the browser// they probbaly run just after the code is parsedconst{default:$}=awaitimport('./@jquery');$('body').css('background','rebeccapurple');</script>

Service Worker

app.get('*',(req,res)=>{consturl=newURL(req.url);constname=req.url.replace(/.*@/,'');if(url.pathname.match(/\+esm/)){res.fetch(`https://cdn.jsdelivr.net${url.pathname}`);}elseif(url.pathname.match(/@/)){if(name.match(/css/)){res.fetch(`https://cdn.jsdelivr.net/npm/${name}`);}else{res.fetch(`https://esm.run/${name}`);}}else{res.fetch(req);}});

The code checks if the URL contain@ in the path and redirect them tohttps://esm.run. If the script import other scrips they usually look like this:

importrequire$$0from"/npm/jquery@3.7.1/+esm"

And needs to be imported from jsDelivr, the same if you import CSS file.Seeexample of loading jQuery Terminal where this code is used.

First load

According to the spec, the default behavior of the Service Worker is to control the HTTP requestsafter reloading the page. To make the SW always in control use this code in your SW:

self.addEventListener('activate',(event)=>{event.waitUntil(clients.claim());});

You can read more in the articleThe service worker lifecyclebyJake Archibald.

Demo

The source code for the demos can be foundin the docs' directory at the gh-pages branch.

API reference

Wayne constrcutor accept object with options:

  • filter - a function that is called with request object, and should return false if the requestshould not be proxied with Service Worker.

Wayne object has those methods that correspond to HTTP methods

  • get
  • post
  • put
  • delete
  • patch

Each method accepts a URL with markers inside curly brackets, those markers will be available fromRequest.params object. The request object is the browser native object of a given request seeMDN for details. The only change to thenative API is that the object has propertyparams.

Here are a few most important Request properties:

  • headers - Headers object to get key/value pairs useObject.fromEntires(req.headers.entries()).
  • method - request method as a string.
  • url - string with full URL.
  • referrer - HTTP referer.
  • arrayBuffer() - Returns a promise that resolves with anArrayBuffer representation of the request body.
  • blob() - Returns a promise that resolves with aBlob representation of the request body.
  • formData() - Returns a promise that resolves with aFormData representation of the request body.
  • json() - Returns a promise that resolves with the result of parsing the request body asJSON.
  • text() - Returns a promise that resolves with a text representation of the request body.

Response object is an instance ofHTTPResponse those have methods:

  • html()
  • json()
  • text()
  • send()

each of those methods accepts string as the first argument. The second argument is options:

  • headers - any headers as key-value pairs or you can passHeaders object.
  • statusText - The status message associated with the status code, e.g., OK.
  • status - The status code for the response, e.g., 200.
  • type - Content-Type of the response (MIME).

Additional methods:

  • redirect() - accept URL or optional first argument that is the number of HTTP code
  • sse([options]) - function creates Server-Sent Event stream, the return object has a methodsend that sends a new event.
  • fetch(url | Request) - method will send a normal HTTP request to the server and return the result to the client. You can use the default Request object from the route.
  • download(data, { filename }) - a method that can be used to trigger file download. The data can be astring orarrayBuffer you can use native fetch API and callawait res.text() orawait res.arrayBuffer() and pass the result as data.

The application also has middleware as in Express.js

  • use(function(err, req, res, next) {}) 4 parameters it's an error handler
  • use(function(req, res, next) {}) 3 parameters it's a middleware

Additional exported functions:

  • FileSystem({ path: string, fs: <FS Module>, prefix: string }) - a function that creates a middleware for the file system. You should use FS that supports Service Worker like the one that usesIndexedDB e.g.BrowserFS orLightingFS.
  • rpc(channel, object) - a function that should be used in the main thread that creates an RPC-like mechanism. The first argument is an instance of a broadcast channel and the second is an object with remote functions.
  • send(channel, method: string, args: any[]) - function sends remote procedure to the main thread.

Story

The idea of using a Service worker to serve pure in-browser HTTP requests has a long history. Ifirst used this technique for myGit Web Terminal and described theusage of it in the article from 2018:How to create Web Server in Browser.In June 2022, I came up with a cool new way of using this technique. While creating PoC for thearticle I'm going to write (will update this story when ready), I realized that I can extract allthe logic of creating those fake HTTP requests into a library. This is how Wayne was born.

The name of the library was inspired by the scene inWayne's World 2 in which Wayne dresses up as a constructionworker.

Watch the video

I highly recommend both movies if you haven't seen them already.

Contribution

If you have any ideas for an improvement, don't hesitate to create an issue.Code contributions are also welcome.

Working on your first Pull Request? You can learn how from thisfree seriesHow to Contribute to an Open Source Project on GitHub

Article about or mention Wayne

Press

Acknowledge

License

Released withMIT license
Copyright (c) 2022-2024Jakub T. Jankiewicz

About

Service Worker Routing library for in browser HTTP requests

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript96.4%
  • Makefile3.6%

[8]ページ先頭

©2009-2025 Movatter.jp