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

JavascriptHelper – ASP.NET MVC 中的 JS 文件管理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (2投票s)

2012 年 6 月 7 日

CPOL

13分钟阅读

viewsIcon

19311

管理 ASP.NET MVC 的 JS 文件。

在使用 Castle Monorail MVC 框架工作了几年后,我决定尝试 ASP.NET MVC,看看它是否赶上了 Monorail。过渡似乎相当顺利,但有一个领域让我惊讶于它的处理方式非常笨拙,那就是 JavaScript 文件的管理。基本上,如果页面的一部分,比如一个辅助函数或一个部分页面,需要一个特定的 JS 文件,你只有两种选择。

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

或者,你可以打破黑匣子,在你的布局中手动添加所需的脚本和 CSS 链接标签。这使你能够将文件标签分组放在正确的位置,CSS 在顶部,JS 在底部。但是,你必须知道页面所有组件所需的所有 JS 文件,包括它们的所有依赖项。如果你将这些放在布局文件中,那么你需要放置网站上任何地方所有页面所需的 JS 文件。

如果有一种方法可以自动找出某个特定页面所需的文件,并且只包含这些文件,而无需我们进行大量思考,那岂不是太棒了?难道不是我们发明计算机就是为了做这类事情吗?

Monorail 也缺少这样一个管理器,但 Monorail 既没有像 ASP.NET MVC 那样的大型企业赞助商,也没有庞大的用户社区基础,我在那里认为有人会写一个。我为 Monorail 写过一个这样的管理器,所以我想那个人会是我……这就引出了 **JavascriptHelper**。

目标

我的 JavascriptHelper 的目标如下:

  • 将所有需要的 JS 文件分组在一起,放在我指定的单个位置(大概是在布局的底部)。
  • 能够在视图、部分视图或辅助函数中指定一个需要的 JS 文件。
  • 自动包含该文件所需的所有 JS 文件。
  • 无论有多少部分请求,都只包含一次文件。
  • 能够在视图、部分视图或辅助函数中指定一个 JS 代码块,并将所有代码块捆绑在一起。
  • 能够指定一个代码块是每页包含一次,还是重复包含。
  • 能够关联一个需要的 CSS 文件,并将其类似地包含在布局中我指定的位置。
  • 能够通过一个简单的名称指定一个需要的文件。
  • 能够轻松地更新一个特定名称指向的文件(例如,当实际文件名包含版本号时,如 jquery-1.5.1.js)。
  • 能够在使用开发/调试期间使用一个文件版本,并轻松切换到另一个(可能是精简版)用于生产。
  • 能够在使用开发/调试期间(我经常离线)使用本地文件版本,并轻松切换到 Google 的 CDN 用于生产。
  • 能够将一组 JS 文件收集到一个单一的、合并的文件中(即时精简)(好的,JavascriptHelper 实际上并没有做到这一点,但实现它只需要重写一个方法——并添加一个处理实际捆绑和精简的控制器。我们最终会实现的)。

用法

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

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

Script.Std()”表示“slider”是我们定义了其特性和依赖项的标准 JavaScript 文件之一。实际上,这可以是一个逗号分隔的列表,所以你在一行中说明你需要的一切。我最初设想只有少数“标准”文件——jQuery、jQuery UI 等——但很快意识到为每个使用的脚本文件分配一个关键字会使生活更容易。事实上,它甚至会接受“self”来加载基于视图名称的脚本和 CSS 文件,也就是说,如果 Script.Std(“self”) 用于 /Home/Index,它将加载 /Scripts/Views/Home/Index.js/Content/Css/Home/index.css(如果它们存在于文件系统中)。如何预定义一个文件,以及在哪里获取 Script 对象,将在下一节讨论。

第一个“Script.AddScript()”调用定义了我们需要的功能。第二个 AddScript 调用该函数,使用部分视图该实例特有的 ID。因此,如果我们的页面有三个这样的部分视图实例,我们只需要定义一次函数,但调用它三次。这是通过给需要不重复的片段命名来处理的。所有具有相同名称的块只渲染一次(实际上,具有与现有块相同名称的块会被忽略,所以请确保你只为单个脚本块使用特定的名称)。

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

<html>
<head>
@Script.InsertCss();
</head>
<body>

/* other content for the page */

@Script.InsertScripts();
</body>
<html>

这将产生如下输出:

<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="/Scripts/jquery-1.6.1.min.js"></script>
<script type="text/javascript" src="/Scripts/ui/jquery.ui.core.js"></script>
<script type="text/javascript" src="/Scripts/ui/jquery.ui.widget.js"></script>
<script type="text/javascript" src="/Scripts/ui/jquery.ui.mouse.js"></script>
<script type="text/javascript" src="/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>

安装

创建 Script 对象。

Script”对象需要根据其使用位置以不同的方式创建。(当然,你可以将其命名为任何你喜欢的名字。我使用大写的“Script”,以便与 @Html 相对应)。

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

@{ var Script = StateTheaterMvc.Component.JavascriptHelper.Create(this); }

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

var script = StateTheaterMvc.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。调试脚本在附加调试器时始终使用。

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

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

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

“pathname=”:指定该文件的路径(相对于上面描述的 localjspath)和文件名。如果这是一个远程文件,它也可以是一个完全限定的 URL。它是必需的。(除非 useGoogle

“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)和文件名。

示例

<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 核心声明为依赖于 jQuery。然后,我们将 widget 组件声明为依赖于核心。而 mouse 组件声明为依赖于 widget。

然后我们可以单独定义组件本身。  “datepicker”依赖于 widget;“slider”依赖于 widget 和 mouse 组件。由于 mouse 已经依赖于 widget,所以在这里同时指定两者并不是必需的,但也没有坏处。所以,如果你要求在你的页面上使用“slider”,那么助手会确保 jQuery、UI 核心、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 组件都提供了“ui”作为它们的 CSS 文件(theme roller 不创建单独的 CSS 文件),所以如果使用其中一个(或两个),就会包含该文件。  CSS 文件没有单独的依赖树,但是上面依赖树中所有依赖的 js 文件的所有关联 CSS 文件都会被包含。所以,如果你想使用单独的 jQuery UI CSS 文件,你可以将“ui.core.css”与 uicode 相关联,“ui.base.css”与“uiwidget”相关联,并将每个组件与自己的 CSS 文件关联,所有必需的文件都将被包含。

API

API 非常简单:

  • Std(string) - 接受一个逗号分隔的脚本文件 ID 列表。
  • AddScript(string id, string script) - 接受一个脚本块作为文本。具有相同 ID 的多个调用只渲染一次。
  • AddScript(string script) - 接受一个脚本块作为文本。每次调用都会渲染。
  • 来自两个 AddScript 方法的所有脚本都将在脚本插入点以一个块的形式渲染。
  • AddOnReadyScript(string script) - 接受一个脚本块作为文本。附加到页面就绪时运行的脚本。所有脚本都将被渲染,并包装在 jQuery 文档就绪事件函数中,位于 InsertOnReady() 的位置。
  • InsertScripts() -- 渲染所有脚本文件和脚本块。
  • InsertOnReady() -- 渲染所有用于启动的脚本块,并包装在 jQuery 就绪函数中。
  • InsertCss() -- 渲染所有 CSS 文件。

下一步是什么?

现在我公开了这个,它基本上还只是一个测试版。我认为在它准备好进行“生产就绪 1.0 版”之前还有一些工作要做。而且我需要一些反馈……。

  • 首先,它应该如何打包?我一直只是将源文件添加到我的项目中,这很简单,但不怎么优雅。另一种选择是为它创建一个程序集,但它只有一个文件,所以这似乎有点过了。我真的很想为此创建一个 NuGet 包,但这个问题需要首先解决。
  • 或者我们想得更长远?微软已经开源了 MVC 框架,并且 现在接受 pull requests。它应该深度嵌入到 MVC 生态系统中吗?
  • 另外,API 如何?方法名是否足够直观?
  • XML 文件是存储依赖信息的好方法吗?
  • 是否有任何功能真的需要添加?

代码

源代码可在我的 GitHub 库中获取(根据 Apache 许可证):http://github.com/jamescurran/JavascriptHelper

© . All rights reserved.