- What is javascript routing
- Пишем современный маршрутизатор на JavaScript
- Требования
- Добавление и удаление маршрутизаторов
- Получение текущего пути
- Навигация
- Наблюдаем за изменениями
- Заключение
- Routing in SPAs
- Why we should use Routing in SPA
- How a routing will help you
- How to achieve this
- History API
- A library that does the heavy lifting
What is javascript routing
One of the more important pieces of any client-side JavaScript framework is the routing of requests when users navigate to different places. Basically a JavaScript router in a client-side application will listen for changes in the hash location and update the data and UI in the page accordingly, making all of the API calls that it needs to do so. But how is this feat accomplished? How do we match a route pattern defined in a JavaScript framework to an actual route navigated to by the user? This is something we will look at in what follows.
Normally, when you talk about matching you immediately think to look at regular expressions. However, the way that routes are defined in client side applications often have patterns that are a little bit different that the actual routes that will be navigated to by users. What do we mean by this? We talked about routing in Backbone.js here. Basically a Backbone.js router is set up like the following…
var Router = Backbone.Router.extend(< routes: < "": "home", // url:event that fires "new": "createNew" "edit/:id": "editItem", "download/*anything": "downloadItem" >, home: function() < alert('We have loaded the home view'); >, createNew: function() < alert('We are going to create something new'); >editItem: function(idParam) < alert('We are going to edit entry number ' + idParam); >>); var router = new Router;
In this setup, when a user navigates to a particular route, the function defined in the routes object will run. So if a user were to navigate to the route #/edit/4, the editItem function will run passing in the id 4.
Notice the parameterized definition of :id with the colon. The approach you have to take when writing a JavaScript router is to match a route like #/edit/4 to a route like edit/:id. It’s the same situation with other JavaScript libraries as well. Let’s look at an AngularJS router. This is an example router from a recipe book application written in Angular…
app.config(function ($routeProvider) < $routeProvider .when('/', < controller: 'HomeController', templateUrl: 'app/views/home.html' >) .when('/login', < controller: 'LoginController', templateUrl: 'app/views/login.html' >) .when('/logout', < controller: 'LogoutController', templateUrl: 'app/views/login.html' >) .when('/signup', < controller: 'SignUpController', templateUrl: 'app/views/signup.html' >) .when('/recipes', < controller: 'RecipesAllController', templateUrl: 'app/views/recipes-all.html' >) .when('/recipe/:id', < controller: 'RecipeDetailsController', templateUrl: 'app/views/recipe-details.html' >) .when('/new', < controller: 'RecipeCreateController', templateUrl: 'app/views/recipe-create.html' >) .when('/edit/:id', < controller: 'RecipeEditController', templateUrl: 'app/views/recipe-edit.html' >) .otherwise(< redirectTo: '/' >); >).run( function($rootScope, $location, userService) < // listener to watch all route changes $rootScope.$on("$routeChangeStart", function(event, next, current) < >); >);
Notice here the same kind of parameterized matching in the routes /recipe/:id and edit/:id. How do you sort through these defined routes when a user navigates to a route to match them up with the desired code that you want to run?
Below is a function that we’ll use to match routes. If we had, say, an object literal of key value pairs of defined routes and callback functions to run for those routes, basically what it does is iterate through all the key value pairs in the collection. It checks to see if the route is a “match” and if it is it will return an object with information about the route. In this simple example, this will essentially boil down to the parameter value that was passed into the route, but we will get a bit more sophisticated with it a bit later.
So if our object literal looked like the following…
var routes = < "/": function() < console.log('This is the home route'); >, "/about": function() < console.log('This is the about route'); >, "/edit/:id": function(obj) < console.log('This is the edit route route with the id ' + obj.id); >, >
Our routing function for matching routes would look like the following…
function matchRoute(url, definedRoute) < // Current route url (getting rid of '#' in hash as well): var urlSegments = url.split('/'); var routeSegments = definedRoute.split('/'); var routeObject = <>; if(urlSegments.length !== routeSegments.length) < // not a match return false; >else < for(var i = 0; i < urlSegments.length; i++) < if(urlSegments[i].toLowerCase() === routeSegments[i].toLowerCase()) < // matched path continue; >else if (routeSegments[i].indexOf(':') === 0) < // matched a param, remove query string (which is handled below) and push id onto object var val = routeSegments[i].replace(':',''); val = val.split('?')[0]; routeObject[val] = urlSegments[i].split('?')[0]; >else < // not a match return false; >> > // after all is finished, return the route object to pass to router for use by controller return routeObject; >
Now if we were going to put this to use, we could create another function to handle changes in the hash and call that function when we listen for changes in the hash …
Finally, we need to make sure that we listen for changes in the hash…
// Listen on hash change. window.addEventListener('hashchange', routerHandler); // Listen on page load. window.addEventListener('load', routerHandler);
Looking pretty good so far. We could also handle the cases where we have a query string (e.g. /#/edit/4?food=cheese). If we update our matchRoute function to include the following, we can handle this scenario…
function matchRoute(url, definedRoute) < // Current route url (getting rid of '#' in hash as well): var urlSegments = url.split('/'); var routeSegments = definedRoute.split('/'); var routeObject = <>; if(urlSegments.length !== routeSegments.length) < // not a match return false; >else < for(var i = 0; i < urlSegments.length; i++) < if(urlSegments[i].toLowerCase() === routeSegments[i].toLowerCase()) < // matched path continue; >else if (routeSegments[i].indexOf(':') === 0) < // matched a param, remove query string (which is handled below) and push id onto object var val = routeSegments[i].replace(':',''); val = val.split('?')[0]; routeObject[val] = urlSegments[i].split('?')[0]; >else < // not a match return false; >> > // did we reach the end? Get querystring, if any. var hash = window.location.hash.split("?")[1]; if(typeof hash !== "undefined") < var queryString = hash.split('&'); var queryStringObject = <>; for(var i = 0; i < queryString.length; i++) < var currentParameter = queryString[i].split("="); queryStringObject[currentParameter[0]] = currentParameter[1]; >routeObject.queryString = queryStringObject; > // after all is finished, return the route object to pass to router for use by controller return routeObject; >
Our example above shows one possible way to handle hash-based routing in JavaScript. There might be other ways to do this. Utilization of RegEx in some manner might be another route (*bad pun groan*) to go, but all in all, whatever you choose, as we have seen here, it does not have to be needlessly complicated. Take a look at the demo below…
Пишем современный маршрутизатор на JavaScript
Простые одностраничные приложения, основанные на React, Vue или чистом JavaScript, окружают нас повсюду. Хороший «одностраничник» предполагает соответствующий механизм маршрутизации.
Такие библиотеки, как «navigo» или «react-router», приносят большую пользу. Но как они работают? Необходимо ли нам импортировать всю библиотеку? Или достаточно какой-то части, скажем, 10%? В действительности, быстрый и полезный маршрутизатор можно легко написать самому, это займет немного времени, а программа будет состоять менее чем из 100 строчек кода.
Требования
Наш маршрутизатор должен быть:
- маршрутизаторы (routes): список зарегистрированных маршрутизаторов
- режим (mode): хеш или история
- корневой элемент (root): корневой элемент приложения, если мы находимся в режиме использования истории
- конструктор (constructor): основная функция для создания нового экземпляра маршрутизатора
class Router < routes = [] mode = null root = '/' constructor(options) < this.mode = window.history.pushState ? 'history' : 'hash' if (options.mode) this.mode = options.mode if (options.root) this.root = options.root >> export default Router
Добавление и удаление маршрутизаторов
Добавление и удаление маршрутизаторов осуществляется через добавление и удаление элементов массива:
class Router < routes = [] mode = null root = '/' constructor(options) < this.mode = window.history.pushState ? 'history' : 'hash' if (options.mode) this.mode = options.mode if (options.root) this.root = options.root >add = (path, cb) => < this.routes.push(< path, cb >) return this > remove = path => < for (let i = 0; i < this.routes.length; i += 1) < if (this.routes[i].path === path) < this.routes.slice(i, 1) return this >> return this > flush = () => < this.routes = [] return this >> export default Router
Получение текущего пути
Мы должны знать, где находимся в приложении в определенный момент времени.
Для этого нам потребуется обработка обоих режимов (истории и хеша). В первом случае, нам нужно удалить путь к корневому элементу из window.location, во втором — «#». Нам также необходима функция (clearSlash) для удаления всех маршрутизаторов (строки от начала до конца):
[. ] clearSlashes = path => path .toString() .replace(/\/$/, '') .replace(/^\//, '') getFragment = () => < let fragment = '' if (this.mode === 'history') < fragment = this.clearSlashes(decodeURI(window.location.pathname + window.location.search)) fragment = fragment.replace(/\?(.*)$/, '') fragment = this.root !== '/' ? fragment.replace(this.root, '') : fragment >else < const match = window.location.href.match(/#(.*)$/) fragment = match ? match[1] : '' >return this.clearSlashes(fragment) > > export default Router
Навигация
Ок, у нас имеется API для добавления и удаления URL. Также у нас имеется возможность получать текущий адрес. Следующий шаг — навигация по маршрутизатору. Работаем со свойством «mode»:
[. ] getFragment = () => < let fragment = '' if (this.mode === 'history') < fragment = this.clearSlashes(decodeURI(window.location.pathname + window.location.search)) fragment = fragment.replace(/\?(.*)$/, '') fragment = this.root !== '/' ? fragment.replace(this.root, '') : fragment >else < const match = window.location.href.match(/#(.*)$/) fragment = match ? match[1] : '' >return this.clearSlashes(fragment) > navigate = (path = '') => < if (this.mode === 'history') < window.history.pushState(null, null, this.root + this.clearSlashes(path)) >else < window.location.href = `$#$` > return this > > export default Router
Наблюдаем за изменениями
Теперь нам нужна логика для отслеживания изменений адреса как с помощью ссылки, так и с помощью созданного нами метода «navigate». Также нам необходимо обеспечить рендеринг правильной страницы при первом посещении. Мы могли бы использовать состояние приложения для регистрации изменений, однако в целях изучения сделаем это с помощью setInterval:
class Router < routes = []; mode = null; root = "/"; constructor(options) < this.mode = window.history.pushState ? "history" : "hash"; if (options.mode) this.mode = options.mode; if (options.root) this.root = options.root; this.listen(); >[. ] listen = () => < clearInterval(this.interval) this.interval = setInterval(this.interval, 50) >interval = () => < if (this.current === this.getFragment()) return this.current = this.getFragment() this.routes.some(route =>< const match = this.current.match(route.path) if (match) < match.shift() route.cb.apply(<>, match) return match > return false >) > > export default Router
Заключение
Наша библиотека готова к использованию. Она состоит всего лишь из 84 строчек кода!
Код и пример использования на Github.
Routing in SPAs
Routing is a concept that exists in server-side applications for a long time.
In a short version, it’s the concept of mapping URL patterns to parts of your application.
Why we should use Routing in SPA
Routing is not an obligation to have in your SPA(single page application), but sure is something worth your time. You and your SPA users will appreciate that you took the time to implement this. Take a look at this example of a SPA without Routing: You have the following website → yourwebsite.com You have a Landing Page, an About Page to talk about yourself, and a Blog Page where you share your posts. Now imagine that they want to go to your Blog Page to check your latest Post. So they click your «Blog button» and they see this. Everything seems fine, you could manage the views with some «if» statements and everything is in place. Until someone tries to share the post or bookmark it. Look at the URL at the top of the image. It says: «yourwebsite.com/». Guess what will show up when you enter URL in a different tab and press enter? Yes, it will show the landing page. Why? Because we are not saving the state into the only thing that at the moment survives a reload, the URL. This is why URLs are much more than just the text that shows in the Address bar, it’s the state of your application at that exact point(not all the state, but the minimum that you need to get the user back into the point they left).
How a routing will help you
As I said before, routing is the concept of mapping URL Patterns into parts of your application and should match the state of your application at that exact point. Probably a good URL for when you are at the Blog page could be → yourwebsite.com/blog Now, when someone enters that your, you know what they are looking for, they want to see the blog page from your website, so you just have to read the URL and show whatever is supposed to be there. Now imagine that they click in the «Post 1» and move there. Now the URL needs to have something that identifies the Post that they want to see, maybe → yourwebsite.com/blog/post-1 Again, this way you know what they are looking for. They want to see the post with the title: post-1 (to be used this way, this needs to be a unique identifier and we only use it to identify post-1) Let’s break down the URL at this page:
- yourwebsite.com/ → this is your domain, where your app lives on the webs.
- /blog/ → this is your page with all your blog posts
- /post-1 → this is your latest blogpost
With a URL like this, your SPA will be able to read it and know what information should be shown. Not always the landing page, but a specific page.
How to achieve this
To make URLs have our state we need to do two possibilities:
- Use the HistoryAPI to change the address bar, add our state to our URL, and read from it to know what to display.
- A library that helps us manage the routing.
History API
This API was introduced with HTML5 and gave us two methods to manage our URLs, pushState() and replaceState().
To achieve our goal we will need to use the pushState() method. This will allow us to add a new entry in the History of the browser, in our case, this means a new page.
The pushState() method takes three parameters: a state object, a title(ignore by all but safari), a URL.
For our example, we will only require to call it with the URL param. To do that we write this.
This line will create a new entry «yourwebsite/blog/» with the usage of the History API.
Imagine the list of your browser history, the pushState() will put a new entry on top and set our address bar to that newly created entry.
A library that does the heavy lifting
Implementing this by yourself with history.pushState() is something that takes a lot of time.
If you are working with SPA, you are probably working with libraries like React, Vue, Svelte, etc.
Those libraries already have good community projects that give us this out of the box without the need to use the time that would be better off investing in your website.
I would say, React-router for React, Vue-router for Vue.
In the case of Svelte, if you need only Client-Side Rendering use svelte-spa-router, if you need server-side-rendering you can use svelte-routing or Sapper, but the later being a framework that you work on top off.
Thanks for reading this blog post, I hope that I could how we can achieve a good routing in SPAs. After this, I will show you how we can implement Routing with Svelte and svelte-spa-router.
I will be posting more content related to Svelte and how to work with it in the next weeks, feel free to follow me here and/or in my twitter.
Looking forward to hearing your thoughts regarding this article and Svelte, share experience with this awesome tool.