MVC4 StyleBundle no resuelve imágenes
Mi pregunta es similar a esta:
ASP.NET MVC 4 Minificación e imágenes de fondo
Excepto que, si puedo, quiero seguir con el propio paquete de MVC. Estoy sufriendo un colapso cerebral al tratar de descubrir cuál es el patrón correcto para especificar paquetes de estilos de modo que funcionen conjuntos de imágenes y CSS independientes como jQuery UI.
Tengo una estructura de sitio MVC típica /Content/css/
que contiene mi CSS base, como styles.css
. Dentro de esa carpeta CSS también tengo subcarpetas como /jquery-ui
la que contiene su archivo CSS más una /images
carpeta. Las rutas de las imágenes en jQuery UI CSS son relativas a esa carpeta y no quiero meterme con ellas.
Según tengo entendido, cuando especifico StyleBundle
necesito especificar una ruta virtual que no coincida también con una ruta de contenido real, porque (suponiendo que estoy ignorando las rutas al Contenido) IIS intentaría resolver esa ruta como un archivo físico. Entonces estoy especificando:
bundles.Add(new StyleBundle("~/Content/styles/jquery-ui")
.Include("~/Content/css/jquery-ui/*.css"));
renderizado usando:
@Styles.Render("~/Content/styles/jquery-ui")
Puedo ver que la solicitud se envía a:
http://localhost/MySite/Content/styles/jquery-ui?v=nL_6HPFtzoqrts9nwrtjq0VQFYnhMjY5EopXsK8cxmg1
Esto devuelve la respuesta CSS correcta y minimizada. Pero luego el navegador envía una solicitud de una imagen relativamente vinculada como:
http://localhost/MySite/Content/styles/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
El cual es un 404
.
Entiendo que la última parte de mi URL jquery-ui
es una URL sin extensión, un controlador para mi paquete, por lo que puedo ver por qué la solicitud relativa de la imagen es simplemente /styles/images/
.
Entonces mi pregunta es ¿cuál es la forma correcta de manejar esta situación?
De acuerdo con este hilo sobre referencias de imágenes y paquetes CSS de MVC4 , si define su paquete como:
bundles.Add(new StyleBundle("~/Content/css/jquery-ui/bundle")
.Include("~/Content/css/jquery-ui/*.css"));
Cuando defina el paquete en la misma ruta que los archivos fuente que componían el paquete, las rutas relativas de las imágenes seguirán funcionando. La última parte de la ruta del paquete es en realidad la file name
de ese paquete específico (es decir, /bundle
puede tener el nombre que desee).
Esto sólo funcionará si estás agrupando CSS desde la misma carpeta (lo cual creo que tiene sentido desde una perspectiva de agrupación).
Actualizar
Según el comentario a continuación de @Hao Kung, alternativamente, esto ahora se puede lograr aplicando CssRewriteUrlTransformation
( Cambiar las referencias URL relativas a archivos CSS cuando estén incluidos ).
NOTA: No he confirmado comentarios sobre problemas con la reescritura en rutas absolutas dentro de un directorio virtual, por lo que es posible que esto no funcione para todos (?).
bundles.Add(new StyleBundle("~/Content/css/jquery-ui/bundle")
.Include("~/Content/css/jquery-ui/*.css",
new CssRewriteUrlTransform()));
La solución Grinn/ThePirat funciona bien.
No me gustó que incluyera el método Include en el paquete y que creara archivos temporales en el directorio de contenido. (¡Terminaron siendo registrados, implementados y luego el servicio no se inició!)
Entonces, para seguir el diseño de Bundling, elegí realizar esencialmente el mismo código, pero en una implementación de 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);
}
}
}
}
Y luego resumimos esto en una implementación de paquete:
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());
}
}
Uso de muestra:
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"
));
Aquí está mi método de extensión para 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(@"\", "/");
}
Mejor aún (en mi humilde opinión), implemente un paquete personalizado que corrija las rutas de las imágenes. Escribí uno para mi aplicación.
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;
}
}
Para usarlo, haga:
bundles.Add(new StyleImagePathBundle("~/bundles/css").Include(
"~/This/Is/Some/Folder/Path/layout.css"));
...en lugar de...
bundles.Add(new StyleBundle("~/bundles/css").Include(
"~/This/Is/Some/Folder/Path/layout.css"));
Lo que hace es (cuando no está en modo de depuración) buscarlo url(<something>)
y reemplazarlo con url(<absolute\path\to\something>)
. Escribí esto hace unos 10 segundos, por lo que es posible que necesite algunos ajustes. He tenido en cuenta las URL totalmente calificadas y los DataURI base64 asegurándome de que no haya dos puntos (:) en la ruta de la URL. En nuestro entorno, las imágenes normalmente residen en la misma carpeta que sus archivos CSS, pero lo he probado tanto con las carpetas principales ( url(../someFile.png)
) como con las secundarias ( url(someFolder/someFile.png
).
No es necesario especificar una transformación ni tener rutas de subdirectorio locas. Después de mucho solucionar el problema, lo aislé según esta regla "simple" (¿es un error?)...
Si la ruta de su paquete no comienza con la raíz relativa de los elementos que se incluyen, entonces no se tendrá en cuenta la raíz de la aplicación web.
Me parece más bien un error, pero de todos modos así es como se soluciona con la versión actual de .NET 4.51. Quizás las otras respuestas fueron necesarias en compilaciones ASP.NET más antiguas, no puedo decir que no tenga tiempo para probar todo eso retrospectivamente.
Para aclarar, aquí hay un ejemplo:
Tengo estos archivos...
~/Content/Images/Backgrounds/Some_Background_Tile.gif
~/Content/Site.css - references the background image relatively, i.e. background: url('Images/...')
Luego configura el paquete como...
BundleTable.Add(new StyleBundle("~/Bundles/Styles").Include("~/Content/Site.css"));
Y renderizarlo como...
@Styles.Render("~/Bundles/Styles")
Y obtenga el "comportamiento" (error), los archivos CSS en sí tienen la raíz de la aplicación (por ejemplo, "http://localhost:1234/MySite/Content/Site.css") pero la imagen CSS dentro de todos comienza "/Content/Images /..." o "/Images/..." dependiendo de si agrego la transformación o no.
Incluso intenté crear la carpeta "Paquetes" para ver si tenía que ver con la ruta existente o no, pero eso no cambió nada. La solución al problema es realmente el requisito de que el nombre del paquete debe comenzar con la ruta raíz.
Lo que significa que este ejemplo se soluciona registrando y renderizando la ruta del paquete como...
BundleTable.Add(new StyleBundle("~/Content/StylesBundle").Include("~/Content/Site.css"));
...
@Styles.Render("~/Content/StylesBundle")
Entonces, por supuesto, se podría decir que esto es RTFM, pero estoy bastante seguro de que otros y yo seleccionamos esta ruta "~/Bundles/..." de la plantilla predeterminada o en algún lugar de la documentación en el sitio web MSDN o ASP.NET, o Me topé con él porque en realidad es un nombre bastante lógico para una ruta virtual y tiene sentido elegir rutas virtuales que no entren en conflicto con los directorios reales.
De todos modos, así es. Microsoft no ve ningún error. No estoy de acuerdo con esto, o debería funcionar como se esperaba o se debería generar alguna excepción, o una anulación adicional para agregar la ruta del paquete que opta por incluir la raíz de la aplicación o no. No puedo imaginar por qué alguien no querría que se incluyera la raíz de la aplicación cuando la había (normalmente, a menos que haya instalado su sitio web con un alias DNS/raíz de sitio web predeterminada). Entonces, en realidad, ese debería ser el valor predeterminado de todos modos.
Tal vez sea parcial, pero me gusta bastante mi solución, ya que no realiza ninguna transformación, expresiones regulares, etc. y tiene la menor cantidad de código :)
Esto funciona para un sitio alojado como directorio virtual en un sitio web IIS y como sitio web raíz en IIS.
Así que creé una implementación de IItemTransform
encapsulado CssRewriteUrlTransform
y la usé VirtualPathUtility
para arreglar la ruta y llamar al código existente:
/// <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()));
}
¿Parece funcionar bien para mi?