IdentityServer里有各种Endpoint,如TokenEndpoint,UserInfoEndpoint,Authorize Endpoint,Discovery Endpoint等等。Endpoint从字面意思来看是“终端节点"或者“终节点”的意思。无独有偶NetCore的路由也有Endpoint的概念。那么我们提出一个问题来,究竟什么是Endpoint?

先来看一段代码,如下所示:

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
} public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureServices(svcs => svcs.AddRouting()).Configure( app => app.UseRouting().UseEndpoints(
//test1
//endpoints => endpoints.MapGet("weather/{city}/{days}", //test2
endpoints =>endpoints.MapGet("weather/{city}/{year}.{month}.{day}", WeatherForecast) ));
}); private static Dictionary<string, string> _cities = new Dictionary<string, string>() {
["001"]="Beijing",
["028"]="Chengdu",
["091"]="Suzhou" }; //RequestDelegate
public static async Task WeatherForecast(HttpContext context)
{
var city = (string)context.GetRouteData().Values["city"];
city = _cities[city]; //test1
//int days=int.Parse(context.GetRouteData().Values["days"].ToString());
//var report = new WeatherReport(city, days); //test2
int year = int.Parse(context.GetRouteData().Values["year"].ToString());
int month= int.Parse(context.GetRouteData().Values["month"].ToString());
int day=int.Parse(context.GetRouteData().Values["day"].ToString());
var report = new WeatherReport(city, new DateTime(year, month, day));
await RenderWeatherAsync(context, report);
} private static async Task RenderWeatherAsync(HttpContext context,WeatherReport report)
{
context.Response.ContentType = "text/html;charset=utf-8";
await context.Response.WriteAsync("<html><head><title>Weather</title></head><body>");
await context.Response.WriteAsync($"<h3>{report.City}</h3>");
foreach(var it in report.WeatherInfos)
{
await context.Response.WriteAsync($"{it.Key.ToString("yyyy-MM-dd")}: ");
await context.Response.WriteAsync($"{it.Value.LowTemperature}--{it.Value.HighTemperature} <br/>");
}
await context.Response.WriteAsync("</body></html>");
}
}

上面的代码很清晰,如果路由格式匹配”weather/{city}/{year}.{month}.{day}”,那么就调用WeatherForecast方法,这里的app.UseRouting().UseEndpoints就用到了Endpoint模式。要了解Endpoint,我们需要抽丝剥茧一步一步了解几个类的方法:

1.我们先看最里面的endpoints =>endpoints.MapGet("weather/{city}/{year}.{month}.{day}", WeatherForecast),这里实际上调用的是EndpointRouteBuilderExtensions类的MapGet方法,代码如下所示:

    /// <summary>
/// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add endpoints.
/// </summary>
public static class EndpointRouteBuilderExtensions
{
// Avoid creating a new array every call
private static readonly string[] GetVerb = new[] { "GET" };
private static readonly string[] PostVerb = new[] { "POST" };
private static readonly string[] PutVerb = new[] { "PUT" };
private static readonly string[] DeleteVerb = new[] { "DELETE" };
private static readonly string[] PatchVerb = new[] { "PATCH" };
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapGet(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, GetVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP POST requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapPost(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, PostVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PUT requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapPut(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, PutVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP DELETE requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapDelete(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, DeleteVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PATCH requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapPatch(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return MapMethods(endpoints, pattern, PatchVerb, requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified HTTP methods and pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <param name="httpMethods">HTTP methods that the endpoint will match.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapMethods(
this IEndpointRouteBuilder endpoints,
string pattern,
IEnumerable<string> httpMethods,
RequestDelegate requestDelegate)
{
if (httpMethods == null)
{
throw new ArgumentNullException(nameof(httpMethods));
} var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate);
builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}");
builder.WithMetadata(new HttpMethodMetadata(httpMethods));
return builder;
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
string pattern,
RequestDelegate requestDelegate)
{
return Map(endpoints, RoutePatternFactory.Parse(pattern), requestDelegate);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
RequestDelegate requestDelegate)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
} if (requestDelegate == null)
{
throw new ArgumentNullException(nameof(requestDelegate));
} const int defaultOrder = 0; //实例化RouteEndpointBuilder
var builder = new RouteEndpointBuilder(
requestDelegate,
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
}; // Add delegate attributes as metadata
var attributes = requestDelegate.Method.GetCustomAttributes(); // This can be null if the delegate is a dynamic method or compiled from an expression tree
if (attributes != null)
{
foreach (var attribute in attributes)
{
builder.Metadata.Add(attribute);
}
} //Adds a RouteEndpoint to the IEndpointRouteBuilder that matches HTTP requests
/// for the specified pattern.
///可以理解为:将某种格式的路由pattern添加至IEndpointRouteBuilder的DataSource属性,为match做准备
var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource == null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
} return dataSource.AddEndpointBuilder(builder);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapGet(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, GetVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP POST requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapPost(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, PostVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PUT requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapPut(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, PutVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP DELETE requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapDelete(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, DeleteVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP PATCH requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The <see cref="Delegate" /> executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapPatch(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return MapMethods(endpoints, pattern, PatchVerb, handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified HTTP methods and pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <param name="httpMethods">HTTP methods that the endpoint will match.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder MapMethods(
this IEndpointRouteBuilder endpoints,
string pattern,
IEnumerable<string> httpMethods,
Delegate handler)
{
if (httpMethods is null)
{
throw new ArgumentNullException(nameof(httpMethods));
} var disableInferredBody = false;
foreach (var method in httpMethods)
{
disableInferredBody = ShouldDisableInferredBody(method);
if (disableInferredBody is true)
{
break;
}
} var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), handler, disableInferredBody);
// Prepends the HTTP method to the DisplayName produced with pattern + method name
builder.Add(b => b.DisplayName = $"HTTP: {string.Join(", ", httpMethods)} {b.DisplayName}");
builder.WithMetadata(new HttpMethodMetadata(httpMethods));
return builder; static bool ShouldDisableInferredBody(string method)
{
// GET, DELETE, HEAD, CONNECT, TRACE, and OPTIONS normally do not contain bodies
return method.Equals(HttpMethods.Get, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Delete, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Head, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Options, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Trace, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Connect, StringComparison.Ordinal);
}
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder Map(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return Map(endpoints, RoutePatternFactory.Parse(pattern), handler);
} /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
Delegate handler)
{
return Map(endpoints, pattern, handler, disableInferBodyFromParameters: false);
} /// <summary>
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
/// requests for non-file-names with the lowest possible priority.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
/// <remarks>
/// <para>
/// <see cref="MapFallback(IEndpointRouteBuilder, Delegate)"/> is intended to handle cases where URL path of
/// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing
/// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to
/// result in an HTTP 404.
/// </para>
/// <para>
/// <see cref="MapFallback(IEndpointRouteBuilder, Delegate)"/> registers an endpoint using the pattern
/// <c>{*path:nonfile}</c>. The order of the registered endpoint will be <c>int.MaxValue</c>.
/// </para>
/// </remarks>
public static RouteHandlerBuilder MapFallback(this IEndpointRouteBuilder endpoints, Delegate handler)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
} return endpoints.MapFallback("{*path:nonfile}", handler);
} /// <summary>
/// Adds a specialized <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that will match
/// the provided pattern with the lowest possible priority.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="handler">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
/// <remarks>
/// <para>
/// <see cref="MapFallback(IEndpointRouteBuilder, string, Delegate)"/> is intended to handle cases where no
/// other endpoint has matched. This is convenient for routing requests to a SPA framework.
/// </para>
/// <para>
/// The order of the registered endpoint will be <c>int.MaxValue</c>.
/// </para>
/// <para>
/// This overload will use the provided <paramref name="pattern"/> verbatim. Use the <c>:nonfile</c> route constraint
/// to exclude requests for static files.
/// </para>
/// </remarks>
public static RouteHandlerBuilder MapFallback(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
} if (handler == null)
{
throw new ArgumentNullException(nameof(handler));
} var conventionBuilder = endpoints.Map(pattern, handler);
conventionBuilder.WithDisplayName("Fallback " + pattern);
conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue);
return conventionBuilder;
} private static RouteHandlerBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
Delegate handler,
bool disableInferBodyFromParameters)
{
if (endpoints is null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (pattern is null)
{
throw new ArgumentNullException(nameof(pattern));
} if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
} const int defaultOrder = 0; var routeParams = new List<string>(pattern.Parameters.Count);
foreach (var part in pattern.Parameters)
{
routeParams.Add(part.Name);
} var routeHandlerOptions = endpoints.ServiceProvider?.GetService<IOptions<RouteHandlerOptions>>(); var options = new RequestDelegateFactoryOptions
{
ServiceProvider = endpoints.ServiceProvider,
RouteParameterNames = routeParams,
ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false,
DisableInferBodyFromParameters = disableInferBodyFromParameters,
}; var requestDelegateResult = RequestDelegateFactory.Create(handler, options); var builder = new RouteEndpointBuilder(
requestDelegateResult.RequestDelegate,
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
}; // REVIEW: Should we add an IActionMethodMetadata with just MethodInfo on it so we are
// explicit about the MethodInfo representing the "handler" and not the RequestDelegate? // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint.
builder.Metadata.Add(handler.Method); // Methods defined in a top-level program are generated as statics so the delegate
// target will be null. Inline lambdas are compiler generated method so they can
// be filtered that way.
if (GeneratedNameParser.TryParseLocalFunctionName(handler.Method.Name, out var endpointName)
|| !TypeHelper.IsCompilerGeneratedMethod(handler.Method))
{
endpointName ??= handler.Method.Name;
builder.DisplayName = $"{builder.DisplayName} => {endpointName}";
} // Add delegate attributes as metadata
var attributes = handler.Method.GetCustomAttributes(); // Add add request delegate metadata
foreach (var metadata in requestDelegateResult.EndpointMetadata)
{
builder.Metadata.Add(metadata);
} // This can be null if the delegate is a dynamic method or compiled from an expression tree
if (attributes is not null)
{
foreach (var attribute in attributes)
{
builder.Metadata.Add(attribute);
}
} var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource is null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
} return new RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder));
}
}
}

在这里我们重点看看下面签名的方法:

 /// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP requests
/// for the specified pattern.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestDelegate">The delegate executed when the endpoint is matched.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder Map(
this IEndpointRouteBuilder endpoints,
RoutePattern pattern,
RequestDelegate requestDelegate)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
} if (pattern == null)
{
throw new ArgumentNullException(nameof(pattern));
} if (requestDelegate == null)
{
throw new ArgumentNullException(nameof(requestDelegate));
} const int defaultOrder = 0; //实例化RouteEndpointBuilder
var builder = new RouteEndpointBuilder(
requestDelegate,
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
}; // Add delegate attributes as metadata
var attributes = requestDelegate.Method.GetCustomAttributes(); // This can be null if the delegate is a dynamic method or compiled from an expression tree
if (attributes != null)
{
foreach (var attribute in attributes)
{
builder.Metadata.Add(attribute);
}
} //Adds a RouteEndpoint to the IEndpointRouteBuilder that matches HTTP requests
/// for the specified pattern.
///可以理解为:将某种格式的路由pattern添加至IEndpointRouteBuilder的DataSource属性,为match做准备
var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource == null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
} return dataSource.AddEndpointBuilder(builder);
}

在这个方法里有一个实例化RouteEndpointBuilder的语句:

  var builder = new RouteEndpointBuilder(
requestDelegate,
pattern,
defaultOrder)
{
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
};

其中RouteEndpointBuilder从字面意思看得出,就是RouteEndpoint的创建者,我们看下它的代码:

 public sealed class RouteEndpointBuilder : EndpointBuilder
{
/// <summary>
/// Gets or sets the <see cref="RoutePattern"/> associated with this endpoint.
/// </summary>
public RoutePattern RoutePattern { get; set; } /// <summary>
/// Gets or sets the order assigned to the endpoint.
/// </summary>
public int Order { get; set; } /// <summary>
/// Constructs a new <see cref="RouteEndpointBuilder"/> instance.
/// </summary>
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
/// <param name="routePattern">The <see cref="RoutePattern"/> to use in URL matching.</param>
/// <param name="order">The order assigned to the endpoint.</param>
public RouteEndpointBuilder(
RequestDelegate requestDelegate,
RoutePattern routePattern,
int order)
{
RequestDelegate = requestDelegate;
RoutePattern = routePattern;
Order = order;
} //生成Route
/// <inheritdoc />
public override Endpoint Build()
{
if (RequestDelegate is null)
{
throw new InvalidOperationException($"{nameof(RequestDelegate)} must be specified to construct a {nameof(RouteEndpoint)}.");
} var routeEndpoint = new RouteEndpoint(
RequestDelegate,
RoutePattern,
Order,
new EndpointMetadataCollection(Metadata),
DisplayName); return routeEndpoint;
}
}

再来看下Endpoint的代码:

   /// <summary>
/// Represents a logical endpoint in an application.
/// </summary>
public class Endpoint
{
/// <summary>
/// Creates a new instance of <see cref="Endpoint"/>.
/// </summary>
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
/// <param name="metadata">
/// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
/// </param>
/// <param name="displayName">
/// The informational display name of the endpoint. May be null.
/// </param>
public Endpoint(
RequestDelegate? requestDelegate,
EndpointMetadataCollection? metadata,
string? displayName)
{
// All are allowed to be null
RequestDelegate = requestDelegate;
Metadata = metadata ?? EndpointMetadataCollection.Empty;
DisplayName = displayName;
} /// <summary>
/// Gets the informational display name of this endpoint.
/// </summary>
public string? DisplayName { get; } /// <summary>
/// Gets the collection of metadata associated with this endpoint.
/// </summary>
public EndpointMetadataCollection Metadata { get; } /// <summary>
/// Gets the delegate used to process requests for the endpoint.
/// </summary>
public RequestDelegate? RequestDelegate { get; } /// <summary>
/// Returns a string representation of the endpoint.
/// </summary>
public override string? ToString() => DisplayName ?? base.ToString();
}

从Build()方法看得出,它利用路由匹配的RoutePattern,生成一个RouteEndpoint实例。RouteEndpoint叫做路由终点,继承自Endpoint。Endpoint有一个重要的RequestDelegate属性,用来处理当前的请求。看到这里,我们开始推断:所谓的Endpoint,无非就是用要匹配的pattern,构造一个RouteEndpoint的过程,其中RouteEndpoint继承自Endpoint。NetCore无非就是利用这个RouteEndpoint来匹配当前的Url,如果匹配得上,就执行RequestDelegate所代表的方法,上文就是WeatherForecast方法,如果匹配不上,则不执行WeatherForecast方法,仅此而已。

2.为了验证,我们再来看 app.UseRouting().UseEndpoints(),实际上调用的是EndpointRoutingApplicationBuilderExtensions类的两个方法:

   public static class EndpointRoutingApplicationBuilderExtensions
{
private const string EndpointRouteBuilder = "__EndpointRouteBuilder";
private const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; /// <summary>
/// Adds a <see cref="EndpointRoutingMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/>.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <remarks>
/// <para>
/// A call to <see cref="UseRouting(IApplicationBuilder)"/> must be followed by a call to
/// <see cref="UseEndpoints(IApplicationBuilder, Action{IEndpointRouteBuilder})"/> for the same <see cref="IApplicationBuilder"/>
/// instance.
/// </para>
/// <para>
/// The <see cref="EndpointRoutingMiddleware"/> defines a point in the middleware pipeline where routing decisions are
/// made, and an <see cref="Endpoint"/> is associated with the <see cref="HttpContext"/>. The <see cref="EndpointMiddleware"/>
/// defines a point in the middleware pipeline where the current <see cref="Endpoint"/> is executed. Middleware between
/// the <see cref="EndpointRoutingMiddleware"/> and <see cref="EndpointMiddleware"/> may observe or change the
/// <see cref="Endpoint"/> associated with the <see cref="HttpContext"/>.
/// </para>
/// </remarks>
public static IApplicationBuilder UseRouting(this IApplicationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
} VerifyRoutingServicesAreRegistered(builder); IEndpointRouteBuilder endpointRouteBuilder;
if (builder.Properties.TryGetValue(GlobalEndpointRouteBuilderKey, out var obj))
{
endpointRouteBuilder = (IEndpointRouteBuilder)obj!;
// Let interested parties know if UseRouting() was called while a global route builder was set
builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
}
else
{
endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder);
builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
} //先交由EndpointRoutingMiddleware中间件匹配Endpoint,然后交由下面的EndpointMiddleware处理
return builder.UseMiddleware<EndpointRoutingMiddleware>(endpointRouteBuilder);
} /// <summary>
/// Adds a <see cref="EndpointMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/>
/// with the <see cref="EndpointDataSource"/> instances built from configured <see cref="IEndpointRouteBuilder"/>.
/// The <see cref="EndpointMiddleware"/> will execute the <see cref="Endpoint"/> associated with the current
/// request.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
/// <param name="configure">An <see cref="Action{IEndpointRouteBuilder}"/> to configure the provided <see cref="IEndpointRouteBuilder"/>.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <remarks>
/// <para>
/// A call to <see cref="UseEndpoints(IApplicationBuilder, Action{IEndpointRouteBuilder})"/> must be preceded by a call to
/// <see cref="UseRouting(IApplicationBuilder)"/> for the same <see cref="IApplicationBuilder"/>
/// instance.
/// </para>
/// <para>
/// The <see cref="EndpointRoutingMiddleware"/> defines a point in the middleware pipeline where routing decisions are
/// made, and an <see cref="Endpoint"/> is associated with the <see cref="HttpContext"/>. The <see cref="EndpointMiddleware"/>
/// defines a point in the middleware pipeline where the current <see cref="Endpoint"/> is executed. Middleware between
/// the <see cref="EndpointRoutingMiddleware"/> and <see cref="EndpointMiddleware"/> may observe or change the
/// <see cref="Endpoint"/> associated with the <see cref="HttpContext"/>.
/// </para>
/// </remarks>
public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
} if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
} VerifyRoutingServicesAreRegistered(builder); VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder); configure(endpointRouteBuilder); // Yes, this mutates an IOptions. We're registering data sources in a global collection which
// can be used for discovery of endpoints or URL generation.
//
// Each middleware gets its own collection of data sources, and all of those data sources also
// get added to a global collection.
var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
foreach (var dataSource in endpointRouteBuilder.DataSources)
{
if (!routeOptions.Value.EndpointDataSources.Contains(dataSource))
{
routeOptions.Value.EndpointDataSources.Add(dataSource);
}
} //交由EndpointMiddleware处理
return builder.UseMiddleware<EndpointMiddleware>();
} private static void VerifyRoutingServicesAreRegistered(IApplicationBuilder app)
{
// Verify if AddRouting was done before calling UseEndpointRouting/UseEndpoint
// We use the RoutingMarkerService to make sure if all the services were added.
if (app.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null)
{
throw new InvalidOperationException(Resources.FormatUnableToFindServices(
nameof(IServiceCollection),
nameof(RoutingServiceCollectionExtensions.AddRouting),
"ConfigureServices(...)"));
}
} private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out IEndpointRouteBuilder endpointRouteBuilder)
{
if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj))
{
var message =
$"{nameof(EndpointRoutingMiddleware)} matches endpoints setup by {nameof(EndpointMiddleware)} and so must be added to the request " +
$"execution pipeline before {nameof(EndpointMiddleware)}. " +
$"Please add {nameof(EndpointRoutingMiddleware)} by calling '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' inside the call " +
$"to 'Configure(...)' in the application startup code.";
throw new InvalidOperationException(message);
} endpointRouteBuilder = (IEndpointRouteBuilder)obj!; // This check handles the case where Map or something else that forks the pipeline is called between the two
// routing middleware.
if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultRouteBuilder && !object.ReferenceEquals(app, defaultRouteBuilder.ApplicationBuilder))
{
var message =
$"The {nameof(EndpointRoutingMiddleware)} and {nameof(EndpointMiddleware)} must be added to the same {nameof(IApplicationBuilder)} instance. " +
$"To use Endpoint Routing with 'Map(...)', make sure to call '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' before " +
$"'{nameof(IApplicationBuilder)}.{nameof(UseEndpoints)}' for each branch of the middleware pipeline.";
throw new InvalidOperationException(message);
}
}
}

2-1首先看UseRouting()方法,这个方法其实就是调用EndpointRoutingMiddleware中间件进行Endpoint的匹配,我们可以看下它的几个方法:

internal sealed partial class EndpointRoutingMiddleware
{
private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched"; private readonly MatcherFactory _matcherFactory;
private readonly ILogger _logger;
private readonly EndpointDataSource _endpointDataSource;
private readonly DiagnosticListener _diagnosticListener;
private readonly RequestDelegate _next; private Task<Matcher>? _initializationTask; public EndpointRoutingMiddleware(
MatcherFactory matcherFactory,
ILogger<EndpointRoutingMiddleware> logger,
IEndpointRouteBuilder endpointRouteBuilder,
DiagnosticListener diagnosticListener,
RequestDelegate next)
{
if (endpointRouteBuilder == null)
{
throw new ArgumentNullException(nameof(endpointRouteBuilder));
} _matcherFactory = matcherFactory ?? throw new ArgumentNullException(nameof(matcherFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener));
_next = next ?? throw new ArgumentNullException(nameof(next)); _endpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources);
} public Task Invoke(HttpContext httpContext)
{
// There's already an endpoint, skip matching completely
var endpoint = httpContext.GetEndpoint();
if (endpoint != null)
{
Log.MatchSkipped(_logger, endpoint);
return _next(httpContext);
} // There's an inherent race condition between waiting for init and accessing the matcher
// this is OK because once `_matcher` is initialized, it will not be set to null again.
var matcherTask = InitializeAsync();
if (!matcherTask.IsCompletedSuccessfully)
{
return AwaitMatcher(this, httpContext, matcherTask);
} var matchTask = matcherTask.Result.MatchAsync(httpContext);
if (!matchTask.IsCompletedSuccessfully)
{
return AwaitMatch(this, httpContext, matchTask);
} return SetRoutingAndContinue(httpContext); // Awaited fallbacks for when the Tasks do not synchronously complete
static async Task AwaitMatcher(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task<Matcher> matcherTask)
{
var matcher = await matcherTask;
//根据httpContext进行匹配
await matcher.MatchAsync(httpContext);
await middleware.SetRoutingAndContinue(httpContext);
} static async Task AwaitMatch(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
{
await matchTask;
await middleware.SetRoutingAndContinue(httpContext);
} } [MethodImpl(MethodImplOptions.AggressiveInlining)]
private Task SetRoutingAndContinue(HttpContext httpContext)
{
// If there was no mutation of the endpoint then log failure
var endpoint = httpContext.GetEndpoint();
if (endpoint == null)
{
Log.MatchFailure(_logger);
}
else
{
// Raise an event if the route matched
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(DiagnosticsEndpointMatchedKey))
{
// We're just going to send the HttpContext since it has all of the relevant information
_diagnosticListener.Write(DiagnosticsEndpointMatchedKey, httpContext);
} Log.MatchSuccess(_logger, endpoint);
} return _next(httpContext);
} // Initialization is async to avoid blocking threads while reflection and things
// of that nature take place.
//
// We've seen cases where startup is very slow if we allow multiple threads to race
// while initializing the set of endpoints/routes. Doing CPU intensive work is a
// blocking operation if you have a low core count and enough work to do.
private Task<Matcher> InitializeAsync()
{
var initializationTask = _initializationTask;
if (initializationTask != null)
{
return initializationTask;
} return InitializeCoreAsync();
} private Task<Matcher> InitializeCoreAsync()
{
var initialization = new TaskCompletionSource<Matcher>(TaskCreationOptions.RunContinuationsAsynchronously);
var initializationTask = Interlocked.CompareExchange(ref _initializationTask, initialization.Task, null);
if (initializationTask != null)
{
// This thread lost the race, join the existing task.
return initializationTask;
} // This thread won the race, do the initialization.
try
{
//用EndpointDataSource初始化matcher
//EndpointDataSource来源于IEndpointRouteBuilder的DataSource属性
//见EndpointRouteBuilderExtensions类的201行
var matcher = _matcherFactory.CreateMatcher(_endpointDataSource); _initializationTask = Task.FromResult(matcher); // Complete the task, this will unblock any requests that came in while initializing.
initialization.SetResult(matcher);
return initialization.Task;
}
catch (Exception ex)
{
// Allow initialization to occur again. Since DataSources can change, it's possible
// for the developer to correct the data causing the failure.
_initializationTask = null; // Complete the task, this will throw for any requests that came in while initializing.
initialization.SetException(ex);
return initialization.Task;
}
} private static partial class Log
{
public static void MatchSuccess(ILogger logger, Endpoint endpoint)
=> MatchSuccess(logger, endpoint.DisplayName); [LoggerMessage(1, LogLevel.Debug, "Request matched endpoint '{EndpointName}'", EventName = "MatchSuccess")]
private static partial void MatchSuccess(ILogger logger, string? endpointName); [LoggerMessage(2, LogLevel.Debug, "Request did not match any endpoints", EventName = "MatchFailure")]
public static partial void MatchFailure(ILogger logger); public static void MatchSkipped(ILogger logger, Endpoint endpoint)
=> MatchingSkipped(logger, endpoint.DisplayName); [LoggerMessage(3, LogLevel.Debug, "Endpoint '{EndpointName}' already set, skipping route matching.", EventName = "MatchingSkipped")]
private static partial void MatchingSkipped(ILogger logger, string? endpointName);
}
}

从Invoke方法看得出来,它根据当前的HttpContext进行Endpoint的匹配,如果当前的HttpContext路由格式匹配成功,那么将当前HttpContext传递给下一个中间件处理,这个从SetRoutingAndContinue方法看得出来。

2-2其次看下UseEndpoints()方法,这个方法就是调用EndpointMiddleware中间件,对上面匹配成功的HttpContext进行处理,并调用HttpContext的EndPoint的RequestDelegate处理当前请求。我们重点看下它的Invoke方法,重点关注var requestTask = endpoint.RequestDelegate(httpContext):

  public Task Invoke(HttpContext httpContext)
{
var endpoint = httpContext.GetEndpoint();
if (endpoint?.RequestDelegate != null)
{
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
{
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() != null &&
!httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey))
{
ThrowMissingAuthMiddlewareException(endpoint);
} if (endpoint.Metadata.GetMetadata<ICorsMetadata>() != null &&
!httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey))
{
ThrowMissingCorsMiddlewareException(endpoint);
}
} Log.ExecutingEndpoint(_logger, endpoint); try
{
//调用HttpContext的Endpoint的RequestDelegate方法处理当前请求
var requestTask = endpoint.RequestDelegate(httpContext);
if (!requestTask.IsCompletedSuccessfully)
{
return AwaitRequestTask(endpoint, requestTask, _logger);
}
}
catch (Exception exception)
{
Log.ExecutedEndpoint(_logger, endpoint);
return Task.FromException(exception);
} Log.ExecutedEndpoint(_logger, endpoint);
return Task.CompletedTask;
} return _next(httpContext); static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger)
{
try
{
await requestTask;
}
finally
{
Log.ExecutedEndpoint(logger, endpoint);
}
}
}

总结:从上面的分析,我们粗略的了解了netcore路由的Endpoint模式其实就是一种用匹配模式构建的终端节点,它主要用来对HttpContext进行路由的匹配,如果匹配成功,则执行Endpoint上的RequestDelegate方法。

NetCore路由的Endpoint模式的更多相关文章

  1. 前端路由两种模式:hash、history

    随着 ajax 的使用越来越广泛,前端的页面逻辑开始变得越来越复杂,特别是spa的兴起,前端路由系统随之开始流行. 从用户的角度看,前端路由主要实现了两个功能(使用ajax更新页面状态的情况下): 记 ...

  2. 【RabbitMQ】4、三种Exchange模式——订阅、路由、通配符模式

    前两篇博客介绍了两种队列模式,这篇博客介绍订阅.路由和通配符模式,之所以放在一起介绍,是因为这三种模式都是用了Exchange交换机,消息没有直接发送到队列,而是发送到了交换机,经过队列绑定交换机到达 ...

  3. 如何去除vue项目中的 # — vue路由的History模式

    前言 在创建的 router 对象中,如果不配置 mode,就会使用默认的 hash 模式,该模式下会将路径格式化为 #! 开头. 添加 mode: 'history' 之后将使用 HTML5 his ...

  4. Vue 的路由实现 Hash模式 和 History模式

    Hash 模式: Hash 模式的工作原理是onhashchange事件,Window对象可以监听这个事件... 可以通过改变路径的哈希值,来实现历史记录的保存,发生变化的hash 都会被浏览器给保存 ...

  5. 数据源管理 | 主从库动态路由,AOP模式读写分离

    本文源码:GitHub·点这里 || GitEE·点这里 一.多数据源应用 1.基础描述 在相对复杂的应用服务中,配置多个数据源是常见现象,例如常见的:配置主从数据库用来写数据,再配置一个从库读数据, ...

  6. Vue路由配置history模式

    我的博客: https://github.com/Daotin/fe-notes/issues vue需要node.js吗? 你可以用 script 标签的形式引入vue.min.js 这样的,不需要 ...

  7. Vue路由的hash模式与history模式的区别?

    1.首先router有两种模式:hash模式(默认).history模式(需配置mode: 'history') hash和history的区别?   hash                    ...

  8. Vue2+Koa2+Typescript前后端框架教程--03后端路由和三层模式配置

    昨天将Koa2的基础框架和自动编译调试重启服务完成,今天开始配置路由和搭建基础的三层架构模式. 路由中间件:koa-router,即路由导航,就是我们平时使用最广泛的get/post方法执行的URL路 ...

  9. .net core i上 K8S(五).netcore程序的hostip模式

    上一章讲了pod的管理,今天再分享一个pod的访问方式 1.Pod的HostIP模式 Pod的HostIP模式,可以通过宿主机访问pod内的服务,创建yaml文件如下 apiVersion: v1 k ...

随机推荐

  1. ABP框架之——数据访问基础架构

    大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享阅读心得,希望我的文章能成为你成长路上的一块垫脚石,我们一起精进. 几乎所有的业务应用程序都要适用一种数据库基础架构,用来实现数据访问逻辑,以便从数 ...

  2. CabloyJS一站式助力微信、企业微信、钉钉开发 - 微信篇

    前言 现在软件开发不仅要面对前端碎片化,还要面对后端碎片化.针对前端碎片化,CabloyJS提供了pc=mobile+pad的跨端自适应方案,参见:自适应布局:pc = mobile + pad 在这 ...

  3. HtmlParse:一款超轻量级的HTML文件解析和爬取工具

    HtmlParse 是一款基于windwos平台的HTML文档解析工具,可快速构建DOM树,从而轻松实现网页元素的爬取工作.DOM树就是一个HTML文档的节点树,每个节点由:标签(Tag).属性(At ...

  4. ES6 伪数组转真数组

    更新日志 2022年6月13日 发布. 2022年5月19日 笔记迁移到博客. 直接上代码 [...a];

  5. 论文解读(SR-GNN)《Shift-Robust GNNs: Overcoming the Limitations of Localized Graph Training Data》

    论文信息 论文标题:Shift-Robust GNNs: Overcoming the Limitations of Localized Graph Training Data论文作者:Qi Zhu, ...

  6. Linux 文件的打包压缩

    压缩和解压 压缩:为了节约磁盘空间. gzip --- .gz bzip2 --- .bz2 xz --- .xz compress --- .z 压缩比例:xz > bzip2 > gz ...

  7. linux查询文件或者文件夹

    查找目录:find /(查找范围) -name '查找关键字' -type d // 查找fastdfs_storage_data文件夹 find / -name fastdfs_storage_da ...

  8. 不是吧?30秒 就能学会一个python小技巧?!

    大家好鸭!我是小熊猫 很多学习Python的朋友在项目实战中会遇到不少功能实现上的问题,有些问题并不是很难的问题,或者已经有了很好的方法来解决.当然,孰能生巧,当我们代码熟练了,自然就能总结一些好用的 ...

  9. Python 用configparser读写ini文件

    一.configparser 简介Python用于读写ini文件的一个官方标准库.具体详见官网链接 二.configparser 部分方法介绍 方法 描述 read(filenames) filesn ...

  10. vue2.0 双向绑定原理分析及简单实现

    Vue用了有一段时间了,每当有人问到Vue双向绑定是怎么回事的时候,总是不能给大家解释的很清楚,正好最近有时间把它梳理一下,让自己理解的更清楚,下次有人问我的时候,可以侃侃而谈. 一.首先介绍Obje ...