MVC4 Less Bundle @import Directory

52

我正在尝试使用MVC4 bundling来组合一些less文件,但看起来我使用的导入路径不正确。我的目录结构是:

static/
    less/
        mixins.less
        admin/
            user.less

在user.less中,我试图使用以下代码导入mixins.less:

@import "../mixins.less";

在我以前使用Chirpy和dotless时,这个方法对我有效,但现在我发现ELMAH在抱怨,显示以下内容:

System.IO.FileNotFoundException: 
    You are importing a file ending in .less that cannot be found.
File name: '../mixins.less'

我应该在MVC4中使用不同的@import吗?

一些额外的信息

以下是我正在尝试的less类和global.asax.cs代码:

LessMinify.cs

...
public class LessMinify : CssMinify
{
    public LessMinify() {}

    public override void Process(BundleContext context, BundleResponse response)
    {
        response.Content = Less.Parse(response.Content);
        base.Process(context, response);
    }
}
...

Global.asax.cs

:全局应用程序类的C#文件。
...
DynamicFolderBundle lessFB = 
    new DynamicFolderBundle("less", new LessMinify(), "*.less");
    
BundleTable.Bundles.Add(lessFB);

Bundle AdminLess = new Bundle("~/AdminLessBundle", new LessMinify());
...
AdminLess.AddFile("~/static/less/admin/user.less");
BundleTable.Bundles.Add(AdminLess);
...

2
与您的问题无关,但你能分享一下你用的 Less 解析器是什么吗? - Shane Courtrille
@ShaneCourtrille 请查看https://nuget.org/packages?q=dotless,我相信我正在使用dotless 1.3。 - JesseBuesking
10个回答

41
我已经写了一篇关于使用LESS CSS与MVC4 Web优化的简短博客文章。
基本上就是使用BundleTransformer.Less Nuget包,并更改您的BundleConfig.cs文件。
已经测试过与bootstrap兼容。 编辑:应该提到我说这个的原因是,我也遇到了@import目录结构问题,而这个库可以正确地处理它。

5
谢谢这个,我一直很苦恼找不到所需的内容。无法相信这篇文章没有被投票。我已经添加了一个带有补充说明的帖子来完善您的解决方案。 - awrigley
6
我不会说它简单或优雅。如果你只是使用LESS,BundleTransformer是一个非常沉重的Nuget包(实际上需要5个以上的Nuget包,并且需要在你的web服务器上安装IE9+)。迈克尔·贝尔德的答案要简单得多。 - arserbin3
Ben,我按照你的博客文章说明以及BundleTransformer.Less文档页面上提供的示例进行操作,但仍然出现了关于dotless无法找到由@import语句引用的文件的错误。我的所有文件都在同一个目录中,所以我只是使用了@import url("filename.less");。我甚至遵循了这篇文章:http://geekswithblogs.net/ToStringTheory/archive/2012/11/30/who-could-ask-for-more-with-less-css-part-2.aspx。有什么想法可能会发生什么?MVC4/.NET 4.5 - ps2goat
@ps2goat 这只是一个猜测,但是在安装BundleTransformer NuGet包时,你是否替换了它要求你替换的两个web.config部分? - Ben Cull
我认为这些要求都不是通过Nuget包自动下载的。我们最终选择了@MichaelBaird的解决方案,因为正如其他人所说,它不需要IE 9+或Visual C++库(这取决于您使用的JavaScript引擎切换器版本)。 - ps2goat
在BundleConfig.cs中添加css.Transforms.Add(cssTransformer);对我很有帮助。谢谢! - VladN

26

在GitHub Gist上有一份代码,可以很好地与@import和dotLess配合使用:https://gist.github.com/2002958

我已经使用Twitter Bootstrap进行了测试,它运行良好。

ImportedFilePathResolver.cs

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

    /// <summary>
    /// Initializes a new instance of the <see cref="ImportedFilePathResolver"/> class.
    /// </summary>
    /// <param name="currentFilePath">The path to the currently processed file.</param>
    public ImportedFilePathResolver(string 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)
    {
        filePath = filePath.Replace('\\', '/').Trim();

        if(filePath.StartsWith("~"))
        {
            filePath = VirtualPathUtility.ToAbsolute(filePath);
        }

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

        return filePath;
    }
}

LessMinify.cs

public class LessMinify : IBundleTransform
{
    /// <summary>
    /// Processes the specified bundle of LESS files.
    /// </summary>
    /// <param name="bundle">The LESS bundle.</param>
    public void Process(BundleContext context, BundleResponse bundle)
    {
        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);

        foreach(FileInfo file in bundle.Files)
        {
            SetCurrentFilePath(lessParser, file.FullName);
            string source = File.ReadAllText(file.FullName);
            content.Append(lessEngine.TransformToCss(source, file.FullName));
            content.AppendLine();

            AddFileDependencies(lessParser);
        }

        bundle.Content = content.ToString();
        bundle.ContentType = "text/css";
        //base.Process(context, bundle);
    }

    /// <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, false);
    }

    /// <summary>
    /// Adds imported files to the collection of files on which the current response is dependent.
    /// </summary>
    /// <param name="lessParser">The LESS parser.</param>
    private void AddFileDependencies(Parser lessParser)
    {
        IPathResolver pathResolver = GetPathResolver(lessParser);

        foreach(string importedFilePath in lessParser.Importer.Imports)
        {
            string fullPath = pathResolver.GetFullPath(importedFilePath);
            HttpContext.Current.Response.AddFileDependency(fullPath);
        }

        lessParser.Importer.Imports.Clear();
    }

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

        return null;
    }

    /// <summary>
    /// Informs the LESS parser about the path to the currently processed file. 
    /// This is done by using 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)
        {
            var fileReader = importer.FileReader as FileReader;

            if(fileReader == null)
            {
                importer.FileReader = fileReader = new FileReader();
            }

            var pathResolver = fileReader.PathResolver as ImportedFilePathResolver;

            if(pathResolver != null)
            {
                pathResolver.CurrentFilePath = currentFilePath;
            }
            else
            {
               fileReader.PathResolver = new ImportedFilePathResolver(currentFilePath);
            }
        }
        else
        {
            throw new InvalidOperationException("Unexpected importer type on dotless parser");
        }


    }
}

1
这正是我所寻找的。非常棒的文章,Michael! - Reaction21
有一个稍微改进的版本声称也适用于 .net 4.5:https://github.com/dotless/dotless/issues/148 https://bitbucket.org/mrcode/bundlingsandbox/changeset/fbfd73a148e6816a366d9c16051fe113990e0b8f - ATL_DEV
6
可能有些.NET 4.5中的变化,但以上代码没有正确缓存导入。为确保正确配置缓存依赖项,在启用优化时,您需要将所有导入路径添加到Bundle.Files集合中。我的工作代码-https://gist.github.com/3924025 - Ben Foster
演示项目中的链接已失效。 - Alex
在我的代码中,bundle.Files没有返回FileInfo。必须调用以下内容以获取文件路径 file.IncludedVirtualPath.Replace("~/", HttpRuntime.AppDomainAppPath); - Hp93
显示剩余2条评论

22

对Ben Cull答案的补充:

我知道这“应该是对Ben Cull帖子的评论”,但它添加了一些无法在评论中添加的额外内容。如果必须的话,请将我投票否决,或者关闭我。

Ben的博客文章已经做到了所有这些,只是没有指定缩小文件大小。

所以按照Ben的建议安装BundleTransformer.Less包,然后,如果您想缩小CSS文件大小,请执行以下操作(在~/App_Start/BundleConfig.cs中):

var cssTransformer = new CssTransformer();
var jsTransformer = new JsTransformer();
var nullOrderer = new NullOrderer();

var css = new Bundle("~/bundles/css")
    .Include("~/Content/site.less");
css.Transforms.Add(cssTransformer);
css.Transforms.Add(new CssMinify());
css.Orderer = nullOrderer;

bundles.Add(css);

新增的行是:

css.Transforms.Add(new CssMinify());

CssMinifySystem.Web.Optimizations 中。

我终于解决了 @import 问题和找不到 .less 文件的问题,我不在乎谁会对我的回答投反对票。

如果相反的话,如果你想投赞成票,请给 Ben 投票。

就这样。


这个可以工作,但似乎导入的文件被多次内联(每次导入一次)。这有点违背了整个捆绑想法的目的,即减小文件大小... - Clement

17

我发现一个非常有效的解决方法是在运行LessMinify.Process()之前设置目录,然后再运行Less.Parse。以下是我的实现方式:

public class LessTransform : IBundleTransform
    {
        private string _path;

        public LessTransform(string path)
        {
            _path = path;
        }

        public void Process(BundleContext context, BundleResponse response)
        {
            Directory.SetCurrentDirectory(_path);

            response.Content = Less.Parse(response.Content);
            response.ContentType = "text/css";
        }
    }

接着在创建less转换对象时传入路径,像这样:

lessBundle.Transforms.Add(
    new LessTransform(HttpRuntime.AppDomainAppPath + "/Content/Less")
);

希望这能帮到你。


5
为什么其他答案要如此复杂,而一个简单的解决方案就足够了呢?谢谢。 - Fabio Milheiro
1
这对我很有帮助,感谢您的发布。这是一个简单的答案,而且运行良好。 - silvio

4
问题在于DynamicFolderBundle读取文件的所有内容并将组合的内容传递给LessMinify。
因此,任何@import都没有引用文件来自哪个位置。
要解决这个问题,我必须将所有"less"文件放到一个位置。
然后,您必须了解文件的排序变得很重要。 因此,我开始使用数字重命名文件(例如:"0 CONSTANTS.less","1 MIXIN.less"),这意味着它们在组合输出的顶部加载,然后进入LessMinify。
如果您调试LessMinify并查看response.Content,您将看到组合的less输出!
希望这可以帮助。

这似乎没有起到任何帮助。我有0colors.less文件,并使用bundle.AddDirectory加载所有在同一个文件夹中的less文件。@import "0colors.less"引发相同的错误。 - Shane Courtrille
在 global.asax.cs 文件中,我有以下代码: DynamicFolderBundle lessFb = new DynamicFolderBundle("less", new LessMinify(), "*.less"); BundleTable.Bundles.Add(lessFb);然后使用路径 /static/less/admin/less(如上例)来获取相对位置。 - TheRealQuagmire

3

以下是我能想到的处理此问题的最简代码版本:

public class LessTransform : IBundleTransform
{
    public void Process(BundleContext context, BundleResponse bundle)
    {
        var pathResolver = new ImportedFilePathResolver(context.HttpContext.Server);
        var lessParser = new Parser();
        var lessEngine = new LessEngine(lessParser);
        (lessParser.Importer as Importer).FileReader = new FileReader(pathResolver);

        var content = new StringBuilder(bundle.Content.Length);
        foreach (var bundleFile in bundle.Files)
        {
            pathResolver.SetCurrentDirectory(bundleFile.IncludedVirtualPath);
            content.Append(lessEngine.TransformToCss((new StreamReader(bundleFile.VirtualFile.Open())).ReadToEnd(), bundleFile.IncludedVirtualPath));
            content.AppendLine();
        }

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

public class ImportedFilePathResolver : IPathResolver
{
    private HttpServerUtilityBase server { get; set; }
    private string currentDirectory { get; set; }

    public ImportedFilePathResolver(HttpServerUtilityBase server)
    {
        this.server = server;
    }

    public void SetCurrentDirectory(string fileLocation)
    {
        currentDirectory = Path.GetDirectoryName(fileLocation);
    }

    public string GetFullPath(string filePath)
    {
        var baseDirectory = server.MapPath(currentDirectory);
        return Path.GetFullPath(Path.Combine(baseDirectory, filePath));
    }
}

这段代码对我有效。无论如何,我在你的代码第14行做了一点改动:using (var stream = new StreamReader(bundleFile.VirtualFile.Open())) { content.Append(lessEngine.TransformToCss(stream.ReadToEnd(), bundleFile.IncludedVirtualPath)); };否则在加载页面后,我无法更改 .less 文件,因为“它正在被另一个进程使用”。 - Mirko Lugano

2

我所做的是:

添加了 Twitter Bootstrap Nuget 模块。

将此内容添加到我的 _Layout.cshtml 文件中:

<link href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Content/twitterbootstrap/less")" rel="stylesheet" type="text/css" />

请注意,我将我的“less”文件夹重命名为twitterbootstrap仅仅是为了展示我可以做到。将所有的less文件移动到一个名为“imports”的子文件夹中,除了bootstrap.less和(用于响应式设计)responsive.less。
~/Content/twitterbootstrap/imports

在web.config中添加了一个配置:

<add key="TwitterBootstrapLessImportsFolder" value="imports" />

创建了两个类(对上述类进行了轻微修改):

using System.Configuration;
using System.IO;
using System.Web.Optimization;
using dotless.Core;
using dotless.Core.configuration;
using dotless.Core.Input;

namespace TwitterBootstrapLessMinify
{
    public class TwitterBootstrapLessMinify : CssMinify
    {
        public static string BundlePath { get; private set; }

        public override void Process(BundleContext context, BundleResponse response)
        {
            setBasePath(context);

            var config = new DotlessConfiguration(dotless.Core.configuration.DotlessConfiguration.GetDefault());
            config.LessSource = typeof(TwitterBootstrapLessMinifyBundleFileReader);

            response.Content = Less.Parse(response.Content, config);
            base.Process(context, response);
        }

        private void setBasePath(BundleContext context)
        {
            var importsFolder = ConfigurationManager.AppSettings["TwitterBootstrapLessImportsFolder"] ?? "imports";
            var path = context.BundleVirtualPath;

            path = path.Remove(path.LastIndexOf("/") + 1);

            BundlePath = context.HttpContext.Server.MapPath(path + importsFolder + "/");
        }
    }

    public class TwitterBootstrapLessMinifyBundleFileReader : IFileReader
    {
        public IPathResolver PathResolver { get; set; }
        private string basePath;

        public TwitterBootstrapLessMinifyBundleFileReader() : this(new RelativePathResolver())
        {
        }

        public TwitterBootstrapLessMinifyBundleFileReader(IPathResolver pathResolver)
        {
            PathResolver = pathResolver;
            basePath = TwitterBootstrapLessMinify.BundlePath;
        }

        public bool DoesFileExist(string fileName)
        {
            fileName = PathResolver.GetFullPath(basePath + fileName);

            return File.Exists(fileName);
        }

        public string GetFileContents(string fileName)
        {
            fileName = PathResolver.GetFullPath(basePath + fileName);

            return File.ReadAllText(fileName);
        }
    }
}

我对IFileReader的实现查看了TwitterBootstrapLessMinify类的静态成员BundlePath。这使我们能够注入一个基础路径供导入使用。我本来想采用不同的方法(通过提供我的类的实例),但我不能这样做。

最后,我在Global.asax中添加了以下几行:

BundleTable.Bundles.EnableDefaultBundles();

var lessFB = new DynamicFolderBundle("less", new TwitterBootstrapLessMinify(), "*.less", false);
BundleTable.Bundles.Add(lessFB);

这有效地解决了导入时不知道从哪导入的问题。

1

接着RockResolve的建议,为了使用MicrosoftAjax压缩器,在web.config中将其作为默认CSS压缩器进行引用,而不是将其作为参数传递。

https://bundletransformer.codeplex.com/wikipage/?title=Bundle%20Transformer%201.7.0%20Beta%201#BundleTransformerMicrosoftAjax_Chapter开始

要使MicrosoftAjaxCssMinifier成为默认的CSS压缩器,以及MicrosoftAjaxJsMinifier成为默认的JS压缩器,您需要更改Web.config文件。在\configuration\bundleTransformer\core\css元素的defaultMinifier属性中,必须设置值等于MicrosoftAjaxCssMinifier,并且在\configuration\bundleTransformer\core\js元素的同一属性中设置为MicrosoftAjaxJsMinifier。


1
截至2013年2月: Michael Baird的优秀解决方案被Ben Cull贴文中提到的“BundleTransformer.Less Nuget Package”所取代。类似的答案在以下链接中: http://blog.cdeutsch.com/2012/08/using-less-and-twitter-bootstrap-in.html Cdeutsch的博客和awrigley的帖子添加了缩小,但显然现在不是正确的方法。
另一个使用相同解决方案的人从BundleTransformer作者那里得到了一些答案: http://geekswithblogs.net/ToStringTheory/archive/2012/11/30/who-could-ask-for-more-with-less-css-part-2.aspx。请查看底部的评论。
总之,使用BundleTransformer.MicrosoftAjax代替内置的缩小器。 例如, css.Transforms.Add(new CssMinify()); 替换为 css.Transforms.Add(new BundleTransformer.MicrosoftAjax());

-1

我曾经遇到过同样的问题,看到了同样的错误信息。在网上寻找解决方案后,我来到了这里。我的问题如下:

在一个 less 文件中,我有一个不正确的样式,在某个时候给了我一个警告。这个 less 文件无法被解析。通过删除不正确的行,我摆脱了错误信息。

希望这能帮助到某些人。


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