Sitecore Bundling

前言

Asp.net MVC預設專案範本中即包含BundleConfig.cs(參考),其作用為執行資源整合,通常用於讓開發者可以減少在View上寫的資源載入,簡單來說就是View上寫一行,後端Bundle結果幫你展開已經在後端設定好的十幾行,並且可以透過一些參數自動選擇想要的檔案,像是範例中的jquery在Scripts資料夾中放入新版本帶版號的檔案後,Bundle結果應該就會選擇最新的版本,預設可透過web.config的設定來開啟是否需要進行檔案內容的優化來縮減網路流量,也就是js minify及css的minify才是Bundle的目的

Asp.net MVC微軟範例 BundleConfig.cs

搭配的View裡面可以看到使用 @Styles.Render("~/Content/css")@Scripts.Render("~/bundles/modernizr")

Asp.net MVC微軟範例 Index.cshtml

開啟Home Controller的Index Action,就會看到Render後的結果和BundleConfig.cs中設定的一致

Asp.net MVC微軟範例 產出html結果

大多數情況下,正式運行的IIS站台使用的web.config會將debug選項關閉,在debug開發情況下,開發者若想在IIS Express看到Bundle的結果需要將web.config的debug選項關閉

1
2
3
<system.web>
<compilation debug="false" targetFramework="4.8" />
</system.web>

關閉debug後就會看到檔案依據後端設定合併

Asp.net MVC微軟範例 產出Bundle結果

連結打開後就會發現是合併並且經過minify的檔案

※預設情況下,Bundle會選擇*.min.js或*.min.css的檔案來用,但沒有的話,測試上為會透過內建的程式做自動壓縮動作
Asp.net MVC微軟範例 modernizr

再來就是開始思考應該這套程式與Sitecore的適合度

Sitecore在資源檔案的使用上,甚至會完全依賴資料庫內容,且為了處理Sitecore的多子站台架構,Sitecore基本上完全管控路由組成來完成內部子站台跨站台共用資源

但Sitecore的模組化設計,Sitecore參考Asp.net Core的方式,開發者可以透過DI(Dependency Injection)的方式附加其所能執行的程式。因此只要在Sitecore處理路由之前注入Bundle使用的路由,就有辦法實現原本Asp.net MVC預設專案範本所使用的Bundle方式

※備註:

  1. 對於Hosting的IIS來說,Sitecore僅為一個站台,註冊的Bundle路由卻需搶在Sitecore之前處理完畢,也就是可能無法讓Sitecore計算當前站台、當前語系、當前使用者
  2. Sitecore仍然掌握除了Bundle以外的路由,也就是Bundle出來的js或css檔案內,若填寫相對路徑的情況下,基本上都會是錯的,建議需要使用bundle的資源檔案內,有實體檔案路徑時,可以自站台根目錄開始撰寫路徑,避免路徑上的錯誤造成404找不到的回應

思考流程

Pipeline注入

Sitecore提供各式各樣的事件流程(參考),開發者可以依照想要執行的項目在Sitecore的程式碼執行流程中新增、取代或刪除Sitecore的執行流程,就目前想要的Bundle效果來說,我們大致上可以參照Asp.net MVC的專案範本中的BundleConfig.cs路徑(App_Start資料夾下),事件應寫於Application Start中,但Sitecore並不建議我們直接去修改或調整一般IIS Hosting使用的global.asax做全域性的修改,開啟檔案就會看到Sitecore給開發者的警告
Sitecore不建議修改global.asax

因此我們轉向Sitecore開發者經常使用的initialize,也就是Sitecore的應用程式初始化事件中,加入註冊Bundle的程式

1
2
3
4
5
6
7
<sitecore>
<pipelines>
<initialize>
<processor patch:before="processor[@type='Sitecore.Pipelines.Loader.ShowVersion, Sitecore.Kernel']" type="{your namespace}, {your assembly name}" />
</initialize>
</pipelines>
</sitecore>

BundleCollection調整

對應的程式碼也與微軟的範例差不多,基本上就是第一個參數是razor對應key,再來就是指定要執行bundle的資源,若有引入順序差別則建議使用Include方法一個一個載入,預設情況為使用檔案字母排序,但也可以自己客製排序邏輯

1
2
3
4
5
6
7
8
9
10
11
12
13
public class RegisterBundleProcessor : Sitecore.Mvc.Pipelines.Loader.InitializeRoutes
{
public override void Process(PipelineArgs args)
{
Register(BundleTable.Bundles);
}

private void Register(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/web/bundle.js").IncludeDirectory("~/web/global/js", "*.js", true));
bundles.Add(new StyleBundle("~/web/bundle.css").IncludeDirectory("~/web/global/css", "*.css", true));
}
}

Razor View

View上需要載入對應的bundle key,這部分就是原原本本的Asp.net MVC,跟Sitecore無關,但還是需要注意出來的網址路由會不會被Sitecore搶走,若是在多層網路架構下,就要再另外注意路由的暢通

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
@Styles.Render("~/web/bundle.css")
</head>
<body>
@Scripts.Render("~/web/bundle.js")
</body>
</html>

檢視BundleTable的Class,其實有包含開關的屬性 - EnableOptimizations,雖然是屬於static靜態,但感覺有操作的空間

1
2
3
4
5
6
7
8
//
// 摘要:
// Gets or sets whether bundling and minification of bundle references is enabled.
//
// 傳回:
// true if bundling and minification of bundle references is enabled; otherwise,
// false.
public static bool EnableOptimizations { get; set; }

在PreprocessRequest事件中,再載入一個檢查QueryString的程式

1
2
3
4
5
6
7
<sitecore>
<pipelines>
<preprocessRequest>
<processor type="{your namespace}, {your assembly name}" />
</preprocessRequest>
</pipelines>
</sitecore>

依樣畫葫蘆,有什麼載入的pipeline事件,背後都有一個對應的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BundleOptimizationProcessor : PreprocessRequestProcessor
{
public override void Process(PreprocessRequestArgs args)
{
EnableOptimizations();
}

public static void EnableOptimizations()
{
var debugMode = HttpContext.Current.Request.QueryString.Get("debug");
if (!string.IsNullOrEmpty(debugMode) && debugMode == "true")
{
BundleTable.EnableOptimizations = false;
}
else
{
BundleTable.EnableOptimizations = true;
}
}
}

透過這樣的設定,就可以簡單透過QueryString的方式來開關要不要做minify了,但還是友善提醒一下,因為這個屬性屬於一個全域物件的靜態屬性,也就是改下去的當下是有機會和不知道從哪來的使用者撞到,造成程式出錯(雖然機率應該是極低,拿平常看不到程式檔案的環境來說,為了提升維護的便利性,這點風險還是挺值得的)

※備註:

  1. 基本上當系統越肥大,東西就會越慢,可想而知就知道客製的Pipeline掛載越多,就需要有系統資源不足或運行變慢的心理準備
  2. 程式碼中使用的key和微軟的範例不同,當開啟EnableOptimizations設定後,key就會是合併後的Url,所以.js和.css等副檔名就會消失了,為了美觀,所以就在key加上副檔名了

優點與缺點

優點

  1. 看微軟的文件就知道,minify資源對於流量縮減是很吸引人的
  2. 自動建立亂數的QueryString在資源檔網址後,當Bundle內的檔案變更時還會跟著稍微變動一下,在調整client端的cache非常好用
  3. Cache機制真的是頗優秀,除了Client端建立固定Hash值的QueryString外,在後端也是建立一組Cache來準備提供給Client的回應(參考)

缺點

  1. 別人做的東西拿來用總是要懷疑一下,但交給別人就是不知道他做了什麼,基本上看到做minify出來的檔案總要試一下有沒有壓縮正確,沒有辦法保證被壓縮過的檔案還能夠正常使用,更何況是runtime執行,也就是放上去之後才有辦法看,一旦錯了但又是在上線結束之後才發現,那就是很抱歉,請等候下次上線,最慘的情況甚至還要提單要求緊急上線做修正
  2. 基本上就跟第一點差不多,就算是微軟爸爸也是會出錯的,雖然會傳回原檔案內容,但到底能不能用總有種看運氣的感覺(參考)
    壓縮失敗
  3. 開發者需要清楚知道前端資源載入的需求順序或者資源間的相依性才好設計各項Bundle內含的資源
  4. 雖然微軟文件中寫可以透過額外的nuget套件加入對less的支援,但實際上簡單測試過後不符合預期,不確定是否是Sitecore的影響,無法成功加入相關的Bundle。在全新建置的微軟範例專案,比照操作後也無法正常運作,有種被放生的感覺,且對於前端建置的程式,應有更適合的前端框架可以使用(如Webpack)

參考文件

  1. 統合和縮製
  2. Sitecore - Dependency injection
  3. Sitecore MVC and pipelines
  4. ASP.NET MVC ScriptBundle Cache 原理剖析
  5. ASP.NET MVC 手動利用System.Web.Optimization綁定壓縮JS與CSS