MVC4 StyleBundle ne résout pas les images

293

Ma question est similaire à ceci:

ASP.NET MVC 4 Minification & Images d'arrière-plan

Sauf que je veux m'en tenir au propre groupement de MVC si je le peux. J'ai un crash cérébral essayant de comprendre quel est le modèle correct pour spécifier des ensembles de styles tels que les ensembles d'images et de CSS autonomes tels que l'interface utilisateur jQuery.

J'ai une structure de site MVC typique avec /Content/css/laquelle contient mon CSS de base tel que styles.css. Dans ce dossier CSS, j'ai également des sous-dossiers tels que celui /jquery-uiqui contient son fichier CSS plus un /imagesdossier. Les chemins des images dans jQuery UI CSS sont relatifs à ce dossier et je ne veux pas les déranger.

Si je comprends bien, lorsque je spécifie un, StyleBundleje dois spécifier un chemin virtuel qui ne correspond pas également à un vrai chemin de contenu, car (en supposant que j'ignore les routes vers le contenu) IIS essaierait alors de résoudre ce chemin en tant que fichier physique. Je précise donc:

bundles.Add(new StyleBundle("~/Content/styles/jquery-ui")
       .Include("~/Content/css/jquery-ui/*.css"));

rendu en utilisant:

@Styles.Render("~/Content/styles/jquery-ui")

Je peux voir la demande envoyée à:

http://localhost/MySite/Content/styles/jquery-ui?v=nL_6HPFtzoqrts9nwrtjq0VQFYnhMjY5EopXsK8cxmg1

Cela renvoie la réponse CSS correcte et réduite. Mais alors le navigateur envoie une demande pour une image relativement liée comme:

http://localhost/MySite/Content/styles/images/ui-bg_highlight-soft_100_eeeeee_1x100.png

Qui est un 404 .

Je comprends que la dernière partie de mon URL jquery-uiest une URL sans extension, un gestionnaire pour mon bundle, donc je peux voir pourquoi la demande relative pour l'image est simplement/styles/images/ .

Donc ma question est quelle est la bonne façon de gérer cette situation?

Tom W Hall
la source
9
après avoir été frustré à maintes reprises avec la nouvelle partie Bundling and Minification, je suis passé à Cassete qui est maintenant gratuit et fonctionne bien mieux!
balexandre
3
Merci pour le lien, Cassette a l'air sympa et je vais certainement le vérifier. Mais je veux m'en tenir à l'approche fournie si possible, cela doit sûrement être possible sans jouer avec les chemins d'image dans les fichiers CSS tiers chaque fois qu'une nouvelle version est publiée. pour l'instant, j'ai conservé mes ScriptBundles (qui fonctionnent bien) mais je suis revenu à des liens CSS simples jusqu'à ce que j'obtienne une résolution. À votre santé.
Tom W Hall
Ajout de l'erreur probable pour des raisons de référencement: le contrôleur pour le chemin '/bundles/images/blah.jpg' est introuvable ou n'implémente pas IController.
Luke Puplett

Réponses:

361

Selon ce fil sur le regroupement css MVC4 et les références d'image , si vous définissez votre bundle comme:

bundles.Add(new StyleBundle("~/Content/css/jquery-ui/bundle")
                   .Include("~/Content/css/jquery-ui/*.css"));

Lorsque vous définissez le regroupement sur le même chemin que les fichiers source qui ont constitué le regroupement, les chemins d'accès relatifs à l'image continueront de fonctionner. La dernière partie du chemin du bundle est vraiment celle file namede ce bundle spécifique (c'est-à-dire, /bundlepeut être n'importe quel nom que vous aimez).

Cela ne fonctionnera que si vous regroupez des CSS à partir du même dossier (ce qui, je pense, est logique du point de vue du regroupement).

Mettre à jour

Selon le commentaire ci-dessous de @Hao Kung, cela peut désormais être réalisé en appliquant un CssRewriteUrlTransformation( Modifier les références d'URL relatives aux fichiers CSS lorsqu'ils sont regroupés ).

REMARQUE: je n'ai pas confirmé les commentaires concernant les problèmes de réécriture vers des chemins absolus dans un répertoire virtuel, donc cela peut ne pas fonctionner pour tout le monde (?).

bundles.Add(new StyleBundle("~/Content/css/jquery-ui/bundle")
                   .Include("~/Content/css/jquery-ui/*.css",
                    new CssRewriteUrlTransform()));
Chris Baxter
la source
1
Légende! Oui, cela fonctionne parfaitement. J'ai du CSS à différents niveaux mais ils ont chacun leurs propres dossiers d'images, par exemple mon site principal CSS est dans le dossier CSS racine et jquery-ui est à l'intérieur avec son propre dossier d'images, donc je spécifie juste 2 paquets, un pour mon CSS de base et un pour jQuery UI - qui n'est peut-être pas ultra-optimal en termes de demandes, mais la vie est courte. À votre santé!
Tom W Hall
3
Oui, malheureusement, jusqu'à ce que le regroupement prenne en charge la réécriture des URL intégrées à l'intérieur du css lui-même, vous avez besoin du répertoire virtuel du regroupement css pour faire correspondre les fichiers css avant le regroupement. C'est pourquoi les bundles de modèles par défaut n'ont pas d'URL comme ~ / bundles / themes, et ressemblent plutôt à la structure du répertoire: ~ / content / theemes / base / css
Hao Kung
27
Ceci est désormais pris en charge via ItemTransforms, .Include ("~ / Content / css / jquery-ui / *. Css", new CssRewriteUrlTransform ())); dans la 1.1Beta1 devrait résoudre ce problème
Hao Kung
2
Est-ce résolu dans Microsoft ASP.NET Web Optimization Framework 1.1.3? J'ai trouvé des informations sur ce qui a changé dans tout cela?
Andrus
13
nouveau CssRewriteUrlTransform () est très bien si vous avez un site Web dans IIS. mais si c'est une application ou une sous-application, cela ne fonctionnera pas, et vous devez recourir à la définition de votre bundle au même endroit que votre CSS.
avidenic
34

La solution Grinn / ThePirat fonctionne bien.

Je n'aimais pas le fait qu'elle ait créé la méthode Include sur le bundle et qu'elle ait créé des fichiers temporaires dans le répertoire de contenu. (ils ont fini par être enregistrés, déployés, alors le service ne démarre pas!)

Donc, pour suivre la conception de Bundling, j'ai choisi de réaliser essentiellement le même code, mais dans une implémentation IBundleTransform ::

class StyleRelativePathTransform
    : IBundleTransform
{
    public StyleRelativePathTransform()
    {
    }

    public void Process(BundleContext context, BundleResponse response)
    {
        response.Content = String.Empty;

        Regex pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);
        // open each of the files
        foreach (FileInfo cssFileInfo in response.Files)
        {
            if (cssFileInfo.Exists)
            {
                // apply the RegEx to the file (to change relative paths)
                string contents = File.ReadAllText(cssFileInfo.FullName);
                MatchCollection matches = pattern.Matches(contents);
                // Ignore the file if no match 
                if (matches.Count > 0)
                {
                    string cssFilePath = cssFileInfo.DirectoryName;
                    string cssVirtualPath = context.HttpContext.RelativeFromAbsolutePath(cssFilePath);
                    foreach (Match match in matches)
                    {
                        // this is a path that is relative to the CSS file
                        string relativeToCSS = match.Groups[2].Value;
                        // combine the relative path to the cssAbsolute
                        string absoluteToUrl = Path.GetFullPath(Path.Combine(cssFilePath, relativeToCSS));

                        // make this server relative
                        string serverRelativeUrl = context.HttpContext.RelativeFromAbsolutePath(absoluteToUrl);

                        string quote = match.Groups[1].Value;
                        string replace = String.Format("url({0}{1}{0})", quote, serverRelativeUrl);
                        contents = contents.Replace(match.Groups[0].Value, replace);
                    }
                }
                // copy the result into the response.
                response.Content = String.Format("{0}\r\n{1}", response.Content, contents);
            }
        }
    }
}

Et puis enveloppé cela dans une implémentation de bundle:

public class StyleImagePathBundle 
    : Bundle
{
    public StyleImagePathBundle(string virtualPath)
        : base(virtualPath)
    {
        base.Transforms.Add(new StyleRelativePathTransform());
        base.Transforms.Add(new CssMinify());
    }

    public StyleImagePathBundle(string virtualPath, string cdnPath)
        : base(virtualPath, cdnPath)
    {
        base.Transforms.Add(new StyleRelativePathTransform());
        base.Transforms.Add(new CssMinify());
    }
}

Exemple d'utilisation:

static void RegisterBundles(BundleCollection bundles)
{
...
    bundles.Add(new StyleImagePathBundle("~/bundles/Bootstrap")
            .Include(
                "~/Content/css/bootstrap.css",
                "~/Content/css/bootstrap-responsive.css",
                "~/Content/css/jquery.fancybox.css",
                "~/Content/css/style.css",
                "~/Content/css/error.css",
                "~/Content/validation.css"
            ));

Voici ma méthode d'extension pour RelativeFromAbsolutePath:

   public static string RelativeFromAbsolutePath(this HttpContextBase context, string path)
    {
        var request = context.Request;
        var applicationPath = request.PhysicalApplicationPath;
        var virtualDir = request.ApplicationPath;
        virtualDir = virtualDir == "/" ? virtualDir : (virtualDir + "/");
        return path.Replace(applicationPath, virtualDir).Replace(@"\", "/");
    }
AcidPAT
la source
Cela me semble aussi le plus propre. Merci. Je vote tous les trois contre vous, car cela semblait être un effort d'équipe. :)
Josh Mouch
Le code tel que vous l'avez maintenant ne fonctionne pas pour moi. J'essaie de le réparer, mais j'ai pensé que je vous le ferais savoir. La méthode context.HttpContext.RelativeFromAbsolutePath n'existe pas. De plus, si le chemin de l'url commence par un "/" (le rendant absolu), votre logique de combinaison de chemin est désactivée.
Josh Mouch
2
@AcidPAT excellent travail. La logique a échoué si l'url avait une chaîne de requête (certaines bibliothèques tierces l'ajoutent, comme FontAwesome pour sa référence .woff.) C'est une solution facile cependant. On peut ajuster le Regex ou corriger relativeToCSSavant d'appeler Path.GetFullPath().
sergiopereira
2
@ChrisMarisic votre code ne semble pas fonctionner - response.Files est un tableau de BundleFiles, ces objets n'ont pas de propriétés telles que "Exists", "DirectoryName" etc.
Nick Coad
2
@ChrisMarisic y a-t-il peut-être un espace de noms que je devrais importer qui fournit des méthodes d'extension pour la classe BundleFile?
Nick Coad
20

Mieux encore (à mon humble avis) implémentez un bundle personnalisé qui corrige les chemins d'image. J'en ai écrit un pour mon application.

using System;
using System.Collections.Generic;
using IO = System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Optimization;

...

public class StyleImagePathBundle : Bundle
{
    public StyleImagePathBundle(string virtualPath)
        : base(virtualPath, new IBundleTransform[1]
      {
        (IBundleTransform) new CssMinify()
      })
    {
    }

    public StyleImagePathBundle(string virtualPath, string cdnPath)
        : base(virtualPath, cdnPath, new IBundleTransform[1]
      {
        (IBundleTransform) new CssMinify()
      })
    {
    }

    public new Bundle Include(params string[] virtualPaths)
    {
        if (HttpContext.Current.IsDebuggingEnabled)
        {
            // Debugging. Bundling will not occur so act normal and no one gets hurt.
            base.Include(virtualPaths.ToArray());
            return this;
        }

        // In production mode so CSS will be bundled. Correct image paths.
        var bundlePaths = new List<string>();
        var svr = HttpContext.Current.Server;
        foreach (var path in virtualPaths)
        {
            var pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);
            var contents = IO.File.ReadAllText(svr.MapPath(path));
            if(!pattern.IsMatch(contents))
            {
                bundlePaths.Add(path);
                continue;
            }


            var bundlePath = (IO.Path.GetDirectoryName(path) ?? string.Empty).Replace(@"\", "/") + "/";
            var bundleUrlPath = VirtualPathUtility.ToAbsolute(bundlePath);
            var bundleFilePath = String.Format("{0}{1}.bundle{2}",
                                               bundlePath,
                                               IO.Path.GetFileNameWithoutExtension(path),
                                               IO.Path.GetExtension(path));
            contents = pattern.Replace(contents, "url($1" + bundleUrlPath + "$2$1)");
            IO.File.WriteAllText(svr.MapPath(bundleFilePath), contents);
            bundlePaths.Add(bundleFilePath);
        }
        base.Include(bundlePaths.ToArray());
        return this;
    }

}

Pour l'utiliser, faites:

bundles.Add(new StyleImagePathBundle("~/bundles/css").Include(
  "~/This/Is/Some/Folder/Path/layout.css"));

...au lieu de...

bundles.Add(new StyleBundle("~/bundles/css").Include(
  "~/This/Is/Some/Folder/Path/layout.css"));

Ce qu'il fait est (lorsqu'il n'est pas en mode débogage) le recherche url(<something>)et le remplace par url(<absolute\path\to\something>). J'ai écrit la chose il y a environ 10 secondes, donc elle pourrait avoir besoin d'un petit ajustement. J'ai pris en compte les URL pleinement qualifiées et les DataURI base64 en m'assurant qu'il n'y a pas de deux-points (:) dans le chemin de l'URL. Dans notre environnement, les images résident normalement dans le même dossier que leurs fichiers css, mais je l'ai testé avec les dossiers parents ( url(../someFile.png)) et les dossiers enfants ( url(someFolder/someFile.png).

Grinn
la source
C'est une excellente solution. J'ai légèrement modifié votre Regex pour qu'il fonctionne également avec des fichiers MOINS, mais le concept d'origine était exactement ce dont j'avais besoin. Merci.
Tim Coulter
1
Vous pouvez également mettre l'initialisation regex en dehors de la boucle. Peut-être en tant que propriété statique en lecture seule.
Miha Markic
12

Il n'est pas nécessaire de spécifier une transformation ou d'avoir des chemins de sous-répertoire fous. Après beaucoup de dépannage, je l'ai isolé à cette règle "simple" (est-ce un bug?) ...

Si le chemin de votre ensemble ne commence pas par la racine relative des éléments inclus, la racine de l'application Web ne sera pas prise en compte.

Cela ressemble plus à un bug pour moi, mais de toute façon c'est comme ça que vous le corrigez avec la version actuelle de .NET 4.51. Peut-être que les autres réponses étaient nécessaires sur les anciennes versions d'ASP.NET, je ne peux pas dire que je n'ai pas le temps de tester rétrospectivement tout cela.

Pour clarifier, voici un exemple:

J'ai ces fichiers ...

~/Content/Images/Backgrounds/Some_Background_Tile.gif
~/Content/Site.css  - references the background image relatively, i.e. background: url('Images/...')

Ensuite, configurez le bundle comme ...

BundleTable.Add(new StyleBundle("~/Bundles/Styles").Include("~/Content/Site.css"));

Et le rendre comme ...

@Styles.Render("~/Bundles/Styles")

Et obtenez le "comportement" (bug), les fichiers CSS eux-mêmes ont la racine de l'application (par exemple "http: // localhost: 1234 / MySite / Content / Site.css") mais l'image CSS dans tous les start "/ Content / Images / ... "ou" / Images / ... "selon que j'ajoute ou non la transformation.

J'ai même essayé de créer le dossier "Bundles" pour voir s'il s'agissait du chemin existant ou non, mais cela n'a rien changé. La solution au problème est vraiment l'exigence que le nom du bundle commence par la racine du chemin.

Ce qui signifie que cet exemple est corrigé en enregistrant et en rendant le chemin du bundle comme ..

BundleTable.Add(new StyleBundle("~/Content/StylesBundle").Include("~/Content/Site.css"));
...
@Styles.Render("~/Content/StylesBundle")

Donc, bien sûr, vous pouvez dire que c'est RTFM, mais je suis sûr que moi et d'autres avons choisi ce chemin "~ / Bundles / ..." à partir du modèle par défaut ou quelque part dans la documentation sur le site Web MSDN ou ASP.NET, ou je suis juste tombé dessus parce qu'en fait, c'est un nom assez logique pour un chemin virtuel et il est logique de choisir de tels chemins virtuels qui n'entrent pas en conflit avec de vrais répertoires.

Quoi qu'il en soit, c'est comme ça. Microsoft ne voit aucun bogue. Je ne suis pas d'accord avec cela, soit cela devrait fonctionner comme prévu, soit une exception devrait être levée, ou un remplacement supplémentaire pour ajouter le chemin du bundle qui opte pour inclure la racine de l'application ou non. Je ne peux pas imaginer pourquoi quelqu'un ne voudrait pas que la racine de l'application soit incluse quand il y en a une (normalement sauf si vous avez installé votre site Web avec un alias DNS / racine de site Web par défaut). Donc, en fait, cela devrait être la valeur par défaut.

Tony Wall
la source
Semble pour moi la "solution" la plus simple. Les autres peuvent avoir des effets secondaires, comme avec image: data.
Fabrice
@MohamedEmaish cela fonctionne, vous vous êtes probablement trompé. Apprenez à tracer les demandes, par exemple, utilisez l'outil Fiddler pour voir quelles URL sont demandées par le navigateur. Le but n'est pas de coder en dur l'intégralité du chemin relatif afin que votre site Web puisse être installé à différents emplacements (chemins racine) sur le même serveur ou que votre produit puisse changer l'URL par défaut sans avoir à réécrire une grande partie du site Web (le point d'avoir et la variable racine d'application).
Tony Wall
Je suis allé avec cette option et cela a très bien fonctionné. Je devais m'assurer que chaque paquet n'avait que des éléments d'un seul dossier (ne peut pas inclure des éléments d'autres dossiers ou sous-dossiers), ce qui est légèrement ennuyeux mais tant qu'il fonctionne, je suis content! Merci pour le post.
hvaughan3
1
Merci. Soupir. Un jour, j'aimerais passer plus de temps à écrire du code qu'à parcourir Stack.
Bruce Pierson
J'ai eu un problème similaire où un jquery-ui personnalisé qui avait des dossiers imbriqués. dès que j'ai nivelé les choses comme ci-dessus, cela a fonctionné. Il n'aime pas les dossiers imbriqués.
Andrei Bazanov
11

J'ai trouvé que CssRewriteUrlTransform ne s'exécute pas si vous faites référence à un *.cssfichier et que vous avez le *.min.cssfichier associé dans le même dossier.

Pour résoudre ce problème, supprimez le *.min.cssfichier ou référencez-le directement dans votre bundle:

bundles.Add(new Bundle("~/bundles/bootstrap")
    .Include("~/Libs/bootstrap3/css/bootstrap.min.css", new CssRewriteUrlTransform()));

Après cela, vos URL seront transformées correctement et vos images devraient être correctement résolues.

ajbeaven
la source
1
Je vous remercie! Après deux jours de recherche en ligne, c'est la première mention que j'ai vue n'importe où de CssRewriteUrlTransform fonctionnant avec des fichiers * .css, mais pas avec le fichier * .min.css associé qui est extrait lorsque vous ne lancez pas un débogage environnement. Semble définitivement comme un bug pour moi. Devra vérifier manuellement le type d'environnement pour définir un bundle avec la version non minimisée pour le débogage, mais au moins j'ai une solution de contournement maintenant!
Sean
1
Cela a résolu le problème pour moi. Cela semble certainement être un bug. Cela n'a aucun sens qu'il doit ignorer CssRewriteUrlTransform s'il trouve un fichier .min.css préexistant.
user1751825
10

Peut-être que je suis biaisé, mais j'aime bien ma solution car elle ne fait aucune transformation, regex, etc. et elle a le moins de code :)

Cela fonctionne pour un site hébergé en tant que répertoire virtuel dans un site Web IIS et en tant que site Web racine sur IIS

J'ai donc créé une implantation d' IItemTransformencapsulé le CssRewriteUrlTransformet utilisé VirtualPathUtilitypour fixer le chemin d'accès et appeler le code existant:

/// <summary>
/// Is a wrapper class over CssRewriteUrlTransform to fix url's in css files for sites on IIS within Virutal Directories
/// and sites at the Root level
/// </summary>
public class CssUrlTransformWrapper : IItemTransform
{
    private readonly CssRewriteUrlTransform _cssRewriteUrlTransform;

    public CssUrlTransformWrapper()
    {
        _cssRewriteUrlTransform = new CssRewriteUrlTransform();
    }

    public string Process(string includedVirtualPath, string input)
    {
        return _cssRewriteUrlTransform.Process("~" + VirtualPathUtility.ToAbsolute(includedVirtualPath), input);
    }
}


//App_Start.cs
public static void Start()
{
      BundleTable.Bundles.Add(new StyleBundle("~/bundles/fontawesome")
                         .Include("~/content/font-awesome.css", new CssUrlTransformWrapper()));
}

Semble bien fonctionner pour moi?

SimonGates
la source
1
C'est parfaitement pour moi. excellente solution. mon vote est +1
imdadhusen
1
Ceci est la bonne réponse. La classe CssUrlTransformWrapper fournie par l'infrastructure résout le problème, sauf qu'elle ne fonctionne pas uniquement lorsque l'application n'est pas à la racine du site Web. Ce wrapper résout succinctement cette lacune.
Nine Tails
7

Bien que la réponse de Chris Baxter aide à résoudre le problème d'origine, cela ne fonctionne pas dans mon cas lorsque l'application est hébergée dans un répertoire virtuel . Après avoir étudié les options, j'ai terminé avec une solution de bricolage.

ProperStyleBundleLa classe inclut du code emprunté à l'original CssRewriteUrlTransformpour transformer correctement les chemins relatifs dans le répertoire virtuel. Il lance également si le fichier n'existe pas et empêche la réorganisation des fichiers dans le bundle (code extrait de BetterStyleBundle).

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Optimization;
using System.Linq;

namespace MyNamespace
{
    public class ProperStyleBundle : StyleBundle
    {
        public override IBundleOrderer Orderer
        {
            get { return new NonOrderingBundleOrderer(); }
            set { throw new Exception( "Unable to override Non-Ordered bundler" ); }
        }

        public ProperStyleBundle( string virtualPath ) : base( virtualPath ) {}

        public ProperStyleBundle( string virtualPath, string cdnPath ) : base( virtualPath, cdnPath ) {}

        public override Bundle Include( params string[] virtualPaths )
        {
            foreach ( var virtualPath in virtualPaths ) {
                this.Include( virtualPath );
            }
            return this;
        }

        public override Bundle Include( string virtualPath, params IItemTransform[] transforms )
        {
            var realPath = System.Web.Hosting.HostingEnvironment.MapPath( virtualPath );
            if( !File.Exists( realPath ) )
            {
                throw new FileNotFoundException( "Virtual path not found: " + virtualPath );
            }
            var trans = new List<IItemTransform>( transforms ).Union( new[] { new ProperCssRewriteUrlTransform( virtualPath ) } ).ToArray();
            return base.Include( virtualPath, trans );
        }

        // This provides files in the same order as they have been added. 
        private class NonOrderingBundleOrderer : IBundleOrderer
        {
            public IEnumerable<BundleFile> OrderFiles( BundleContext context, IEnumerable<BundleFile> files )
            {
                return files;
            }
        }

        private class ProperCssRewriteUrlTransform : IItemTransform
        {
            private readonly string _basePath;

            public ProperCssRewriteUrlTransform( string basePath )
            {
                _basePath = basePath.EndsWith( "/" ) ? basePath : VirtualPathUtility.GetDirectory( basePath );
            }

            public string Process( string includedVirtualPath, string input )
            {
                if ( includedVirtualPath == null ) {
                    throw new ArgumentNullException( "includedVirtualPath" );
                }
                return ConvertUrlsToAbsolute( _basePath, input );
            }

            private static string RebaseUrlToAbsolute( string baseUrl, string url )
            {
                if ( string.IsNullOrWhiteSpace( url )
                     || string.IsNullOrWhiteSpace( baseUrl )
                     || url.StartsWith( "/", StringComparison.OrdinalIgnoreCase )
                     || url.StartsWith( "data:", StringComparison.OrdinalIgnoreCase )
                    ) {
                    return url;
                }
                if ( !baseUrl.EndsWith( "/", StringComparison.OrdinalIgnoreCase ) ) {
                    baseUrl = baseUrl + "/";
                }
                return VirtualPathUtility.ToAbsolute( baseUrl + url );
            }

            private static string ConvertUrlsToAbsolute( string baseUrl, string content )
            {
                if ( string.IsNullOrWhiteSpace( content ) ) {
                    return content;
                }
                return new Regex( "url\\(['\"]?(?<url>[^)]+?)['\"]?\\)" )
                    .Replace( content, ( match =>
                                         "url(" + RebaseUrlToAbsolute( baseUrl, match.Groups["url"].Value ) + ")" ) );
            }
        }
    }
}

Utilisez-le comme StyleBundle:

bundles.Add( new ProperStyleBundle( "~/styles/ui" )
    .Include( "~/Content/Themes/cm_default/style.css" )
    .Include( "~/Content/themes/custom-theme/jquery-ui-1.8.23.custom.css" )
    .Include( "~/Content/DataTables-1.9.4/media/css/jquery.dataTables.css" )
    .Include( "~/Content/DataTables-1.9.4/extras/TableTools/media/css/TableTools.css" ) );
nrodic
la source
2
Belle solution, mais échoue toujours (tout comme CssRewriteUrlTransform) si vous avez un URI de données dans votre CSS (par exemple "data: image / png; base64, ..."). Vous ne devez pas modifier les URL commençant par "data:" dans RebaseUrlToAbsolute ().
miles82
1
@ miles82 Bien sûr! Merci de l'avoir signalé. J'ai changé RebaseUrlToAbsolute ().
nrodic
6

Depuis la v1.1.0-alpha1 (package de pré-version), le framework utilise le VirtualPathProvider pour accéder aux fichiers plutôt que de toucher le système de fichiers physique.

Le transformateur mis à jour peut être vu ci-dessous:

public class StyleRelativePathTransform
    : IBundleTransform
{
    public void Process(BundleContext context, BundleResponse response)
    {
        Regex pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);

        response.Content = string.Empty;

        // open each of the files
        foreach (var file in response.Files)
        {
            using (var reader = new StreamReader(file.Open()))
            {
                var contents = reader.ReadToEnd();

                // apply the RegEx to the file (to change relative paths)
                var matches = pattern.Matches(contents);

                if (matches.Count > 0)
                {
                    var directoryPath = VirtualPathUtility.GetDirectory(file.VirtualPath);

                    foreach (Match match in matches)
                    {
                        // this is a path that is relative to the CSS file
                        var imageRelativePath = match.Groups[2].Value;

                        // get the image virtual path
                        var imageVirtualPath = VirtualPathUtility.Combine(directoryPath, imageRelativePath);

                        // convert the image virtual path to absolute
                        var quote = match.Groups[1].Value;
                        var replace = String.Format("url({0}{1}{0})", quote, VirtualPathUtility.ToAbsolute(imageVirtualPath));
                        contents = contents.Replace(match.Groups[0].Value, replace);
                    }

                }
                // copy the result into the response.
                response.Content = String.Format("{0}\r\n{1}", response.Content, contents);
            }
        }
    }
}
Ben Foster
la source
En fait, ce que cela fait si vous remplacez les URL relatives dans CSS par des URL absolues.
Fabrice
6

Voici une transformation d'ensemble qui remplacera les URL css par des URL relatives à ce fichier css. Ajoutez-le simplement à votre ensemble et cela devrait résoudre le problème.

public class CssUrlTransform: IBundleTransform
{
    public void Process(BundleContext context, BundleResponse response) {
        Regex exp = new Regex(@"url\([^\)]+\)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
        foreach (FileInfo css in response.Files) {
            string cssAppRelativePath = css.FullName.Replace(context.HttpContext.Request.PhysicalApplicationPath, context.HttpContext.Request.ApplicationPath).Replace(Path.DirectorySeparatorChar, '/');
            string cssDir = cssAppRelativePath.Substring(0, cssAppRelativePath.LastIndexOf('/'));
            response.Content = exp.Replace(response.Content, m => TransformUrl(m, cssDir));
        }
    }


    private string TransformUrl(Match match, string cssDir) {
        string url = match.Value.Substring(4, match.Length - 5).Trim('\'', '"');

        if (url.StartsWith("http://") || url.StartsWith("data:image")) return match.Value;

        if (!url.StartsWith("/"))
            url = string.Format("{0}/{1}", cssDir, url);

        return string.Format("url({0})", url);
    }

}
Ciprian Gavrilovici
la source
Comment l'utiliser ?, C'est me montrer une exception:cannot convert type from BundleFile to FileInfo
Stiger
@Stiger change css.FullName.Replace (en css.VirtualFile.VirtualPath.Replace (
lkurylo
Je me trompe peut-être, mais cela foreach réécrit-il toutes les URL à chaque itération et les laisse-t-il par rapport au dernier fichier CSS qu'il a vu?
Andyrooger
4

Une autre option consisterait à utiliser le module de réécriture d'URL IIS pour mapper le dossier d'images d'ensemble virtuel au dossier d'images physiques. Vous trouverez ci-dessous un exemple de règle de réécriture que vous pourriez utiliser pour un ensemble appelé "~ / ensembles / votre page / styles" - notez les correspondances d'expression régulière sur les caractères alphanumériques ainsi que les traits d'union, les traits de soulignement et les points, qui sont courants dans les noms de fichiers image .

<rewrite>
  <rules>
    <rule name="Bundle Images">
      <match url="^bundles/yourpage/images/([a-zA-Z0-9\-_.]+)" />
      <action type="Rewrite" url="Content/css/jquery-ui/images/{R:1}" />
    </rule>
  </rules>
</rewrite>

Cette approche crée un petit surcoût supplémentaire, mais vous permet d'avoir plus de contrôle sur les noms de vos bundles, et réduit également le nombre de bundles que vous devrez peut-être référencer sur une page. Bien sûr, si vous devez référencer plusieurs fichiers css tiers contenant des références de chemin d'image relatives, vous ne pouvez toujours pas contourner la création de plusieurs ensembles.

DanO
la source
4

La solution Grinn est excellente.

Cependant, cela ne fonctionne pas pour moi lorsqu'il y a des références relatives au dossier parent dans l'URL. c'est à direurl('../../images/car.png')

J'ai donc légèrement modifié la Includeméthode afin de résoudre les chemins pour chaque correspondance d'expressions régulières, en permettant des chemins relatifs et en option pour intégrer les images dans le CSS.

J'ai également changé l'IF DEBUG pour vérifier BundleTable.EnableOptimizationsau lieu de HttpContext.Current.IsDebuggingEnabled.

    public new Bundle Include(params string[] virtualPaths)
    {
        if (!BundleTable.EnableOptimizations)
        {
            // Debugging. Bundling will not occur so act normal and no one gets hurt. 
            base.Include(virtualPaths.ToArray());
            return this;
        }
        var bundlePaths = new List<string>();
        var server = HttpContext.Current.Server;
        var pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);
        foreach (var path in virtualPaths)
        {
            var contents = File.ReadAllText(server.MapPath(path));
            var matches = pattern.Matches(contents);
            // Ignore the file if no matches
            if (matches.Count == 0)
            {
                bundlePaths.Add(path);
                continue;
            }
            var bundlePath = (System.IO.Path.GetDirectoryName(path) ?? string.Empty).Replace(@"\", "/") + "/";
            var bundleUrlPath = VirtualPathUtility.ToAbsolute(bundlePath);
            var bundleFilePath = string.Format("{0}{1}.bundle{2}",
                                               bundlePath,
                                               System.IO.Path.GetFileNameWithoutExtension(path),
                                               System.IO.Path.GetExtension(path));
            // Transform the url (works with relative path to parent folder "../")
            contents = pattern.Replace(contents, m =>
            {
                var relativeUrl = m.Groups[2].Value;
                var urlReplace = GetUrlReplace(bundleUrlPath, relativeUrl, server);
                return string.Format("url({0}{1}{0})", m.Groups[1].Value, urlReplace);
            });
            File.WriteAllText(server.MapPath(bundleFilePath), contents);
            bundlePaths.Add(bundleFilePath);
        }
        base.Include(bundlePaths.ToArray());
        return this;
    }


    private string GetUrlReplace(string bundleUrlPath, string relativeUrl, HttpServerUtility server)
    {
        // Return the absolute uri
        Uri baseUri = new Uri("http://dummy.org");
        var absoluteUrl = new Uri(new Uri(baseUri, bundleUrlPath), relativeUrl).AbsolutePath;
        var localPath = server.MapPath(absoluteUrl);
        if (IsEmbedEnabled && File.Exists(localPath))
        {
            var fi = new FileInfo(localPath);
            if (fi.Length < 0x4000)
            {
                // Embed the image in uri
                string contentType = GetContentType(fi.Extension);
                if (null != contentType)
                {
                    var base64 = Convert.ToBase64String(File.ReadAllBytes(localPath));
                    // Return the serialized image
                    return string.Format("data:{0};base64,{1}", contentType, base64);
                }
            }
        }
        // Return the absolute uri 
        return absoluteUrl;
    }

J'espère que ça aide, salut.

thepirat000
la source
2

Vous pouvez simplement ajouter un autre niveau de profondeur à votre chemin de bundle virtuel

    //Two levels deep bundle path so that paths are maintained after minification
    bundles.Add(new StyleBundle("~/Content/css/css").Include("~/Content/bootstrap/bootstrap.css", "~/Content/site.css"));

C'est une réponse super low-tech et une sorte de hack mais cela fonctionne et ne nécessitera aucun prétraitement. Étant donné la longueur et la complexité de certaines de ces réponses, je préfère le faire de cette façon.

Brian Rosamilia
la source
Cela n'aide pas lorsque vous avez votre application Web en tant qu'application virtuelle dans IIS. Je veux dire que cela peut fonctionner, mais vous devez nommer votre application virtuelle IIS comme dans votre code, ce qui n'est pas ce que vous voulez, non?
psulek
J'ai le même problème lorsque l'application est une application virtuelle dans IIS. Cette réponse m'aide.
BILL
2

J'ai eu ce problème avec des bundles ayant un chemin d'accès aux images incorrect et CssRewriteUrlTransformne résolvant pas ..correctement les chemins parent relatifs (il y avait aussi un problème avec les ressources externes comme les polices Web). C'est pourquoi j'ai écrit cette transformation personnalisée (semble faire tout ce qui précède correctement):

public class CssRewriteUrlTransform2 : IItemTransform
{
    public string Process(string includedVirtualPath, string input)
    {
        var pathParts = includedVirtualPath.Replace("~/", "/").Split('/');
        pathParts = pathParts.Take(pathParts.Count() - 1).ToArray();
        return Regex.Replace
        (
            input,
            @"(url\(['""]?)((?:\/??\.\.)*)(.*?)(['""]?\))",
            m => 
            {
                // Somehow assigning this to a variable is faster than directly returning the output
                var output =
                (
                    // Check if it's an aboslute url or base64
                    m.Groups[3].Value.IndexOf(':') == -1 ?
                    (
                        m.Groups[1].Value +
                        (
                            (
                                (
                                    m.Groups[2].Value.Length > 0 ||
                                    !m.Groups[3].Value.StartsWith('/')
                                )
                            ) ?
                            string.Join("/", pathParts.Take(pathParts.Count() - m.Groups[2].Value.Count(".."))) :
                            ""
                        ) +
                        (!m.Groups[3].Value.StartsWith('/') ? "/" + m.Groups[3].Value : m.Groups[3].Value) +
                        m.Groups[4].Value
                    ) :
                    m.Groups[0].Value
                );
                return output;
            }
        );
    }
}

Edit: je ne l'ai pas réalisé, mais j'ai utilisé des méthodes d'extension personnalisées dans le code. Le code source de ceux-ci est:

/// <summary>
/// Based on: http://stackoverflow.com/a/11773674
/// </summary>
public static int Count(this string source, string substring)
{
    int count = 0, n = 0;

    while ((n = source.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1)
    {
        n += substring.Length;
        ++count;
    }
    return count;
}

public static bool StartsWith(this string source, char value)
{
    if (source.Length == 0)
    {
        return false;
    }
    return source[0] == value;
}

Bien sûr, il devrait être possible de le remplacer String.StartsWith(char)par String.StartsWith(string).

jahu
la source
Je n'ai pas de surcharge String.Count () qui accepte une chaîne ( m.Groups[2].Value.Count("..")ne fonctionne pas.) Et Value.StartsWith('/')ne fonctionne pas non plus car StartsWith attend une chaîne au lieu d'un caractère.
jao
@jao my bad J'ai inclus mes propres méthodes d'extension dans le code sans m'en rendre compte.
jahu
1
@jao a ajouté le code source de ces méthodes d'extension à la réponse.
jahu
1

Après une petite enquête, j'ai conclu ce qui suit: Vous avez 2 options:

  1. aller avec des transformations. Package très utile pour cela: https://bundletransformer.codeplex.com/ vous avez besoin de la transformation suivante pour chaque bundle problématique:

    BundleResolver.Current = new CustomBundleResolver();
    var cssTransformer = new StyleTransformer();
    standardCssBundle.Transforms.Add(cssTransformer);
    bundles.Add(standardCssBundle);

Avantages: de cette solution, vous pouvez nommer votre bundle comme vous le souhaitez => vous pouvez combiner des fichiers css en un seul bundle à partir de répertoires différents. Inconvénients: vous devez transformer chaque bundle problématique

  1. Utilisez la même racine relative pour le nom du bundle, comme l'emplacement du fichier css. Avantages: aucune transformation n'est nécessaire. Inconvénients: Vous avez des limites sur la combinaison de feuilles CSS de différents répertoires en un seul paquet.
Kovács Ede
la source
0

CssRewriteUrlTransformrésolu mon problème.
Si votre code ne charge toujours pas les images après utilisation CssRewriteUrlTransform, changez le nom de votre fichier CSS à partir de:

.Include("~/Content/jquery/jquery-ui-1.10.3.custom.css", new CssRewriteUrlTransform())

À:

.Include("~/Content/jquery/jquery-ui.css", new CssRewriteUrlTransform())

D'une certaine manière. (Les points) ne reconnaissent pas dans l'url.

Nalan Madheswaran
la source
0

N'oubliez pas de corriger plusieurs inclusions CSS dans un ensemble, telles que:

bundles.Add(new StyleBundle("~/Content/styles/jquery-ui")
    .Include("~/Content/css/path1/somestyle1.css", "~/Content/css/path2/somestyle2.css"));

Vous ne pouvez pas simplement ajouter new CssRewriteUrlTransform()à la fin comme vous pouvez le faire avec un fichier CSS car la méthode ne le prend pas en charge, vous devez donc utiliser Includeplusieurs fois :

bundles.Add(new StyleBundle("~/Content/styles/jquery-ui")
    .Include("~/Content/css/path1/somestyle1.css", new CssRewriteUrlTransform())
    .Include("~/Content/css/path2/somestyle2.css", new CssRewriteUrlTransform()));
SharpC
la source