DDGarfield 2022-06-23 19:02:57 阅读数:274
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 ,js
css
html
) 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 :
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 .
npm run dev
Class of results :class VueCliServerInfo
{
public int Port { get; set; }
}
private static async Task<VueCliServerInfo> StartVueCliServerAsync(
string sourcePath, string npmScriptName, ILogger logger)
{
// Omit code
}
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;
});
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);
});
}
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
}
Next into StartVueCliServerAsync
Internal , perform node
process , perform npm start
command .
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();
}
}
}
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();
}
}
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);
}
};
}
}
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));
**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 :
SetResult()
to Task
Finish signal 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;
}
}
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 ofThread.Sleep
, And you can write continuation later :ContinueWith ”
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 .
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