【问题标题】:ASP.NET Core's asp-append-version attribute not working for static files outside of the wwwroot directoryASP.NET Core asp-append-version 属性不适用于 wwwroot 目录之外的静态文件
【发布时间】:2017-03-13 23:21:06
【问题描述】:

我有一个 ASP.NET Core 项目,其中包含 wwwroot 目录和 bower_components 目录中的静态文件。

我可以通过将其添加到我的Startup.cs 类来提供这些文件:

StaticFileOptions rootFileOptions = new StaticFileOptions();
rootFileOptions.OnPrepareResponse = staticFilesResponseHandler;
StaticFileOptions bowerFileOptions = new StaticFileOptions();
bowerFileOptions.OnPrepareResponse = staticFilesResponseHandler;
string bowerDirectory = Path.Combine(Directory.GetCurrentDirectory(), "bower_components");
PhysicalFileProvider bowerPhysicalFileProvider = new PhysicalFileProvider(bowerDirectory);
bowerFileOptions.FileProvider = bowerPhysicalFileProvider;
bowerFileOptions.RequestPath = new PathString("/bower");
app.UseStaticFiles(rootFileOptions);
app.UseStaticFiles(bowerFileOptions);

然后从我的观点中引用它们如下:

<script type="text/javascript" src="/bower/jquery/dist/jquery.min.js" asp-append-version="true"></script>
<script type="text/javascript" src="/Libs/jQuery-UI/jquery-ui.min.js" asp-append-version="true"></script>

尽管asp-append-version 似乎对位于wwwroot 下的资源工作得很好,但对于wwwroot 之外的资源,它似乎完全被忽略了。不过,所有资源都得到了适当的服务;没有404或任何东西。上述代码生成的 HTML 如下:

<script type="text/javascript" src="/bower/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="/Libs/jQuery-UI/jquery-ui.min.js?v=YZKMNaPD9FY0wb12QiluqhIOWFhZXnjgiRJoxErwvwI"></script>

我做错了什么?

【问题讨论】:

标签: c# asp.net asp.net-core wwwroot asp-append-version


【解决方案1】:

这个问题很老,但问题仍然存在,虽然 Artak 提供的解决方案有效,但在大多数情况下它在概念上是不正确的。首先让我们看看问题的根源:

asp-append-version 使用IHostingEnvironment.WebRootFileProvider 查找文件,默认情况下PhysicalFileProvider 指向wwwroot 文件夹。

核心文档有一个关于如何serve files outside of web root 的示例:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles(); // For the wwwroot folder

    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(Directory.GetCurrentDirectory(), "MyStaticFiles")),
        RequestPath = "/StaticFiles"
    });
}

这允许您从wwwrootMyStaticFiles 文件夹中提供静态文件。如果你有图片\MyStaticFiles\pic1.jpg,可以通过两种方式参考:

<img src="~/pic1.jpg" />
<img src="~/StaticFiles/pic1.jpg" />

两者都将同样有效。这在概念上是不正确的,因为您为路径指定了别名/StaticFiles,因此它的文件不应与根/ 组合。但至少它有效,它给你你想要的。

遗憾的是,asp-append-version 并不知道这一切。它应该,但它没有。应该是因为它旨在与静态文件(JavaScript、CSS 和图像)一起使用,所以如果我们更改配置以提供来自不同文件夹的静态文件,asp-append-version 会获得这些配置的副本。没有,所以我们需要单独配置,修改IHostingEnvironment.WebRootFileProvider

Artak 建议使用CompositeFileProvider,它允许我们将多个文件提供程序分配给IHostingEnvironment.WebRootFileProvider。这确实有效,但它有一个基本问题。 CompositeFileProvider 不允许我们像在 StaticFileOptions 中那样定义 RequestPath。作为一种解决方法,Artak 建议我们不应该使用前缀,这利用了上述错误行为,即文件可以以两种方式引用。为了演示这个问题,假设另一个文件夹的结构如下:

|_ MyStaticFiles
       |_ HTML
       | |_ 隐私.html
       | |_ 常见问题.html
       |_ 图片
         |_ 图像1.jpg

现在,MyStaticFiles\images 文件夹中的所有文件会发生什么变化?假设 wwwroot 也有 images 文件夹,对于两个同名文件夹,它会起作用还是给你一个错误?文件~/images/image1.jpg 来自哪里?

无论它是否有效,通常有一个重要原因导致您将静态文件放在wwwroot 以外的文件夹中。这通常是因为这些静态文件是您不希望与网站设计文件混合的例如内容文件。

我们需要一个允许我们为每个文件夹指定RequestPath 的提供程序。由于 Core 目前没有这样的提供者,我们只能选择自己编写。虽然不难,但这不是许多程序员喜欢处理的任务。这是一个快速实现,它并不完美,但它可以完成工作。它基于example provided by Marius Zkochanowski 并进行了一些改进:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Extensions.FileProviders {
  class CompositeFileWithOptionsProvider : IFileProvider {
    private readonly IFileProvider _webRootFileProvider;
    private readonly IEnumerable<StaticFileOptions> _staticFileOptions;

    public CompositeFileWithOptionsProvider(IFileProvider webRootFileProvider, params StaticFileOptions[] staticFileOptions)
  : this(webRootFileProvider, (IEnumerable<StaticFileOptions>)staticFileOptions) { }

    public CompositeFileWithOptionsProvider(IFileProvider webRootFileProvider, IEnumerable<StaticFileOptions> staticFileOptions) {
      _webRootFileProvider = webRootFileProvider ?? throw new ArgumentNullException(nameof(webRootFileProvider));
      _staticFileOptions = staticFileOptions;
    }

    public IDirectoryContents GetDirectoryContents(string subpath) {
      var provider = GetFileProvider(subpath, out string outpath);
      return provider.GetDirectoryContents(outpath);
    }

    public IFileInfo GetFileInfo(string subpath) {
      var provider = GetFileProvider(subpath, out string outpath);
      return provider.GetFileInfo(outpath);
    }

    public IChangeToken Watch(string filter) {
      var provider = GetFileProvider(filter, out string outpath);
      return provider.Watch(outpath);
    }

    private IFileProvider GetFileProvider(string path, out string outpath) {
      outpath = path;
      var fileProviders = _staticFileOptions;
      if (fileProviders != null) {
        foreach (var item in fileProviders) {
          if (path.StartsWith(item.RequestPath, StringComparison.Ordinal)) {
            outpath = path.Substring(item.RequestPath.Value.Length, path.Length - item.RequestPath.Value.Length);
            return item.FileProvider;
          }
        }
      }
      return _webRootFileProvider;
    }
  }
}

现在我们可以更新 Artak 的示例以使用新的提供程序:

app.UseStaticFiles(); //For the wwwroot folder.
//This serves static files from the given folder similar to IIS virtual directory.
var options = new StaticFileOptions {
  FileProvider = new PhysicalFileProvider(Configuration.GetValue<string>("ContentPath")),
  RequestPath = "/Content"
};
//This is required for asp-append-version (it needs to know where to find the file to hash it).
env.WebRootFileProvider = new CompositeFileWithOptionsProvider(env.WebRootFileProvider, options);
app.UseStaticFiles(options); //For any folders other than wwwroot.

在这里,我从配置文件中获取路径,因为它通常甚至完全位于应用程序文件夹之外。现在您可以使用/Content 而不是~/ 来引用您的内容文件。示例:

<img src="~/Content/images/pic1.jpg" asp-append-version="true" />

【讨论】:

    【解决方案2】:

    在引用jquery/dist/jquery.min.js 时,请考虑删除/bower/ 前缀,如下所示:

    <script type="text/javascript" src="~/jquery/dist/jquery.min.js"></script>
    

    另外,您应该在Startup.Configure 方法中设置HostingEnvironment.WebRootFileProvider,如下所示:

    var compositeProvider = new CompositeFileProvider(
        env.WebRootFileProvider,
        new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "bower_components")));
    env.WebRootFileProvider = compositeProvider;
    var options = new StaticFileOptions()
    {
        FileProvider = compositeProvider,
        RequestPath = "/bower"
    };
    app.UseStaticFiles(options);
    

    希望这会有所帮助。

    【讨论】:

    • Artak,这个解决方案使它工作,但它并不理想。将不同的文件夹与wwwroot 组合在一起并假装所有静态文件都来自根目录在概念上是不正确的。在大多数情况下,正确的做法是像 IIS 那样使用虚拟目录,将它们添加到根目录。因此,来自wwwroot 的文件将从/ 提供,来自其他文件夹的文件(在 OP 情况下为 bower_components)将从根下的指定名称(在 OP 情况下为/bower)提供。
    • CompositeFileProvider 不允许这样做,因此应添加另一个接受 StaticFileOptions (或类似)的提供程序。我知道实现这样的提供程序相对容易,但我认为它是核心的基本功能。人们不必删除前缀(在 OP 情况下为/bower)即可使其正常工作。
    【解决方案3】:

    我做错了什么?

    什么都没有。 According to ASP.NET Core's source,他们为此任务创建了一个从WebRootPathwwwroot 开始的FileVersionProvider

    private void EnsureFileVersionProvider()
    {
        if (_fileVersionProvider == null)
        {
            _fileVersionProvider = new FileVersionProvider(
                HostingEnvironment.WebRootFileProvider,
                Cache,
                ViewContext.HttpContext.Request.PathBase);
        }
    }
    

    【讨论】:

    • 查看代码似乎与 FileVersionProvider 相关的所有内容都是私有的。我对标签助手了解不多:有没有办法继承和扩展原始标签助手?或者以某种方式扩展这种特定行为?谢谢
    • 我认为这是一个非常好的行为。因为它迫使您捆绑静态文件,然后将版本应用于该单个文件。您不应直接引用 bowernode_js 文件夹。使用github.com/madskristensen/BundlerMinifier 完成此任务。
    • 我同意您提供的示例,但总的来说我不同意。我们有一个单独的 angular-cli 客户端项目(对于任何基于 webpack 的客户端项目都相同),它编译并将其自己的源代码捆绑到经典的dist 文件夹中。我们不可能使用 BundleMinifier,而且我不确定我们应该将客户端构建输出保存到 asp.net wwwroot 文件夹中。
    • 为什么不呢?将.angular-cli.json 中的outDir 设置为wwwroot,然后使用此解决方案:michaco.net/blog/…
    • 是的,我知道你可以。我只是不确定我们应该这样做。我会考虑的。不过,感谢您指出该帖子。
    猜你喜欢
    • 2016-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-31
    • 1970-01-01
    • 1970-01-01
    • 2017-03-15
    • 2020-09-07
    相关资源
    最近更新 更多