【源碼解讀】Vue與ASP.NET Core WebAPI的集成
在前面博文【Vue】Vue 與 ASP.NET Core WebAPI 的集成中,介紹了集成原理:在中間件管道中注冊SPA終端中間件,整個注冊過程中,終端中間件會調(diào)用node,執(zhí)行npm start命令啟動vue開發(fā)服務(wù)器,向中間件管道添加路由匹配,即非 api 請求(請求靜態(tài)文件,js css html)都代理轉(zhuǎn)發(fā)至SPA開發(fā)服務(wù)器。
注冊代碼如下:
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
}
“可以看到先注冊了能夠匹配
”API請求的屬性路由。
如果上面的屬性路由無法匹配,請求就會在中間件管道中傳遞,至下一個中間件:SPA的終端中間件
以上便是集成原理。接下來我們對其中間件源碼進(jìn)行解讀。整體還是有蠻多值得解讀學(xué)習(xí)的知識點(diǎn):
異步編程 內(nèi)聯(lián)中間件 啟動進(jìn)程 事件驅(qū)動
1.異步編程-ContinueWith
我們先忽略調(diào)用npm start命令執(zhí)行等細(xì)節(jié)。映入我們眼簾的便是異步編程。眾所周知,vue執(zhí)行npm start(npm run dev)的一個比較花費(fèi)時間的過程。要達(dá)成我們完美集成的目的:我們注冊中間件,就需要等待vue前端開發(fā)服務(wù)器啟動后,正常使用,接收代理請求至這個開發(fā)服務(wù)器。這個等待后一個操作完成后再做其他操作,這就是一個異步編程。
建立需要返回 npm run dev結(jié)果的類:
class?VueCliServerInfo
{
????public?int?Port?{?get;?set;?}
}
編寫異步代碼,啟動前端開發(fā)服務(wù)器
private?static?async?Task?StartVueCliServerAsync(
????????????string?sourcePath,?string?npmScriptName,?ILogger?logger)
{
????//省略代碼
}
1.1 ContinueWith
編寫繼續(xù)體
ContinueWith本身就會返回一個Task
var?vueCliServerInfoTask?=?StartVueCliServerAsync(sourcePath,?npmScriptName,?logger);
//繼續(xù)體
var?targetUriTask?=?vueCliServerInfoTask.ContinueWith(
????task?=>
????{
????????return?new?UriBuilder("http",?"localhost",?task.Result.Port).Uri;
????});
1.2 內(nèi)聯(lián)中間件
繼續(xù)使用這個繼續(xù)體返回的 task,并 applicationBuilder.Use()配置一個內(nèi)聯(lián)中間件,即所有請求都代理至開發(fā)服務(wù)器
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>?baseUriTaskFactory )
{
????var?applicationBuilder?=?spaBuilder.ApplicationBuilder;
????var?applicationStoppingToken?=?GetStoppingToken(applicationBuilder);
????//省略部分代碼
????//?Proxy?all?requests?to?the?SPA?development?server
????applicationBuilder.Use(async?(context,?next)?=>
???????????????????????????{
???????????????????????????????var?didProxyRequest?=
???????????????????????????????????await?SpaProxy.PerformProxyRequest(
???????????????????????????????????context,?neverTimeOutHttpClient,?baseUriTaskFactory(),?applicationStoppingToken,
???????????????????????????????????proxy404s:?true);
???????????????????????????});
}
所有的后續(xù)請求,都會類似 nginx 一樣的操作:
public?static?async?Task<bool>?PerformProxyRequest(
????HttpContext?context,
????HttpClient?httpClient,
????Task?baseUriTask,
????CancellationToken?applicationStoppingToken,
????bool?proxy404s )
{
????//省略部分代碼...
????//獲取task的結(jié)果,即開發(fā)服務(wù)器uri
????var?baseUri?=?await?baseUriTask;
????//把請求代理至開發(fā)服務(wù)器
????//接收開發(fā)服務(wù)器的響應(yīng)?給到?context,由asp.net?core響應(yīng)
}
2.啟動進(jìn)程-ProcessStartInfo
接下來進(jìn)入StartVueCliServerAsync的內(nèi)部,執(zhí)行node進(jìn)程,執(zhí)行npm start命令。
2.1 確定 vue 開發(fā)服務(wù)器的端口
確定一個隨機(jī)的、可用的開發(fā)服務(wù)器端口,代碼如下:
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 執(zhí)行 npm 命令
確定好可用的端口,根據(jù)前端項(xiàng)目目錄spa.Options.SourcePath = "ClientApp";
private?static?async?Task?StartVueCliServerAsync(
????string?sourcePath,?string?npmScriptName,?ILogger?logger)
{
????var?portNumber?=?TcpPortFinder.FindAvailablePort();
????logger.LogInformation($"Starting?Vue/dev-server?on?port?{portNumber}...");
????//執(zhí)行命令
????var?npmScriptRunner?=?new?NpmScriptRunner(
????????//sourcePath,?npmScriptName,?$"--port?{portNumber}");
????????sourcePath,?npmScriptName,?$"{portNumber}");
}
NpmScriptRunner內(nèi)部便在開始調(diào)用 node 執(zhí)行 cmd 命令:
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);
????????//讀取文本輸出流
????????StdOut?=?new?EventedStreamReader(process.StandardOutput);
????????//讀取錯誤輸出流
????????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;
????//構(gòu)造函數(shù)中啟動線程讀流
????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)
????????????{
????????????????//觸發(fā)事件的方法
????????????????OnClosed();
????????????????break;
????????????}
????????????//觸發(fā)事件的方法
????????????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);
????????????????//觸發(fā)事件的方法
????????????????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 讀取并輸出 npm 命令執(zhí)行的日志
npmScriptRunner.AttachToLogger(logger);
注冊OnReceivedLine與OnReceivedChunk事件,由讀文本流和錯誤流觸發(fā):
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 讀取輸出流至開發(fā)服務(wù)器啟動成功
正常情況下,Vue開發(fā)服務(wù)器啟動成功后,如下圖:
所以代碼中只需要讀取輸入流中的http://localhost:port,這里使用了正則匹配:
Match?openBrowserLine;
openBrowserLine?=?await?npmScriptRunner.StdOut.WaitForMatch(
????new?Regex("-?Local:???(http:\\S+/)",?RegexOptions.None,?RegexMatchTimeout));
2.5 異步編程-TaskCompletionSource
**TaskCompletionSource也是一種創(chuàng)建Task的方式。**這里的異步方法WaitForMatch便使用了TaskCompletionSource,會持續(xù)讀取流,每一行文本輸出流,進(jìn)行正則匹配:
匹配成功便調(diào)用 SetResult()給Task完成信號匹配失敗便調(diào)用 SetException()給Task異常信號
internal?class?EventedStreamReader
{
????public?Task?WaitForMatch(Regex?regex)
????{
????????var?tcs?=?new?TaskCompletionSource();
????????var?completionLock?=?new?object();
????????OnReceivedLineHandler?onReceivedLineHandler?=?null;
????????OnStreamClosedHandler?onStreamClosedHandler?=?null;
????????//C#7.0?本地函數(shù)
????????void?ResolveIfStillPending(Action?applyResolution)
????????{
????????????lock?(completionLock)
????????????{
????????????????if?(!tcs.Task.IsCompleted)
????????????????{
????????????????????OnReceivedLine?-=?onReceivedLineHandler;
????????????????????OnStreamClosed?-=?onStreamClosedHandler;
????????????????????applyResolution();
????????????????}
????????????}
????????}
????????onReceivedLineHandler?=?line?=>
????????{
????????????var?match?=?regex.Match(line);
????????????//匹配成功
????????????if?(match.Success)
????????????{
????????????????ResolveIfStillPending(()?=>?tcs.SetResult(match));
????????????}
????????};
????????onStreamClosedHandler?=?()?=>
????????{
????????????//一直到文本流結(jié)束
????????????ResolveIfStillPending(()?=>?tcs.SetException(new?EndOfStreamException()));
????????};
????????OnReceivedLine?+=?onReceivedLineHandler;
????????OnStreamClosed?+=?onStreamClosedHandler;
????????return?tcs.Task;
????}
}
2.6 確保開發(fā)服務(wù)器訪問正常
并從正則匹配結(jié)果獲取uri,即使在Vue CLI提示正在監(jiān)聽請求之后,如果過快地發(fā)出請求,在很短的一段時間內(nèi)它也會給出錯誤(可能就是代碼層級才會出現(xiàn))。所以還得繼續(xù)添加異步方法WaitForVueCliServerToAcceptRequests()確保開發(fā)服務(wù)器的的確確準(zhǔn)備好了。
private?static?async?Task?StartVueCliServerAsync(
????string?sourcePath,?string?npmScriptName,?ILogger?logger)
{
????var?portNumber?=?TcpPortFinder.FindAvailablePort();
????logger.LogInformation($"Starting?Vue/dev-server?on?port?{portNumber}...");
????//執(zhí)行命令
????var?npmScriptRunner?=?new?NpmScriptRunner(
????????//sourcePath,?npmScriptName,?$"--port?{portNumber}");
????????sourcePath,?npmScriptName,?$"{portNumber}");
????npmScriptRunner.AttachToLogger(logger);
????Match?openBrowserLine;
????//省略部分代碼
????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)
????????????{
????????????????//它創(chuàng)建Task,但并不占用線程
????????????????await?Task.Delay(500);
????????????????if?(timeoutMilliseconds?10000)
????????????????{
????????????????????timeoutMilliseconds?+=?3000;
????????????????}
????????????}
????????}
????}
}
“”
Task.Delay()的魔力:創(chuàng)建 Task,但并不占用線程,相當(dāng)于異步版本的Thread.Sleep,且可以在后面編寫繼續(xù)體:ContinueWith
3.總結(jié)
3.1 異步編程
通過 ContinueWiht繼續(xù)體返回Task的特性創(chuàng)建Task,并在后續(xù)配置內(nèi)聯(lián)中間件時使用這個Task
app.Use(async?(context,?next)=>{
});
使ASP.NET Core的啟動與中間件注冊順滑。
通過 TaskCompletionSource可以在稍后開始和結(jié)束的任意操作中創(chuàng)建Task,這個Task,可以手動指示操作何時結(jié)束(SetResult),何時發(fā)生故障(SetException),這兩種狀態(tài)都意味著Task完成tcs.Task.IsCompleted,對經(jīng)常需要等 IO-Bound 類工作比較理想。

