最近做項目用到 ASP.NET Web Optimizatoin Framework,發現 Sea.js 的依賴加載在 Release 版本下不能很好的工作了——因為 Web.Optimizatoin 合并了所有腳本。同時由于寫慣了 Java
程式和 C# 程式,對于沒有命名空間概念的
Sea.js
和 RequireJS
也感覺不爽。考慮了下,覺得子產品管理其實并不複雜,是以将之前在《ASP.NET MVC4 捆綁(Bundle)技術下的 JavaScript》 中提到的 js-modular
的基礎上進行了改進,最終産生了 jNs。
jNs 是一個具有命名空間概念的 JavaScript 子產品管理工具。與 Sea.js 和 ReqireJS 等子產品管理工具不同,jNs 隻管理子產品的定義和使用,而不負責加載,非常适合釋出合并 JavaScript 代碼的 Web 項目,比如使用了 ASP.NET Web Optimization Framework 提供的 Script Bundle 功能的 ASP.NET 項目,以及使用 UglifyJS 壓縮合并腳本的項目等。
jNs 托管在 git.oschina.net 上,其 README 中已經有詳情的示例,是以這裡就不廢話了。這裡主要說說在 ASP.NET MVC 項目裡怎麼使用。
第一,定義合理的腳本結構
下面展示了一個來自某個實際項目的腳本目錄(有所精簡),也許不是最好的結構,但我個人覺得很清晰(用方括号
[]
括起來的是目錄)。
[Scripts]
│ - jNs-1.0.0.js
│ - jNs-1.0.0.min.js
│ - jquery-2.1.3.js
│ - jquery-2.1.3.min.js
├─[core]
│ └─[co]
│ │ - app.js
│ │ - compatible.js
│ │ - util.js
│ ├─[app]
│ │ │ - ajax.js
│ │ │ - dialog.js
│ │ │ - page.js
│ │ └ - ui.js
│ └─[util]
│ │ - case.js
│ │ - debug.js
│ └ - format.js
└─[page]
├─[home]
│ └ - index.js
└─[user]
└ - index.js
其中有兩個比較關鍵的主目錄,一個是
core
,一個是
page
。
core
是整個項目中需要使用到的子產品,在
bunles
中配置成一個名為
~/bundle/core
的
ScriptBundle
。Release 版本下會被打包成一個合并的腳本檔案。大部分的頁面隻需要引用
~/bundles/core
就可以了,因為大部分的公用邏輯都在這裡了。
但是仍然會有一部分頁面比較特殊,需要有自己的腳本。那麼這些腳本就按一定的路徑儲存在
page
目錄下。
core
中的目錄結構與子產品的結構對應,這樣查找腳本檔案的時候就比較友善,比如示例中的結構對應的子產品全稱(含命名空間)就是:
co.app
co.app.ajax
co.app.dialog
co.app.page
co.app.ui
co.compatible
co.util
co.util.case
co.util.debug
co.util.format
這個結構看起來就像 Java 一樣。不過與 Java 不同,目錄不必與命名空間(或包)對應,檔案名也不必與子產品名相同——這似乎更像 C#。
還有一點需要注意的是,在同一個命名空間下,子級命名空間和子產品名是可以相同的,這也算是 jNs 的特點之一吧。
有了合理的結構,還需要解決下面這個問題。
第二,保證 jNs.js 在子產品定義之前加載
雖然一般認為在
ScriptBunlde
中
Include
的腳本會順序加載,或者說在 Release 版本下按順序合并,是以認為可以這樣寫配置:
// BundleConfig.cs
// 注意:這是錯誤的示例
public static void RegisterBundles(BundleCollection bundles)
{
bunles.Add(new ScriptBundle("~/bundles/core")
.Include("~/scripts/jNs-{version}.js").
.IncludeDirectory("~/scripts/core/", "*.js", true)
);
}
可惜事實不是。我閱讀了它的源碼,發現它使用了一個
Dictionary
來進行中間過程的處理,是以最終輸出的順序完全是不确定的。是以 jNs 有可能在某個子產品檔案之後加載,那麼子產品檔案中的
jNs(...)
就會出錯——因為還沒定義。
public class AsIsBundleOrderer : DefaultBundleOrderer
{
public override IEnumerable<BundleFile> OrderFiles(
BundleContext context,
IEnumerable<BundleFile> files
)
{
var originalList = files.ToList();
IEnumerable<BundleFile> orderFiles = base.OrderFiles(context, originalList);
return orderFiles.OrderBy(f => originalList.IndexOf(f));
}
}
public static void RegisterBundles(BundleCollection bundles)
{
var bundle = new ScriptBundle("~/bundles/core")
.Include("~/Scripts/jNs-{version}.js")
.IncludeDirectory("~/Scripts/core/", "*.js", true);
bundle.Orderer = new AsIsBundleOrder();
bundles.Add(bundle);
}