ASP.NET Core 中消失的 Display Modes 與全新作法

Display Modes 並非與 RWD 互斥的技術,個人認為 Display Modes 與 RWD 應該是相輔相成,以此技術區分桌面與行動版本,在行動版本中引入 RWD 技術 (一般情況下在桌面版搞 RWD 究竟有多少價值?),這時候 RWD 的跨度可以不用十分巨大複雜,是作為採用後端 View 專案中最佳的前端實踐。

前言

在那個篤信 RWD 是手機、平板、桌面多平台網頁唯一救贖的無可救藥時代(疑?現在還是嗎),無論專案型態、也搞不清楚合不合適,所有專案中的角色清一色的談起 RWD,以致於 RWD 的泛商業化用詞法氾濫到令人難耐,無法呼吸。

然而更早之前的 M 版網頁則更是令人難過痛苦難熬。

簡單舉例 M 版網頁以域名、或是路徑區別的處理方式,以下其中一種區別出行動版本的做法

  • 網域名稱,例 http://m.domain.com/
  • HTTP Path,例 http://domain.com/m

事實上許多優異的 web 框架都提供了更加理性貼近事實的解決方案,例如:Laravel、RoR,然而或許它們缺乏了一個類似 RWD 一樣響亮可以朗朗上口的語彙,以至於被冷落或不被理解,在 ASP.NET MVC 中它們管叫 Display Modes。

什麼是 Display Modes

Display Modes 在 ASP.NET MVC 4 的時候推出,已經熟悉 Display Modes 的朋友們請略過此段。

簡單的說 Display Modes 主要仍為以後端 View 為出發點的技術,在對於相同內容網頁的情求時,根據請求方裝置的特性,提供適合的頁面版本。採用 Display Modes 技術,往往為伴隨著一個特性「URL 唯一」,這一點是 M 版網頁所做不到的。

講述相同內容的網頁,雖為不同的佈局(Layout)以及天差地遠的側邊欄(Asides)以及選單樣式,但是都為同一個 URL 所有,這種情況是 SEO 友好的。頁面設計的難度也可以此出現區別,不為難設計師,說真的,畢竟不是所有類型的網頁都適合設計為 RWD。

Display Modes 具體的實踐,舉例來說若有個 ProductController 控制器以及 Index 方法,視圖為 ~/Views/Product/Index.cshtml 作為商品介紹頁,程式執行後可透過 URL /Product/Index 訪問該頁。我們能在後端接受到網頁請求時,根據客戶端條件(通常是 UserAgent),給予一個檔名後綴,例如 "Mobile",如下圖,詳細可參考亂馬客的 ASP.NET MVC 4 中的 Display Mode 文章。

Display Modes 的機制讓 MVC 在執行時期知道,目前的請求者是否具備有 "Mobile" 的條件,若是則優先地提供 "Mobile" 的版本,在前述 ~/Views/Product 的目錄中 Index.Mobile.cshtml 會優先成為返回的視圖,若該檔案不存在,則採用預設的 Index.cshtml,解決方案既優雅又容錯。

ProductController 的 Index 方法,試圖提供 "Mobile" 版本時的尋找順序

  1. ~/Views/Product/Index.Mobile.cshtml
  2. ~/Views/Product/Index.cshtml
  3. ~/Views/Shared/Index.Mobile.cshtml
  4. ~/Views/Shared/Index.cshtml

在 ASP.NET MVC 4 & 5 View 的技術體系中,所有與 View 相關的檔案取用都一致的參考著 Display Modes 的準則,也就是說:Layout、EditorTemplates、DisplayTemplates、PartialView、View 皆一體適用,這點我曾在 ASP.NET MVC 5 開發美學的 View 章節中提出說明。

需要注意的是 Display Modes 並非與 RWD 互斥的技術,個人認為 Display Modes 與 RWD 應該是相輔相成,以此技術區分桌面與行動版本,在行動版本中引入 RWD 技術 (一般情況下在桌面版搞 RWD 究竟有多少價值?),這時候 RWD 的跨度可以不用十分巨大複雜,是作為採用後端 View 專案中最佳的前端實踐。

然而,在脫胎換骨的 ASP.NET Core 中,我卻不見了 Display Modes 的蹤跡,驚訝莫名尋找解決方案時,發現了 ViewLocationExpander。

新玩家 ViewLocationExpander

因前述,筆者狐疑地在 GitHub 上提問 後才驚覺 Display Modes 在 ASP.NET Core 上竟然除役了,不是延期支援而是直接取消,在 ASP.NET Core 的官方說明文件對此議題亦為從缺待補,GitHub 相關 repo 的討論串並沒有顯著提供的替代方案,但有些前輩指出以 ViewLocationExpander 來處理的方向。

ASP.NET Core 上的替代方案

在 ASP.NET Core 正式提出解決方法前,筆者想為目前從缺的這個角色做出些貢獻,因為實在很受不了開口閉口就只有 RWD 的傢伙們

public class DisplayModesViewLocationExpander : IViewLocationExpander
{
    private static Regex _b = new Regex(@"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
    private static Regex _v = new Regex(@"1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);

    public void PopulateValues(ViewLocationExpanderContext context)
    {
        var httpContext = context.ActionContext.HttpContext;
        var userAgent = httpContext.Request.Headers["User-Agent"].FirstOrDefault();

        context.Values["Suffix"] = _b.IsMatch(userAgent) || _v.IsMatch(userAgent.Substring(0, 4))
                                   ? "Mobile"
                                   : null;
    }

    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        var suffix = context.Values["Suffix"];

        if (string.IsNullOrEmpty(suffix))
        {
            foreach (var viewLocation in viewLocations)
            {
                yield return viewLocation;
            }
        }

        foreach (var viewLocation in viewLocations)
        {
            yield return Path.ChangeExtension(viewLocation, $".{suffix}.cshtml");
            yield return viewLocation;
        }
    }
}

在 Startup.cs 的 ConfigureServices 方法中註冊一下我們的 DisplayModesViewLocationExpander,就大功告成了。

services.Configure(options =>
{
    options.ViewLocationExpanders.Add(typeof(DisplayModesViewLocationExpander));
});

結尾

實際研究下來,私以為 ViewLocationExpander 可以說是 Razor Engine View 搜尋策略與 Display Modes 的合體版,使用上能夠更加貼切的定製出應用程式專屬的視圖匹配引擎,如果不喜歡 Index.cshtml / Index.Mobile.cshtml 在專案中的呈現方式,那麼你可以在 ViewLocationExpander 直接改用目錄的方式區份出兩個版本。

未來在應用上有新的構想,或者 ASP.NET Core 出現的官方版本的替代做法再更新給各位。