PageRenderTime 40ms CodeModel.GetById 2ms app.highlight 32ms RepoModel.GetById 2ms app.codeStats 0ms

/09-ORGANISATION-CODE.md

https://github.com/fabiansky/backbone.en.douceur
Markdown | 662 lines | 487 code | 175 blank | 0 comment | 0 complexity | 43e2aa3577311a73caac982d2985a0f4 MD5 | raw file
  1#Organiser son code
  2
  3>*Sommaire*
  4
  5>>- *Namespaces & Modules, à l'ancienne*
  6>>- *"Loader" javascript, comme les vrais*
  7
  8
  9>*Notre application (côté client) tient dans une seule page. Elle mélange du code HTML et du code Javascript, et cela dans un seul fichier commence à devenir difficilement lisible, difficilement modifiable et donc difficilement maintenable. J’aimerais ajouter la possibilité d’ajouter ou de modifier des posts, mais … Faisons d’abord le ménage et rangeons notre code..*
 10
 11Il y a de nombreuses querelles de chapelle autour du sujet de l’organisation du code (des façons de le faire), et je vous avoue que je n’ai pas encore fait complètement mon choix, mais l’essentiel est de produire quelque chose qui fonctionne (correctement) et que vous pourrez facilement faire évoluer. Je vais donc vous présenter 2 méthodes, la méthode « à l’ancienne » qui a le mérite de fonctionner, d’être rapide, et la méthode « hype » pour les champions qui est intéressante à connaître tout particulièrement dans le cadre de gros projets.
 12
 13>>**Remarque** : pour la méthode "hype", j'utilise le(s) outil(s) (yepnope) que je préfère, il est tout à fait possible de suivre le principe décrit avec d'autres comme require.js et d'autres ... Je n'ai pas la science infuse, je me sens plus à l'aise avec YepNope ... c'est tout, et tant que ça marche ;) ...
 14
 15##"A l'ancienne"
 16
 17###Namespace
 18
 19Créer une application Backbone, c’est écrire des modèles, des vues, des templates, etc. … Et une des bonnes pratiques pour parvenir à garder ceci bien organisé et d’utiliser le **namespacing** (comme en .Net, Java, …) de la façon suivante :
 20
 21*Namespace Blog :*
 22
 23```javascript
 24var Blog = {
 25	Models: {},
 26	Collections: {},
 27	Views: {},
 28	Router: {}
 29}
 30```
 31
 32Vous enregistrez ceci dans un fichier `Blog.js`, que vous pensez à référencer dans votre page html :
 33
 34```html
 35<script src="Blog.js"></script>
 36```
 37
 38Ainsi par la suite vous pourrez déclarer et faire référence à vos composants de la manière suivante :
 39
 40```javascript
 41Blog.Models.Post = Backbone.Model.extend({
 42	//...
 43});
 44
 45Blog.Collections.Posts = Backbone.Collection.extend({
 46	//...
 47});
 48Blog.Views.PostForm = Backbone.View.extend({
 49	//...
 50});
 51
 52//etc…
 53```
 54
 55
 56Puis les utiliser comme ceci :
 57
 58```javascript
 59var myPost = new Blog.Models.Post();
 60var posts = new Blog.Collections.Posts();
 61var postForm = new Blog.Views.PostForm();
 62```
 63
 64Du coup, à la lecture du code, on voit tout de suite que `myPost` est un modèle, `posts` une collection et `postForm` une vue.
 65
 66Cela va donc permettre de créer d’autres fichiers javascript spécifiques à chacun de vos composants, par exemple un fichier `post.js` avec le code de votre modèle `post` *(en général j’y ajoute aussi la collection correspondante)*, puis autant de fichiers javascript que de vues. Cela va augmenter le nombre de vos fichiers, mais à chaque modification vous n’aurez à vous concentrer que sur une seule portion de code.
 67
 68Puis vous pouvez aussi organiser vos fichiers javascript dans des répertoires. Voici comment je procède :
 69
 70![BB](RSRC/09_01_ORGA.png)\
 71
 72Je crée un répertoire `libs/vendors` pour tous les scripts « qui ne sont pas de moi » (jquery, backbone, etc. …), puis je range mes modèles dans un répertoire `models`, mes vues dans un répertoire `views`. Au même endroit que ma page `index.html`, je positionne le fichier `routes.js `(mon routeur) ainsi que le fichier `Blog.js` qui contient mes **« namspaces »**, et enfin je déclare tout ceci dans ma page `index.html `:
 73
 74
 75```html
 76<!-- === Frameworks === -->
 77<script src="libs/vendors/jquery-1.7.2.js"></script>
 78<script src="libs/vendors/underscore.js"></script>
 79<script src="libs/vendors/backbone.js"></script>
 80<script src="libs/vendors/mustache.js"></script>
 81
 82<!-- === code applicatif === -->
 83
 84<script src="Blog.js"></script>
 85<script src="models/Post.js"></script> <!-- Models & Collection -->
 86
 87<!-- Backbone Views -->
 88<script src="views/SidebarView.js"></script>
 89<script src="views/PostsListView.js"></script>
 90<script src="views/MainView.js"></script>
 91<script src="views/LoginView.js"></script>
 92<script src="views/PostView.js"></script>
 93
 94<script src="routes.js"></script>
 95```
 96
 97Pour ensuite écrire mon code javascript de « lancement », clairement simplifié par rapport à ce que nous avons fait jusqu’ici :
 98
 99```html
100<script>
101	$(function (){
102
103	window.blogPosts = new Blog.Collections.Posts();
104
105	window.mainView = new Blog.Views.MainView({
106		collection: blogPosts
107	});
108
109	/*======= Authentification =======*/
110	window.loginView = new Blog.Views.LoginView();
111	/*======= Fin authentification =======*/
112
113	window.postView = new Blog.Views.PostView();
114
115
116	window.router = new Blog.Router.RoutesManager({
117		collection: blogPosts
118	});
119	Backbone.history.start();
120	});
121</script>
122```
123
124>>**Remarque** : Vous n’êtes pas obligés de faire comme moi, adaptez selon vos goûts ou les normes imposées sur les projets. Et n’hésitez pas à me contacter pour me donner des astuces pour améliorer mon organisation de code.
125
126
127###Modules
128
129J’aime bien combiner la notion de module à la notion de namespace, ce qui permet d’associer à notre namespace une notion de variable ou de fonctions privées, mais aussi de créer un système de plugin. Je vous montre avec le code, ce sera plus « parlant » :
130
131Avant nous avions donc ceci :
132
133*Namespace Blog :*
134
135```javascript
136var Blog = {
137  Models: {},
138  Collections: {},
139  Views: {},
140  Router: {}
141}
142```
143
144Que nous allons changer par ceci :
145
146*Module+Namespace :*
147
148```javascript
149var Blog = (function() {
150  var blog = {};
151
152  blog.Models = {};
153  blog.Collections = {};
154  blog.Views = {};
155  blog.Router = {};
156
157  return blog;
158  }());
159```
160
161Cela signifie que seul la variable `blog` sera exposée par l’entremise de la variable `Blog`. Tout ce qui est entre `(function () {` et `}());` sera exécuté. A l’intérieur de cette closure vous pouvez coder des variables et des méthodes privées.
162
163Ensuite vous allez pouvoir déclarer des « plug-ins » à votre module de la manière suivante (par exemple):
164
165```javascript
166var Blog = (function(blog) {
167
168  blog.Models.Post = Backbone.Model.extend({
169    urlRoot: "/blogposts"
170
171  });
172
173  blog.Collections.Posts = Backbone.Collection.extend({
174    model: blog.Models.Post,
175    all: function() {
176      this.url = "/blogposts";
177      return this;
178    },
179    query: function(query) {
180      this.url = "/blogposts/query/" + query;
181      return this;
182    }
183
184  });
185
186  return blog;
187}(Blog));
188```
189
190###Au final, nous aurons ...
191
192Avec les principes décrits plus haut, nous allons donc pouvoir "découper" notre code afin de bien tout ordonner, et nous obtiendrons les fichiers suivants :
193
194*Blog.js :*
195
196```javascript
197var Blog = (function() {
198  var blog = {};
199
200  blog.Models = {};
201  blog.Collections = {};
202  blog.Views = {};
203  blog.Router = {};
204
205  return blog;
206}());
207```
208
209*routes.js :*
210
211```javascript
212var Blog = (function(blog) {
213
214  blog.Router.RoutesManager = Backbone.Router.extend({
215    initialize: function(args) {
216      this.collection = args.collection;
217    },
218    routes: {
219      "post/:id_post": "displayPost",
220      "hello": "hello",
221      "*path": "root"
222    },
223    root: function() {
224      this.collection.all().fetch({
225        success: function(result) {
226          //ça marche !!!
227        }
228      });
229    },
230
231    hello: function() {
232      $(".hero-unit > h1").html("Hello World !!!");
233    },
234
235    displayPost: function(id_post) {
236
237      var tmp = new blog.Models.Post({
238        id: id_post
239      });
240
241      tmp.fetch({
242        success: function(result) {
243          postView.render(result);
244        }
245      });
246    }
247  });
248
249  return blog;
250}(Blog));
251```
252
253*models/post.js :*
254
255```javascript
256var Blog = (function(blog) {
257
258  blog.Models.Post = Backbone.Model.extend({
259    urlRoot: "/blogposts"
260  });
261
262  blog.Collections.Posts = Backbone.Collection.extend({
263    model: blog.Models.Post,
264    all: function() {
265      this.url = "/blogposts";
266      return this;
267    },
268    query: function(query) {
269      this.url = "/blogposts/query/" + query;
270      return this;
271    }
272  });
273
274  return blog;
275}(Blog));
276```
277
278*views/SidebarView.js :*
279
280```javascript
281var Blog = (function(blog) {
282
283  blog.Views.SidebarView = Backbone.View.extend({
284    el: $("#blog_sidebar"),
285    initialize: function() {
286      this.template = $("#blog_sidebar_template").html();
287    },
288    render: function() {
289      var renderedContent = Mustache.to_html(this.template, {
290        posts: this.collection.toJSON()
291      });
292
293      this.$el.html(renderedContent);
294    }
295  });
296
297  return blog;
298}(Blog));
299```
300
301*views/PostsListViews.js :*
302
303```javascript
304var Blog = (function(blog) {
305
306blog.Views.PostsListView = Backbone.View.extend({
307  el: $("#posts_list"),
308  initialize: function() {
309    this.template = $("#posts_list_template").html();
310  },
311  render: function() {
312    var renderedContent = Mustache.to_html(this.template, {
313      posts: this.collection.toJSON()
314    });
315
316    this.$el.html(renderedContent);
317  }
318});
319
320return blog;
321}(Blog));
322```
323
324*views/MainView.js :*
325
326```javascript
327var Blog = (function(blog) {
328
329  blog.Views.MainView = Backbone.View.extend({
330    initialize: function() {
331
332      this.collection.comparator = function(model) {
333        return -(new Date(model.get("date")).getTime());
334      }
335
336      _.bindAll(this, 'render');
337      this.collection.bind('reset', this.render);
338      this.collection.bind('change', this.render);
339      this.collection.bind('add', this.render);
340      this.collection.bind('remove', this.render);
341
342      this.sidebarView = new blog.Views.SidebarView();
343      this.postsListView = new blog.Views.PostsListView({
344        collection: this.collection
345      });
346
347    },
348    render: function() {
349
350      //this.collection.models = this.collection.models.reverse();
351      this.sidebarView.collection = new blog.Collections.Posts(this.collection.first(3));
352      this.sidebarView.render();
353      this.postsListView.render();
354    }
355  });
356
357  return blog;
358}(Blog));
359```
360
361*views/LoginView.js :*
362
363```javascript
364var Blog = (function(blog) {
365
366  blog.Views.LoginView = Backbone.View.extend({
367    el: $("#blog_login_form"),
368
369    initialize: function() {
370      var that = this;
371      this.template = $("#blog_login_form_template").html();
372
373      //on vérifie si pas déjà authentifié
374      $.ajax({
375        type: "GET",
376        url: "/alreadyauthenticated",
377        error: function(err) {
378          console.log(err);
379        },
380        success: function(dataFromServer) {
381
382          if (dataFromServer.firstName) {
383            that.render("Bienvenue", dataFromServer);
384          } else {
385            that.render("???", {
386              firstName: "John",
387              lastName: "Doe"
388            });
389          }
390        }
391      })
392
393    },
394
395    render: function(message, user) {
396
397      var renderedContent = Mustache.to_html(this.template, {
398        message: message,
399        firstName: user ? user.firstName : "",
400        lastName: user ? user.lastName : ""
401      });
402      this.$el.html(renderedContent);
403    },
404    events: {
405      "click  .btn-primary": "onClickBtnLogin",
406      "click  .btn-inverse": "onClickBtnLogoff"
407    },
408    onClickBtnLogin: function(domEvent) {
409
410      var fields = $("#blog_login_form :input"),
411        that = this;
412
413      $.ajax({
414        type: "POST",
415        url: "/authenticate",
416        data: {
417          email: fields[0].value,
418          password: fields[1].value
419        },
420        dataType: 'json',
421        error: function(err) {
422          console.log(err);
423        },
424        success: function(dataFromServer) {
425
426          if (dataFromServer.infos) {
427            that.render(dataFromServer.infos);
428          } else {
429            if (dataFromServer.error) {
430              that.render(dataFromServer.error);
431            } else {
432              that.render("Bienvenue", dataFromServer);
433            }
434          }
435
436        }
437      });
438    },
439    onClickBtnLogoff: function() {
440
441      var that = this;
442      $.ajax({
443        type: "GET",
444        url: "/logoff",
445        error: function(err) {
446          console.log(err);
447        },
448        success: function(dataFromServer) {
449          console.log(dataFromServer);
450          that.render("???", {
451            firstName: "John",
452            lastName: "Doe"
453          });
454        }
455      })
456    }
457
458  });
459
460  return blog;
461}(Blog));
462```
463
464*views/PostView.js :*
465
466```javascript
467var Blog = (function(blog) {
468
469  blog.Views.PostView = Backbone.View.extend({
470    el: $("#posts_list"),
471    initialize: function() {
472      this.template = $("#post_details_template").html();
473    },
474    render: function(post) {
475      var renderedContent = Mustache.to_html(this.template, {
476        post: post.toJSON()
477      });
478
479      this.$el.html(renderedContent);
480    }
481  });
482
483  return blog;
484  }(Blog));
485```
486
487... Sauvegardez tout ça et essayez, normalement cela devrait fonctionner et vous verrez qu'à l'usage, le code en devient plus lisible. Mais passons donc à la 2ème méthode.
488
489##Méthode “hype”, comme les vrais
490
491Plus les fichiers javascript se multiplient, plus la gestion des `<script src=”…”>` devient pénible, sans compter que dans certains cas il est nécessaire de gérer l’ordre d’inclusion, que l’on aimerait pouvoir déclencher un traitement uniquement lorsque l’on est sûr que notre script est chargé, etc. …
492
493Pour répondre à ces types de problématiques, il existe ce que l’on appelle des “loaders” (chargeurs) de script, tels :
494
495- Require.js (probalement le plus connu et le plus utilisé) [http://requirejs.org/](http://requirejs.org/)
496- Head.js [http://headjs.com/](http://headjs.com/)
497- YepNope [http://yepnopejs.com/](http://yepnopejs.com/) d'Alex Sexton
498- Etc. ...
499
500Il y a beaucoup de débats autour des « javascript resources loaders », « est-ce bien ou mal ? » « Cela ralentit le chargement de la page web », « c’est génial il faut généraliser son utilisation » , ... Mon propos n’est pas de participer au débat mais de vous montrer « rapidement » de quelle façon on peut les utiliser. (Cependant si votre application est simple, cela ne vaut pas la peine d’en utiliser, si ce n’est à titre éducatif ou pour le plaisir).
501
502>>**Remarque** : si vous souhaitez creuser le sujet je vous engage à lire « Non-onload-blocking async JS » de  Stoyan Stefanov : [http://www.phpied.com/non-onload-blocking-async-js/](http://www.phpied.com/non-onload-blocking-async-js/).
503
504En ce qui nous concerne, j’ai choisi **YepNope** parce que nettement plus simple (plus léger aussi) que Require.js et développé par Alex Sexton, ce qui est un gage de qualité (excellent développeur javascript et qui prend le temps de répondre à vos questions, ce qui n’est pas négligeable).
505
506###Préparation
507
508Commencez par télécharger la dernière version de YepNope ici :
509
510- [https://github.com/SlexAxton/yepnope.js/archives/master](https://github.com/SlexAxton/yepnope.js/archives/master)
511
512Puis, dézipper et copier ensuite le fichier `yepnope.js` ou sa version minifiée (dans mon cas j’ai utilisé la version 1.5.4) dans le répertoire `public/libs/vendors` de votre application.
513
514Nous allons maitenant supprimer toutes les références de script que nous avions dans la page index.html :
515
516```html
517<!-- === Frameworks === -->
518<script src="libs/vendors/jquery-1.7.2.js"></script>
519<script src="libs/vendors/underscore.js"></script>
520<script src="libs/vendors/backbone.js"></script>
521<script src="libs/vendors/mustache.js"></script>
522
523<!-- === code applicatif === -->
524
525<script src="Blog.js"></script>
526<script src="models/Post.js"></script> <!-- Models & Collection -->
527
528<!-- Backbone Views -->
529<script src="views/SidebarView.js"></script>
530<script src="views/PostsListView.js"></script>
531<script src="views/MainView.js"></script>
532<script src="views/LoginView.js"></script>
533<script src="views/PostView.js"></script>
534
535<script src="routes.js"></script>
536```
537
538Et nous écrivons ceci à la place :
539
540```html
541<script src="libs/vendors/yepnope.1.5.4-min.js"></script>
542<script src="main.js"></script>
543```
544
545Et c’est donc dans le script `main.js` que nous allons procéder au chargement de nos différents scripts de la manière suivante :
546
547*1ère utilisation de yepnope :*
548
549```javascript
550yepnope({
551  load: {
552    jquery: 'libs/vendors/jquery-1.7.2.js',
553    underscore: 'libs/vendors/underscore.js',
554    backbone: 'libs/vendors/backbone.js',
555    mustache: 'libs/vendors/mustache.js',
556
557    //NameSpace
558    blog: 'Blog.js',
559
560    //Models
561    posts: 'models/post.js',
562
563    //Controllers
564    sidebarview: 'views/SidebarView.js',
565    postslistviews: 'views/PostsListView.js',
566    mainview: 'views/MainView.js',
567    loginview: 'views/LoginView.js',
568    postview: 'views/PostView.js',
569
570    //Routes
571    routes: 'routes.js'
572
573  },
574
575  callback: {
576    "routes": function() {
577      console.log("routes loaded ...");
578    }
579  },
580  complete: function() {
581    //...
582  }
583});
584```
585
586Vous l’aurez compris, le paramètre `load` sert à définir les scripts à charger. Vous notez aussi que l’on peut donner un alias à chacun des scripts (sinon YepNope le fera automatiquement à partir du nom du fichier javascript), alias que l’on peut ensuite utiliser dans le paramètre `callback` pour déclencher un traitement une fois que le script est inclus dans la page (dans notre exemple c’est au moment de l’inclusion du fichier `routes.js`).
587
588Et enfin, il y a aussi le paramètre `complete` qui permet de lancer un traitement une fois tous les scripts inclus.
589
590Nous allons donc déplacer le javascript restant dans notre page à l’intérieur de complete, pour finalement obtenir ceci :
591
592*Code définitif :*
593
594```javascript
595yepnope({
596  load: {
597    jquery: 'libs/vendors/jquery-1.7.2.js',
598    underscore: 'libs/vendors/underscore.js',
599    backbone: 'libs/vendors/backbone.js',
600    mustache: 'libs/vendors/mustache.js',
601
602    //NameSpace
603    blog: 'Blog.js',
604
605    //Models
606    posts: 'models/post.js',
607
608    //Controllers
609    sidebarview: 'views/SidebarView.js',
610    postslistviews: 'views/PostsListView.js',
611    mainview: 'views/MainView.js',
612    loginview: 'views/LoginView.js',
613    postview: 'views/PostView.js',
614
615    //Routes
616    routes: 'routes.js'
617
618  },
619
620  callback: {
621    "routes": function() {
622      console.log("routes loaded ...");
623    }
624  },
625  complete: function() {
626    $(function() {
627
628      console.log("Lauching application ...");
629
630      window.blogPosts = new Blog.Collections.Posts();
631
632      window.mainView = new Blog.Views.MainView({
633        collection: blogPosts
634      });
635
636      /*======= Authentification =======*/
637      window.loginView = new Blog.Views.LoginView();
638      /*======= Fin authentification =======*/
639
640      window.postView = new Blog.Views.PostView();
641
642      window.router = new Blog.Router.RoutesManager({
643        collection: blogPosts
644      });
645
646      Backbone.history.start();
647
648    });
649  }
650});
651```
652
653Et voilà ! Vous disposez maintenant d’un code structuré, d’un outil de chargement de script facile à utiliser et modifier : désactivation ou changement provisoire de librairie pour tests par exemple mais aussi chargement conditionnel de script en fonction du contexte, … Je ne vous ai dévoilé qu’une infime partie de YepNope qui en dépit de sa taille est très puissant. Lisez la documentation, vous verrez …
654
655Maintenant que notre projet est "propre", nous allons dans le chapitre suivant en profiter pour sécuriser un peu plus notre application.
656
657
658
659
660
661
662