Frontend – for backend developers – Managing dependencies



Frontend – for backend developers – Managing dependencies

1 0


front-for-back-devs


On Github jelz / front-for-back-devs

Frontend

for backend developers

Jakub Elzbieciak

Target audience

Backend developers who:

  • have to work on client-side, but don't like it at all
  • are cooperating with frontend developers in their work
  • would like to work on client-side, but don't know how to start (and do it right later on)

Agenda

  • Managing in-browser dependencies
  • Building client-side projects
  • Writing modular JavaScript code
  • Designing and consuming stateless APIs
  • Wiring up cross-domain communication
  • Demo + Q&A

Managing dependencies

Frontend has its own composer

Still checking 3rd party libraries into your repo?

Please don't.

When was the last time you've pushed /vendor?

Introducing bower

Dependency management tool for frontend code!

Just create bower.json file.

Even better – use generator: bower init.

Installing jQuery

bower install jquery --save

Angular needed?

bower install angular --save

I would definitely make use of moment.js:

bower install moment --save

bower.json file

{
  "name": "awesome-app",
  "version": "0.0.1",
  "authors": ["Jakub Elzbieciak <jelz@post.pl>"],
  "license": "MIT",
  "private": true,
  "dependencies": {
    "jquery": "~2.1.1",
    "angular": "~1.2.16",
    "moment": "~2.6.0"
  }
}
        

private option

bower is not only about installing dependencies.

You can also publish your code in the bower repository.

When working on your client's code, private option protects you from publishing code by accident.

Always getting the same version

bower uses schema described in semantic version proposal.

As long as you use exact version notation and GitHub tag doesn't change, you'll be getting the same code.

shrinkwrap feature is on its way there (think composer.lock file).

Topic covered?

Nope.

echo /bower_components >> .gitignore

Now we're done.

Building projects

Shorten distance between your IDE and your browser

Problems with assetic

In assetic, during the development, your webserver prepares and manipulates asset collection on every request.

On the deploy day, in production mode, you do assetic:dump.

If you're not lucky enough, then you'll end up tracking missing ; the whole afternoon.

Did I say that you can't relay on filenames then?

Leaner approach

Scripts, styles, images, fonts – these are all static files.

On change of a source file, regenerate related static assets.

Then access the asset using (un)limited greatness of HTTP.

Introducing Grunt.js

It's like ant, but gets you covered with nearly every frontend task that you've could imagine.

More than 30 official plugins. > 2.5k community plugins.

Including: cleaning, copying, linting, concatenating, minifying, testing, building, compiling, optimizing...

Let's start with Grunt

Install executable: sudo npm install -g grunt-cli

Install locally: npm install grunt --save-dev

Install first plugin:

npm install grunt-contrib-jshint --save-dev

touch Gruntfile.js

Our first task

grunt.initConfig({
    jshint: {
        all: {
            src: ['Gruntfile.js', 'index.js', 'src/**/*.js']
        }
    }
});
        

Execute:

[ jakub@mainframe: ~/Projects/awsome-app (develop *) ]$ grunt jshint:all
Running "jshint:all" task
>> 80 files lint free.

Done, without errors.
        

Pack it together

Install plugin:

npm install grunt-contrib-concat --save-dev

{
    concat: {
        options: { separator: ';' },
        bundle: {
            src: ['src/**/*.js', '!src/**/*Test.js'],
            dest: 'public/js/bundle.js'
        }
    }
}
        

Use it (0.2s for 80 file that you've seen before):

{% if !app.debug %}

{% endif %}
        

Add some style

Plugin:

npm install grunt-contrib-less --save-dev

{
    less: {
        bundle {
            options: {
                relativeUrls: true,
                paths: ['bower_components/bootstrap/less']
            },
            files: {
                'css/bundle.css': ['main.less', 'src/**/*.less']
            }
        }
    }
}
        
{% if !app.debug %}

{% endif %}
        

CTRL+S

Plugin:

npm install grunt-contrib-watch --save-dev

{
    watch: {
        all: {
            files: ['src/**/*.js', 'src/**/**/*.less', 'src/**/*.html'],
            tasks: ['less:bundle', 'concat:bundle],
        }
    }
}
        

Fire and forget:

[ jakub@mainframe: ~/Projects/awsome-app (develop *) ]$ grunt watch:all
Running "watch:all" (watch) task
Waiting...

>> File "app.js" changed.
Running "concat:bundle" (concat) task
(...)
        

Notify your browser

grunt-contrib-watch supports livereload out of the box.

{
    watch: {
        all: { /* ... */ },
        options: {
            livereload: true
        }
    }
}
        

Reload webapp when any related source changes:

{% if app.debug %}

{% endif %}
        

Continuous everything

I've never run into the situation when my CTRL+S sequence took longer than a quater of a second.

So I lint every file on CTRL+S. So I test every file on CTRL+S.

I mark build as failed on any error and push OS notification.

I see the problem in top right corner of my screen, my app doesn't work until I fix both lint and test errors.

No dirty hacks, no technological debt. Only good code sees the daylight.

Writing modular code

require that works in a browser

The simplest module

The simplest module system in the front-end world is file.

One functionality goes into one file, template goes into second file, some utility functions go into thrid file...

Each file is attached into webapp using <script /> tag.

Problems with files

These files has to modify global namespace somehow All JavaScript files has to be attached in the right order Loading files asynchronously (that means faster) leads to complicated "semaphore" code Maintaining long list of files quickly becomes pain in the brain

Solution 1: AMD

AMD stands for Asynchronous Module Definition.

Let's save this in calc.js file:

define(function() {
    return {
        add: function(a, b) {
            return a+b;
        }
    };
});
        

Use it in main.js:

define(function(require) {
    var calc = require('calc');
    console.log(calc.add(2, 3)); // 6
});
        

AMD, part 2

How does it work?

Just attach AMD loader and point to the entry point of application:

<script
    src="/bower_components/requirejs/require.js"
    data-main="js/main.js"
></script>
        

All modules used in require calls will be resolved asynchronously on browser's runtime.

Beside of asynchronous loading, dependency order is kept!

Solution 2: CommonJS

Lets save this in calc.js:

module.exports = {
    mul: function(a, b) {
        return a*b;
    }
};
        

Use it in main.js:

var calc = require('./calc');

console.log(calc.mul(2, 8)); // 16
        

CommonJS, part 2

How does it work?

It doesn't. Right now. In the browser.

You have to build it, starting at your application's entry point. Modules will be tracked and included in one file:

browserify main.js > build/bundle.js
        

The code will be concatenated in the right order, so everything will work just fine:


    

Comparison chart

AMD CommonJS Build step required No Yes Asynchronous loading Yes No Native browser support Full In the future Template handling Using plugin Using extension

Essential tools

For AMD: r.js optimizer that compacts modules into one file, so in production server won't be DDoSed with requests.

For CommonJS: component.js that introduces extended build system, handling templates, CSS files and image files the same way we do with JavaScript modules.

Working with APIs

Get SoC and good architecture for free

Single Page Apps

Create stateless, RESTfull HTTP API that exposes data, behaviors and business logic Create rich JavaScript client that renders user interface for interacting with the API Setup communication channel between these two parts

Profit!

Separation of Concerns: API handles database-heavy stuff: entity relations, transactions, business processing. Client handles entire User Experience.

Good architecture: each part of application serves its own purpose and doesn't know too much its counterparts. Wanna native smartphone application? API's already there.

Scalability: is API stateless? Then scaling is about hardware $$$, not developers and time.

Building blocks

Token-based authentication

Identity provider

Stateless API

localStorage

Cross-Origin Resource Sharing

Token-base auth

Assume that we've got confirmed user identity.

Generate pseudo-random token, link it with user's account and return the token to client. Store it on client side and sign every request to API with it:

PUT /user/123
X-App-Token: 391e962681972d5625fc3574e112a4bdae603550
        

Server can determine user's identity and prepare personalized response to be returned. HTTP 403 is one of personalized responses, too.

Identity provider

We've assumed that we've confirmed identity of user.

Common approach proposes integrating users database with with token database, with simple flow: give us your credentials, we'll give you token.

But there, we're creating identity provider on our own. Keep in mind that identity can be validated by Facebook, Google, Mozilla Persona, some OAuth2 implementation, etc.

Only token store is specific to your application.

Stateless API

Sessions and cookies are not cool. Stateless APIs are cool, because they don't have concept of session.

Stateless API should, given the proper information (correct token and request body), be able to create complete response for client application. What happened before and will happen in the future shouldn't matter at all.

Stateless APIs are really cool, because with n HTTP nodes, any of these nodes can process any request without any overhead and additional communication. Horizontal scaling!

localStorage

We have to keep some data on client-side. Token, user identity, some application-wide configuration, cached hashmap that represents access records, etc.

Use localStorage:

localStorage.setItem('app.token', '391e962681972d5625fc3574e112a4bdae603550');

// later on
$.ajax({
    type: 'GET', url: '/api/user/me',
    headers: { 'X-App-Token': localStorage.getItem('app.token') }
});
        

There's also sessionStorage with shorter lifetime.

CORS

Longer: Cross-Origin Resource Sharing.

Topic of the next chapter ↗

API documentation

There probably will be two teams working on this type of applications: backend and frontend team.

It's nice to have a contract (in a form of API documentation) between teams to follow and rely on.

Things to specify: API paths, HTTP methods used, allowed headers, required body structure and format, query string parameters, constraints on parameters, response format and response codes.

For Symfony2, there's NelmioApiDocBundle.

Talking cross-domain

Distribute parts of your system in < 100 LOC

Hungry?

There's a JavaScript application that allows us to order food: http://zjedzmyobiadek.pl/.

When a meal, through UI, is composed, request hits http://zjedzmyobiadek.pl/api/1.0/order.

Works just fine.

No meat today

API's getting bigger – API team moved it on another server: http://api.zjedzmyobiadek.pl/.

Mr Zbigniew, sys-admin in a big Polish corporation, wants to integrate intranet page with meal ordering service. Application is hosted under: http://intra.gliwice.zus.gov.pl/.

Attempt to distribute system. Result: it doesn't work.

Security

Every modern browser checks the destination of every AJAX-based request.

If the destination differs from the orgin, communication is cutted down.

Difference is checked on every possible level: domain, subdomain, port and protocol level.

It's called Same origin policy.

Dirty hack - JSONP

Register some function:

window.jsonpCb = function(data) { console.log(data); };
        

Add <script /> tag (same origin policy doesn't apply):

$('body').append('');            
        
If the given URL returns:
jsonpCb({ message: 'hello', test: true });
        

then we've got a connection.

But it's GET / query string limited. And looks ugly. There has to be a better way.

Setup CORS

When doing an AJAX-base request, set a domain name as Origin header.

If server's response has Access-Control-Allow-Origin set to the same value, then connection will be estabilished.

Allowed methods and optional headers should be specified using Access-Control-Allow-Methods and Access-Control-Allow-Headers.

Finally some PHP code

protected function prepareHeaders(Request $req) {
    $originHeader = $req->headers->get('Origin');
    $allowedOrigin = null;

    foreach ($this->allowedOrigins as $pattern) {
        if (preg_match($pattern, $originHeader)) {
            $allowedOrigin = $originHeader;
            break;
        }
    }

    return array(
        'Access-Control-Allow-Origin' => $allowedOrigin,
        'Access-Control-Allow-Methods' => 'GET, POST, PUT, OPTIONS',
        'Access-Control-Allow-Headers' => 'X-App-Token, Content-Type'
    );
}
        

Preflight request

So called preflight request is just OPTIONS /api/call that should respond with 200 OK if it's safe to continue.

When using AJAX with CORS, browser will do preflight request in two cases:

method of AJAX call is other than HEAD, GET, POST AJAX call contains non-standard headers

Preflight Controller

Symfony2 controller service + routing configuration:

<service
    id="zjedzmy_core.controller.preflight"
    class="Zjedzmy\CoreBundle\Controller\PreflightController"
/>
        
<import
    resource="zjedzmy_core.controller.preflight"
    type="rest" prefix="/api/1.0"
/>
        

Action for all OPTIONS calls:

/**
 * @Rest\Options("/{slug}", requirements={"slug": ".*"})
 */
public function preflightAction() {
    return new Response();
}            
        

Now...

... we can create something.

Demo

Q&A

Thank you!