如何在Express/Jade中正确渲染局部视图并使用AJAX加载JavaScript文件?

39

摘要

我正在使用Express + Jade制作我的Web应用程序,并且在呈现AJAX导航的部分视图方面遇到了一些困难。

我有两个不同的问题,但它们是完全相关的,因此我将它们放在同一个帖子中。我想这将是一个长帖子,但如果您已经遇到过相同的问题,我保证它很有趣。如果有人愿意花时间阅读并提出解决方案,我将不胜感激。

简明扼要:2个问题

  • 使用Express + Jade呈现AJAX导航的视图片段的最干净最快速的方法是什么?
  • 应如何加载与每个视图相关的JavaScript文件?

要求

  • 我的Web应用程序需要与禁用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文件只包含实际内容,没有任何布局、块、扩展或包含:

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

优点:

  • 现在我的内容只需定义一次,这更好。

缺点:

  • 我仍然需要两个文件才能呈现一个页面。
  • 我必须在每个路由上使用相同的技术,我只是将我的代码重复问题从Jade转移到了Express。Snap。

解决方案3:与解决方案2相同,但解决了代码重复问题。

使用Alex Ford的技术,我可以在middleware.js中定义自己的render函数:

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决定要渲染什么呢?乍一看,这对我来说非常不舒服,因为我认为模板引擎应该处理任何逻辑。但是让我们尝试一下。

首先,我需要传递一个变量给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.jadepage_b.jade,它们都有自己的客户端JavaScript文件js/page_a.jsjs/page_b.js,当这些页面通过AJAX加载时会发生什么?

首先,我需要在layout.jade中定义一个extraJS块:

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")
如果我在我的URL栏中输入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")
那看起来不错。但是如果我现在使用我的 AJAX 导航到 page_b 会发生什么?我的页面将是以下编译版本:

那看起来不错。但是如果我现在使用我的 AJAX 导航page_b 会发生什么?我的页面将是以下的编译版本:

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.jsjs/page_b.js都在同一个页面上加载。如果存在冲突(相同的变量名等),会发生什么?此外,如果我使用AJAX返回到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 B !
                  script(src="js/page_a.js")
               script(src="js/jquery.min.js")
               script(src="js/page_a.js")
同一页面上加载相同的JavaScript文件(page_a.js)两次!这会导致冲突,每个事件会触发两次吗? 无论如何,我认为这不是干净的代码。
因此,您可以说特定的JS文件应该在我的“块内容”中,以便在转到另一页时将其删除。 因此,我的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(),但客户端必须知道脚本的文件名,查看它们是否已经加载,可能会删除它们。我认为不应该在客户端完成。 我应该如何处理通过AJAX加载的JavaScript文件?是使用不同的服务器端策略/模板引擎?还是客户端处理? 如果你已经阅读到这里,你就是一个真正的英雄,我很感激你,但如果你能给我一些建议,我会更加感激 :)

你解决了如何在局部渲染后加载JavaScript文件的问题吗?我现在也遇到了类似的问题。 - Kyle Bachan
2
我希望我能够给这个问题点赞 +10。 - Segfault
我希望我能够给这个更多的赞。目前我正在尝试在添加部分后加载CSS和JS文件。我的解决方法很糟糕,但我正在使用res.render回调将Jade模板转换为字符串,然后将HTML和其他文件的引用作为对象发送,然后再用JavaScript处理。我相信这是一个可怕的做法,但我不喜欢在我的正文中使用链接标签。 - Snymax
嗨,感谢回复。我相当长时间之前在Jade的git仓库上报告了一个错误(实际上并不是真正的错误)。我遇到的问题与此无关,但请查看该主题的最后两篇帖子。Forbes Lindesay提供了有关我们问题的建议。 - Waldo Jeffers
3个回答

3

非常好的问题。虽然我没有完美的解决方案,但我会提供一个我喜欢的对您的第三个解决方案的变体。和第三个解决方案一样的思路,但是将_full文件的jade模板移动到您的代码中,因为它是样板文件,当需要一个完整页面时可以由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中并发送pull请求的方法。例如,如果jade "extends"关键字带有一个参数来禁用xhr请求的布局扩展...

对于您的第二个问题,我不确定任何模板引擎能够帮助您。如果您使用ajax导航,则无法通过某些后端模板魔术卸载页面_a.js。我认为您必须使用传统的javascript隔离技术(客户端)。针对您的具体问题:首先,在闭包中实现页面特定的逻辑(和变量),并在必要时进行合理的协作。其次,您不需要过多担心挂钩双事件处理程序。假设主内容在ajax导航时被清除,并且那些附加了事件处理程序的元素也是调用get reset的元素(当然),当新的ajax内容加载到DOM中时,它们将被重置。


这是一个有趣的回答@Segfault,谢谢。我为我的应用程序选择了解决方案#4。关于第二个问题,您提供了很好的建议,尽管我不同意您对事件部分的看法。事件可以绑定到文档(事件委托...),也可以绑定到窗口(resize,这是一个糟糕的例子,但您明白我的意思)。 - Waldo Jeffers
是的,@WaldoJeffers,你说得很对,我想你仍然需要稍微担心一下事件。我认为这对于一般情况来说很难解决... - Segfault

1

我不确定这是否真的能帮到你,但我用Express和ejs模板解决了同样的问题。

我的文件夹:

  • app.js
  • views/header.ejs
  • views/page.ejs
  • views/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 %><% } %>

非常简单但完美运行。

0

有几种方法可以完成你所请求的操作,但所有这些方法在架构上都很糟糕。现在你可能没有注意到,但随着时间的推移,情况会越来越糟。

听起来很奇怪,但你真的不想通过AJAX传输你的JS、CSS。

你想要的是能够通过AJAX获取标记,并在此之后执行一些Javascript。此外,你希望在执行此操作时具有灵活性和合理性。可伸缩性也很重要。

常见的方法是:

  1. 预加载所有可能在特定页面及其 AJAX 请求处理程序中使用的 js 文件。如果您担心潜在的名称冲突或脚本副作用,请使用 browserify。使用异步方式(script async)进行加载,以避免让用户等待。

  2. 开发一个架构,使您能够在客户端基本上执行以下操作:loadPageAjax('page_a').then(...).catch(...)。重点是,由于您已经预先下载了所有 JS 模块,要在请求的调用方而不是被调用方运行 AJAX 页面相关代码。

因此,您的解决方案#4几乎很好。只需将其用于获取HTML,而不是脚本。

这种技术可以在不构建某些模糊的架构的情况下实现您的目标,而是使用单一、强大、有限的方式。

非常乐意回答任何进一步的问题,并劝说您不要做您即将要做的事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接