15

バックグラウンド

まず背景を説明します。私は、現在 IIS でホストされている OWIN 経由で構成された Web API を使用するバックエンド サーバーを、将来的には他の OWIN サポート ホストと組み合わせて、AngularJS を使用するフロントエンドに結合しようとするプロジェクトに取り組んでいます。

AngularJS フロントエンドは完全に静的コンテンツです。MVC/Razor、WebForms、Bundles などのサーバー側のテクノロジ、およびフロントエンドとそれが使用するアセットに関係するものはすべて回避し、代わりに Node.js、Grunt/Gulp などを使用した最新かつ最高の手法を採用します。 . CSS のコンパイル、バンドル、ミニフィケーションなどを処理します。ここでは説明しない理由により、フロントエンド プロジェクトとサーバー プロジェクトを同じプロジェクト内の別々の場所に保持します (ホスト プロジェクトにすべてを直接貼り付けるのではなく (crud を参照)下図)。

MyProject.sln
server
  MyProject.Host
     MyProject.Host.csproj
     Startup.cs
     (etc.)
frontend
  MyProjectApp
     app.js
     index.html
     MyProjectApp.njproj
     (etc.)

したがって、フロントエンドに関する限り、私がする必要があるのは、ホストに静的コンテンツを提供させることだけです。Express.js では、これは簡単です。OWIN では、Microsoft.Owin.StaticFilesミドルウェアを使用してこれを簡単に行うことができました。うまく機能します (非常に滑らかです)。

これが私のOwinStartup構成です:

string dir = AppDomain.CurrentDomain.RelativeSearchPath; // get executing path
string contentPath = Path.GetFullPath(Path.Combine(dir, @"../../../frontend/MyProjectApp")); // resolve nearby frontend project directory

app.UseFileServer(new FileServerOptions
{
    EnableDefaultFiles = true,
    FileSystem = new PhysicalFileSystem(contentPath),
    RequestPath = new PathString(string.Empty) // starts at the root of the host
});

// ensure the above occur before map handler to prevent native static content handler
app.UseStageMarker(PipelineStage.MapHandler);

キャッチ

基本的に、frontend/MyProjectAppMyProject.Host のルート内にあるかのようにすべてをホストします。当然のことながら、存在しないファイルを要求すると、IIS は 404 エラーを生成します。

これは AngularJS アプリであり、html5mode. index.htmlユーザーが AngularJS (この例では物理的に存在するファイル以外のもの) にドロップすると、そのルートが AngularJS アプリで有効な場合でも、404 が返されます。index.htmlしたがって、要求されたファイルが存在しない場合にファイルを返すために OWIN ミドルウェアが必要であり、それが本当に 404 であるかどうかを AngularJS アプリに判断させます。

SPA と AngularJS に精通している場合、これは通常の簡単な方法です。MVC または ASP.NET ルーティングを使用している場合は、 my を返す MVC コントローラーへのデフォルト ルートを設定するだけで済みますindex.html。ただし、私は MVC を使用していないと既に述べており、これを可能な限りシンプルかつ軽量に保つように努めています。

このユーザーは同様のジレンマを抱えていて、IIS の書き換えで解決しました。私の場合、それは機能しません。なぜなら、a) 書き換え URL モジュールが見つけられる場所に私のコンテンツが物理的に存在しないため、常に返さindex.htmlれ、b) IIS に依存しないが内部で処理されるものが欲しいからです。フレキシブルに使えるOWINミドルウェア。

TL;DNR 私、大声で泣いてしまいました。

簡単に、どうすれば 404 Not Found をインターセプトし、OWIN ミドルウェアを使用して (注:リダイレクトしないで) my FileServer-servedのコンテンツを返すことができindex.htmlますか?

4

4 に答える 4

15

OWIN を使用している場合は、これを使用できるはずです。

using AppFunc = Func<
       IDictionary<string, object>, // Environment
       Task>; // Done

public static class AngularServerExtension
{
    public static IAppBuilder UseAngularServer(this IAppBuilder builder, string rootPath, string entryPath)
    {
        var options = new AngularServerOptions()
        {
            FileServerOptions = new FileServerOptions()
            {
                EnableDirectoryBrowsing = false,
                FileSystem = new PhysicalFileSystem(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath))
            },
            EntryPath = new PathString(entryPath)
        };

        builder.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions);

        return builder.Use(new Func<AppFunc, AppFunc>(next => new AngularServerMiddleware(next, options).Invoke));    
    }
}

public class AngularServerOptions
{
    public FileServerOptions FileServerOptions { get; set; }

    public PathString EntryPath { get; set; }

    public bool Html5Mode
    {
        get
        {
            return EntryPath.HasValue;
        }
    }

    public AngularServerOptions()
    {
        FileServerOptions = new FileServerOptions();
        EntryPath = PathString.Empty;
    }
}

public class AngularServerMiddleware
{
    private readonly AngularServerOptions _options;
    private readonly AppFunc _next;
    private readonly StaticFileMiddleware _innerMiddleware;

    public AngularServerMiddleware(AppFunc next, AngularServerOptions options)
    {
        _next = next;
        _options = options;

        _innerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions);
    }

    public async Task Invoke(IDictionary<string, object> arg)
    {
        await _innerMiddleware.Invoke(arg);
        // route to root path if the status code is 404
        // and need support angular html5mode
        if ((int)arg["owin.ResponseStatusCode"] == 404 && _options.Html5Mode)
        {
            arg["owin.RequestPath"] = _options.EntryPath.Value;
            await _innerMiddleware.Invoke(arg);
        }
    }
}
于 2015-06-09T19:47:02.913 に答える
2

私はこの小さなミドルウェア コンポーネントを作成しましたが、やり過ぎなのか、非効率なのか、それとも他の落とし穴があるのか​​はわかりません。基本的には同じ用途を取り入れているだけでFileServerOptionsFileServerMiddleware最も重要な部分はFileSystem私たちが使用しているものです。前述のミドルウェアの前に配置され、要求されたパスが存在するかどうかをすばやく確認します。そうでない場合、リクエストパスは「index.html」に書き換えられ、そこから通常の StaticFileMiddleware が引き継ぎます。

明らかに、異なるルート パスに対して異なるデフォルト ファイルを定義する方法を含め、再利用のためにクリーンアップされる可能性があります。 feature2" および "/feature2/default.html" など)。

しかし、今のところ、これは私にとってはうまくいきます。これは、明らかに Microsoft.Owin.StaticFiles に依存しています。

public class DefaultFileRewriterMiddleware : OwinMiddleware
{
    private readonly FileServerOptions _options;

    /// <summary>
    /// Instantiates the middleware with an optional pointer to the next component.
    /// </summary>
    /// <param name="next"/>
    /// <param name="options"></param>
    public DefaultFileRewriterMiddleware(OwinMiddleware next, FileServerOptions options) : base(next)
    {
        _options = options;
    }

    #region Overrides of OwinMiddleware

    /// <summary>
    /// Process an individual request.
    /// </summary>
    /// <param name="context"/>
    /// <returns/>
    public override async Task Invoke(IOwinContext context)
    {
        IFileInfo fileInfo;
        PathString subpath;

        if (!TryMatchPath(context, _options.RequestPath, false, out subpath) ||
            !_options.FileSystem.TryGetFileInfo(subpath.Value, out fileInfo))
        {
            context.Request.Path = new PathString(_options.RequestPath + "/index.html");
        }

        await Next.Invoke(context);
    }

    #endregion

    internal static bool PathEndsInSlash(PathString path)
    {
        return path.Value.EndsWith("/", StringComparison.Ordinal);
    }

    internal static bool TryMatchPath(IOwinContext context, PathString matchUrl, bool forDirectory, out PathString subpath)
    {
        var path = context.Request.Path;

        if (forDirectory && !PathEndsInSlash(path))
        {
            path += new PathString("/");
        }

        if (path.StartsWithSegments(matchUrl, out subpath))
        {
            return true;
        }
        return false;
    }
}
于 2014-11-20T10:54:07.627 に答える
1

ここでのハビエル・フィゲロア による回答は機能し、本当に役に立ちます! ありがとうございます!ただし、奇妙な動作が 1 つあります。何も存在しない場合 (エントリ ファイルを含む)、パイプラインを 2 回実行します。たとえば、以下のテストは、その実装を適用すると失敗します。nextUseHtml5Mode

[Test]
public async Task ShouldRunNextMiddlewareOnceWhenNothingExists()
{
    // ARRANGE
    int hitCount = 0;
    var server = TestServer.Create(app =>
    {
        app.UseHtml5Mode("test-resources", "/does-not-exist.html");
        app.UseCountingMiddleware(() => { hitCount++; });
    });

    using (server)
    {
        // ACT
        await server.HttpClient.GetAsync("/does-not-exist.html");

        // ASSERT
        Assert.AreEqual(1, hitCount);
    }
}

誰かが興味を持っている場合、私の上記のテストに関するいくつかのメモ:

上記のテストに合格するために行った実装は次のとおりです。

namespace Foo 
{
    using AppFunc = Func<IDictionary<string, object>, Task>;

    public class Html5ModeMiddleware
    {
        private readonly Html5ModeOptions m_Options;
        private readonly StaticFileMiddleware m_InnerMiddleware;
        private readonly StaticFileMiddleware m_EntryPointAwareInnerMiddleware;

        public Html5ModeMiddleware(AppFunc next, Html5ModeOptions options)
        {
            if (next == null) throw new ArgumentNullException(nameof(next));
            if (options == null) throw new ArgumentNullException(nameof(options));

            m_Options = options;
            m_InnerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions);
            m_EntryPointAwareInnerMiddleware = new StaticFileMiddleware((environment) =>
            {
                var context = new OwinContext(environment);
                context.Request.Path = m_Options.EntryPath;
                return m_InnerMiddleware.Invoke(environment);

            }, options.FileServerOptions.StaticFileOptions);
        }

        public Task Invoke(IDictionary<string, object> environment) => 
            m_EntryPointAwareInnerMiddleware.Invoke(environment);
    }
}

拡張子はかなり似ています:

namespace Owin
{
    using AppFunc = Func<IDictionary<string, object>, Task>;

    public static class AppBuilderExtensions
    {
        public static IAppBuilder UseHtml5Mode(this IAppBuilder app, string rootPath, string entryPath)
        {
            if (app == null) throw new ArgumentNullException(nameof(app));
            if (rootPath == null) throw new ArgumentNullException(nameof(rootPath));
            if (entryPath == null) throw new ArgumentNullException(nameof(entryPath));

            var options = new Html5ModeOptions
            {
                EntryPath = new PathString(entryPath),
                FileServerOptions = new FileServerOptions()
                {
                    EnableDirectoryBrowsing = false,
                    FileSystem = new PhysicalFileSystem(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath))
                }
            };

            app.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions);

            return app.Use(new Func<AppFunc, AppFunc>(next => new Html5ModeMiddleware(next, options).Invoke));
        }
    }
}
于 2016-08-11T13:47:23.157 に答える