On Github jelz / front-for-back-devs
Jakub Elzbieciak
Backend developers who:
Frontend has its own composer
Please don't.
When was the last time you've pushed /vendor?
Dependency management tool for frontend code!
Just create bower.json file.
Even better – use generator: bower init.
bower install jquery --save
Angular needed?
bower install angular --save
I would definitely make use of moment.js:
bower install moment --save
{ "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" } }
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.
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).
Nope.
echo /bower_components >> .gitignore
Now we're done.
Shorten distance between your IDE and your browser
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?
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.
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...
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
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.
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 %}
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 %}
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 (...)
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 %}
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.
require that works in a browser
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.
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 });
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!
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
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:
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.
Get SoC and good architecture for free
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.
Token-based authentication
Identity provider
Stateless API
localStorage
Cross-Origin Resource Sharing
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.
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.
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!
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.
Longer: Cross-Origin Resource Sharing.
Topic of the next chapter ↗
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.
Distribute parts of your system in < 100 LOC
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.
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.
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.
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.
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.
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' ); }
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 headersSymfony2 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(); }
... we can create something.