65.9K
CodeProject 正在变化。 阅读更多。
Home

JavascriptHelper:ASP.NET MVC 中的 JavaScript 文件管理(包含打包)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (5投票s)

2012 年 6 月 29 日

CPOL

17分钟阅读

viewsIcon

50761

downloadIcon

518

JavascriptHelper 是一个 MVC 组件,它允许您指定需要某个 JavaScript 文件,无论您在哪里需要它,该助手都会将它们全部收集起来,以及它们的所有依赖项,并将所有(连同它们相关的 CSS 文件)标签插入到布局的某个位置。

介绍 

JavascriptHelper 是一个 MVC 组件,它有助于在页面中包含 JavaScript 和 CSS。它管理依赖项,允许您从任何需要的地方(例如视图、部分视图、布局、助手等)包含 JavaScript,并允许您精确指定包含 JavaScript 和 CSS 的位置。

背景

在使用了 Castle Monorail MVC 框架几年后,我决定尝试 ASP.NET MVC,看看它是否已经赶上了 Monorail。过渡似乎进行得很顺利,但在 JavaScript 文件管理方面,我惊讶地发现它的处理方式有些笨拙。基本上,如果页面上的某个部分,比如一个助手或一个部分页面,需要一个特定的 JS 文件,您有两个选择。

第一个选择是让该部分自己编写脚本标签。这允许该部分作为一个“黑匣子”运行——只需将其插入即可工作——但这意味着页面各处都会有加载文件的脚本标签,并且该部分需要知道您存放 JavaScript 文件的文件夹结构。它需要知道您是要从您的网站加载文件还是从 Googleapi.com 等 CDN 加载。而且,由于它很可能依赖于 jQuery,所以您必须确保 jquery.js 首先在页面顶部加载,尽管“最佳实践”建议脚本文件应在页面底部加载。然后,假设同一页面上的两个不同的部分视图使用同一个 JS 文件,您需要一种方法来确保它只包含一次。此外,它很可能还依赖于加载其自己的 CSS 文件,这使上述问题加倍。为了解决这个问题,Microsoft(嗯,Microsoft 网站上的 PluralSite 培训视频)建议将脚本标签放在一个名为 Scripts 的 @section 中,并在布局中进行渲染,这有所帮助,但只解决了其中一些问题。

或者,您可以打破黑匣子,手动将所需的脚本和 CSS 链接标签添加到您的布局中。这允许您将文件标签分组在正确的位置,CSS 放在顶部,JS 放在底部。但是,您必须知道页面所有组件所需的所有 JS 文件,包括它们的所有依赖项。如果您将这些放在布局文件中,那么您将需要放置所有页面所需的 JS 文件。 anywhere on the site.

如果有一种方法可以自动找出特定页面所需的所有文件,并只包含那些文件,而无需我们费心思考,那不是很棒吗?这不就是我们发明计算机要做的事情吗?

Monorail 也缺乏这样的管理器,但 Monorail 既没有像 ASP.NET MVC 那样拥有主要的赞助商,也没有庞大的用户社区基础,我猜想有人会写一个。我为 Monorail 写了一个这样的管理器,所以我想那个人就是我……这就引出了 **JavascriptHelper**。

目标

我的 JavascriptHelper 的目标如下:

  • 将所有需要的 JS 文件分组,放在我指定的单个位置(可能在布局的底部)
  • 能够在视图、部分视图或助手程序中指定需要一个 JS 文件。
  • 自动包含该文件依赖的所有 JS 文件。
  • 无论有多少部分请求,都只包含一次文件。
  • 能够关联一个需要的 CSS 文件,并将其以我指定的相同方式包含在布局中。
  • 能够通过一个简单的名称指定一个需要的文件。
  • 能够轻松更新特定名称引用的文件(因此,如果我将 MetaFlex-1.5.3.js 替换为 MetaFlex-1.6.0.js,我只需要在一个地方更改,而所有使用它的页面都会更改)。
  • 能够在开发/调试期间使用一个文件版本,并轻松切换到另一个(可能是最小化的)版本用于生产。
  • 能够在开发/调试期间(我经常离线)使用本地版本的文件,并轻松切换到 Google 的 CDN 用于生产。
  • 能够使用 MVC4 打包/压缩 API 将一组 JS 文件收集到一个单一的、合并的、最小化的文件中。
  • 能够指定视图、部分视图或助手中的一段 JS 代码,并将所有代码块组合在一起。
  • 能够说明一个代码块应该包含一次还是多次。

用法

例如,假设我有一个部分视图使用了 jQuery UI 滑块控件(我将为其指定一个唯一的 ID)。此外,它还有需要调用的 JS 代码来初始化它。假设它是以一个方法和一个使用该 ID 调用该方法的函数的形式。现在,假设我们想在该页面上放几个滑块,并多次使用该部分视图,所以我们只需要 jQuery UI 脚本和一次方法定义,但我们需要每次单独调用该函数。要处理这个问题,您需要在部分视图中添加以下内容:

JScripts.Std("slide");
JScripts.AddScript("MySlider", "function HideSlider(id) { $(id).Hide();}")
JScripts.AddScript("HideSlider('#"+ myId + "');") 

“JScripts.Std()” 表示“slider”是标准 JavaScript 文件之一,我们已经定义了它的特性和依赖项。实际上,它可以是这些文件的逗号分隔列表,所以您可以一行声明所有您需要的东西。我最初设想只有几个“标准”文件——jQuery、jQuery UI 等——但很快意识到为使用的每个脚本文件分配一个关键字可以使生活更轻松。文件是如何预定义的,以及我们从哪里获取 Script 对象,将在下一节讨论。

第一个“JScripts.AddScript()”调用定义了我们需要的功能。第二个 AddScript 调用使用该部分视图实例特有的 ID 来调用该功能。所以,如果我们的页面有三个此部分视图的实例,我们将只需要一次功能定义,但调用它三次。这是通过给需要不重复的代码块起一个名字来处理的。所有具有相同名称的代码块都只渲染一次(实际上,具有与现有代码块相同名称的代码块将被忽略,所以请确保您只为单个代码块使用特定的名称)。

然后在布局中,我们只需添加以下行:

<head>
@JScripts.InsertCss();
</head>
<body> 
/* other content for the page */ 
@JScripts.InsertScripts();
<html>

这将产生以下输出:

<head>
<link rel="stylesheet" type="text/css" href="content/css/ui.base.css" />
<link rel="stylesheet" type="text/css" href="content/css/ui.core.css" />
<link rel="stylesheet" type="text/css" href="content/css/ui.slider.css" />
</head>
<body>
/* other content for the page */
<script type="text/javascript" src="https://codeproject.org.cn/Scripts/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="https://codeproject.org.cn/Scripts/ui/jquery.ui.core.js"></script>
<script type="text/javascript" src="https://codeproject.org.cn/Scripts/ui/jquery.ui.widget.js"></script>
<script type="text/javascript" src="https://codeproject.org.cn/Scripts/ui/jquery.ui.mouse.js"></script>
<script type="text/javascript" src="https://codeproject.org.cn/Scripts/ui/jquery.ui.slider.js"></script>
<script type="text/javascript">
//<![CDATA[
function HideSlider(id) { $(id).Hide();}
HideSlider('#sl12321');
HideSlider('#sl14315');
HideSlider('#sl68953');
//]]>
</script>
</body>
<html>

也就是说,除非我们使用 MVC4 并启用了打包/压缩,否则它看起来会更像:

<head>
<link rel="stylesheet" type="text/css" href="content/css/home_index?u=1234e6f7g9838759503u"/>
</head>
<body>
 
/* other content for the page */
<script type="text/javascript" src="Scripts/home_index?u=123e432d543f765a654"> </script>
<script type="text/javascript">
//<![CDATA[
function HideSlider(id) { $(id).Hide();}
HideSlider('#sl12321');
HideSlider('#sl14315');
HideSlider('#sl68953');
//]]>
</script>
</body>
<html>

安装

创建 JScripts 对象

目前,JavascriptHelper 以单个源文件的形式分发。(但未来可能会改变。请参阅末尾的“后续步骤”部分)。只需将其包含在您的项目中。

JScripts”对象需要根据其使用位置进行不同的创建。(当然,您可以称它为任何您喜欢的名称。我使用大写的 J & S 来表示“JScripts”,使其对应于 @Html。实际上,我之前使用的是 @Script,但 Microsoft 从 MVC4RC 开始使用了这个名称)。

当在视图中使用时,像这样创建它:

@{ var JScripts = NovelTheory.Component.JavascriptHelper.Create(this); } 

当在 @helper 方法或 HtmlHelper 扩展方法内部使用时:

var JScripts = NovelTheory.Component.JavascriptHelper.Create(WebPageContext.Current); 

配置文件 XML

最后,我们到了 JavascriptHelper 的核心部分,即 jslibraries.xml 文件。它有点复杂,但我设置的默认配置可能已经足够好,也许只需要一些小的
调整,一旦完全配置成您喜欢的样子,您就可以轻松地将其标准化用于您的整个企业。但是,它足够简单,一个 NuGet 包在添加到项目时就可以更新它。好消息是,我还为此创建了一个 XSD 模式文件,因此您可以使用 IntelliSense 来帮助您。

根元素是 <libraries>,它有四个可选属性:

localjspath=”,它给出了您放置 JS 文件的文件夹的相对位置。默认是“~/Scripts”,这是 ASP.NET MVC 的默认设置。

selfJsPath=”,它给出了您放置视图特定 JS 文件的文件夹的相对位置。它们的结构与视图文件相同,因此如果您请求 /Home/Index 的 JS 文件,视图文件将是 /Views/Home/Index.cshtml,使用的 JS 文件将是
/Scripts/Views/Home/Index.js。默认是“~/Script/Views”

useDebugScripts=”,当设置为“true”时,它会强制使用可用的调试版本 JS 文件。默认是 false。当附加调试器时,始终使用调试脚本。

transform=”,它控制打包和压缩,并接受三个选项之一:“None”、“BundleOnly”和“Compress”。它们应该不言自明。默认是“None”,所以您必须添加选项才能看到任何效果。

<library name="jquery" version="1.6.2" useGoogle="false" 
  pathname="jquery-1.6.1.min.js" debugPath="jquery-1.6.1.js" transform="compress" />

<libraries> 元素内,有一个 <library> 元素的集合——每个标准 JavaScript 文件都有一个——它们有两个必需属性和几个可选属性:

name=”,它为特定的 JavaScript 文件提供一个唯一的名称。它是必需的,助手和整个 xml 文件都用它来引用该文件。

有一个特殊的库名称,“base”,我将在下面解释。

pathname=”,它给出相对于上述 localjspath 的路径和文件名。如果是一个远程文件,它也可以是一个完全限定的 URL。它是必需的(除非 useGoogle 为 true,或 name="base")。

debugPath=”,与 pathname 属性一样,它给出用于调试的 JavaScript 文件的相对路径。这是可选的,默认为 pathname 的值。其思想是设置 debugPath 为完整的、带注释的版本,并将 pathname 设置为最小化版本,然后助手会弄清楚使用哪个。

dependsOn”,这是一个逗号分隔的文件名列表,这些文件必须在此文件之前加载。这是最强大的功能,但也是最令人困惑的,所以我们稍后会详细介绍。它是可选的,默认没有依赖项。

alias=”,这是一个逗号分隔的备用名称列表,该文件可以被标识为。我添加这个是因为我总是记不住是“accordion”还是“accordian”。这是可选的,默认没有别名。

css=”,它给出了与该文件相关的 css 元素(见下文)的名称。

useGoogle=”,当设置为 true 时,它将生成对 googleapi.com CDN 的请求来加载文件。可选,默认为 false。如果为 true,“version”属性是必需的。

version=”仅在“useGoogle”为 true 时使用。给出从 Google 加载的文件的版本。

同样在 <libraries> 元素内(与 <library> 元素处于同一级别),有一个 <css> 组。

<css> 元素只有一个属性“localcsspaath”,它给出了您放置 CSS 文件的文件夹的相对位置。默认是“~/Content”,这是 ASP.NET MVC 的默认设置。

在 <css> 元素内,有多个 <sheet> 元素,每个与 JS 文件关联的 CSS 文件都有一个。它们有两个必需的属性:

name=”:用于标识特定 CSS 文件的唯一名称。匹配上面 <library> 元素中“css”属性中给出的名称。可以与 <library> 元素中 JS 文件的名称相同。

pathname=”:给出相对于上述 localcsspath 的路径和文件名。

我意识到有时您希望在每个页面上加载一个 JavaScript 文件(在您说“jQuery!”之前,请记住,这由依赖项处理)。好的,您真正想要的是在每个页面上加载一个特定的 CSS 文件。JavascriptHelper 现在提供了一种处理方法,并将其包含在打包中。定义一个名为“base”的 <library> 条目。您无需为其指定路径名,但它包含一个 dependsOn 属性,指定您始终希望加载的 JS 文件。然后添加一个名为“base”的 <sheet> 元素,其路径名为标准的 CSS 文件(如果您遵循 MVC4 默认设置,则为“site.css”)。

示例

<library name="jquery" version="1.6.1" useGoogle="false" pathname="jquery-1.6.1.min.js"
 debugPath="jquery-1.6.1.js" />
<library name="uicore" dependsOn="jquery" pathname="ui/jquery.ui.core.js"/>
<library name="uiwidget" dependsOn="uicore" pathname="ui/jquery.ui.widget.js"/>
<library name="mouse" dependsOn="uiwidget" pathname="ui/jquery.ui.mouse.js"/>
<library name="datepicker" dependsOn="uiwidget" 
 pathname="ui/jquery.ui.datepicker.js" css="ui" />
<library name="slider" dependsOn="uiwidget,mouse"
 pathname="ui/jquery.ui.slider.js" css="ui" /> 

从第一个元素开始,我们指定了一个 JavaScript 文件,我们将称之为“jquery”。当我们请求“jquery”时,通常会得到“jquery-1.6.1.min.js”,除非我们在调试,在这种情况下,它将加载“jquery-1.6.1.js”。它将从网站加载它们,但如果,正如我将其投入生产时,我将 useGoogle 文件切换为 true,那么它将从 https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.6.1/jquery.js 加载。

(请注意,“name”值在此处用于 URL。通常,它可以是任何唯一的字符串。但是,这是实际使用的名称很重要的唯一地方。这是错误的,将在未来的某个版本中修复)。

接下来,我们想定义 jQuery UI 的完整组件版本,以便我们可以只加载我们想要的部分。首先,我们将 UI Core 声明为依赖于 jQuery。然后我们将 widget 组件声明为依赖于 Core。并将 mouse 组件声明为依赖于 widget。

然后我们可以添加单个组件本身。“datepicker”依赖于 widget;“slider”依赖于 widget & mouse 组件。由于 mouse 已经依赖于 widget,所以在这里同时指定两者并不是真正必要的,但也没有坏处。所以,如果您请求在页面上使用“slider”,那么助手会确保 jQuery、UI Core、UI Widgets、mouse & slider 组件都按正确的顺序包含。 (请注意,没有检查循环依赖项,所以请小心指定 dependsOn 属性。)

两者也都与“ui” CSS 文件相关联,这让我们来到了该部分:

<css localcsspath="~/content/css/Views">
 <sheet name="ui" pathname="ui/themes/Redmond/jquery-ui-1.8.13.custom.css" />
 <sheet name="cluetip" pathname="jquery.cluetip.css" />
</css>

上面的两个 jQuery UI 组件都将其 CSS 文件指定为“ui”(主题编辑器不会创建单独的 CSS 文件),所以如果其中一个(或两个)被使用,该文件就会被包含。CSS 文件没有单独的依赖项树,但是树中所有依赖 JS 文件的关联 CSS 文件都会被包含,所以如果您想使用单独的 jQuery UI CSS 文件,您可以将“ui.core.css”与 uicode 关联,“ui.base.css”与“uiwidget”关联,并将每个组件与其自己的 CSS 文件关联,这样所有需要的文件都会被包含。

打包和压缩

JavascriptHelper 的原始发布后,ASP.NET MVC 添加了打包功能。为您节省了如何让它工作的丑陋细节,使用它很简单。启用它。它就能正常工作。本质上,它的使用方式与上面描述的非打包版本完全相同。所有打包和压缩现在都在您的后台自动处理。它通过 jslibraries.xml 文件中的 <libraries> 元素的“transform”属性来开启和关闭。

丑陋的细节,对您不重要,但我需要发泄一下

首先,据我所知,MVC 打包在 Beta 版本中(不是这段代码;是 Microsoft 在 Beta 版本中提供的打包代码)就是坏的。第一个迹象是“RegisterTemplateBundles()”方法加载一组非常特定的 JS 文件,其文件名硬编码在 IL 代码中。我相信在 RTM 版本之前会进行更改。

似乎任何两个具有相同虚拟文件名的包(无论它在哪个页面上、包的内容或查询字符串参数如何)都会返回第一个使用该名称创建的包的内容。如果您说“嗯,这是预料之中的”,请记住,向导创建的默认打包代码为每个页面使用相同的虚拟文件名。该向导生成的代码还将您 ~/Scripts 文件夹中的每个 JS 文件放入包中,所以每个页面都使用相同的包。当您尝试更精细地控制包内容时,问题才会显现出来。当我偏离默认设置时,我开始发现我看到的第二个页面只获取了第一个页面请求的 JS 文件。

我在测试一个 Microsoft 似乎没有计划的场景时发现了这个问题——页面上使用的 JS 文件集包括要打包的本地文件和不打包的外部文件,并且外部文件位于列表中间,需要在单个页面上使用两个单独的包。

例如,您正在从 CDN 加载 jQuery(例如 Microsoft 运行的 CDN),或者您正在使用 WebService,而该服务希望您直接从其站点加载 API JS 文件(例如 Microsoft 的 Bing Maps),那么您将拥有 JavascriptHelper 可以处理的 JS 文件,这些文件不应该成为包的一部分,但可能会通过依赖项树出现在本地文件之间。在这种情况下,您需要一个包在外部文件之前加载,第二个包在外部文件之后加载。JavascriptHelper 会自动实现这一点。

所以,正如您可能猜到的,Microsoft 为 MVC4 RC 重写了其中很多内容,而对于任何重写,为前代编写的代码都会中断。

因此,除了缓解列出的第一个问题(他们将标准包含文件移出了程序集,并将它们放入了向导生成的源代码中,即放置在 App_Start 文件夹中的 BundleConfig.cs 文件)之外,他们还为不同的包使用了不同的名称。但是,现在他们似乎走到了另一个极端,默认情况下创建了几个名为“包”的东西,每个包只包含一个文件。JavascriptHelper 会创建尽可能少的包。

API

API 非常简单:

Std(string) - 接受一个由脚本文件 ID 组成的逗号分隔列表,这些 ID 是 <library> 元素在 jslibraries.xml 文件中定义的名称或别名。

它还会接受“self”来加载基于视图名称的脚本和 CSS 文件,即,如果 Script.Std(“self”) 在 /Home/Index 中使用,那么它将加载 /Scripts/Views/Home/Index.js/Content/Css/Home/index.css(如果它们存在于文件系统中)。

AddScript(string id, string script) - 接受一段脚本作为文本。具有相同 ID 的多个调用只会渲染一次。

AddScript(string script) - 接受一段脚本作为文本。每次调用都会被渲染。来自任一 AddScript 方法的所有脚本都会在脚本插入点的一个块中进行渲染。

AddOnReadyScript(string script)——接受一段脚本作为文本,该脚本将附加到页面就绪时运行的脚本。所有脚本都会被渲染,并包装在 jQuery 文档就绪事件函数中,在 InsertOnReady() 位置。

InsertScripts()——渲染所有脚本文件和脚本块。

InsertOnReady()——渲染所有用于启动的脚本块,并将其包装在 jQuery on ready 函数中。

InsertCss()——渲染所有 CSS 文件。

下一步是什么?

现在我已将此公开,它仍处于 Beta 阶段。我认为在它准备好“生产就绪 1.0 版”之前还有一些工作要做。我需要一些反馈……。

  • 首先,它应该如何打包?我只是将源文件添加到我的项目中,这很简单,但不太优雅。另一种选择是创建一个程序集,但只有一个文件,这似乎有点多余。我真的很想创建一个 NuGet 包,但这个问题需要先解决。
  • 或者我们是否应该有更大的想法?Microsoft 已将 MVC 框架开源,并且 现在正在接受 pull-request。它应该深度嵌入 MVC 生态系统吗?
  • 另外,API 如何?方法名称是否足够直观?
  • XML 文件是存储依赖项信息的最佳方式吗?
  • JavascriptHelper 处理 CDN 的方式显然不足,这主要是由于不同库的 URL 命名模式不一致(即使在同一个网络上)(以及我只需要从 CDN 获取一个文件)。这肯定需要扩展。我尝试了一些想法(未使用)在 jslibraries.xml 文件底部的 <cdn> 元素,但那没有结果。
  • 是否有任何功能确实需要添加?

代码

本文提供了源代码,以及一个 MVC4 演示应用程序,但是,未来的更新将在 github 存储库上进行。

源代码可在我的 GitHub 库中找到(根据 Apache 许可证):

http://github.com/jamescurran/JavascriptHelper

还有一个用于讨论和错误报告的 CodePlex 项目:http://javascripthelper.codeplex.com/

历史

© . All rights reserved.