PageRenderTime 52ms CodeModel.GetById 20ms RepoModel.GetById 0ms 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. >*Sommaire*
  3. >>- *Namespaces & Modules, à l'ancienne*
  4. >>- *"Loader" javascript, comme les vrais*
  5. >*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. Jaimerais ajouter la possibilité dajouter ou de modifier des posts, mais Faisons dabord le ménage et rangeons notre code..*
  6. Il y a de nombreuses querelles de chapelle autour du sujet de lorganisation du code (des façons de le faire), et je vous avoue que je nai pas encore fait complètement mon choix, mais lessentiel 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 « à lancienne » 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.
  7. >>**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 ;) ...
  8. ##"A l'ancienne"
  9. ###Namespace
  10. Créer une application Backbone, cest écrire des modèles, des vues, des templates, etc. Et une des bonnes pratiques pour parvenir à garder ceci bien organisé et dutiliser le **namespacing** (comme en .Net, Java, ) de la façon suivante :
  11. *Namespace Blog :*
  12. ```javascript
  13. var Blog = {
  14. Models: {},
  15. Collections: {},
  16. Views: {},
  17. Router: {}
  18. }
  19. ```
  20. Vous enregistrez ceci dans un fichier `Blog.js`, que vous pensez à référencer dans votre page html :
  21. ```html
  22. <script src="Blog.js"></script>
  23. ```
  24. Ainsi par la suite vous pourrez déclarer et faire référence à vos composants de la manière suivante :
  25. ```javascript
  26. Blog.Models.Post = Backbone.Model.extend({
  27. //...
  28. });
  29. Blog.Collections.Posts = Backbone.Collection.extend({
  30. //...
  31. });
  32. Blog.Views.PostForm = Backbone.View.extend({
  33. //...
  34. });
  35. //etc…
  36. ```
  37. Puis les utiliser comme ceci :
  38. ```javascript
  39. var myPost = new Blog.Models.Post();
  40. var posts = new Blog.Collections.Posts();
  41. var postForm = new Blog.Views.PostForm();
  42. ```
  43. Du coup, à la lecture du code, on voit tout de suite que `myPost` est un modèle, `posts` une collection et `postForm` une vue.
  44. Cela va donc permettre de créer dautres 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 jy 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 naurez à vous concentrer que sur une seule portion de code.
  45. Puis vous pouvez aussi organiser vos fichiers javascript dans des répertoires. Voici comment je procède :
  46. ![BB](RSRC/09_01_ORGA.png)\
  47. Je 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 `:
  48. ```html
  49. <!-- === Frameworks === -->
  50. <script src="libs/vendors/jquery-1.7.2.js"></script>
  51. <script src="libs/vendors/underscore.js"></script>
  52. <script src="libs/vendors/backbone.js"></script>
  53. <script src="libs/vendors/mustache.js"></script>
  54. <!-- === code applicatif === -->
  55. <script src="Blog.js"></script>
  56. <script src="models/Post.js"></script> <!-- Models & Collection -->
  57. <!-- Backbone Views -->
  58. <script src="views/SidebarView.js"></script>
  59. <script src="views/PostsListView.js"></script>
  60. <script src="views/MainView.js"></script>
  61. <script src="views/LoginView.js"></script>
  62. <script src="views/PostView.js"></script>
  63. <script src="routes.js"></script>
  64. ```
  65. Pour ensuite écrire mon code javascript de « lancement », clairement simplifié par rapport à ce que nous avons fait jusquici :
  66. ```html
  67. <script>
  68. $(function (){
  69. window.blogPosts = new Blog.Collections.Posts();
  70. window.mainView = new Blog.Views.MainView({
  71. collection: blogPosts
  72. });
  73. /*======= Authentification =======*/
  74. window.loginView = new Blog.Views.LoginView();
  75. /*======= Fin authentification =======*/
  76. window.postView = new Blog.Views.PostView();
  77. window.router = new Blog.Router.RoutesManager({
  78. collection: blogPosts
  79. });
  80. Backbone.history.start();
  81. });
  82. </script>
  83. ```
  84. >>**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 nhésitez pas à me contacter pour me donner des astuces pour améliorer mon organisation de code.
  85. ###Modules
  86. Jaime bien combiner la notion de module à la notion de namespace, ce qui permet dassocier à 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 » :
  87. Avant nous avions donc ceci :
  88. *Namespace Blog :*
  89. ```javascript
  90. var Blog = {
  91. Models: {},
  92. Collections: {},
  93. Views: {},
  94. Router: {}
  95. }
  96. ```
  97. Que nous allons changer par ceci :
  98. *Module+Namespace :*
  99. ```javascript
  100. var Blog = (function() {
  101. var blog = {};
  102. blog.Models = {};
  103. blog.Collections = {};
  104. blog.Views = {};
  105. blog.Router = {};
  106. return blog;
  107. }());
  108. ```
  109. Cela signifie que seul la variable `blog` sera exposée par lentremise de la variable `Blog`. Tout ce qui est entre `(function () {` et `}());` sera exécuté. A lintérieur de cette closure vous pouvez coder des variables et des méthodes privées.
  110. Ensuite vous allez pouvoir déclarer des « plug-ins » à votre module de la manière suivante (par exemple):
  111. ```javascript
  112. var Blog = (function(blog) {
  113. blog.Models.Post = Backbone.Model.extend({
  114. urlRoot: "/blogposts"
  115. });
  116. blog.Collections.Posts = Backbone.Collection.extend({
  117. model: blog.Models.Post,
  118. all: function() {
  119. this.url = "/blogposts";
  120. return this;
  121. },
  122. query: function(query) {
  123. this.url = "/blogposts/query/" + query;
  124. return this;
  125. }
  126. });
  127. return blog;
  128. }(Blog));
  129. ```
  130. ###Au final, nous aurons ...
  131. Avec 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 :
  132. *Blog.js :*
  133. ```javascript
  134. var Blog = (function() {
  135. var blog = {};
  136. blog.Models = {};
  137. blog.Collections = {};
  138. blog.Views = {};
  139. blog.Router = {};
  140. return blog;
  141. }());
  142. ```
  143. *routes.js :*
  144. ```javascript
  145. var Blog = (function(blog) {
  146. blog.Router.RoutesManager = Backbone.Router.extend({
  147. initialize: function(args) {
  148. this.collection = args.collection;
  149. },
  150. routes: {
  151. "post/:id_post": "displayPost",
  152. "hello": "hello",
  153. "*path": "root"
  154. },
  155. root: function() {
  156. this.collection.all().fetch({
  157. success: function(result) {
  158. //ça marche !!!
  159. }
  160. });
  161. },
  162. hello: function() {
  163. $(".hero-unit > h1").html("Hello World !!!");
  164. },
  165. displayPost: function(id_post) {
  166. var tmp = new blog.Models.Post({
  167. id: id_post
  168. });
  169. tmp.fetch({
  170. success: function(result) {
  171. postView.render(result);
  172. }
  173. });
  174. }
  175. });
  176. return blog;
  177. }(Blog));
  178. ```
  179. *models/post.js :*
  180. ```javascript
  181. var Blog = (function(blog) {
  182. blog.Models.Post = Backbone.Model.extend({
  183. urlRoot: "/blogposts"
  184. });
  185. blog.Collections.Posts = Backbone.Collection.extend({
  186. model: blog.Models.Post,
  187. all: function() {
  188. this.url = "/blogposts";
  189. return this;
  190. },
  191. query: function(query) {
  192. this.url = "/blogposts/query/" + query;
  193. return this;
  194. }
  195. });
  196. return blog;
  197. }(Blog));
  198. ```
  199. *views/SidebarView.js :*
  200. ```javascript
  201. var Blog = (function(blog) {
  202. blog.Views.SidebarView = Backbone.View.extend({
  203. el: $("#blog_sidebar"),
  204. initialize: function() {
  205. this.template = $("#blog_sidebar_template").html();
  206. },
  207. render: function() {
  208. var renderedContent = Mustache.to_html(this.template, {
  209. posts: this.collection.toJSON()
  210. });
  211. this.$el.html(renderedContent);
  212. }
  213. });
  214. return blog;
  215. }(Blog));
  216. ```
  217. *views/PostsListViews.js :*
  218. ```javascript
  219. var Blog = (function(blog) {
  220. blog.Views.PostsListView = Backbone.View.extend({
  221. el: $("#posts_list"),
  222. initialize: function() {
  223. this.template = $("#posts_list_template").html();
  224. },
  225. render: function() {
  226. var renderedContent = Mustache.to_html(this.template, {
  227. posts: this.collection.toJSON()
  228. });
  229. this.$el.html(renderedContent);
  230. }
  231. });
  232. return blog;
  233. }(Blog));
  234. ```
  235. *views/MainView.js :*
  236. ```javascript
  237. var Blog = (function(blog) {
  238. blog.Views.MainView = Backbone.View.extend({
  239. initialize: function() {
  240. this.collection.comparator = function(model) {
  241. return -(new Date(model.get("date")).getTime());
  242. }
  243. _.bindAll(this, 'render');
  244. this.collection.bind('reset', this.render);
  245. this.collection.bind('change', this.render);
  246. this.collection.bind('add', this.render);
  247. this.collection.bind('remove', this.render);
  248. this.sidebarView = new blog.Views.SidebarView();
  249. this.postsListView = new blog.Views.PostsListView({
  250. collection: this.collection
  251. });
  252. },
  253. render: function() {
  254. //this.collection.models = this.collection.models.reverse();
  255. this.sidebarView.collection = new blog.Collections.Posts(this.collection.first(3));
  256. this.sidebarView.render();
  257. this.postsListView.render();
  258. }
  259. });
  260. return blog;
  261. }(Blog));
  262. ```
  263. *views/LoginView.js :*
  264. ```javascript
  265. var Blog = (function(blog) {
  266. blog.Views.LoginView = Backbone.View.extend({
  267. el: $("#blog_login_form"),
  268. initialize: function() {
  269. var that = this;
  270. this.template = $("#blog_login_form_template").html();
  271. //on vérifie si pas déjà authentifié
  272. $.ajax({
  273. type: "GET",
  274. url: "/alreadyauthenticated",
  275. error: function(err) {
  276. console.log(err);
  277. },
  278. success: function(dataFromServer) {
  279. if (dataFromServer.firstName) {
  280. that.render("Bienvenue", dataFromServer);
  281. } else {
  282. that.render("???", {
  283. firstName: "John",
  284. lastName: "Doe"
  285. });
  286. }
  287. }
  288. })
  289. },
  290. render: function(message, user) {
  291. var renderedContent = Mustache.to_html(this.template, {
  292. message: message,
  293. firstName: user ? user.firstName : "",
  294. lastName: user ? user.lastName : ""
  295. });
  296. this.$el.html(renderedContent);
  297. },
  298. events: {
  299. "click .btn-primary": "onClickBtnLogin",
  300. "click .btn-inverse": "onClickBtnLogoff"
  301. },
  302. onClickBtnLogin: function(domEvent) {
  303. var fields = $("#blog_login_form :input"),
  304. that = this;
  305. $.ajax({
  306. type: "POST",
  307. url: "/authenticate",
  308. data: {
  309. email: fields[0].value,
  310. password: fields[1].value
  311. },
  312. dataType: 'json',
  313. error: function(err) {
  314. console.log(err);
  315. },
  316. success: function(dataFromServer) {
  317. if (dataFromServer.infos) {
  318. that.render(dataFromServer.infos);
  319. } else {
  320. if (dataFromServer.error) {
  321. that.render(dataFromServer.error);
  322. } else {
  323. that.render("Bienvenue", dataFromServer);
  324. }
  325. }
  326. }
  327. });
  328. },
  329. onClickBtnLogoff: function() {
  330. var that = this;
  331. $.ajax({
  332. type: "GET",
  333. url: "/logoff",
  334. error: function(err) {
  335. console.log(err);
  336. },
  337. success: function(dataFromServer) {
  338. console.log(dataFromServer);
  339. that.render("???", {
  340. firstName: "John",
  341. lastName: "Doe"
  342. });
  343. }
  344. })
  345. }
  346. });
  347. return blog;
  348. }(Blog));
  349. ```
  350. *views/PostView.js :*
  351. ```javascript
  352. var Blog = (function(blog) {
  353. blog.Views.PostView = Backbone.View.extend({
  354. el: $("#posts_list"),
  355. initialize: function() {
  356. this.template = $("#post_details_template").html();
  357. },
  358. render: function(post) {
  359. var renderedContent = Mustache.to_html(this.template, {
  360. post: post.toJSON()
  361. });
  362. this.$el.html(renderedContent);
  363. }
  364. });
  365. return blog;
  366. }(Blog));
  367. ```
  368. ... 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.
  369. ##Méthode hype, comme les vrais
  370. Plus 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 lordre dinclusion, que lon aimerait pouvoir déclencher un traitement uniquement lorsque lon est sûr que notre script est chargé, etc.
  371. Pour répondre à ces types de problématiques, il existe ce que lon appelle des loaders (chargeurs) de script, tels :
  372. - Require.js (probalement le plus connu et le plus utilisé) [http://requirejs.org/](http://requirejs.org/)
  373. - Head.js [http://headjs.com/](http://headjs.com/)
  374. - YepNope [http://yepnopejs.com/](http://yepnopejs.com/) d'Alex Sexton
  375. - Etc. ...
  376. Il y a beaucoup de débats autour des « javascript resources loaders », « est-ce bien ou mal ? » « Cela ralentit le chargement de la page web », « cest génial il faut généraliser son utilisation » , ... Mon propos nest 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 den utiliser, si ce nest à titre éducatif ou pour le plaisir).
  377. >>**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/).
  378. En ce qui nous concerne, jai 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 nest pas négligeable).
  379. ###Préparation
  380. Commencez par télécharger la dernière version de YepNope ici :
  381. - [https://github.com/SlexAxton/yepnope.js/archives/master](https://github.com/SlexAxton/yepnope.js/archives/master)
  382. Puis, dézipper et copier ensuite le fichier `yepnope.js` ou sa version minifiée (dans mon cas jai utilisé la version 1.5.4) dans le répertoire `public/libs/vendors` de votre application.
  383. Nous allons maitenant supprimer toutes les références de script que nous avions dans la page index.html :
  384. ```html
  385. <!-- === Frameworks === -->
  386. <script src="libs/vendors/jquery-1.7.2.js"></script>
  387. <script src="libs/vendors/underscore.js"></script>
  388. <script src="libs/vendors/backbone.js"></script>
  389. <script src="libs/vendors/mustache.js"></script>
  390. <!-- === code applicatif === -->
  391. <script src="Blog.js"></script>
  392. <script src="models/Post.js"></script> <!-- Models & Collection -->
  393. <!-- Backbone Views -->
  394. <script src="views/SidebarView.js"></script>
  395. <script src="views/PostsListView.js"></script>
  396. <script src="views/MainView.js"></script>
  397. <script src="views/LoginView.js"></script>
  398. <script src="views/PostView.js"></script>
  399. <script src="routes.js"></script>
  400. ```
  401. Et nous écrivons ceci à la place :
  402. ```html
  403. <script src="libs/vendors/yepnope.1.5.4-min.js"></script>
  404. <script src="main.js"></script>
  405. ```
  406. Et cest donc dans le script `main.js` que nous allons procéder au chargement de nos différents scripts de la manière suivante :
  407. *1ère utilisation de yepnope :*
  408. ```javascript
  409. yepnope({
  410. load: {
  411. jquery: 'libs/vendors/jquery-1.7.2.js',
  412. underscore: 'libs/vendors/underscore.js',
  413. backbone: 'libs/vendors/backbone.js',
  414. mustache: 'libs/vendors/mustache.js',
  415. //NameSpace
  416. blog: 'Blog.js',
  417. //Models
  418. posts: 'models/post.js',
  419. //Controllers
  420. sidebarview: 'views/SidebarView.js',
  421. postslistviews: 'views/PostsListView.js',
  422. mainview: 'views/MainView.js',
  423. loginview: 'views/LoginView.js',
  424. postview: 'views/PostView.js',
  425. //Routes
  426. routes: 'routes.js'
  427. },
  428. callback: {
  429. "routes": function() {
  430. console.log("routes loaded ...");
  431. }
  432. },
  433. complete: function() {
  434. //...
  435. }
  436. });
  437. ```
  438. Vous laurez compris, le paramètre `load` sert à définir les scripts à charger. Vous notez aussi que lon peut donner un alias à chacun des scripts (sinon YepNope le fera automatiquement à partir du nom du fichier javascript), alias que lon 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 cest au moment de linclusion du fichier `routes.js`).
  439. Et enfin, il y a aussi le paramètre `complete` qui permet de lancer un traitement une fois tous les scripts inclus.
  440. Nous allons donc déplacer le javascript restant dans notre page à lintérieur de complete, pour finalement obtenir ceci :
  441. *Code définitif :*
  442. ```javascript
  443. yepnope({
  444. load: {
  445. jquery: 'libs/vendors/jquery-1.7.2.js',
  446. underscore: 'libs/vendors/underscore.js',
  447. backbone: 'libs/vendors/backbone.js',
  448. mustache: 'libs/vendors/mustache.js',
  449. //NameSpace
  450. blog: 'Blog.js',
  451. //Models
  452. posts: 'models/post.js',
  453. //Controllers
  454. sidebarview: 'views/SidebarView.js',
  455. postslistviews: 'views/PostsListView.js',
  456. mainview: 'views/MainView.js',
  457. loginview: 'views/LoginView.js',
  458. postview: 'views/PostView.js',
  459. //Routes
  460. routes: 'routes.js'
  461. },
  462. callback: {
  463. "routes": function() {
  464. console.log("routes loaded ...");
  465. }
  466. },
  467. complete: function() {
  468. $(function() {
  469. console.log("Lauching application ...");
  470. window.blogPosts = new Blog.Collections.Posts();
  471. window.mainView = new Blog.Views.MainView({
  472. collection: blogPosts
  473. });
  474. /*======= Authentification =======*/
  475. window.loginView = new Blog.Views.LoginView();
  476. /*======= Fin authentification =======*/
  477. window.postView = new Blog.Views.PostView();
  478. window.router = new Blog.Router.RoutesManager({
  479. collection: blogPosts
  480. });
  481. Backbone.history.start();
  482. });
  483. }
  484. });
  485. ```
  486. Et voilà ! Vous disposez maintenant dun code structuré, dun 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é quune infime partie de YepNope qui en dépit de sa taille est très puissant. Lisez la documentation, vous verrez
  487. Maintenant que notre projet est "propre", nous allons dans le chapitre suivant en profiter pour sécuriser un peu plus notre application.