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

Posted

技术标签:

【中文标题】如何正确渲染部分视图,并使用 Express/Jade 在 AJAX 中加载 JavaScript 文件?【英文标题】:How to properly render partial views, and load JavaScript files in AJAX using Express/Jade? 【发布时间】:2014-09-27 20:16:31 【问题描述】:

总结

我正在为我的 Web 应用程序使用 Express + Jade,但我正在为我的 AJAX 导航渲染部分视图而苦苦挣扎。

我有两个不同的问题,但它们是完全相关的,所以我将它们包含在同一个帖子中。 我想这将是一篇很长的文章,但我保证如果你已经在同样的问题上苦苦挣扎,那它会很有趣。如果有人花时间阅读并提出解决方案,我将不胜感激。

TL;DR: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");
);

优势

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

缺点

对于一个页面,我仍然需要两个文件。 我必须在 Express 中的每条路线上使用相同的技术。一世 刚刚将我的代码重复问题从 Jade 移至 Express。快拍。

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

使用Alex Ford's technique,我可以在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

我不喜欢为 one 页面使用 two 文件,那么如果我让 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.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 会发生什么?我的页面将是以下的编译版本:

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 文件应该在我的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

对吗? Err....如果我去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 文件吗?我现在遇到了类似的问题。 我希望我可以 +10 这个问题 我希望我能更多地支持这个。目前我在添加部分后尝试加载 css 和 js 文件。我的解决方法很糟糕,但我使用 res.render 回调将翡翠模板转换为字符串,然后发送 html 和对对象中其他文件的引用,然后我使用 javascript 处理这些文件。我确信这是一种可怕的方法,但是我不喜欢我的身体中的链接标签 您好,谢谢您的回复。我很久以前在 Jade 的 git repo 上 reported a bug (事实上,这不是一个真正的错误)。我遇到的问题与此无关,请查看关于该主题的最后两篇文章。 Forbes Lindesay 就我们的问题提供了建议。 【参考方案1】:

很好的问题。我没有完美的选择,但我会提供我喜欢的解决方案#3 的变体。与解决方案 #3 相同的想法,但将 _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 并向他们发送拉取请求。例如,如果翡翠“扩展”关键字采用了一个可以禁用扩展 xhr 请求布局的参数......

对于您的第二个问题,我不确定是否有任何模板引擎可以帮助您。如果您使用 ajax 导航,当导航通过一些后端模板魔术发生时,您不能很好地“卸载”page_a.js。我认为您必须为此(客户端)使用传统的javascript隔离技术。针对您的具体问题:首先在闭包中实现页面特定逻辑(和变量),并在必要时在它们之间进行合理的合作。其次,您无需过多担心连接双事件处理程序。假设主要内容在 ajax 导航中被清除,并且那些附加了事件处理程序的元素会在新的 ajax 内容加载到 DOM 时调用 get reset(当然)。

【讨论】:

这是一个有趣的答案@Segfault,谢谢。我为我的应用程序使用了这个解决方案#4。关于第二个问题,好的建议,虽然我不得不在事件部分不同意你的看法。事件可以绑定到文档(事件委托...)或窗口(@98​​7654322@,这是一个糟糕的例子,但你明白了)。 是的,好点@WaldoJeffers 我想你仍然需要担心这些事件。我认为这对于一般情况很难解决......【参考方案2】:

不确定它是否真的能帮助你,但我用 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 %><%  %>

非常简单,但效果很好。

【讨论】:

【参考方案3】:

有几种方法可以按照您的要求进行操作,但所有方法在架构上都很糟糕。你现在可能没有注意到,但随着时间的推移,情况会越来越糟。

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

您想要的是能够通过 AJAX 获取标记并在此之后执行一些 Javascript。此外,您希望在执行此操作时保持灵活和理性。可扩展性也很重要。

常见的方式是:

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

    开发一种架构,使您基本上可以在客户端执行此操作:loadPageAjax('page_a').then(...).catch(...)。关键是,由于您已经预先下载了所有 JS 模块,因此要在请求的 caller 而不是 callee 处运行您的 AJAX 页面相关代码。

所以,您的解决方案#4 几乎是好的。只需使用它来获取 HTML,而不是脚本。

这种技术可以在不构建一些晦涩的架构的情况下完成您的目标,而是使用单一健壮的目标受限方式。

很乐意回答任何进一步的问题,并说服你不要做你想做的事。

【讨论】:

以上是关于如何正确渲染部分视图,并使用 Express/Jade 在 AJAX 中加载 JavaScript 文件?的主要内容,如果未能解决你的问题,请参考以下文章

渲染脚本以在局部视图中使用?

在菜单选择上渲染部分视图

使用 Rails 助手渲染部分

如何使用 AJAX 渲染部分内容?拉拉维尔 5.2

关于ASP.NET MVC部分视图渲染问题。

如何在控制器中为 Json 渲染部分视图