如何将打包和压缩的文件上传到Windows Azure CDN

27

我正在使用ASP.NET MVC 4的注捆绑和缩小功能,它位于Microsoft.AspNet.Web.Optimization命名空间中(例如@Styles.Render("~/content/static/css"))。

我想与Windows Azure CDN结合使用。

我尝试编写自定义的BundleTransform,但是其中的内容还没有被优化。

我也研究了解析和在运行时上传优化流的方法,但这感觉像是一种hack,我不太喜欢。

@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);
}

我怎样才能自动上传捆绑和压缩后的文件到我的Windows Azure CDN?


Nate Totten做了类似这样的事情:https://github.com/ntotten/wa-cdnhelpers/wiki。不过请访问该存储库的主页...看起来他现在推荐其他解决方案。 - user94559
请问一下,这个 _versionHash 参数在哪里?谢谢。 - Barbaros Alp
@BarbarosAlp _versionHash 表示添加到您的资源中的查询字符串 v。在上面的实现中,它将与先前缓存的字符串进行比较。 - Martin Buberl
已经过去将近3年了,有人找到了OTB解决方案吗? - TWilly
4个回答

18

在Hao的建议下,我扩展了Bundle和IBundleTransform。

将AzureScriptBundle或AzureStyleBundle添加到bundles中;

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,则将使用Blob的Uri而不是CDN。
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();
        }
    }
}

谢谢,这是一个非常好的解决方案。我采用了它,并进行了修改,以便在静态主机DNS(Amazon CloudFront)上通用使用。 - Zack Z.
@ZackZ,你能发布那个类吗? - manishKungwani
每次调用Process,您是否会创建一个新的哈希?如果文件没有更改,这不意味着浏览器将重新下载该文件,而不应该吗? - Barbaros Alp
@BarbarosAlp 很久没有关注 Bundling 了,所以我不确定将文件推送到 Blob 存储对于新哈希的捆绑会产生什么影响。也许有人知道可以发表评论或测试并留下评论。 - Daniel
1
抱歉,您已经对压缩后的CSS或JS内容进行了哈希处理。因此,如果内容没有更改,则哈希字符串将保持不变。谢谢。 - Barbaros Alp

14

目前还没有很好的方法来实现这个功能。我们正在设想一种长期的工作流程,即添加构建时捆绑支持。然后,您可以运行一个构建任务(或者如果您喜欢,运行一个exe),生成捆绑包,然后能够将其上传到AzureCDN。最后,您只需在BundleCollection上启用UseCDN,Script/Style帮助程序就会自动切换为使用链接到您的AzureCDN的链接,并且正确地回退到本地捆绑包。

对于短期来说,我认为您要做的是在构建捆绑包时将其上传到AzureCDN?

一种方法是使用BundleTransform,这可能有点取巧,但您可以在捆绑包中添加一个BundleTransform。由于它是最后一个,BundleResponse.Content实际上是最终的捆绑响应。此时,您可以将其上传到CDN。这样讲清楚了吗?


谢谢你的回答。在构建时打包不支持重新压缩,如果底层文件改变则哈希值会不同。如果CDN支持可以在环境级别上配置,这样只有生产/预生产环境才使用来自CDN的捆绑包,我觉得这很不错。是的,当捆绑包首次构建时,我尝试将其上传到CDN。我会尝试你建议的添加另一个BundleTransform来解决这个问题。 - Martin Buberl
@MartinBuberl,你最终解决了这个问题吗?这里的解决方案是什么? - Adam Tuliper
如果Azure CDN支持拉取(像其他提供商一样),那将是很棒的,这样它就不需要上传任何内容,而只需请求您的服务器。因此,如果您请求my.cdn.com/myfile.jpg并且它没有被缓存,CDN将使用website.com/myfile.jpg从您的服务器获取它。非常简单易用,可以使用Bundling功能轻松支持。 - MartinF
1
这个是在Mvc 5中实现的吗? - systempuntoout

3
你可以将原始域名定义为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()));
}

2

针对@manishKungwani在之前评论中的请求,只需设置UseCdn,然后使用cdnHost重载来创建bundle。我使用这个方法将其放入了一个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();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接