Lessons learned modernizing Frontend

Lessons learned modernizing Frontend

tldr; Strategy taken on rebuilding our main website

The past years few year we gradually began to change out main application from a gigantic jQuery monolithic structure to a more modular and service oriented application. The current application was not bad, it did share code with our other sites by composer and separation of code was in place. But we wanted es6 and more modularity and also to remove the dependency of jQuery in the future. In this document I will discuss the strategy we have taken and the hurdles we had to overcome.

Since we could replace the whole site we needed to have a gradual approach, which settled on the final strategy:

1 – keep the LAMP stack for the SEO important parts of the site, might one day migrate to a SSR react solution if the services in the backend are ready

2- migrate the JavaScript needed for the LAMP stack frontend from JQuery and Composer to ES6 and NPM/Webppack

3- Replace the dynamic parts of the site, filters, search results, price grids etc with React.

4- Keep everything in the Frontend in sync with a global frontend application state with the help of Redux, this state should only store information needed for the business logic. This step really made it possible to slowly migrate to a newer stack in the frontend.

5- create a event dispatcher which uses native javaScript to replace the jQuery event system. On difficulty we had was to replace The Jquery Ajax Complete event, Mostly in IE11

build sytem

The build system we previously had was heavily dependent on Assetic by symfony and composer. We replaced this with NPM and Webpack. In webpack we created 3 main build roads for the JavaScript

1- legacy code: we import the legacy scripts with ‘script-loader’, the best way would be to rewrite all to es6 modules, unfortunately we have literally hundreds and hundreds of legacy files tightly connected to jQuery.

2- modern js: es6 code new code is written here en sometimes legacy code is migrated to this section, shared code is moved to a jest unit tested repo, shared with npm instead of composer vendor module

3- react: we decided to make the dynamic parts (filtering and result listings) of out website with react

Styling

Our website was based on compass, which provided us with a lot of magic, but also a lot of breaking code when we tried to remove the dependency on Compass

First step we replace all the styling which depended on Ruby functions of Compass and we include all the uses scss compass mixins our self.

Second step was to remove file per file the dependency on the Compass mixins this also took some time

Replacing jQuery modules

we had lots and lots of jQuery modules, which sometimes caused problems and fixing sometimes was hard because of the size of some of the modules. For the future we decided to replace all the modules ourselves. In the end a basic Modal, Slider eg. are actually very simple. Currently out newly written modules are living in a separate repo, the modules only change css classes and styling is not available. Styling is the responsibility of the site which is implementing the module.

testing

Testing was also added to the stack, we used some testing in the past with Jasmine, since Jest looks a lot like Jasmine and has a lot of momentum we made sure that the new shared repos we tested with Jest. Jest proved a really nice experience. We use it for React as well as for regular es6 modules. A lot of things worked seamlessly even integrating locale and formatting libraries.

Prevent regression brought us to cypress.io a brilliant suite. The one drawback of cypress is that it only works with chrome, but luckily the functional changes and features between browser do not differ as much as they did in the past. The advantages however are enormous, in half a day I had several integration tests running on my machine. And one day later we had cypress integrated in Jenkins. Cypress also helps with keeping the tests clean by checking  and testing on the fly. Elements not found result in an test error for example.

Lessons learned

Always stick as close as you can to the W3C standards, to future proof you site. Be hesitant to use plugins and pick them wisely, for sliders for example we chose the excellent Siema slider (https://github.com/pawelgrzybek/siema) which is a perfect es6 minimal slider which makes it easy to build complex sliders with. like the example below.

Another standard is NPM this is the starting point for our front-end scripts, and it will be even when the new Webpack arives. Composer also hooks into npm scripts to have a single update command.

Also make sure you create a system for “Single source of truth”. This really helps with keeping things clear,and keeping the decreasing “old” in sync with the increasing “new”. We picked redux for this, but there are several options.

When you choose (or are forced) to slowly migrate legacy to modern frontend, then it is no shame to use some global variables. As long as you can remove it when the full migration has been fulfilled. For example we use globals for: the state, the event systems and the error logging.

Svg in JavaScript

Recently I had to implement some SVG into JavaScript for a yourtube placeholder for lazy loading and it turned out to be quite easy. The main thing you need to be aware of is the different namespace which svg is using. After that it becomes very easy. You can run the following in the console and see the youtube play button.

const svgNS = 'http://www.w3.org/2000/svg';

const playButton = document.createElementNS(svgNS, 'svg');
playButton.setAttribute('fill', 'red');
playButton.setAttribute('width', '72');
playButton.setAttribute('height', '72');
playButton.setAttribute('viewbox', '0 0 72 72');
playButton.setAttribute('style', 'position: absolute; top: 50%; left: 50%; transform: translate(-36px, -36px)');

const rect = document.createElementNS(svgNS,'rect');
rect.setAttribute('fill', 'white');
rect.setAttribute('x', '10');
rect.setAttribute('y', '15');
rect.setAttribute('width', '52');
rect.setAttribute('height', '42');

const youtube = document.createElementNS(svgNS,'path');
youtube.setAttributeNS(null, 'd', 'M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z');
youtube.setAttribute('transform', 'scale(3)');

playButton.appendChild(rect);
playButton.appendChild(youtube);

document.body.appendChild(playButton);

Using SVG in this way makes it also more easy to create interactive SVG’s, it is easy to add an id to each SVG element and add some behavour.

Overcoming CORS on localhost

Recently I wanted to consume the Google places API and use it in a react view. Immediately I got stuck on the CORS (Cross-Origin Resource Sharing) notification, which made it impossible to use the api.

I tried several solutions, booting chrome with specific security flags, extensions and proxying with browsersync. Nothing worked on my Ubuntu machine.

Solution create your own Node serverside endpoint.

I created a express endpoint which passes the server-side result of the api call to http://locahost:3003/api . You can use it by installing express (express-framework) and calling the script below in node.

const express = require('express');
const fetch = require('node-fetch');

const proxyService = express();
const host = 'localhost';
const port = 3003;

proxyService.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
});

proxyService.get('/api', function(req, res, next) {
    fetch(`https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=51.985103,5.898730&radius=500&types=point_of_interest&key=YOUR_API_KEY`)
        .then(function(res) {
            return res.json();
        }).then(function(json) {
        res.send(json);
    });
});

const server = proxyService.listen(port, function () {
    console.log('API result available at http://%s:%s', host, port);
});