בפוסט הקודם סקרנו את נושא הכריכה והכיווץ (Bundling and Minification, B&M) של קבצי ג’אווה-סקריפט ו-CSS. וראינו על קצה המזלג איך ניתן להתערב בתהליך באמצעות מימוש של IBundleTransform.

    הפעם נעסוק עוד בהתאמה אישית של B&M באמצעות יצירת Bundle שונה מה-Bundles הסטנדרטיים (ScriptBundle ו-StyleBundle), שיפעיל מימוש נוסף של IBundleTransform.

    רקע – קצת על Resources

    כל מתכנת ‎.net מתחיל לומד להכיר את הקונספט של Resources (כרגיל, לשם נוחות הכתיבה והקריאה אני אשתמש בהמשך, לסירוגין, גם במונח העברי, משאבים).

    העובדה שאנחנו מוציאים את הטקסטים מחוץ לקוד, נותנת לנו מספר יתרונות:

    • גם מי שלא מבין בתכנות יכול לטפל בטקסטים. וכמי שבחר בקריירה של מתכנת ולא של עורך לשוני, זה מסיר ממני עול גדול.
    • אפשר לשנות אותם יחסית בקלות ובכל שלב (אפילו אחרי שהתוכנה מקומפלת ורצה, אם הם כתובים ב-assembly נפרד).
    • וכמובן, מימוש פשוט של ריבוי שפות (ובצורה כללית יותר Internationalization ו-localization).

    הבעיה בפיתוח צד-לקוח

    כאשר מפתחים אתרים ואפליקציות Web, ניתן להשתמש ב-Resources בצד השרת (או בדפים שנוצרים דינמית על ידי השרת), בצד הלקוח לעומת זאת אין לנו גישה ל-Resources, ואי אפשר לכתוב, לדוגמה, קוד Javascript שמשתמש בהם. כמו הקוד הבא:

    Code Snippet
    1. function ask () {
    2.     var result = confirm(JsResource.ConfirmQuestion);
    3.     if (result) {
    4.         alert(JsResource.ConfirmMessage);
    5.     } else {
    6.         alert(JsResource.CancelMessage);
    7.     }
    8. }

    קוד דומה לו היה רץ בצד השרת היה עובד היטב (אם אכן JsResource קיים), אבל בצד הלקוח הדפדפן לא מכיר את JsResource.

    קיימים מספר פתרונות לבעיה, ביניהם:

    • כתיבת HttpHandler (קובץ asax) שמחזיר את הערכים שבקובץ משאבים אחד או יותר.
    • להוסיף את הערכים לקובץ ה-cshtml בצורה ידנית, זה אחר זה. או באמצעות לולאה שרצה על כל קובץ ה-Resources.

    אני רוצה להציע בפוסט זה שיטה שלטעמי היא טובה יותר.  הוספת קובץ המשאבים ל-Bundle והפיכתו ל-Javascript באמצעות מימוש מותאם אישית של IBundleTransform.  לשם כך יש צורך ליצור Bundle מותאם אישית, שנוכל לכלול בו את המשאבים הדרושים, ושיפעיל Transform שהופך אותם לקוד JS.

    שלב 1 בפתרון: ScriptAndResourcesBundle

    זה הקוד של ה-Bundle:

    Code Snippet
    1. public class ScriptAndResourcesBundle : Bundle
    2. {
    3.  
    4.     public ScriptAndResourcesBundle(string virtualPath) : this(virtualPath, null) { }
    5.     public ScriptAndResourcesBundle(string virtualPath, string cdnPath) : base(virtualPath, cdnPath, new ResourcesBundleTransform(), new JsMinify()) { }
    6.  
    7.  
    8.     public IList<Type> resourcesList = new List<Type>();
    9.     
    10.  
    11.     public ScriptAndResourcesBundle IncludeResource(params Type[] resources)
    12.     {
    13.         foreach (Type resource in resources)
    14.         {
    15.             resourcesList.Add(resource);
    16.         }
    17.         return this;
    18.     }
    19.  
    20.     public new ScriptAndResourcesBundle Include(params string[] virtualPaths)
    21.     {
    22.         return (ScriptAndResourcesBundle)base.Include(virtualPaths);
    23.     }
    24.     public new ScriptAndResourcesBundle Include(string virtualPath, params IItemTransform[] transforms)
    25.     {
    26.         return (ScriptAndResourcesBundle)base.Include(virtualPath, transforms);
    27.     }
    28.  
    29.  
    30. }

    כמה הבהרות והערות:

    • קודם כל, המימוש רחוק מלהיות מלא או מושלם, בסוף הפוסט אני אפרט כמה מהדברים שכדאי לשנות.
    • העדפתי ליצור Bundle שמאפשר לכרוך יחד את הקוד ואת המשאבים כי, בדרך כלל, הם קשורים זה לזה. במידת הצורך ניתן ליצור Bundle שמכיל רק Resources.
    • (שורות 4-5) הבנאים של המחלקה. מכניסים לרשימת ה-Transforms שתים:  JsMinify הסטנדרטי, ולפניו את ResourcesBundleTransform שנכיר עוד מעט.
      שימו לב: ה-ResourcesBundleTransform נוסף ראשון כדאי שגם הקוד שהוא יוצר יעבור מיניפיקציה.
    • (8-18) כאן מוגדר המשתנה שמכיל את רשימת ה-Resources שנכללים ב-Bundle הזה, והפונקציה שמאפשרת להוסיף אליה (הפונקציה מחזירה את האובייקט עצמו כדי לאפשר שרשור, כמו בדוגמא בהמשך).
    • (20-27) אני מסתיר (Hiding) את הפונקציות Include המקוריות בשביל להחזיר ערך מסוג ScriptAndResourcesBundle, ולא Bundle כללי, גם זה כדי לאפשר שרשור.

    ועכשיו הדוגמה לשימוש ב-ScriptAndResourcesBundle:

    Code Snippet
    1. public static void RegisterBundles(BundleCollection bundles)
    2. {
    3.     bundles.Add(new ScriptAndResourcesBundle(“~/scriptsBundle”)
    4.         .Include(“~/content/code.js”)
    5.         .IncludeResource(typeof(Resources.JsResource))
    6.         .Include(“~/content/code2.js”));
    7. }

    כמו שניתן לראות אפשר לשרשר בזה אחר זה, גם הוספה של קבצי קוד, וגם הוספה של אובייקטי Resources.

    שלב 2: ResourcesBundleTransform

    עכשיו נותר לכתוב את החלק שעושה את הפלא והופך בפועל את קובץ המשאבים לקוד Javascript.

    Code Snippet
    1. public class ResourcesBundleTransform : IBundleTransform
    2. {
    3.     public void Process(BundleContext context, BundleResponse response)
    4.     {
    5.         ScriptAndResourcesBundle bundle = (ScriptAndResourcesBundle)context.BundleCollection.FirstOrDefault(x => x.Path == context.BundleVirtualPath);
    6.         if (bundle == null)
    7.             return;
    8.  
    9.         var processedResources = bundle.resourcesList.Select(x => ProcessResource(x));
    10.         response.Content = string.Join(“;”, processedResources) + response.Content;
    11.     }
    12.  
    13.     private string ProcessResource(Type resource)
    14.     {
    15.         var propertyValues = resource
    16.             .GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Static)
    17.             .Where(x => x.PropertyType == typeof(string))
    18.             .Select(x => string.Format(“{0}: ‘{1}'”, x.Name, x.GetValue(null)));
    19.         return string.Format(“var {0} = {{ {1} }};”, resource.Name, string.Join(“,”, propertyValues));
    20.     }
    21. }

     

    • (שורה 3) השיטה Process היא זו שנקראת מה-Bundle כדי שה-Transform יעשה את העבודה.
    • (שורה 5) מוצאת את  ה-Bundle שעליו הופנקציה פועלת כרגע.
    • (13-15) הפונקציה הזו (ProcessResource) עושה את עיקר העבודה.
      • באמצעות Reflection היא מוצאת את המאפיינים שהם Static, Public יש להם שיטת Get והם מסוג מחרוזת. זה מסנן את רק את המאפיינים שאנחנו כתבנו ולא את הדברים שמובנים בכל Resources.
      • אנחנו יוצרים מכל מאפיין מחרוזת במבנה “שם: ‘ערך'”. ואת כולם יחד מחברים לאובייקט Javascript אחד ארוך.
      • טיפ: בשורה 19 עשיתי שימוש בסוגריים מסולסלים כפולים ({{ {1} }}), אחרת הפונקציה string.Format הייתה קורסת (ראו כאן: http://stackoverflow.com/a/3773868).
    • (9-10) כאן אני קורא ל-ProcessResource הנ”ל לכל אחד מקבצי המשאבים שהוכללו ב-Bundle, ומוסיף אותם לתחילת response.Content, שהוא המשתנה שמכיל את התוצאה של ה-B&M.

    התוצאה

    כך נראה קובץ ה-Bundle אחרי כל העיבודים (אני חילקתי אותו לשתי שורות):

    Code Snippet
    var JsResource={CancelMessage:“Canceled!”,ConfirmMessage:“Confirmed!”,ConfirmQuestion:“Are you confirm?”};
    (function(){var n=confirm(JsResource.ConfirmQuestion);n?alert(JsResource.ConfirmMessage):alert(JsResource.CancelMessage)})()

    ניתן לראות את קוד ה-JS המכווץ (זה הקוד שהוצג בתחילת הפוסט, שימו לב מה קרה ל-if) ולפניו את ה-Resources שהוספנו, שגם הם עברו כיווץ (כל הרווחים נמחקו).

    מה הלאה?

    הקוד שמופיע עד כאן עובד היטב, אבל הוא לא מושלם, יש כמה דברים שהייתי מתקן בו (את חלקם לא עשיתי רק כדי לקצר כאן את הקוד):

    • כדאי למנוע הכנסת ערכים כפולים ל-resourcesList.
    • לא עשיתי Override ל-IncludeDirectory.
    • כדי לאפשר את הגישה ל-resourcesList מה-ResourcesBundleTransform הגדרתי אותו Public, עדיף כמובן לעשות אותו פרטי ולאפשר לקבל אותו. 
    • והפרט החשוב ביותר: מנגנון לוקליזציה – שיאפשר להחזיר את המשאבים בשפה הנכונה (בעזרת ה’ אני אכתוב על זה באחד הפוסטים הבאים).

    אבל לפני שאני עושה את כל זה יש לי רעיון שאולי ישאיר אבק לכל מה שכתבתי כאן, אלא שהוא עדיין צריך עיבוד. בעזרת השם בקרוב.