web-dev-qa-db-fra.com

Comment utiliser ASP.Net MVC 4 pour regrouper des fichiers MOINS en mode Release?

J'essaie d'avoir MOINS de fichiers dans mon projet Web et d'avoir l'appel de fonctionnalité de regroupement MVC 4 dans la bibliothèque dotLess pour transformer le MOINS en CSS, puis minimiser le résultat et le donner au navigateur.

J'ai trouvé un exemple sur le site ASP.NET (sous la rubrique LESS, CoffeeScript, SCSS, Sass Bundling. ). Cela m'a donné une classe LessTransform qui ressemble à ceci:

public class LessTransform : IBundleTransform
{
    public void Process(BundleContext context, BundleResponse response)
    {
        response.Content = dotless.Core.Less.Parse(response.Content);
        response.ContentType = "text/css";
    }
}

et cette ligne dans ma classe BundleConfig:

bundles.Add(new Bundle(
    "~/Content/lessTest", 
    new LessTransform(), 
    new CssMinify()).Include("~/Content/less/test.less"));

enfin j'ai la ligne suivante dans mon _Layout.cshtml, dans le <head>:

@Styles.Render("~/Content/lessTest")

Si j'ai le site en mode débogage, cela est rendu au navigateur:

<link href="/Content/less/test.less" rel="stylesheet"/>

Les règles du fichier .less sont appliquées et ce lien montre que le MOINS a été correctement transformé en CSS.

Cependant, si je mets le site en mode release, ceci est rendu:

<link href="/Content/less?v=lEs-HID6XUz3s2qkJ35Lvnwwq677wTaIiry6fuX8gz01" rel="stylesheet"/>

Les règles du fichier .less ne sont pas appliquées, car le fait de suivre le lien donne une erreur 404 d'IIS.

Il semble donc que quelque chose ne va pas avec le regroupement. Comment faire en sorte que cela fonctionne en mode de publication ou comment savoir exactement ce qui ne va pas?

25
Graham Clark

Modifié le 08/12/2019 Ce n'est plus une réponse acceptable à ce problème car il y a eu des changements de rupture dans ASP.NET au fil des ans. Il y a d'autres réponses plus bas qui ont modifié ce code ou fourni d'autres réponses pour vous aider à résoudre ce problème.

Il semble que le moteur sans point ait besoin de connaître le chemin du fichier de paquet actuellement traité pour résoudre les chemins @import. Si vous exécutez le code de processus que vous avez ci-dessus, le résultat de dotless.Core.Less.Parse () est une chaîne vide lorsque le fichier .less en cours d'analyse a d'autres fichiers moins importés.

La réponse de Ben Foster ici corrigera cela en lisant d'abord les fichiers importés:

Importer des fichiers et DotLess

Modifiez votre fichier LessTransform comme suit:

public class LessTransform : IBundleTransform
{
    public void Process(BundleContext context, BundleResponse bundle)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        if (bundle == null)
        {
            throw new ArgumentNullException("bundle");
        }

        context.HttpContext.Response.Cache.SetLastModifiedFromFileDependencies();

        var lessParser = new Parser();
        ILessEngine lessEngine = CreateLessEngine(lessParser);

        var content = new StringBuilder(bundle.Content.Length);

        var bundleFiles = new List<FileInfo>();

        foreach (var bundleFile in bundle.Files)
        {
            bundleFiles.Add(bundleFile);

            SetCurrentFilePath(lessParser, bundleFile.FullName);
            string source = File.ReadAllText(bundleFile.FullName);
            content.Append(lessEngine.TransformToCss(source, bundleFile.FullName));
            content.AppendLine();

            bundleFiles.AddRange(GetFileDependencies(lessParser));
        }

        if (BundleTable.EnableOptimizations)
        {
            // include imports in bundle files to register cache dependencies
            bundle.Files = bundleFiles.Distinct();
        }

        bundle.ContentType = "text/css";
        bundle.Content = content.ToString();
    }

    /// <summary>
    /// Creates an instance of LESS engine.
    /// </summary>
    /// <param name="lessParser">The LESS parser.</param>
    private ILessEngine CreateLessEngine(Parser lessParser)
    {
        var logger = new AspNetTraceLogger(LogLevel.Debug, new Http());
        return new LessEngine(lessParser, logger, true, false);
    }

    /// <summary>
    /// Gets the file dependencies (@imports) of the LESS file being parsed.
    /// </summary>
    /// <param name="lessParser">The LESS parser.</param>
    /// <returns>An array of file references to the dependent file references.</returns>
    private IEnumerable<FileInfo> GetFileDependencies(Parser lessParser)
    {
        IPathResolver pathResolver = GetPathResolver(lessParser);

        foreach (var importPath in lessParser.Importer.Imports)
        {
            yield return new FileInfo(pathResolver.GetFullPath(importPath));
        }

        lessParser.Importer.Imports.Clear();
    }

    /// <summary>
    /// Returns an <see cref="IPathResolver"/> instance used by the specified LESS lessParser.
    /// </summary>
    /// <param name="lessParser">The LESS parser.</param>
    private IPathResolver GetPathResolver(Parser lessParser)
    {
        var importer = lessParser.Importer as Importer;
        var fileReader = importer.FileReader as FileReader;

        return fileReader.PathResolver;
    }

    /// <summary>
    /// Informs the LESS parser about the path to the currently processed file. 
    /// This is done by using a custom <see cref="IPathResolver"/> implementation.
    /// </summary>
    /// <param name="lessParser">The LESS parser.</param>
    /// <param name="currentFilePath">The path to the currently processed file.</param>
    private void SetCurrentFilePath(Parser lessParser, string currentFilePath)
    {
        var importer = lessParser.Importer as Importer;

        if (importer == null)
            throw new InvalidOperationException("Unexpected dotless importer type.");

        var fileReader = importer.FileReader as FileReader;

        if (fileReader == null || !(fileReader.PathResolver is ImportedFilePathResolver))
        {
            fileReader = new FileReader(new ImportedFilePathResolver(currentFilePath));
            importer.FileReader = fileReader;
        }
    }
}

public class ImportedFilePathResolver : IPathResolver
{
    private string currentFileDirectory;
    private string currentFilePath;

    public ImportedFilePathResolver(string currentFilePath)
    {
        if (string.IsNullOrEmpty(currentFilePath))
        {
            throw new ArgumentNullException("currentFilePath");
        }

        CurrentFilePath = currentFilePath;
    }

    /// <summary>
    /// Gets or sets the path to the currently processed file.
    /// </summary>
    public string CurrentFilePath
    {
        get { return currentFilePath; }
        set
        {
            currentFilePath = value;
            currentFileDirectory = Path.GetDirectoryName(value);
        }
    }

    /// <summary>
    /// Returns the absolute path for the specified improted file path.
    /// </summary>
    /// <param name="filePath">The imported file path.</param>
    public string GetFullPath(string filePath)
    {
        if (filePath.StartsWith("~"))
        {
            filePath = VirtualPathUtility.ToAbsolute(filePath);
        }

        if (filePath.StartsWith("/"))
        {
            filePath = HostingEnvironment.MapPath(filePath);
        }
        else if (!Path.IsPathRooted(filePath))
        {
            filePath = Path.GetFullPath(Path.Combine(currentFileDirectory, filePath));
        }

        return filePath;
    }
}
13
Emma Middlebrook

En complément de la réponse acceptée , j'ai créé une classe LessBundle, qui est le moins équivalent de la classe StyleBundle.

Le code LessBundle.cs est:

using System.Web.Optimization;

namespace MyProject
{
    public class LessBundle : Bundle
    {
        public LessBundle(string virtualPath) : base(virtualPath, new IBundleTransform[] {new LessTransform(), new CssMinify()})
        {

        }

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

        }
    }
}

L'utilisation est similaire à la classe StyleBundle, en spécifiant un fichier LESS au lieu d'un fichier CSS.

Ajoutez les éléments suivants à votre méthode BundleConfig.RegisterBundles (BundleCollection) :

bundles.Add(new LessBundle("~/Content/less").Include(
                 "~/Content/MyStyles.less"));

Mise à jour

Cette méthode fonctionne bien avec l'optimisation désactivée, mais j'ai rencontré quelques problèmes mineurs (avec les chemins de ressources CSS) lorsque l'optimisation a été activée. Après une heure de recherche sur le problème, j'ai découvert que j'avais réinventé la roue ...

Si vous faites voulez la fonctionnalité LessBundle que je décris ci-dessus, consultez System.Web.Optimization.Less .

Le paquet NuGet peut être trouvé ici .

15
David Kirkland

La réponse acceptée ne fonctionne pas avec les modifications récentes apportées à ASP.NET, elle n'est donc plus correcte.

J'ai corrigé la source dans la réponse acceptée:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.Hosting;
using System.Web.Optimization;
using dotless.Core;
using dotless.Core.Abstractions;
using dotless.Core.Importers;
using dotless.Core.Input;
using dotless.Core.Loggers;
using dotless.Core.Parser;

namespace Web.App_Start.Bundles
{
    public class LessTransform : IBundleTransform
    {
        public void Process(BundleContext context, BundleResponse bundle)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            if (bundle == null)
            {
                throw new ArgumentNullException("bundle");
            }

            context.HttpContext.Response.Cache.SetLastModifiedFromFileDependencies();

            var lessParser = new Parser();
            ILessEngine lessEngine = CreateLessEngine(lessParser);

            var content = new StringBuilder(bundle.Content.Length);

            var bundleFiles = new List<BundleFile>();

            foreach (var bundleFile in bundle.Files)
            {
                bundleFiles.Add(bundleFile);

                var name = context.HttpContext.Server.MapPath(bundleFile.VirtualFile.VirtualPath);
                SetCurrentFilePath(lessParser, name);
                using (var stream = bundleFile.VirtualFile.Open())
                using (var reader = new StreamReader(stream))
                {
                    string source = reader.ReadToEnd();
                    content.Append(lessEngine.TransformToCss(source, name));
                    content.AppendLine();
                }

                bundleFiles.AddRange(GetFileDependencies(lessParser));
            }

            if (BundleTable.EnableOptimizations)
            {
                // include imports in bundle files to register cache dependencies
                bundle.Files = bundleFiles.Distinct();
            }

            bundle.ContentType = "text/css";
            bundle.Content = content.ToString();
        }

        /// <summary>
        /// Creates an instance of LESS engine.
        /// </summary>
        /// <param name="lessParser">The LESS parser.</param>
        private ILessEngine CreateLessEngine(Parser lessParser)
        {
            var logger = new AspNetTraceLogger(LogLevel.Debug, new Http());
            return new LessEngine(lessParser, logger, true, false);
        }

        /// <summary>
        /// Gets the file dependencies (@imports) of the LESS file being parsed.
        /// </summary>
        /// <param name="lessParser">The LESS parser.</param>
        /// <returns>An array of file references to the dependent file references.</returns>
        private IEnumerable<BundleFile> GetFileDependencies(Parser lessParser)
        {
            IPathResolver pathResolver = GetPathResolver(lessParser);

            foreach (var importPath in lessParser.Importer.Imports)
            {
                yield return
                    new BundleFile(pathResolver.GetFullPath(importPath),
                        HostingEnvironment.VirtualPathProvider.GetFile(importPath));
            }

            lessParser.Importer.Imports.Clear();
        }

        /// <summary>
        /// Returns an <see cref="IPathResolver"/> instance used by the specified LESS lessParser.
        /// </summary>
        /// <param name="lessParser">The LESS parser.</param>
        private IPathResolver GetPathResolver(Parser lessParser)
        {
            var importer = lessParser.Importer as Importer;
            var fileReader = importer.FileReader as FileReader;

            return fileReader.PathResolver;
        }

        /// <summary>
        /// Informs the LESS parser about the path to the currently processed file. 
        /// This is done by using a custom <see cref="IPathResolver"/> implementation.
        /// </summary>
        /// <param name="lessParser">The LESS parser.</param>
        /// <param name="currentFilePath">The path to the currently processed file.</param>
        private void SetCurrentFilePath(Parser lessParser, string currentFilePath)
        {
            var importer = lessParser.Importer as Importer;

            if (importer == null)
                throw new InvalidOperationException("Unexpected dotless importer type.");

            var fileReader = importer.FileReader as FileReader;

            if (fileReader == null || !(fileReader.PathResolver is ImportedFilePathResolver))
            {
                fileReader = new FileReader(new ImportedFilePathResolver(currentFilePath));
                importer.FileReader = fileReader;
            }
        }
    }
}

Veuillez noter qu'un problème connu avec ce code est que LESS @imports doit utiliser son chemin complet, c'est-à-dire que vous devez utiliser @import "~/Areas/Admin/Css/global.less"; au lieu de @import "global.less";.

3
Ian Newson

On dirait que cela fonctionne - j'ai changé la méthode Process pour itérer sur la collection de fichiers:

public void Process(BundleContext context, BundleResponse response)
{
    var builder = new StringBuilder();
    foreach (var fileInfo in response.Files)
    {
        using (var reader = fileInfo.OpenText())
        {
            builder.Append(dotless.Core.Less.Parse(reader.ReadToEnd()));
        }
    }

    response.Content = builder.ToString();
    response.ContentType = "text/css";
}

Cela casse s'il y a @import instructions dans vos fichiers less cependant, dans ce cas, vous devez faire un peu plus de travail, comme ceci: https://Gist.github.com/chrisortman/2002958

2
Graham Clark

Déjà quelques bonnes réponses, voici une solution très simple que j'ai trouvée pour moi-même en essayant d'ajouter des bundles MVC qui concernent les fichiers less.

Après avoir créé votre fichier less (par exemple, test.less), faites un clic droit dessus et sous l'option Web Compiler ( obtenez-le ici ), sélectionnez Compile File.

Cela génère le fichier css résultant à partir de votre less, ainsi que sa version réduite. (test.css et test.min.css).

Sur votre bundle, référez-vous simplement au fichier css généré

style = new StyleBundle("~/bundles/myLess-styles")
    .Include("~/Content/css/test.css", new CssRewriteUrlTransform());

bundles.Add(style);

Et selon vous, référencez ce bundle:

@Styles.Render("~/bundles/myLess-styles")

Cela devrait bien fonctionner.

1
chiapa