Как загружать связанные и минимизированные файлы в CDN Windows Azure

Я использую функции объединения и минимизации ASP.NET MVC 4 в Microsoft.AspNet.Web.Optimization пространство имен (например, @Styles.Render("~/content/static/css")).

Я хотел бы использовать его в сочетании с CDN Windows Azure.

Я пытался написать собственный BundleTransform, но контент там еще не оптимизирован.

Я также изучал синтаксический анализ и загрузку оптимизированного потока во время выполнения, но мне это кажется хаком, и мне это не очень нравится:

@StylesCdn.Render(Url.AbsoluteContent(
    Styles.Url("~/content/static/css").ToString()
    ));

public static IHtmlString Render(string absolutePath)
{
    // get the version hash
    string versionHash = HttpUtility.ParseQueryString(
        new Uri(absolutePath).Query
        ).Get("v");

    // only parse and upload to CDN if version hash is different
    if (versionHash != _versionHash)
    {
        _versionHash = versionHash;

        WebClient client = new WebClient();
        Stream stream = client.OpenRead(absolutePath);

        UploadStreamToAzureCdn(stream);
    }

    var styleSheetLink = String.Format(
        "<link href=\"{0}://{1}/{2}/{3}?v={4}\" rel=\"stylesheet\" type=\"text/css\" />",
        cdnEndpointProtocol, cdnEndpointUrl, cdnContainer, cdnCssFileName, versionHash
        );

    return new HtmlString(styleSheetLink);
}

Как я могу автоматически загружать объединенные и минимизированные версии в свою CDN Windows Azure?


person Martin Buberl    schedule 21.08.2012    source источник
comment
Нейт Тоттен сделал что-то вроде этого: github.com/ntotten/wa-cdnhelpers/wiki . Тем не менее, посетите главную страницу этого репозитория... похоже, что в наши дни он рекомендует другие решения.   -  person user94559    schedule 25.08.2012
comment
Не могли бы вы сказать мне, где находится этот параметр _versionHash? Спасибо.   -  person Barbaros Alp    schedule 11.06.2015
comment
@BarbarosAlp _versionHash представляет строку запроса v, которая добавляется к вашим ресурсам. В приведенной выше реализации он сравнивает ее с ранее кэшированной строкой.   -  person Martin Buberl    schedule 11.06.2015
comment
Прошло почти 3 года, кто-нибудь нашел решение OTB?   -  person TWilly    schedule 22.06.2015


Ответы (4)


Так что в настоящее время нет отличного способа сделать это. Более долгосрочный рабочий процесс, который мы предполагаем, — это добавление поддержки объединения во время сборки. Затем вы запустите задачу сборки (или запустите исполняемый файл, если хотите), чтобы сгенерировать пакеты, а затем сможете загрузить их в AzureCDN. Наконец, вы просто включаете UseCDN в BundleCollection, и помощники Script/Style просто автоматически переключаются на отрисовку ссылок на ваш AzureCDN с правильным возвратом к вашим локальным пакетам.

В краткосрочной перспективе, я думаю, вы пытаетесь загрузить свой пакет в AzureCDN, когда пакет впервые создан?

BundleTransform — это один из способов сделать это, я думаю, это немного хак, но вы можете добавить BundleTransform последним в свой пакет. С момента своего последнего ответа BundleResponse.Content фактически является окончательным ответом пакета. В этот момент вы можете загрузить его на свой CDN. Имеет ли это смысл?

person Hao Kung    schedule 24.08.2012
comment
Спасибо за Ваш ответ. Объединение во время сборки не поддерживает повторную минификацию с другим хэшем в случае изменения базовых файлов. Я был бы рад, если бы поддержка CDN настраивалась на уровне среды, чтобы только среды prod/int использовали пакеты из CDN. Да, я пытался загрузить его в CDN, когда бандл только собирался. Я попробую то, что вы предложили, и добавлю еще один BundleTransform только для решения этой проблемы. - person Martin Buberl; 25.08.2012
comment
@MartinBuberl у тебя когда-нибудь это работало? Какое здесь было рабочее решение? - person Adam Tuliper - MSFT; 09.11.2012
comment
Было бы здорово, если бы Azure CDN поддерживал сквозную передачу (как и другие провайдеры), поэтому вместо того, чтобы загружать что-либо, он просто запрашивал бы ваши серверы. Поэтому, если вы запросите my.cdn.com/myfile.jpg, и он не кэшируется, cdn получит его с вашего сервера, используя веб-сайт.com/myfile.jpg. Очень просто и легко поддерживать с помощью функции объединения. - person MartinF; 12.08.2013
comment
Это реализовано в Mvc 5? - person systempuntoout; 02.08.2014

Следуя совету Хао, я расширил Bundle и IBundleTransform.

Добавление AzureScriptBundle или AzureStyleBundle в пакеты;

bundles.Add(new AzureScriptBundle("~/bundles/modernizr.js", "cdn").Include("~/Scripts/vendor/modernizr.custom.68789.js"));

Результаты в;

<script src="//127.0.0.1:10000/devstoreaccount1/cdn/modernizr.js?v=g-XPguHFgwIb6tGNcnvnI_VY_ljCYf2BDp_NS5X7sAo1"></script>

Если CdnHost не задан, вместо CDN будет использоваться Uri большого двоичного объекта.

Сорт

using System;
using System.Text;
using System.Web;
using System.Web.Optimization;
using System.Security.Cryptography;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace SiegeEngineWebRole.BundleExtentions
{
    public class AzureScriptBundle : Bundle
    {
        public AzureScriptBundle(string virtualPath, string containerName, string cdnHost = "")
            : base(virtualPath, null, new IBundleTransform[] { new JsMinify(), new AzureBlobUpload { ContainerName = containerName, CdnHost = cdnHost } })
        {
            ConcatenationToken = ";";
        }
    }

    public class AzureStyleBundle : Bundle
    {
        public AzureStyleBundle(string virtualPath, string containerName, string cdnHost = "")
            : base(virtualPath, null, new IBundleTransform[] { new CssMinify(), new AzureBlobUpload { ContainerName = containerName, CdnHost = cdnHost } })
        {
        }
    }

    public class AzureBlobUpload : IBundleTransform
    {
        public string ContainerName { get; set; }
        public string CdnHost { get; set; }

        static AzureBlobUpload()
        {
        }

        public virtual void Process(BundleContext context, BundleResponse response)
        {
            var file = VirtualPathUtility.GetFileName(context.BundleVirtualPath);

            if (!context.BundleCollection.UseCdn)
            {
                return;
            }
            if (string.IsNullOrWhiteSpace(ContainerName))
            {
                throw new Exception("ContainerName Not Set");
            }

            var conn = CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue("DataConnectionString"));
            var blob = conn.CreateCloudBlobClient()
                .GetContainerReference(ContainerName)
                .GetBlobReference(file);

            blob.Properties.ContentType = response.ContentType;
            blob.UploadText(response.Content);

            var uri = string.IsNullOrWhiteSpace(CdnHost) ? blob.Uri.AbsoluteUri.Replace("http:", "").Replace("https:", "") : string.Format("//{0}/{1}/{2}", CdnHost, ContainerName, file);

            using (var hashAlgorithm = CreateHashAlgorithm())
            {
                var hash = HttpServerUtility.UrlTokenEncode(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(response.Content)));
                context.BundleCollection.GetBundleFor(context.BundleVirtualPath).CdnPath = string.Format("{0}?v={1}", uri, hash);
            }
        }

        private static SHA256 CreateHashAlgorithm()
        {
            if (CryptoConfig.AllowOnlyFipsAlgorithms)
            {
                return new SHA256CryptoServiceProvider();
            }

            return new SHA256Managed();
        }
    }
}
person Daniel    schedule 06.12.2012
comment
Спасибо, это действительно хорошее решение. Я взял его и модифицировал для общего использования со статическим хостом DNS (Amazon CloudFront). - person Zack Z.; 19.06.2014
comment
@ЗакЗ. Вы можете опубликовать этот класс? - person manishKungwani; 20.06.2014
comment
Каждый раз, когда вы вызываете Process, вы создаете новый Hash? Что, если файл не изменяется, не означает ли это, что браузер повторно загрузит файл, хотя этого делать не следует? - person Barbaros Alp; 11.06.2015
comment
@BarbarosAlp Прошло много времени с тех пор, как я смотрел на объединение, поэтому я не уверен на 100%, какое влияние окажет перемещение файлов в хранилище BLOB-объектов на объединение в отношении нового хэша. Может быть, кто-то, кто знает, может присоединиться к ответу или проверить его и оставить комментарий. - person Daniel; 13.06.2015
comment
Извините, вы хешируете содержимое минимизированного css или js. Поэтому, если в содержимом нет изменений, хешированная строка остается прежней. Спасибо. - person Barbaros Alp; 25.06.2015

Вы можете определить исходный домен как веб-сайт Azure (вероятно, это было добавлено намного позже исходного вопроса) .

Если у вас есть конечная точка CDN, вам нужно будет разрешить для нее строку запроса, а затем вы можете напрямую ссылаться на пакеты через CDN:

<link href="//az888888.vo.msecnd.net/Content/css-common?v=ioYVnAg-Q3qYl3Pmki-qdKwT20ESkdREhi4DsEehwCY1" rel="stylesheet"/>

Я также создал этот помощник для добавления имени хоста CDN:

public static IHtmlString RenderScript(string virtualPath)
{
    if (HttpContext.Current.IsDebuggingEnabled)
        return Scripts.Render(virtualPath);
    else
        return new HtmlString(String.Format(
            CultureInfo.InvariantCulture, 
            Scripts.DefaultTagFormat, 
            "//CDN_HOST" + Scripts.Url(virtualPath).ToHtmlString()));
}
person Jenya Y.    schedule 01.10.2014

Для @manishKungwani, запрошенного в предыдущем комментарии. Просто установите UseCdn, а затем используйте перегрузку cdnHost для создания пакета. Я использовал это для добавления домена AWS CloudFront (xxx.cloudfront.net), но, оглядываясь назад, он должен был иметь более общее имя для использования с любым другим провайдером CDN.

public class CloudFrontScriptBundle : Bundle
{
    public CloudFrontScriptBundle(string virtualPath, string cdnHost = "")
        : base(virtualPath, null, new IBundleTransform[] { new JsMinify(), new CloudFrontBundleTransformer { CdnHost = cdnHost } })
    {
        ConcatenationToken = ";";
    }
}

public class CloudFrontStyleBundle : Bundle
{
    public CloudFrontStyleBundle(string virtualPath, string cdnHost = "")
        : base(virtualPath, null, new IBundleTransform[] { new CssMinify(), new CloudFrontBundleTransformer { CdnHost = cdnHost } })
    {
    }
}

public class CloudFrontBundleTransformer : IBundleTransform
{
    public string CdnHost { get; set; }

    static CloudFrontBundleTransformer()
    {
    }

    public virtual void Process(BundleContext context, BundleResponse response)
    {
        if (context.BundleCollection.UseCdn && !String.IsNullOrWhiteSpace(CdnHost))
        {
            var virtualFileName = VirtualPathUtility.GetFileName(context.BundleVirtualPath);
            var virtualDirectory = VirtualPathUtility.GetDirectory(context.BundleVirtualPath);

            if (!String.IsNullOrEmpty(virtualDirectory))
                virtualDirectory = virtualDirectory.Trim('~');

            var uri = string.Format("//{0}{1}{2}", CdnHost, virtualDirectory, virtualFileName);
            using (var hashAlgorithm = CreateHashAlgorithm())
            {
                var hash = HttpServerUtility.UrlTokenEncode(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(response.Content)));
                context.BundleCollection.GetBundleFor(context.BundleVirtualPath).CdnPath = string.Format("{0}?v={1}", uri, hash);
            }
        }
    }

    private static SHA256 CreateHashAlgorithm()
    {
        if (CryptoConfig.AllowOnlyFipsAlgorithms)
        {
            return new SHA256CryptoServiceProvider();
        }

        return new SHA256Managed();
    }
}
person Zack Z.    schedule 25.06.2014