web-dev-qa-db-fra.com

Comment rendre correctement des vues partielles et charger des fichiers JavaScript dans AJAX en utilisant Express / Jade?

Résumé

J'utilise Express + Jade pour mon application Web et j'ai du mal à rendre des vues partielles pour ma navigation AJAX.

J'ai en quelque sorte deux questions différentes, mais elles sont totalement liées, je les ai donc incluses dans le même message. Je suppose que ce sera un long post, mais je vous garantis que c'est intéressant si vous avez déjà eu du mal avec les mêmes problèmes. J'apprécierais beaucoup que quelqu'un prenne le temps de lire et de proposer une solution.

TL; DR: 2 questions

  • Quelle est la méthode la plus propre et la plus rapide pour rendre des fragments de vues pour un = AJAX navigation avec Express + Jade?
  • Comment charger les fichiers JavaScript relatifs à chaque vue?

Exigences

  • Mon application Web doit être compatible avec les utilisateurs qui ont désactivé
    JavaScript
  • Si JavaScript est activé, seul le contenu de la page (et non la mise en page entière) doit être envoyé du serveur au client
  • L'application doit être rapide et charger le moins d'octets possible

Problème n ° 1: ce que j'ai essayé

Solution 1: avoir différents fichiers pour AJAX & requêtes non AJAX

Mon layout.jade est:

doctype html
    html(lang="fr")
        head
            // Shared CSS files go here
            link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
        body
            div#main_content
                block content
            // Shared JS files go here
            script(src="js/jquery.min.js")

Ma page_full.jade est:

extends layout.jade

block content
    h1 Hey Welcome !

Ma page_ajax est:

h1 Hey Welcome

Et enfin dans router.js (Express):

app.get("/page",function(req,res,next){
   if (req.xhr) res.render("page_ajax.jade");
   else res.render("page_full.jade");
});

Inconvénients :

  • Comme vous l'avez probablement deviné, je dois modifier mes vues deux fois à chaque fois que je dois changer quelque chose. Assez frustrant.

Solution 2: même technique avec "include"

Mon layout.jade reste inchangé:

doctype html
    html(lang="fr")
        head
            // Shared CSS files go here
            link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
        body
            div#main_content
                block content
            // Shared JS files go here
            script(src="js/jquery.min.js")

Ma page_full.jade est maintenant:

extends layout.jade

block content
   include page.jade

Ma page.jade contient le contenu réel sans aucune mise en page/bloc/étendre/inclure:

h1 Hey Welcome

Et j'ai maintenant dans router.js (Express):

app.get("/page",function(req,res,next){
   if (req.xhr) res.render("page.jade");
   else res.render("page_full.jade");
});

Avantages :

  • Maintenant, mon contenu n'est défini qu'une seule fois, c'est mieux.

Inconvénients :

  • J'ai encore besoin de fichiers deux pour la page un.
  • Je dois utiliser la même technique dans Express sur chaque route. Je viens de déplacer mon problème de répétition de code de Jade vers Express. Casser.

Solution 3: identique à la solution 2 mais corrige le problème de répétition du code.

En utilisant la technique d'Alex Ford , je pouvais définir ma propre fonction render dans middleware.js :

app.use(function (req, res, next) {  
    res.renderView = function (viewName, opts) {
        res.render(viewName + req.xhr ? null : '_full', opts);
        next();
    };
 });

Et puis changez router.js (Express) en:

app.get("/page",function(req,res,next){
    res.renderView("/page");
});

laissant les autres fichiers inchangés.

Avantages

  • Il a résolu le problème de répétition du code

Inconvénients

  • J'ai encore besoin de fichiers deux pour la page un.
  • Définir ma propre méthode renderView semble un peu sale. Après tout, je m'attends à ce que mon moteur/framework de modèle gère cela pour moi.

Solution 4: déplacer la logique vers Jade

Je n'aime pas utiliser les fichiers deux pour la page un, alors que se passe-t-il si je laisse Jade décider quoi rendre au lieu d'Express? À première vue, cela me semble très inconfortable, car je pense que le moteur de modèle ne devrait pas gérer la logique du tout. Mais essayons.

Tout d'abord, je dois passer une variable à Jade qui lui dira de quel type de demande il s'agit:

Dans middleware.js (Express)

app.use(function (req, res, next) {  
    res.locals.xhr = req.xhr;
 });

Alors maintenant, mon layout.jade serait le même qu'avant:

doctype html
    html(lang="fr")
        head
            // Shared CSS files go here
            link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
        body
            div#main_content
                block content
            // Shared JS files go here
            script(src="js/jquery.min.js")

Et ma page.jade serait:

if (!locals.xhr)
    extends layout.jade

block content
   h1 Hey Welcome !

Super hein? Sauf que cela ne fonctionnera pas car les extensions conditionnelles sont impossibles dans Jade. J'ai donc pu déplacer le test de page.jade vers layout.jade :

if (!locals.xhr)
    doctype html
       html(lang="fr")
           head
               // Shared CSS files go here
               link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
           body
               div#main_content
                    block content
                // Shared JS files go here
                script(src="js/jquery.min.js")
 else
     block content

et page.jade reviendrait à:

extends layout.jade

block content
   h1 Hey Welcome !

Avantages :

  • Maintenant, je n'ai qu'un seul fichier par page
  • Je n'ai pas à répéter le test req.xhr Dans chaque itinéraire ou dans chaque vue

Inconvénients :

  • Il y a de la logique dans mon modèle. Pas bon

Sommaire

Ce sont toutes des techniques auxquelles j'ai pensé et essayé, mais aucune ne m'a vraiment convaincu. Est-ce que je fais quelque chose de mal ? Existe-t-il des techniques plus propres? Ou dois-je utiliser un autre moteur/framework de modèle?


Problème n ° 2

Que se passe-t-il (avec l'une de ces solutions) si une vue possède ses propres fichiers JavaScript?

Par exemple, en utilisant la solution # 4, si j'ai deux pages, page_a.jade et page_b.jade qui ont tous deux leurs propres fichiers JavaScript côté client js/page_a.js et js/page_b.js , que leur arrive-t-il lorsque les pages sont chargées en AJAX?

Tout d'abord, je dois définir un bloc extraJS dans layout.jade :

if (!locals.xhr)
    doctype html
       html(lang="fr")
           head
               // Shared CSS files go here
               link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
           body
               div#main_content
                    block content
                // Shared JS files go here
                script(src="js/jquery.min.js")
                // Specific JS files go there
                block extraJS
 else
     block content
     // Specific JS files go there
     block extraJS

puis page_a.jade serait:

extends layout.jade

block content
   h1 Hey Welcome !
block extraJS
   script(src="js/page_a.js")

Si je tapais localhost/page_a Dans ma barre d'URL (demande non AJAX), j'obtiendrais une version compilée de:

doctype html
       html(lang="fr")
           head
               link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
            body
               div#main_content
                  h1 Hey Welcome A !
               script(src="js/jquery.min.js")
               script(src="js/page_a.js")

Ça à l'air bon. Mais que se passerait-il si je passais maintenant à page_b En utilisant ma navigation AJAX? Ma page serait une version compilée de:

doctype html
       html(lang="fr")
           head
               link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
           body
               div#main_content
                  h1 Hey Welcome B !
                  script(src="js/page_b.js")
               script(src="js/jquery.min.js")
               script(src="js/page_a.js")

js/page_a.js et js/page_b.js sont tous deux chargés sur la même page. Que se passe-t-il en cas de conflit (même nom de variable etc ...)? De plus, si je reviens à localhost/page_a en utilisant AJAX, j'aurais ceci:

doctype html
       html(lang="fr")
           head
               link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
           body
               div#main_content
                  h1 Hey Welcome B !
                  script(src="js/page_a.js")
               script(src="js/jquery.min.js")
               script(src="js/page_a.js")

Le même fichier JavaScript ( page_a.js ) est chargé deux fois sur la même page! Va-t-il provoquer des conflits, un double déclenchement de chaque événement? Que ce soit le cas ou non, je ne pense pas que ce soit du code propre.

Vous pourriez donc dire que des fichiers JS spécifiques devraient être dans mon block content Afin qu'ils soient supprimés lorsque je vais sur une autre page. Ainsi, mon layout.jade devrait être:

if (!locals.xhr)
    doctype html
       html(lang="fr")
           head
               // Shared CSS files go here
               link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
           body
               div#main_content
                    block content
                    block extraJS
                // Shared JS files go here
                script(src="js/jquery.min.js")
 else
     block content
     // Specific JS files go there
     block extraJS

Droite ? Euh .... Si je vais à localhost/page_a, Je vais obtenir une version compilée de:

doctype html
       html(lang="fr")
           head
               link(type="text/css",rel="stylesheet",href="css/bootstrap.min.css")
            body
               div#main_content
                  h1 Hey Welcome A !
                  script(src="js/page_a.js")
               script(src="js/jquery.min.js")

Comme vous l'avez peut-être remarqué, js/page_a.js est en fait chargé avant jQuery, donc ça ne marchera pas , car jQuery n'est pas encore défini ... Donc je ne sais pas quoi faire pour ce problème . J'ai pensé à gérer les requêtes de script côté client, en utilisant (par exemple) jQuery.getScript(), mais le client devrait connaître le nom de fichier des scripts, voir s'ils sont déjà chargés, peut-être les supprimer. Je ne pense pas que cela devrait être fait côté client.

Comment dois-je gérer les fichiers JavaScript chargés via AJAX? Côté serveur en utilisant un moteur de stratégie/modèle différent? Côté client?

Si vous êtes arrivé jusqu'ici, vous êtes un vrai héros, et je vous en suis reconnaissant, mais je vous serais encore plus reconnaissant si vous pouviez me donner quelques conseils :)

38
Waldo Jeffers

Grande question. Je n'ai pas une option parfaite, mais je vous proposerai une variante de votre solution # 3 que j'aime. Même idée que la solution # 3 mais déplacez le modèle jade pour le fichier _full dans votre code, car il est passe-partout et javascript peut le générer en cas de besoin pour une page complète. Avertissement: non testé, mais je suggère humblement:

app.use(function (req, res, next) {
    var template = "extends layout.jade\n\nblock content\n    include ";  
    res.renderView = function (viewName, opts) {
        if (req.xhr) {
            var renderResult = jade.compile(template + viewName + ".jade\n", opts);
            res.send(renderResult);
        } else {
            res.render(viewName, opts);
        }
        next();
    };
 });

Et vous pouvez devenir plus intelligent avec cette idée à mesure que vos scénarios deviennent plus compliqués, par exemple en enregistrant ce modèle dans un fichier avec des espaces réservés pour les noms de fichiers.

Bien sûr, ce n'est pas encore une solution parfaite. Vous implémentez des fonctionnalités qui devraient vraiment être gérées par votre moteur de modèle, de la même manière que votre objection d'origine à la solution # 3. Si vous finissez par écrire plus de quelques dizaines de lignes de code pour cela, essayez de trouver un moyen d'intégrer la fonctionnalité dans Jade et de leur envoyer une demande d'extraction. Par exemple, si le mot clé jade "extend" a pris un argument qui pourrait désactiver l'extension de la mise en page pour les requêtes xhr ...

Pour votre deuxième problème, je ne suis pas sûr qu'un moteur de modèle puisse vous aider. Si vous utilisez la navigation ajax, vous ne pouvez pas très bien "décharger" page_a.js lorsque la navigation s'effectue via une magie de modèle d'arrière-plan. Je pense que vous devez utiliser des techniques d'isolation javascript traditionnelles pour cela (côté client). À vos préoccupations spécifiques: implémentez la logique (et les variables) spécifiques à la page dans les fermetures, pour commencer, et établissez une coopération judicieuse entre elles si nécessaire. Deuxièmement, vous n'avez pas à vous soucier trop de connecter des gestionnaires d'événements doubles. En supposant que le contenu principal est effacé lors de la navigation ajax et que ce sont également les éléments auxquels les gestionnaires d'événements ont été attachés qui appellent get reset (bien sûr) lorsque le nouveau contenu ajax est chargé dans le DOM.

3
Segfault

Je ne sais pas si cela va vraiment vous aider, mais j'ai résolu le même problème avec Express et le modèle ejs.

mon dossier:

  • app.js
  • views/header.ejs
  • views/page.ejs
  • views/footer.ejs

mon app.js

app.get('/page', function(req, res) {
    if (req.xhr) {
        res.render('page',{ajax:1});
    } else {
        res.render('page',{ajax:0});
    }
});

ma page.ejs

<% if (ajax == 0) { %> <% include header %> <% } %>

content

<% if (ajax == 0) { %> <% include footer %><% } %>

très simple mais fonctionne parfaitement.

1
lmaix

Il existe plusieurs façons de faire ce que vous avez demandé, mais elles sont toutes mauvaises sur le plan architectural. Vous ne le remarquerez peut-être pas maintenant, mais ce sera de pire en pire avec le temps.

Cela semble bizarre à ce stade, mais vous ne voulez pas vraiment vous transmettre JS, CSS via AJAX.

Ce que vous voulez, c'est pouvoir obtenir le balisage via AJAX et faire du Javascript après cela. De plus, vous voulez être flexible et rationnel en faisant cela. L'évolutivité est également importante.

La voie commune est:

  1. Préchargez tous les fichiers js pourraient être utilisés sur la page particulière et ses gestionnaires de requêtes AJAX. Utilisez browserify si vous avez peur des conflits de noms potentiels ou du script Chargez-les de manière asynchrone ( script async ) pour ne pas faire attendre l'utilisateur.

  2. Développez une architecture qui vous permet de faire, fondamentalement, ceci chez le client: loadPageAjax('page_a').then(...).catch(...). Le point est, puisque vous avez pré-téléchargé tous les modules JS, de vous exécuter le code lié à la page AJAX à l'appelant de la demande et non à l'appelé .

Donc, votre Solution # 4 est presque bonne. Utilisez-le simplement pour récupérer le code HTML, mais pas les scripts.

Cette technique permet d'atteindre votre objectif sans construire une architecture obscure, mais en utilisant la seule méthode robuste à objectif limité.

Sera heureux de répondre à toutes vos questions et de vous convaincre de ne pas faire ce que vous allez faire.

0
Kirill Rogovoy