前一篇文章,我用 ASP.NET MVC 寫了一個即時顯示網站應用程式 CPU% 及記憶體用量的小視窗,美中不足是專案結構有點小複雜,包含 css、js、ttf 字型、cshtml、Models 類別、Controller,還需要改 web.config 才能下載 .ttf:

如果今天我覺得這功能不錯,想加進另一個 ASP.NET MVC 專案,依目前架構我得複製一堆檔案,修改 Namespace 什麼的,程序繁瑣又容易出錯,稱不上好用。那麼,何不把這堆東西包成一個類別程式庫(Class Library),任何 ASP.NET MVC 網站需要加入即時監看視窗,參照這個 .dll 檔就全部搞定,聽起來是不是比較高級?

好,就來動手試試看吧!

先建了一個 Class Libraray,把網站狀態監控相關的檔案都搬過進去:

有件神奇的事,ASP.NET MVC 會自動為名稱結尾 Controller 的類別建立路由,即使 WebStatsMonitorController 放在外部 DLL 專案,只要 ASP.NET MVC 專案參照該 DLL,網址輸入 ~/WebStatsMonitor 便能導向 WebStatsMonitorController,不需額外設定。

這裡有個重要技巧 - 將 Res 下檔案的 Build Action 設成 Embedded Resource,再寫一個 Resource() Action 依 resName 參數吐回 js、css、html、ttf 等檔案。把所需檔案包進 DLL,部署時不用拖著一大串拖油瓶,才稱得上乾淨俐落:

WebStatsMonitorController 加入一個 ActionResult Resource(string resName) 傳入 Res 資料夾內嵌的各個檔案。原本的 Index.cshtml 被我轉成靜態 HTML 檔 - View.html,原本 MVC 能自動將 "~/path" 轉成當下網站路徑,現在要花點功夫自己轉換。而 "~/Content/webstats.css" 等外部資源 URL,因應資料內嵌要改成 "~/WebStatesMonitor/Resource?resName=webstats.css",這部分用 string.Replace("~/", Request.Application) 簡單搞定就好。

WebStatsMonitorController.cs 長這樣:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace WebStatsMonitor
{
    public class WebStatsMonitorController : Controller
    {
        public ActionResult Index()
        {
            return RedirectToAction("Resource", new { resName = "View.html" });
        }

        public ActionResult Stats()
        {
            return this.Json(new
            {
                CPU = SelfPerfMonitor.GetCpuUsage(),
                RAM = SelfPerfMonitor.GetMemUsage()
            }, JsonRequestBehavior.AllowGet);
        }

        static Assembly assembly = typeof(WebStatsMonitorController).Assembly;
        static string namespacePrefix = typeof(WebStatsMonitorController).FullName.Replace(nameof(WebStatsMonitorController), "Res.");

        public ActionResult Resource(string resName)
        {
            var resStream = assembly.GetManifestResourceStream(namespacePrefix + resName);
            if (resStream == null) return new HttpNotFoundResult();
            byte[] content;
            using (var ms = new MemoryStream())
            {
                resStream.CopyTo(ms);
                content = ms.ToArray();
            }
            var extName = Path.GetExtension(resName).ToLower();
            switch(extName)
            {
                case ".html":
                case ".css":
                    var text = Encoding.UTF8.GetString(content);
                    text = text.Replace("~/", Request.ApplicationPath);
                    return Content(text, "text/" + extName.TrimStart('.'));
                case ".ttf":
                    return File(content, "application/font-sfnt");
                case ".js":
                    return File(content, "text/javascript");
                default:
                    throw new NotImplementedException();
            }
        }
    }
}

Index.cshtml 轉成的 View.html 如下,注意其中的 webstats.css、jquery.min.js 路徑被改成,~/WebStatsMonitor/Resource?resName=...,Resource() Action 會將 ~/ 換成網站應用程式的根網址。

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Web CPU & Memory</title>
    <link href="~/WebStatsMonitor/Resource?resName=webstats.css" rel="stylesheet" />
</head>
<body>
    <div class="frame">
        <div class="row">
            <span class="hdr">CPU</span> <span class="field" id="cpuLoad"></span>
        </div>
        <div class="row">
            <span class="hdr">RAM</span> <span class="field" id="memUsage"></span>
        </div>
    </div>
    <script src="~/WebStatsMonitor/Resource?resName=jquery.min.js"></script>
    <script>
        $(function () {
            //TODO 實際運用時改走 SignalR/WebSocket 較有效率
            function refresh() {
                $.getJSON("~/WebStatsMonitor/Stats").done(function (res) {
                    var cpuCss = "";
                    var cpuValue = parseInt(res.CPU.replace("%"));
                    if (cpuValue > 85) cpuCss = "alert";
                    else if (cpuValue > 50) cpuCss = "warn";
                    $("#cpuLoad").text(res.CPU).attr("data-tag", cpuCss);
                    $("#memUsage").text(res.RAM);
                });
            }
            refresh();
            setInterval(refresh, 2000);
        });
        
    </script>
</body>
</html>

大家可能有注意到,webstats.css 也需要做 "~/..." 網址轉換,原因是其中有 font-face 指向 ttf 檔網址。以 MVC Action 取代靜態檔案後,web.config 中的 <mimeMap fileExtension=".ttf" mimeType="application/font-sfnt"/> 倒是可不用加:

@font-face {
    font-family: LCD;
    src: url('~/WebStatsMonitor/Resource?resName=DS-DIGI.TTF');
}

就醬,一個可攜式的即時網站應用程式 CPU 與記憶體用量顯示器元件就完成了。來試試在網站引用它有多簡單,開一個空白網站,參照 WebStatsMonitor.dll,在根目錄建一個 demo.html 加入 <iframe style="border: none" src="WebStatsMonitor"></iframe>,薑薑薑薑~~

完整元件版程式碼我也放上 Github 了(在 classlib Branch),有興趣同學請自取。

【下集預告】文章所示範的方法,在檔案內嵌後需要將靜態檔案連結由 ~/... 改成 Resource?resName=...,而原來的 cshtml 換成 html,若網頁複雜修改工程將很可觀,能不能做到沿用 ~/... 靜態檔網址,甚至用內嵌 cshtml 檔產生 View 呢?我會這樣寫當然就是可以囉,但我們下次再聊。

Tips of how to move ASP.NET MVC functions into a class library and make it pluggable.


Comments

Be the first to post a comment

Post a comment