298 votes

MVC4 StyleBundle ne résout pas les images

Ma question est similaire à celle-ci :

ASP.NET MVC 4 Minification et images d'arrière-plan

Sauf que je veux m'en tenir à l'empaquetage propre à MVC si je peux. Je suis en train de me casser la tête à essayer de comprendre quel est le bon modèle pour spécifier les bundles de style de façon à ce que les css autonomes et les jeux d'images tels que jQuery UI fonctionnent.

J'ai une structure de site MVC typique avec /Content/css/ qui contient mes CSS de base tels que styles.css . Dans ce dossier css, j'ai aussi des sous-dossiers tels que /jquery-ui qui contient son fichier CSS ainsi qu'un /images dossier. Les chemins des images dans le CSS de jQuery UI sont relatifs à ce dossier et je ne veux pas les modifier.

Si je comprends bien, lorsque je spécifie un StyleBundle Je dois spécifier un chemin virtuel qui ne correspond pas à un chemin de contenu réel, car (en supposant que j'ignore les routes vers le contenu) IIS essaierait alors de résoudre ce chemin comme un fichier physique. Je spécifie 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 sortir à :

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

Il renvoie la réponse CSS correcte et minifiée. Mais ensuite, 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

Ce qui est un 404 .

Je comprends que la dernière partie de mon URL jquery-ui est une URL sans extension, un gestionnaire pour mon paquet, 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 procéder de gérer cette situation ?

9 votes

Après avoir été frustré encore et encore avec la nouvelle partie Bundling and Minification, je suis passé à Cassete La sorcière est maintenant gratuite et fonctionne beaucoup mieux !

3 votes

Merci pour le lien, Cassette a l'air bien 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 avoir à modifier les chemins d'accès aux images 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 aux liens CSS simples jusqu'à ce que je trouve une solution. Merci.

0 votes

Ajout de l'erreur probable pour des raisons de référencement : Le contrôleur du chemin '/bundles/images/blah.jpg' n'a pas été trouvé ou n'implémente pas IController.

365voto

Chris Baxter Points 8085

D'après ce fil de discussion sur Regroupement css et références d'images MVC4 si vous définissez votre paquet comme.. :

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

Si vous définissez le paquet sur le même chemin que les fichiers sources qui le composent, les chemins relatifs des images fonctionneront toujours. La dernière partie du chemin d'accès du paquet est en fait le chemin d'accès de file name pour cette liasse spécifique (c'est-à-dire, /bundle peut être le nom que vous voulez).

Cela ne fonctionnera que si vous regroupez des feuilles de style CSS provenant du même dossier (ce qui me semble logique du point de vue du regroupement).

Mise à jour

Comme indiqué dans le commentaire ci-dessous de @Hao Kung, il est possible d'y parvenir en appliquant une méthode d'évaluation de l'impact sur l'environnement. CssRewriteUrlTransformation ( Modification des références URL relatives aux fichiers CSS lorsqu'ils sont groupés ).

NOTE : Je n'ai pas confirmé les commentaires concernant les problèmes de réécriture en 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()));

1 votes

Une légende ! Oui, cela fonctionne parfaitement. J'ai des CSS à différents niveaux mais ils ont chacun leurs propres dossiers d'images, par exemple, le CSS de mon site principal est dans le dossier Root CSS et jquery-ui est à l'intérieur avec son propre dossier d'images, donc je spécifie juste 2 bundles, un pour mon CSS de base et un pour jQuery UI - ce qui n'est peut-être pas ultra-optimal en termes de requêtes, mais la vie est courte. A la vôtre !

3 votes

Malheureusement, jusqu'à ce que le bundling prenne en charge la réécriture des urls intégrées dans le css lui-même, le répertoire virtuel du bundle css doit correspondre aux fichiers css avant le bundling. C'est pourquoi les paquets de modèles par défaut n'ont pas d'url comme ~/bundles/themes, et ressemblent plutôt à la structure de répertoire : ~/content/theemes/base/css

1 votes

@HaoKung - Je comprends ce que vous dites mais cela n'explique pas pourquoi new StyleBundle("~/Content/css") ne correspond pas à "~/Content/site.css", et pourtant cela fonctionne toujours. Et si je veux ajouter mon CSS personnalisé à cette même méthode Include, il ne se résout jamais. Avez-vous une idée de la raison de cette situation ?

34voto

AcidPAT Points 373

La solution de Grinn / ThePirat fonctionne bien.

Je n'ai pas aimé le fait qu'il utilise la méthode Include sur le bundle et qu'il crée des fichiers temporaires dans le répertoire du contenu. (ils ont fini par être archivés, déployés, puis le service n'a pas démarré).

Ainsi, pour suivre la conception du Bundling, j'ai choisi d'exécuter 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 il a emballé tout ça dans un paquet d'implemetation :

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

Utilisation de l'échantillon :

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(@"\", "/");
    }

0 votes

Cela me semble aussi le plus propre. Merci. Je vous vote tous les trois parce que ça semblait être un travail d'équipe. :)

0 votes

Le code tel que vous l'avez maintenant ne fonctionne pas pour moi. J'essaie de le corriger, mais j'ai pensé que je devais vous le faire savoir. La méthode context.HttpContext.RelativeFromAbsolutePath n'existe pas. De plus, si le chemin d'accès à l'URL commence par un "/" (ce qui le rend absolu), la logique de combinaison des chemins d'accès est incorrecte.

0 votes

@Josh - Je viens d'ajouter la déclaration pour RelativeFromAbsolutePath à la réponse. J'ai toujours utilisé "~/..." pour tous les noms de fichiers CSS afin qu'il n'y ait aucune ambiguïté.

20voto

Grinn Points 1966

Mieux encore (IMHO), implémentez un Bundle personnalisé qui corrige les chemins des images. 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 (lorsqu'il n'est pas en mode débogage) c'est rechercher url(<something>) et le remplace par url(<absolute\path\to\something>) . J'ai écrit ce texte il y a environ 10 secondes, il peut donc nécessiter quelques ajustements. J'ai pris en compte les URL entièrement qualifiées et les DataURI en 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 deux dossiers parents ( url(../someFile.png) ) et les dossiers enfants ( url(someFolder/someFile.png ).

0 votes

C'est une excellente solution. J'ai légèrement modifié votre Regex pour qu'il fonctionne également avec les fichiers LESS, mais le concept original était exactement ce dont j'avais besoin. Merci.

1 votes

Vous pouvez également placer l'initialisation du regex en dehors de la boucle. Peut-être comme une propriété statique en lecture seule.

13voto

Code Chief Points 422

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

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

Il me semble qu'il s'agit plutôt d'un bogue, mais de toute façon, c'est ainsi qu'on le corrige avec la version actuelle de .NET 4.51. Peut-être que les autres réponses étaient nécessaires sur des builds ASP.NET plus anciens, je ne peux pas le dire, 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 paquet comme...

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

Et le rendre comme...

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

Pour obtenir le "comportement" (bug), les fichiers CSS eux-mêmes ont le Root de l'application (par exemple "http://localhost:1234/MySite/Content/Site.css") mais les images CSS à l'intérieur commencent toutes "/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 si cela avait à voir avec le chemin existant ou non, mais cela n'a rien changé. La solution au problème est en fait l'exigence que le nom du bundle doit commencer par le chemin Root.

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

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

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

Bref, c'est comme ça. Microsoft ne voit pas de bogue. Je ne suis pas d'accord avec cela, soit cela devrait fonctionner comme prévu, soit une exception devrait être levée, soit une surcharge supplémentaire pour ajouter le chemin du bundle qui choisit d'inclure ou non l'application Root. Je ne peux pas imaginer pourquoi quelqu'un ne voudrait pas inclure la racine de l'application quand il y en a une (normalement, à moins que vous ayez 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 de toute façon.

0 votes

Il me semble que c'est la "solution" la plus simple. Les autres peuvent avoir des effets secondaires, comme avec image:data.

0 votes

@MohamedEmaish ça marche, vous vous êtes probablement trompé. Apprenez à tracer les requêtes, par exemple en utilisant l'outil Fiddler pour voir quelles URL sont demandées par le navigateur. Le but n'est pas de coder en dur l'ensemble du chemin relatif afin que votre site Web puisse être installé à différents endroits (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 (l'intérêt d'avoir une variable racine d'application).

0 votes

J'ai choisi cette option et cela a très bien fonctionné. J'ai dû m'assurer que chaque paquet ne contenait que des éléments d'un seul dossier (il ne peut pas inclure des éléments d'autres dossiers ou sous-dossiers), ce qui est légèrement ennuyeux mais tant que cela fonctionne, je suis content ! Merci pour le post.

6voto

Ben Foster Points 11699

À partir de la version v1.1.0-alpha1 (paquet de pré-version), le cadre utilise l'option 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);
            }
        }
    }
}

0 votes

En fait, ce que cela fait, c'est remplacer les URL relatives en CSS par des URL absolues.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X