[source code interpretation] Vue and asp Net core webapi integration

DDGarfield 2022-06-23 19:02:57 阅读数:274

sourcecodeinterpretationvueasp

Blog ahead 【Vue】Vue And ASP.NET Core WebAPI Integration of , The principle of integration is introduced : Register in the middleware pipeline SPA Terminal Middleware , Throughout the registration process , The terminal middleware will call node, perform npm start Command to start vue Development server , Add route matching to middleware pipeline , It's not api request ( Request static file ,jscsshtml) Forward to SPA Development server .

The registration code is as follows :

public void Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder app, IWebHostEnvironment env)
{
#region +Endpoints
// Execute the matched endpoint.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
//spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseVueCliServer(npmScript: "start");
//spa.UseProxyToSpaDevelopmentServer("http://localhost:8080");
}
});
#endregion
}

“ You can see that if you register first, you can match API Attribute routing of the request . ”

If the above attribute routing does not match , The request is passed through the middleware pipeline , Go to the next middleware :SPA The terminal middleware of

That's the principle of integration . Next, we will interpret the source code of the middleware . On the whole, there are still quite a lot of knowledge points worth reading and learning :

  • Asynchronous programming
  • Inline middleware
  • Start the process
  • Event driven

1. Asynchronous programming -ContinueWith

Let's ignore the call first npm start Command execution and other details . What we see is asynchronous programming . as everyone knows ,vue perform npm start(npm run dev) It's a time-consuming process . We want to achieve the goal of perfect integration : We register middleware , We need to wait vue After the front end development server starts , Normal use , Receive proxy requests to this development server . Wait for the next operation to complete before doing other operations , This is an asynchronous programming .

  • You need to return to npm run dev Class of results :
class VueCliServerInfo
{
public int Port { get; set; }
}
  • Write asynchronous code , Start the front end development server
private static async Task<VueCliServerInfo> StartVueCliServerAsync(
string sourcePath, string npmScriptName, ILogger logger)
{
// Omit code
}

1.1 ContinueWith

  • Write continuation

ContinueWith Itself will return a Task

var vueCliServerInfoTask = StartVueCliServerAsync(sourcePath, npmScriptName, logger);
// Continuance
var targetUriTask = vueCliServerInfoTask.ContinueWith(
task =>
{
return new UriBuilder("http", "localhost", task.Result.Port).Uri;
});

1.2 Inline middleware

  • Continue to use this continuation body to return task, and applicationBuilder.Use() Configure an inline middleware , That is, all requests are proxied to the development server
SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
{
var timeout = spaBuilder.Options.StartupTimeout;
return targetUriTask.WithTimeout(timeout,
$"The Vue CLI process did not start listening for requests " +
$"within the timeout period of {timeout.Seconds} seconds. " +
$"Check the log output for error information.");
});
public static void UseProxyToSpaDevelopmentServer(
this ISpaBuilder spaBuilder,
Func<Task<Uri>> baseUriTaskFactory)
{
var applicationBuilder = spaBuilder.ApplicationBuilder;
var applicationStoppingToken = GetStoppingToken(applicationBuilder);
// Omitted code
// Proxy all requests to the SPA development server
applicationBuilder.Use(async (context, next) =>
{
var didProxyRequest =
await SpaProxy.PerformProxyRequest(
context, neverTimeOutHttpClient, baseUriTaskFactory(), applicationStoppingToken,
proxy404s: true);
});
}
  • All subsequent requests , It's going to be similar nginx Same operation :
public static async Task<bool> PerformProxyRequest(
HttpContext context,
HttpClient httpClient,
Task<Uri> baseUriTask,
CancellationToken applicationStoppingToken,
bool proxy404s)
{
// Omitted code ...
// obtain task Result , Development server uri
var baseUri = await baseUriTask;
// Proxy requests to the development server
// Receive the response from the development server Give to the context, from asp.net core Respond to
}

2. Start the process -ProcessStartInfo

Next into StartVueCliServerAsync Internal , perform node process , perform npm start command .

2.1 determine vue Development server port

Determine a random 、 Available development server ports , The code is as follows :

internal static class TcpPortFinder
{
public static int FindAvailablePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
try
{
return ((IPEndPoint)listener.LocalEndpoint).Port;
}
finally
{
listener.Stop();
}
}
}

2.2 perform npm command

Determine the available ports , According to the front end project directory spa.Options.SourcePath = "ClientApp";

private static async Task<VueCliServerInfo> StartVueCliServerAsync(
string sourcePath, string npmScriptName, ILogger logger)
{
var portNumber = TcpPortFinder.FindAvailablePort();
logger.LogInformation($"Starting Vue/dev-server on port {portNumber}...");
// Carry out orders
var npmScriptRunner = new NpmScriptRunner(
//sourcePath, npmScriptName, $"--port {portNumber}");
sourcePath, npmScriptName, $"{portNumber}");
}

NpmScriptRunner Inside, it starts calling node perform cmd command :

internal class NpmScriptRunner
{
public EventedStreamReader StdOut { get; }
public EventedStreamReader StdErr { get; }
public NpmScriptRunner(string workingDirectory, string scriptName, string arguments)
{
var npmExe = "npm";
var completeArguments = $"run {scriptName} {arguments ?? string.Empty}";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
npmExe = "cmd";
completeArguments = $"/c npm {completeArguments}";
}
var processStartInfo = new ProcessStartInfo(npmExe)
{
Arguments = completeArguments,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = workingDirectory
};
var process = LaunchNodeProcess(processStartInfo);
// Read the text output stream
StdOut = new EventedStreamReader(process.StandardOutput);
// Read error output stream
StdErr = new EventedStreamReader(process.StandardError);
}
}
private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
{
try
{
var process = Process.Start(startInfo);
process.EnableRaisingEvents = true;
return process;
}
catch (Exception ex)
{
var message = $"Failed to start 'npm'. To resolve this:.\n\n"
+ "[1] Ensure that 'npm' is installed and can be found in one of the PATH directories.\n"
+ $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n"
+ " Make sure the executable is in one of those directories, or update your PATH.\n\n"
+ "[2] See the InnerException for further details of the cause.";
throw new InvalidOperationException(message, ex);
}
}
internal class EventedStreamReader
{
public delegate void OnReceivedChunkHandler(ArraySegment<char> chunk);
public delegate void OnReceivedLineHandler(string line);
public delegate void OnStreamClosedHandler();
public event OnReceivedChunkHandler OnReceivedChunk;
public event OnReceivedLineHandler OnReceivedLine;
public event OnStreamClosedHandler OnStreamClosed;
private readonly StreamReader _streamReader;
private readonly StringBuilder _linesBuffer;
// Start thread read stream in constructor
public EventedStreamReader(StreamReader streamReader)
{
_streamReader = streamReader ?? throw new ArgumentNullException(nameof(streamReader));
_linesBuffer = new StringBuilder();
Task.Factory.StartNew(Run);
}
private async Task Run()
{
var buf = new char[8 * 1024];
while (true)
{
var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length);
if (chunkLength == 0)
{
// How to trigger an event
OnClosed();
break;
}
// How to trigger an event
OnChunk(new ArraySegment<char>(buf, 0, chunkLength));
var lineBreakPos = Array.IndexOf(buf, '\n', 0, chunkLength);
if (lineBreakPos < 0)
{
_linesBuffer.Append(buf, 0, chunkLength);
}
else
{
_linesBuffer.Append(buf, 0, lineBreakPos + 1);
// How to trigger an event
OnCompleteLine(_linesBuffer.ToString());
_linesBuffer.Clear();
_linesBuffer.Append(buf, lineBreakPos + 1, chunkLength - (lineBreakPos + 1));
}
}
}
private void OnChunk(ArraySegment<char> chunk)
{
var dlg = OnReceivedChunk;
dlg?.Invoke(chunk);
}
private void OnCompleteLine(string line)
{
var dlg = OnReceivedLine;
dlg?.Invoke(line);
}
private void OnClosed()
{
var dlg = OnStreamClosed;
dlg?.Invoke();
}
}

2.3 Read and output npm Log of command execution

npmScriptRunner.AttachToLogger(logger);

register OnReceivedLine And OnReceivedChunk event , Triggered by read text stream and error stream :

internal class EventedStreamReader
{
public void AttachToLogger(ILogger logger)
{
StdOut.OnReceivedLine += line =>
{
if (!string.IsNullOrWhiteSpace(line))
{
logger.LogInformation(StripAnsiColors(line));
}
};
StdErr.OnReceivedLine += line =>
{
if (!string.IsNullOrWhiteSpace(line))
{
logger.LogError(StripAnsiColors(line));
}
};
StdErr.OnReceivedChunk += chunk =>
{
var containsNewline = Array.IndexOf(
chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0;
if (!containsNewline)
{
Console.Write(chunk.Array, chunk.Offset, chunk.Count);
}
};
}
}

2.4 Read the output stream to the development server and start successfully

Under normal circumstances ,Vue After the development server starts successfully , Here's the picture :

So just read the input code in the stream http://localhost:port, Regular matching is used here :

Match openBrowserLine;
openBrowserLine = await npmScriptRunner.StdOut.WaitForMatch(
new Regex("- Local: (http:\\S+/)", RegexOptions.None, RegexMatchTimeout));

2.5 Asynchronous programming -TaskCompletionSource

**TaskCompletionSource It's also a way to create Task The way .** The asynchronous method here WaitForMatch I used TaskCompletionSource, Will continue to read the stream , Every line of text output stream , Regular matching :

  • If the match is successful, call SetResult() to Task Finish signal
  • If the match fails, call SetException() to Task Abnormal signal
internal class EventedStreamReader
{
public Task<Match> WaitForMatch(Regex regex)
{
var tcs = new TaskCompletionSource<Match>();
var completionLock = new object();
OnReceivedLineHandler onReceivedLineHandler = null;
OnStreamClosedHandler onStreamClosedHandler = null;
//C#7.0 Local function
void ResolveIfStillPending(Action applyResolution)
{
lock (completionLock)
{
if (!tcs.Task.IsCompleted)
{
OnReceivedLine -= onReceivedLineHandler;
OnStreamClosed -= onStreamClosedHandler;
applyResolution();
}
}
}
onReceivedLineHandler = line =>
{
var match = regex.Match(line);
// The match is successful
if (match.Success)
{
ResolveIfStillPending(() => tcs.SetResult(match));
}
};
onStreamClosedHandler = () =>
{
// Until the end of the text stream
ResolveIfStillPending(() => tcs.SetException(new EndOfStreamException()));
};
OnReceivedLine += onReceivedLineHandler;
OnStreamClosed += onStreamClosedHandler;
return tcs.Task;
}
}

2.6 Ensure that the development server access is normal

And get... From regular matching results uri, Even in Vue CLI Prompt is listening after the request , If a request is made too quickly , In a very short period of time, it also gives errors ( Maybe it's the code level that comes up ). So we have to continue to add asynchronous methods WaitForVueCliServerToAcceptRequests() Make sure the development server is really ready .

private static async Task<VueCliServerInfo> StartVueCliServerAsync(
string sourcePath, string npmScriptName, ILogger logger)
{
var portNumber = TcpPortFinder.FindAvailablePort();
logger.LogInformation($"Starting Vue/dev-server on port {portNumber}...");
// Carry out orders
var npmScriptRunner = new NpmScriptRunner(
//sourcePath, npmScriptName, $"--port {portNumber}");
sourcePath, npmScriptName, $"{portNumber}");
npmScriptRunner.AttachToLogger(logger);
Match openBrowserLine;
// Omitted code
openBrowserLine = await npmScriptRunner.StdOut.WaitForMatch(
new Regex("- Local: (http:\\S+/)", RegexOptions.None, RegexMatchTimeout));
var uri = new Uri(openBrowserLine.Groups[1].Value);
var serverInfo = new VueCliServerInfo { Port = uri.Port };
await WaitForVueCliServerToAcceptRequests(uri);
return serverInfo;
}
private static async Task WaitForVueCliServerToAcceptRequests(Uri cliServerUri)
{
var timeoutMilliseconds = 1000;
using (var client = new HttpClient())
{
while (true)
{
try
{
await client.SendAsync(
new HttpRequestMessage(HttpMethod.Head, cliServerUri),
new CancellationTokenSource(timeoutMilliseconds).Token);
return;
}
catch (Exception)
{
// It creates Task, But it doesn't take up threads
await Task.Delay(500);
if (timeoutMilliseconds < 10000)
{
timeoutMilliseconds += 3000;
}
}
}
}
}

Task.Delay() The magic of : establish Task, But it doesn't take up threads , Equivalent to the asynchronous version of Thread.Sleep, And you can write continuation later :ContinueWith ”

3. summary

3.1 Asynchronous programming

  • adopt ContinueWiht Continue to return to Task Feature creation of Task, And use this in subsequent configuration of inline middleware Task
app.Use(async (context, next)=>{
});

send ASP.NET Core The startup and middleware registration are smooth .

  • adopt TaskCompletionSource You can create... In any operation that starts and ends later Task, This Task, You can manually indicate when the operation ends (SetResult), When it breaks down (SetException), Both States mean Task complete tcs.Task.IsCompleted, Yes, we often need to wait IO-Bound This kind of work is ideal .
版权声明:本文为[DDGarfield]所创,转载请带上原文链接,感谢。 https://qdmana.com/2022/174/202206231758266520.html