prouter
v10.0.27
Published
Fast, unopinionated, minimalist client-side routing library inspired by the simplicity and flexibility of express middlewares
Downloads
1,933
Maintainers
Readme
prouter
Fast, unopinionated, minimalist client-side routing library inspired by the simplicity and flexibility of express middlewares.
Essentially, give prouter
a list of path expressions (routes) and a callback function (handler) for each one, and prouter
will automatically invoke these callbacks according to the active path in the URL.
Why prouter?
- Performance: fast and tiny size (currently under 5kb before gzipping) are both must-haves to smoothly run in any mobile or desktop browser.
- KISS principle everywhere: do only one thing and do it well, routing! Guards? conditional execution? generic pre and post middlewares? all that and more is easily achievable with prouter (see examples below).
- Learn once: express router is very powerful, flexible, and simple, why not bring a similar API to the frontend? Under the hood, prouter uses the same (wonderful) library that
express
for parsing routes path-to-regexp (so it allows the same flexibility to declare routes). Read more about the concept of middlewares here. - Unobtrusive: it is designed from the beginning to play well with vanilla JavaScript or with any other library or framework.
- Forward-thinking: written in TypeScript for the future and transpiled to es5 with UMD format for the present... thus it transparently supports any module style: es6, commonJS, AMD. By default, prouter uses the modern history API for routing.
- Unit tests for every feature are created.
Do you like Prouter? please give it a 🌟
Installation
# With NPM
npm install prouter --save
# Or with Yarn
yarn prouter --save
# Or just include it using a 'script' tag in your HTML file
<script src="https://cdn.jsdelivr.net/npm/prouter/prouter.min.js"></script>
Examples
basic
// Using es6 modules
import { browserRouter } from 'prouter';
// Instantiate the router
const router = browserRouter();
// Declare the paths and its respective handlers
router
.use('/', async (req, resp) => {
const people = await personService.find();
const html = PersonListCmp(people);
document.querySelector('.router-outlet') = html;
// end the request-response cycle
resp.end();
})
.use('/about', (req, resp) => {
document.querySelector('.router-outlet') =
`<h1>Some static content for the About page.</h1>`;
// end the request-response cycle
resp.end();
});
// start listening for navigation events
router.listen();
guard middleware which conditionally avoid executing next handlers and prevent changing the path in the URL
// Using commonJs modules
const prouter = require('prouter');
// Instantiate the router
const router = prouter.browserRouter({
processHashChange: true // this allows to process 'hash' changes in the URL.
});
// Declare the paths and its respective handlers
router
.use('*', (req, resp, next) => {
// this handler will run for any routing event, before any other handlers
const isAllowed = authService.validateHasAccessToUrl(req.path);
if (!isAllowed) {
showAlert("You haven't rights to access the page: " + destPath);
// end the request-response cycle, avoid executing other handlers
// and prevent changing the path in the URL.
resp.preventNavigation = true;
resp.end();
return;
}
// pass control to the next handler
next();
})
.use('/', (req, resp) => {
// do some stuff...
// and end the request-response cycle
resp.end();
})
.use('/admin', (req, resp) => {
// do some stuff...
// and end the request-response cycle
resp.end();
});
// start listening for navigation events
router.listen();
// programmatically try to navigate to any route in your router
router.push('/admin');
run a generic middleware (for doing some generic stuff) after running specific handlers
import { browserRouter } from 'prouter';
// Instantiate the router
const router = browserRouter();
// Declare the paths and its respective handlers
router
.use('/', async (req, resp, next) => {
const people = await personService.find();
const html = PersonListCmp(people);
document.querySelector('.router-outlet') = html;
// pass control to the next handler
next();
})
.use('*', (req, resp) => {
// do some (generic) stuff...
// and end the request-response cycle
resp.end();
});
// start listening for navigation events
router.listen();
modularize your routing code in different files using Router Group
import { browserRouter, routerGroup } from 'prouter';
// this can be in a different file for modularization of the routes,
// and then import it in your main routes file and mount it.
const productRouterGroup = routerGroup();
productRouterGroup
.use('/', (req, resp) => {
// do some stuff...
// and end the request-response cycle
resp.end();
})
.use('/create', (req, resp) => {
// do some stuff...
// and end the request-response cycle
resp.end();
})
.use('/:id(\\d+)', (req, resp) => {
const id = req.params.id;
// do some stuff with the 'id'...
// and end the request-response cycle
resp.end();
});
// Instantiate the router
const router = browserRouter();
// Declare the paths and its respective handlers
router
.use('*', (req, resp, next) => {
// this handler will run for any routing event, before any other handlers
console.log('request info', req);
// pass control to the next handler
next();
})
.use('/', (req, resp) => {
// do some stuff...
// and end the request-response cycle
resp.end();
})
// mount the product's group of handlers using this base path
.use('/product', productRouterGroup);
// start listening for the routing
router.listen();
// programmatically navigate to the detail of the product with this ID
router.push('/product/123');
full example: modularized routing, generic pre handler acting as a guard, generic post handler
import { browserRouter, routerGroup } from 'prouter';
// this can be in a different file for modularization of the routes,
// and then import it in your main routes file and mount it.
const productRouterGroup = routerGroup();
productRouterGroup
.use('/', (req, resp, next) => {
// do some stuff...
// and pass control to the next handler
next();
})
.use('/create', (req, resp, next) => {
// do some stuff...
// and pass control to the next handler
next();
})
.use('/:id(\\d+)', (req, resp, next) => {
const id = req.params.id;
// do some stuff with the 'id'...
// and pass control to the next handler
next();
});
// Instantiate the router
const router = browserRouter();
// Declare the paths and its respective handlers
router
.use('*', (req, resp, next) => {
// this handler will run for any routing event, before any other handlers
const isAllowed = authService.validateHasAccessToUrl(req.path);
if (!isAllowed) {
showAlert("You haven't rights to access the page: " + destPath);
// end the request-response cycle, avoid executing next handlers
// and prevent changing the path in the URL.
resp.preventNavigation = true;
resp.end();
return;
}
// pass control to the next handler
next();
})
.use('/', (req, resp, next) => {
const doInfiniteScroll = () => {
// do infinite scroll ...
};
const onNavigation = (navigationEvt) => {
console.log('new path', navigationEvt.oldPath);
console.log('old path', navigationEvt.newPath);
// if navigating, then remove the listener for the window.scroll.
router.off('navigation', onNavigation);
window.removeEventListener('scroll', doInfiniteScroll);
};
window.addEventListener('scroll', doInfiniteScroll);
// subscribe to the navigation event
router.on('navigation', onNavigation);
// and pass control to the next handler
next();
})
.use('/login', () => {
openLoginModal();
// as this route opens a modal, we would want to prevent navigation in this handler,
// so end the request-response cycle, avoid executing next handlers
// and prevent changing the path in the URL.
resp.preventNavigation = true;
resp.end();
})
.use('/admin', (req, resp, next) => {
// do some stuff...
// and pass control to the next handler
next();
})
// mount the product's group of handlers using this base path
.use('/product', productRouterGroup)
.use('*', (req, res, next) => {
// this handler will run for any routing event, after the other handlers
// req.listening will be true when this callback was called due to a
// client-side navigation (useful to differentiate client-side vs
// server-side rendering - when using a mix of both SSR and CSR)
if (req.listening) {
const title = inferTitleFromPath(req.path, APP_TITLE);
updatePageTitle(title);
}
// end the request-response cycle
resp.end();
});
// start listening for the routing
router.listen();
// the below code is an example about how you could capture clicks on links,
// and accordingly, trigger routing navigation in your app
// (typically, you would put it in a separated file)
export function isNavigationPath(path: string) {
return !!path && !path.startsWith('javascript:void');
}
export function isExternalPath(path: string) {
return /^https?:\/\//.test(path);
}
export function isApplicationPath(path: string) {
return isNavigationPath(path) && !isExternalPath(path);
}
document.body.addEventListener('click', (evt) => {
const target = evt.target as Element;
let link: Element;
if (target.nodeName === 'A') {
link = target;
} else {
link = target.closest('a');
if (!link) {
return;
}
}
const url = link.getAttribute('href');
// do nothing if it is not an app's internal link
if (!isApplicationPath(url)) {
return;
}
// avoid the default browser's behaviour when clicking on a link
// (i.e. do not reload the page).
evt.preventDefault();
// it is a normal app's link, so trigger the routing navigation
router.push(url);
});