Как правильно отображать частичные представления и загружать файлы JavaScript в AJAX с помощью Express/Jade?

Сводка

Я использую Express + Jade для своего веб-приложения, и я борюсь с рендерингом частичных представлений для моей навигации AJAX.

У меня есть два разных вопроса, но они полностью связаны, поэтому я включил их в один пост. Я предполагаю, что это будет длинный пост, но я гарантирую, что он будет интересным, если вы уже боролись с теми же проблемами. Я был бы очень признателен, если бы кто-то нашел время, чтобы прочитать и предложить решение.

TL;DR : 2 вопроса

  • Каков самый чистый и самый быстрый способ рендеринга фрагментов представлений для навигации AJAX с помощью Express + Jade?
  • Как следует загружать файлы JavaScript для каждого представления?

Требования

  • Мое веб-приложение должно быть совместимо с пользователями, у которых отключен
    JavaScript.
  • Если JavaScript включен, с сервера на клиент должен отправляться только собственный контент страницы (а не весь макет).
  • Приложение должно быть быстрым и загружать как можно меньше байтов.

Проблема №1: что я пробовал

Решение 1: наличие разных файлов для запросов AJAX и не-AJAX

Мой layout.jade:

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")

Мой page_full.jade:

extends layout.jade

block content
    h1 Hey Welcome !

Мой page_ajax:

h1 Hey Welcome

И, наконец, в router.js (Express):

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

Недостатки:

  • Как вы, наверное, догадались, мне приходится редактировать свои представления дважды каждый раз, когда мне нужно что-то изменить. Довольно неприятно.

Решение 2: та же техника с `include`

Мой layout.jade остается неизменным:

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")

Мой page_full.jade теперь:

extends layout.jade

block content
   include page.jade

Мой page.jade содержит фактический контент без какого-либо layout/block/extend/include:

h1 Hey Welcome

И теперь у меня в router.js (Express):

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

Преимущества:

  • Теперь мой контент определяется только один раз, это лучше.

Недостатки:

  • Мне по-прежнему нужны два файла для одной страницы.
  • Мне приходится использовать один и тот же метод в Express на каждом маршруте. Я только что перенес проблему повторения кода с Jade на Express. Щелчок.

Решение 3: то же, что и Решение 2, но устраняет проблему повторения кода.

Используя метод Алекса Форда, я смог определить моя собственная функция render в middleware.js:

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

Затем измените router.js (Express) на:

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

оставив остальные файлы без изменений.

Преимущества

  • Это решило проблему повторения кода

Недостатки

  • Мне по-прежнему нужны два файла для одной страницы.
  • Определение моего собственного метода renderView кажется немного грязным. В конце концов, я ожидаю, что мой механизм шаблонов/фреймворк справится с этим за меня.

Решение 4. Перенос логики в Jade

Мне не нравится использовать два файла для одной страницы, так что, если я позволю Jade решать, что отображать вместо Express ? На первый взгляд, мне это кажется очень неудобным, потому что я считаю, что шаблонизатор вообще не должен обрабатывать какую-либо логику. Но давайте попробуем.

Во-первых, мне нужно передать Jade переменную, которая сообщит ему, что это за запрос:

В middleware.js (Express)

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

Итак, теперь мой layout.jade будет таким же, как и раньше:

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")

И мой page.jade будет:

if (!locals.xhr)
    extends layout.jade

block content
   h1 Hey Welcome !

Отлично да? За исключением того, что это не сработает, потому что условные расширения невозможны в Jade. Поэтому я мог бы перенести тест с page.jade на 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

и page.jade вернется к:

extends layout.jade

block content
   h1 Hey Welcome !

Преимущества:

  • Теперь у меня есть только один файл на странице
  • Мне не нужно повторять тест req.xhr на каждом маршруте или в каждом представлении.

Недостатки:

  • В моем шаблоне есть логика. Фигово

Резюме

Все эти приемы я придумал и испробовал, но ни один из них меня не убедил. Я делаю что-то неправильно ? Существуют ли более чистые методы? Или мне следует использовать другой механизм шаблонов/фреймворк?


Проблема №2

Что происходит (с любым из этих решений), если в представлении есть собственные файлы JavaScript?

Например, используя решение № 4, если у меня есть две страницы, page_a.jade и page_b.jade, каждая из которых имеет свои собственные файлы JavaScript на стороне клиента js/ page_a.js и js/page_b.js, что происходит с ними, когда страницы загружаются в AJAX?

Во-первых, мне нужно определить блок extraJS в 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

и тогда page_a.jade будет:

extends layout.jade

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

Если бы я набрал localhost/page_a в адресной строке (запрос без AJAX), я бы получил скомпилированную версию:

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")

Выглядит хорошо. Но что произойдет, если я перейду к page_b с помощью навигации AJAX? Моя страница будет скомпилированной версией:

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 и js/page_b.js загружаются на одну и ту же страницу. Что произойдет, если возникнет конфликт (то же имя переменной и т. д.)? Кроме того, если я вернусь к localhost/page_a с помощью AJAX, у меня будет следующее:

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")

Один и тот же файл JavaScript (page_a.js) загружается дважды на одну и ту же страницу! Не вызовет ли это конфликтов, двойного срабатывания каждого события? Независимо от того, так это или нет, я не думаю, что это чистый код.

Таким образом, вы можете сказать, что определенные файлы JS должны быть в моем block content, чтобы они удалялись, когда я перехожу на другую страницу. Таким образом, мой 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
                    block extraJS
                // Shared JS files go here
                script(src="js/jquery.min.js")
 else
     block content
     // Specific JS files go there
     block extraJS

Правильно ? Эээ.... Если я перейду к localhost/page_a, я получу скомпилированную версию:

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")

Как вы могли заметить, js/page_a.js на самом деле загружается до jQuery, поэтому он не будет работать, потому что jQuery еще не определен... Так что я не не знаю, что делать с этой проблемой. Я думал об обработке запросов сценариев на стороне клиента, используя (например) jQuery.getScript(), но клиент должен был бы знать имя файла сценариев, посмотреть, загружены ли они уже, возможно, удалить их. Я не думаю, что это должно быть сделано на стороне клиента.

Как мне обрабатывать файлы JavaScript, загружаемые через AJAX? На стороне сервера используется другой механизм стратегии/шаблона? На стороне клиента?

Если вы зашли так далеко, вы настоящий герой, и я благодарен, но я был бы еще более благодарен, если бы вы могли дать мне несколько советов :)


person Waldo Jeffers    schedule 05.08.2014    source источник
comment
Вы выяснили, как загружать файлы javascript после рендеринга частичных фрагментов? У меня сейчас аналогичная проблема.   -  person Kyle Bachan    schedule 30.10.2014
comment
Хотел бы я +10 к этому вопросу   -  person Segfault    schedule 11.11.2014
comment
Я хотел бы проголосовать за это больше. в настоящее время я пытаюсь загрузить файлы css и js после добавления частичного. мой обходной путь отстой, но я использую обратный вызов res.render для преобразования нефритового шаблона в строку, а затем отправляю html и ссылки на другие файлы в объекте, который затем обрабатываю с помощью javascript. Я уверен, что это ужасный способ сделать это, однако мне не нравятся теги ссылок в моем теле.   -  person Snymax    schedule 23.11.2014
comment
Привет, спасибо за ответы. Я сообщил об ошибке в репозитории Jade git довольно давно (что, по сути, , не было настоящей ошибкой). Проблема, с которой я столкнулся, не связана с ней, но проверьте два последних сообщения в этой теме. Forbes Lindesay предоставил предложения относительно нашей проблемы.   -  person Waldo Jeffers    schedule 24.11.2014


Ответы (3)


Отличный вопрос. У меня нет идеального варианта, но я предложу вариант вашего решения №3, который мне нравится. Та же идея, что и в решении № 3, но переместите шаблон jade для файла _full в свой код, так как это шаблон, и javascript может генерировать его, когда это необходимо для полной страницы. отказ от ответственности: не проверено, но я скромно предлагаю:

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();
    };
 });

И вы можете использовать эту идею более разумно, поскольку ваши сценарии становятся более сложными, например, сохраняя этот шаблон в файл с заполнителями для имен файлов.

Конечно, это еще не идеальное решение. Вы реализуете функции, которые действительно должны обрабатываться вашим механизмом шаблонов, как и ваше первоначальное возражение против решения № 3. Если вы в конечном итоге напишете для этого более пары десятков строк кода, попробуйте найти способ встроить эту функцию в Jade и отправить им запрос на включение. Например, если ключевое слово jade «расширяет» принимает аргумент, который может отключить расширение макета для запросов xhr...

Что касается вашей второй проблемы, я не уверен, что какой-либо механизм шаблонов может вам помочь. Если вы используете навигацию ajax, вы не можете очень хорошо «выгрузить» page_a.js, когда навигация происходит с помощью какой-то магии внутреннего шаблона. Я думаю, что для этого (на стороне клиента) вам нужно использовать традиционные методы изоляции javascript. Что касается ваших конкретных проблем: для начала реализуйте логику (и переменные) для конкретной страницы в замыканиях и разумно взаимодействуйте между ними, где это необходимо. Во-вторых, вам не нужно слишком беспокоиться о подключении обработчиков двойных событий. Предполагая, что основной контент очищается при навигации ajax, а также это элементы, к которым были прикреплены обработчики событий, которые вызовут сброс (конечно), когда новый контент ajax загружается в DOM.

person Segfault    schedule 11.11.2014
comment
Это интересный ответ @Segfault, спасибо. Я выбрал это решение № 4 для своего приложения. Что касается второй проблемы, хорошие предложения, хотя мне придется не согласиться с вами в части событий. События могут быть привязаны к документу (делегирование событий...) или к окну (resize, это ужасный пример, но вы поняли идею). - person Waldo Jeffers; 24.11.2014
comment
да, хороший момент @WaldoJeffers, я полагаю, вам все еще нужно немного беспокоиться о событиях. Я думаю, что это трудно решить для общего случая... - person Segfault; 24.11.2014

Не уверен, что это действительно поможет вам, но я решил ту же проблему с помощью шаблона Express и ejs.

моя папка:

  • app.js
  • просмотры/header.ejs
  • просмотры/page.ejs
  • просмотров /footer.ejs

мой app.js

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

моя страница.ejs

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

content

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

очень просто, но работает идеально.

person lmaix    schedule 06.05.2015

Есть несколько способов сделать то, что вы просили, но все они плохи с точки зрения архитектуры. Вы можете не замечать этого сейчас, но со временем будет все хуже и хуже.

Звучит странно на данный момент, но вы на самом деле не хотите передавать вам JS, CSS через AJAX.

Что вам нужно, так это иметь возможность получить разметку через AJAX и после этого выполнить некоторый Javascript. Кроме того, вы хотите быть гибким и рациональным, делая это. Также важна масштабируемость.

Общий способ:

  1. Предварительная загрузка всех файлов js может использоваться на конкретной странице и ее обработчиках запросов AJAX. Используйте browserify, если вы боитесь возможных конфликтов имен или побочных эффектов сценария. Загружайте их асинхронно (асинхронный скрипт), чтобы не заставлять пользователя ждать.

  2. Разработайте архитектуру, которая позволит вам сделать, в основном, это на клиенте: loadPageAjax('page_a').then(...).catch(...). Дело в том, что, поскольку вы предварительно загрузили все модули JS, чтобы запустить код, связанный с AJAX-страницей, в вызывающем запросе, а не в вызывающем.

Итак, ваше Решение №4 почти подходит. Просто используйте его для получения HTML, но не скриптов.

Этот метод достигает вашей цели, не создавая какой-то непонятной архитектуры, а используя единственный надежный способ, ограниченный целью.

Буду рад ответить на любые дополнительные вопросы и убедить вас не делать то, что вы собираетесь.

person Kirill Rogovoy    schedule 07.12.2015