Minify HTML, CSS, JS trong MVC với ZetaProducerHtmlCompressor

Thảo luận trong 'Lập Trình Website MVC5 & MVC6' bắt đầu bởi seolagi, 1/10/20.

  1. seolagi

    seolagi

    Vip Member

    Tham gia ngày:
    16/4/14
    Bài viết:
    680
    Đã được thích:
    55
    Điểm thành tích:
    28
    Mình có đọc có nhiều bạn hỏi cách tối ưu Minify HTML, CSS style, javascript, JS trong MVC như thế nào? và đặc biệt phiên bản MVC 5, MVC 6 trở lên như thế nào? hôm nay mình thay mặt Admin viết bài này, vì kiến thức được chia sẻ từ admin itseovn.

    Cách Minify HTML mình sử dụng ZetaProducerHtmlCompressor để nén lại, tối ưu hóa tốc độ PageSpeed Insights của Google.

    Trước tiên bạn tải Nuget của ZetaProducerHtmlCompressor tại link sau (sau đó add dll vào web): https://www.nuget.org/packages/ZetaProducerHtmlCompressor/
    Hoặc vào Tools-> Nuget package manager -> Manager nuget package for solution... để add thêm vào code của web.

    Thêm xong, bạn có thể xem hướng dẫn của nhà cung cấp tại link sau: https://gist.github.com/herman1vdb/a026e84330b481448b17 hoặc tiếp tục theo dõi bài viết của mình nhé.

    Thêm ZetaProducerHtmlCompressor xong bạn tạo 1 ActionFilterAttribute có tên: MinifyHtmlAttribute.cs hoặc add thêm vào ActionFilterAttribute có sẵn nội dung sau:

    Mã:
    using System;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Optimization;
    using ZetaProducerHtmlCompressor.Internal;
    
    namespace ITSEOVN.Utils
    {
        public sealed class MinifyHtmlAttribute : ActionFilterAttribute
        {
            public override void OnActionExecuting(
              ActionExecutingContext filterContext)
            {
                var response = filterContext.HttpContext.Response;
    
                if (response.Filter == null) return;
    
                HtmlCompressor compressor = new HtmlCompressor();
                ResponseFilterStream filter = new ResponseFilterStream(response.Filter);
                compressor.setRemoveComments(true);
                compressor.setCssCompressor(new CssCompressor(filterContext.HttpContext));
                compressor.setCompressCss(true);
                compressor.setJavaScriptCompressor(new JsCompressor(filterContext.HttpContext));
                compressor.setCompressJavaScript(true);
                filter.TransformString += s =>
                {
                    return compressor.compress(s);
                };
                response.Filter = filter;
            }
    
            private class CssCompressor : ICompressor
            {
                HttpContextBase HttpContext { get; set; }
                public CssCompressor(HttpContextBase httpContext)
                {
                    HttpContext = httpContext;
                }
                public string compress(string source)
                {
                    var bundleContext = new BundleContext(HttpContext, BundleTable.Bundles, "~/css/inline");
                    var bundle = BundleTable.Bundles.Single(b => b.Path == "~/css/inline");
                    var bundleResponse = bundle.GenerateBundleResponse(bundleContext);
                    bundleResponse.Content = source;
                    bundleResponse.ContentType = "text/css";
                    new CssMinify().Process(bundleContext, bundleResponse);
                    if (bundleResponse.Content.Contains("Minification failed"))
                    {
                        return source;
                    }
                    return bundleResponse.Content;
                }
            }
    
            private class JsCompressor : ICompressor
            {
                HttpContextBase HttpContext { get; set; }
                public JsCompressor(HttpContextBase httpContext)
                {
                    HttpContext = httpContext;
                }
                public string compress(string source)
                {
                    var bundleContext = new BundleContext(HttpContext, BundleTable.Bundles, "~/script/inline");
                    var bundle = BundleTable.Bundles.Single(b => b.Path == "~/script/inline");
                    var bundleResponse = bundle.GenerateBundleResponse(bundleContext);
                    bundleResponse.Content = source;
                    bundleResponse.ContentType = "text/javascript";
                    new JsMinify().Process(bundleContext, bundleResponse);
                    if (bundleResponse.Content.Contains("Minification failed"))
                    {
                        return source;
                    }
                    return bundleResponse.Content;
                }
            }
            /// <summary>
            /// A semi-generic Stream implementation for Response.Filter with
            /// an event interface for handling Content transformations via
            /// Stream or String.
            /// <remarks>
            /// Use with care for large output as this implementation copies
            /// the output into a memory stream and so increases memory usage.
            /// </remarks>
            /// </summary>
            private class ResponseFilterStream : Stream
            {
                /// <summary>
                /// The original stream
                /// </summary>
                Stream _stream;
    
                /// <summary>
                /// Current position in the original stream
                /// </summary>
                long _position;
    
                /// <summary>
                /// Stream that original content is read into
                /// and then passed to TransformStream function
                /// </summary>
                MemoryStream _cacheStream = new MemoryStream(5000);
    
                /// <summary>
                /// Internal pointer that that keeps track of the size
                /// of the cacheStream
                /// </summary>
                int _cachePointer = 0;
    
    
                /// <summary>
                ///
                /// </summary>
                /// <param name="responseStream"></param>
                public ResponseFilterStream(Stream responseStream)
                {
                    _stream = responseStream;
                }
    
    
                /// <summary>
                /// Determines whether the stream is captured
                /// </summary>
                private bool IsCaptured
                {
                    get
                    {
    
                        if (CaptureStream != null || CaptureString != null ||
                            TransformStream != null || TransformString != null)
                            return true;
    
                        return false;
                    }
                }
    
                /// <summary>
                /// Determines whether the Write method is outputting data immediately
                /// or delaying output until Flush() is fired.
                /// </summary>
                private bool IsOutputDelayed
                {
                    get
                    {
                        if (TransformStream != null || TransformString != null)
                            return true;
    
                        return false;
                    }
                }
    
    
                /// <summary>
                /// Event that captures Response output and makes it available
                /// as a MemoryStream instance. Output is captured but won't
                /// affect Response output.
                /// </summary>
                public event Action<MemoryStream> CaptureStream;
    
                /// <summary>
                /// Event that captures Response output and makes it available
                /// as a string. Output is captured but won't affect Response output.
                /// </summary>
                public event Action<string> CaptureString;
    
    
    
                /// <summary>
                /// Event that allows you transform the stream as each chunk of
                /// the output is written in the Write() operation of the stream.
                /// This means that that it's possible/likely that the input
                /// buffer will not contain the full response output but only
                /// one of potentially many chunks.
                ///
                /// This event is called as part of the filter stream's Write()
                /// operation.
                /// </summary>
                public event Func<byte[], byte[]> TransformWrite;
    
    
                /// <summary>
                /// Event that allows you to transform the response stream as
                /// each chunk of bytep[] output is written during the stream's write
                /// operation. This means it's possibly/likely that the string
                /// passed to the handler only contains a portion of the full
                /// output. Typical buffer chunks are around 16k a piece.
                ///
                /// This event is called as part of the stream's Write operation.
                /// </summary>
                public event Func<string, string> TransformWriteString;
    
                /// <summary>
                /// This event allows capturing and transformation of the entire
                /// output stream by caching all write operations and delaying final
                /// response output until Flush() is called on the stream.
                /// </summary>
                public event Func<MemoryStream, MemoryStream> TransformStream;
    
                /// <summary>
                /// Event that can be hooked up to handle Response.Filter
                /// Transformation. Passed a string that you can modify and
                /// return back as a return value. The modified content
                /// will become the final output.
                /// </summary>
                public event Func<string, string> TransformString;
    
    
                protected virtual void OnCaptureStream(MemoryStream ms)
                {
                    if (CaptureStream != null)
                        CaptureStream(ms);
                }
    
    
                private void OnCaptureStringInternal(MemoryStream ms)
                {
                    if (CaptureString != null)
                    {
                        string content = HttpContext.Current.Response.ContentEncoding.GetString(ms.ToArray());
                        OnCaptureString(content);
                    }
                }
    
                protected virtual void OnCaptureString(string output)
                {
                    if (CaptureString != null)
                        CaptureString(output);
                }
    
                protected virtual byte[] OnTransformWrite(byte[] buffer)
                {
                    if (TransformWrite != null)
                        return TransformWrite(buffer);
                    return buffer;
                }
    
                private byte[] OnTransformWriteStringInternal(byte[] buffer)
                {
                    Encoding encoding = HttpContext.Current.Response.ContentEncoding;
                    string output = OnTransformWriteString(encoding.GetString(buffer));
                    return encoding.GetBytes(output);
                }
    
                private string OnTransformWriteString(string value)
                {
                    if (TransformWriteString != null)
                        return TransformWriteString(value);
                    return value;
                }
    
    
                protected virtual MemoryStream OnTransformCompleteStream(MemoryStream ms)
                {
                    if (TransformStream != null)
                        return TransformStream(ms);
    
                    return ms;
                }
    
    
    
    
                /// <summary>
                /// Allows transforming of strings
                ///
                /// Note this handler is internal and not meant to be overridden
                /// as the TransformString Event has to be hooked up in order
                /// for this handler to even fire to avoid the overhead of string
                /// conversion on every pass through.
                /// </summary>
                /// <param name="responseText"></param>
                /// <returns></returns>
                private string OnTransformCompleteString(string responseText)
                {
                    if (TransformString != null)
                        TransformString(responseText);
    
                    return responseText;
                }
    
                /// <summary>
                /// Wrapper method form OnTransformString that handles
                /// stream to string and vice versa conversions
                /// </summary>
                /// <param name="ms"></param>
                /// <returns></returns>
                internal MemoryStream OnTransformCompleteStringInternal(MemoryStream ms)
                {
                    if (TransformString == null)
                        return ms;
    
                    //string content = ms.GetAsString();
                    string content = HttpContext.Current.Response.ContentEncoding.GetString(ms.ToArray());
    
                    content = TransformString(content);
                    byte[] buffer = HttpContext.Current.Response.ContentEncoding.GetBytes(content);
                    ms = new MemoryStream();
                    ms.Write(buffer, 0, buffer.Length);
                    //ms.WriteString(content);
    
                    return ms;
                }
    
                /// <summary>
                ///
                /// </summary>
                public override bool CanRead
                {
                    get { return true; }
                }
    
                public override bool CanSeek
                {
                    get { return true; }
                }
                /// <summary>
                ///
                /// </summary>
                public override bool CanWrite
                {
                    get { return true; }
                }
    
                /// <summary>
                ///
                /// </summary>
                public override long Length
                {
                    get { return 0; }
                }
    
                /// <summary>
                ///
                /// </summary>
                public override long Position
                {
                    get { return _position; }
                    set { _position = value; }
                }
    
                /// <summary>
                ///
                /// </summary>
                /// <param name="offset"></param>
                /// <param name="direction"></param>
                /// <returns></returns>
                public override long Seek(long offset, System.IO.SeekOrigin direction)
                {
                    return _stream.Seek(offset, direction);
                }
    
                /// <summary>
                ///
                /// </summary>
                /// <param name="length"></param>
                public override void SetLength(long length)
                {
                    _stream.SetLength(length);
                }
    
                /// <summary>
                ///
                /// </summary>
                public override void Close()
                {
                    _stream.Close();
                }
    
                /// <summary>
                /// Override flush by writing out the cached stream data
                /// </summary>
                public override void Flush()
                {
    
                    if (IsCaptured && _cacheStream.Length > 0)
                    {
                        // Check for transform implementations
                        _cacheStream = OnTransformCompleteStream(_cacheStream);
                        _cacheStream = OnTransformCompleteStringInternal(_cacheStream);
    
                        OnCaptureStream(_cacheStream);
                        OnCaptureStringInternal(_cacheStream);
    
                        // write the stream back out if output was delayed
                        if (IsOutputDelayed)
                            _stream.Write(_cacheStream.ToArray(), 0, (int)_cacheStream.Length);
    
                        // Clear the cache once we've written it out
                        _cacheStream.SetLength(0);
                    }
    
                    // default flush behavior
                    _stream.Flush();
                }
    
                /// <summary>
                ///
                /// </summary>
                /// <param name="buffer"></param>
                /// <param name="offset"></param>
                /// <param name="count"></param>
                /// <returns></returns>
                public override int Read(byte[] buffer, int offset, int count)
                {
                    return _stream.Read(buffer, offset, count);
                }
    
    
                /// <summary>
                /// Overriden to capture output written by ASP.NET and captured
                /// into a cached stream that is written out later when Flush()
                /// is called.
                /// </summary>
                /// <param name="buffer"></param>
                /// <param name="offset"></param>
                /// <param name="count"></param>
                public override void Write(byte[] buffer, int offset, int count)
                {
                    if (IsCaptured)
                    {
                        // copy to holding buffer only - we'll write out later
                        _cacheStream.Write(buffer, 0, count);
                        _cachePointer += count;
                    }
    
                    // just transform this buffer
                    if (TransformWrite != null)
                        buffer = OnTransformWrite(buffer);
                    if (TransformWriteString != null)
                        buffer = OnTransformWriteStringInternal(buffer);
    
                    if (!IsOutputDelayed)
                        _stream.Write(buffer, offset, buffer.Length);
    
                }
    
            }
        }
    }
    Tiếp theo truy cập vào FilterConfig.cs add thêm code sau vào:
    Mã:
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        //filters.Add(new HandleErrorAttribute());
         filters.Add(new MinifyHtmlAttribute());
    }
    

    Như vậy là web của bạn đã tự nén Minify HTML rồi nhé.

    -------------------------------------------------------
    Thường mình chỉ dùng để Minify HTML với zetaproducerhtmlcompressor còn dùng để nén JS và CSS sẽ tốn thời gian sử lý code khi website chạy. Mình Minify CSS, JS cách tốt nhất bạn sử dụng BuildBundlerMinifier tại link sau: https://www.nuget.org/packages/BuildBundlerMinifier/

    Nếu bạn vẫn muốn nen Minify CSS và JS bằng zetaproducerhtmlcompressor thì làm như sau:

    Để Minify CSS, JS thì bạn làm tiếp như sau: tạo thêm file Minify_Inline.cs và chèn code sau vô:
    Mã:
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Optimization;
     
    namespace ITSEOVN.Utils
    {
        public static class Minify_Inline
        {
            public static IHtmlString InlineScripts(this HtmlHelper htmlHelper, string bundleVirtualPath)
            {
                return htmlHelper.InlineBundle(bundleVirtualPath, htmlTagName: "script");
            }
     
            public static IHtmlString InlineStyles(this HtmlHelper htmlHelper, string bundleVirtualPath)
            {
                return htmlHelper.InlineBundle(bundleVirtualPath, htmlTagName: "style");
            }
     
            private static IHtmlString InlineBundle(this HtmlHelper htmlHelper, string bundleVirtualPath, string htmlTagName)
            {
                string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath);
                string htmlTag = string.Format("<{0}>{1}</{0}>", htmlTagName, bundleContent);
     
                return new HtmlString(htmlTag);
            }
     
            private static string LoadBundleContent(HttpContextBase httpContext, string bundleVirtualPath)
            {
                var bundleContext = new BundleContext(httpContext, BundleTable.Bundles, bundleVirtualPath);
     
                List<string> list = new List<string>();
                foreach (var item in BundleTable.Bundles)
                {
                    list.Add(item.Path);
                }
     
                var tmp = BundleTable.Bundles.ToList();
     
                var bundle = BundleTable.Bundles.Single(b => b.Path == bundleVirtualPath);
                var bundleResponse = bundle.GenerateBundleResponse(bundleContext);
     
                return bundleResponse.Content;
            }
        }
    }
    Sử dụng: script src="[url_file]"></script> hoặc <link src="[url_file]"/> thì bạn thay đổi theo dạng sau @Html.InlineScripts("[url_file]") và @Html.InlineStyles("[url_file]")

    Lưu ý: nhớ đổi namespace thành tên của webstie bạn nhé.
     
    Cảm ơn đã xem bài:

    Minify HTML, CSS, JS trong MVC với ZetaProducerHtmlCompressor

    danh sách diễn đàn rao vặt gov chất lượng
    Last edited by a moderator: 1/10/20


Like và Share ủng hộ ITSEOVN