.NET MVC project bundle & minifier to msbuild task

寫 ASP NET MVC 專案的開發者或多或少都知道在處理前端 js 與 css 的打包,

預設會使用 System.Web.Optimization 下的 BundleCollection 去處理。

最近剛好有遇到一個比較特殊的情況是想把打包出來的檔案丟上去自家的 CDN Server 來增進讀取速度。

記得 Visual Studio 2013 後的 webessentials 就能在開發者本地去產出這些 bundle 後的檔案,

Visual Studio 2017 把這些功能都拆成小的 Extension: Bundler and Minifier

看完介紹及照著使用方法操作其實就可以在本地環境內產出需要的包。

這裡就不多介紹汙辱大家的智商。

bundleconfig.json

關鍵是 bundleconfig.json 這支檔案,詳細記載著產出的規格。

團隊成員還是會很依賴編輯 BundleConfig.cs 這個習慣,

為了不破壞大家的開發節奏,那我就特地去寫 T4 來產生這支 bundleconfig.master.json

( 註: 故意取跟預設的 bundleconfig.json 不同檔名,下文會提及整合 MSBuild 有安裝 extension 的發開者會與 Task 互相搶奪資源 )

BundleConfig.tt

比較要注意的是輸出路徑我特意改成專案目錄下的 dist 資料夾內,

其他的設定都是直接從 BundleConfig.cs parser 過來。

<#@ template  debug="true" hostSpecific="true" language="C#" #>
<#@ Assembly Name="System.Core" #>
<#@ Assembly Name="System.Windows.Forms" #>
<#@ Assembly name="$(SolutionDir)\Master\bin\Newtonsoft.Json.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="Newtonsoft.Json" #>
<#@ output extension="/" #>


<#
	// output file
	var outputFileName = "bundleconfig.master.json";
	// current folder directory
	var currentFolder = Path.GetDirectoryName(Host.TemplateFile);
	// master project folder directory
	var masterProjFolder = Directory.GetParent(currentFolder).FullName;

	// WriteLine(masterProjFolder);

	Dictionary<string, List<string>> bundleDic = new Dictionary<string, List<string>>();

	string line;
	StreamReader file = new StreamReader(Path.Combine(currentFolder, "BundleConfig.cs"));

	while ((line = file.ReadLine()) != null)
	{
		if (line.Contains("ScriptBundle")) {
			var virtualPath = string.Empty;
			Match bundleMatch = Regex.Match(line, @"~/Scripts/(?<virtual>[A-Za-z0-9\-]+)", RegexOptions.IgnoreCase);

			if (bundleMatch.Success) {
				virtualPath = bundleMatch.Groups["virtual"].Value + ".js";
				// WriteLine(virtualPath);
			}

			List<string> fileList = new List<string>();

			while ((line = file.ReadLine()).Trim() != "));")
			{
				Match patternMatch = Regex.Match(line, @"~/(?<virtual>[A-Za-z0-9_\-\/\.]+"", ""\*\.js)", RegexOptions.IgnoreCase);
				if (patternMatch.Success)
				{
					var pattern = patternMatch.Groups["virtual"].Value;
					pattern = pattern.Replace("\", \"", "/");
					// WriteLine(pattern);
					fileList.Add(pattern);
				}

				Match fileMatch = Regex.Match(line, @"~/(?<virtual>[A-Za-z0-9_\-\/\.]+\.js)", RegexOptions.IgnoreCase);
				if (fileMatch.Success)
				{
					var filePath = fileMatch.Groups["virtual"].Value;
					// WriteLine(filePath);
					fileList.Add(filePath);
				}
			}

			bundleDic.Add(virtualPath, fileList);
		}

		if (line.Contains("StyleBundle")) {
			var virtualPath = string.Empty;
			Match bundleMatch = Regex.Match(line, @"~/Content/(?<virtual>[A-Za-z0-9\-]+)", RegexOptions.IgnoreCase);

			if (bundleMatch.Success) {
				virtualPath = bundleMatch.Groups["virtual"].Value + ".css";
				// WriteLine(virtualPath);
			}

			List<string> fileList = new List<string>();

			while ((line = file.ReadLine()).Trim() != "));")
			{
				Match patternMatch = Regex.Match(line, @"~/(?<virtual>[A-Za-z0-9_\-\/\.]+"", ""\*\.css)", RegexOptions.IgnoreCase);
				if (patternMatch.Success)
				{
					var pattern = patternMatch.Groups["virtual"].Value;
					pattern = pattern.Replace("\", \"", "/");
					// WriteLine(pattern);
					fileList.Add(pattern);
				}

				Match fileMatch = Regex.Match(line, @"~/(?<virtual>[A-Za-z0-9_\-\/\.]+\.css)", RegexOptions.IgnoreCase);
				if (fileMatch.Success)
				{
					var filePath = fileMatch.Groups["virtual"].Value;
					// WriteLine(filePath);
					fileList.Add(filePath);
				}
			}

			bundleDic.Add(virtualPath, fileList);
		}
	}

	List<BundlePack> packs = new List<BundlePack>();

	foreach (KeyValuePair<string, List<string>> item in bundleDic) {
		var pack = new BundlePack {
			outputFileName = "dist/" + item.Key,
			inputFiles = item.Value,
			includeInProject = false
		};

		packs.Add(pack);
	}

	string output = JsonConvert.SerializeObject(packs);
	// WriteLine(output);
	
	System.IO.File.WriteAllText(Path.Combine(masterProjFolder, outputFileName), output);
#>

<#+
	public class BundlePack {
		public string outputFileName { get; set; }
		public List<string> inputFiles { get; set; }
		public bool includeInProject { get; set; }
	}
#>
Extension 不一定裝在每位團隊成員內的 Visual Studio 環境

就算你成功了產出 bundleconfig.json,但是您團隊成員不一定想安裝在他的 IDE。

所以去檢查有沒有釋出 .dll 或是 .exe 在 Nuget 上,

剛好有個很符合目前的專案 (BuildBundlerMinifier-Typescript)。

整合 MSbuild 

編輯 .csproj 掛入 Task 整合至日常 build。

OK !打完收工!