【源码解读】Vue与ASP.NET Core WebAPI的集成
在前面博文【Vue】Vue 與 ASP.NET Core WebAPI 的集成中,介紹了集成原理:在中間件管道中注冊SPA終端中間件,整個注冊過程中,終端中間件會調用node,執行npm start命令啟動vue開發服務器,向中間件管道添加路由匹配,即非 api 請求(請求靜態文件,js css html)都代理轉發至SPA開發服務器。
注冊代碼如下:
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的終端中間件
以上便是集成原理。接下來我們對其中間件源碼進行解讀。整體還是有蠻多值得解讀學習的知識點:
異步編程
內聯中間件
啟動進程
事件驅動
1.異步編程-ContinueWith
我們先忽略調用npm start命令執行等細節。映入我們眼簾的便是異步編程。眾所周知,vue執行npm start(npm run dev)的一個比較花費時間的過程。要達成我們完美集成的目的:我們注冊中間件,就需要等待vue前端開發服務器啟動后,正常使用,接收代理請求至這個開發服務器。這個等待后一個操作完成后再做其他操作,這就是一個異步編程。
建立需要返回npm run dev結果的類:
編寫異步代碼,啟動前端開發服務器
1.1 ContinueWith
編寫繼續體
ContinueWith本身就會返回一個Task
var?vueCliServerInfoTask?=?StartVueCliServerAsync(sourcePath,?npmScriptName,?logger);//繼續體 var?targetUriTask?=?vueCliServerInfoTask.ContinueWith(task?=>{return?new?UriBuilder("http",?"localhost",?task.Result.Port).Uri;});1.2 內聯中間件
繼續使用這個繼續體返回的 task,并applicationBuilder.Use()配置一個內聯中間件,即所有請求都代理至開發服務器
所有的后續請求,都會類似 nginx 一樣的操作:
2.啟動進程-ProcessStartInfo
接下來進入StartVueCliServerAsync的內部,執行node進程,執行npm start命令。
2.1 確定 vue 開發服務器的端口
確定一個隨機的、可用的開發服務器端口,代碼如下:
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 執行 npm 命令
確定好可用的端口,根據前端項目目錄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}...");//執行命令var?npmScriptRunner?=?new?NpmScriptRunner(//sourcePath,?npmScriptName,?$"--port?{portNumber}");sourcePath,?npmScriptName,?$"{portNumber}"); }NpmScriptRunner內部便在開始調用 node 執行 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;//構造函數中啟動線程讀流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){//觸發事件的方法OnClosed();break;}//觸發事件的方法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);//觸發事件的方法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 命令執行的日志
npmScriptRunner.AttachToLogger(logger);
注冊OnReceivedLine與OnReceivedChunk事件,由讀文本流和錯誤流觸發:
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 讀取輸出流至開發服務器啟動成功
正常情況下,Vue開發服務器啟動成功后,如下圖:
所以代碼中只需要讀取輸入流中的http://localhost:port,這里使用了正則匹配:
Match?openBrowserLine; openBrowserLine?=?await?npmScriptRunner.StdOut.WaitForMatch(new?Regex("-?Local:???(http:\\S+/)",?RegexOptions.None,?RegexMatchTimeout));2.5 異步編程-TaskCompletionSource
**TaskCompletionSource也是一種創建Task的方式。**這里的異步方法WaitForMatch便使用了TaskCompletionSource,會持續讀取流,每一行文本輸出流,進行正則匹配:
匹配成功便調用SetResult()給Task完成信號
匹配失敗便調用SetException()給Task異常信號
2.6 確保開發服務器訪問正常
并從正則匹配結果獲取uri,即使在Vue CLI提示正在監聽請求之后,如果過快地發出請求,在很短的一段時間內它也會給出錯誤(可能就是代碼層級才會出現)。所以還得繼續添加異步方法WaitForVueCliServerToAcceptRequests()確保開發服務器的的確確準備好了。
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}...");//執行命令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){//它創建Task,但并不占用線程await?Task.Delay(500);if?(timeoutMilliseconds?<?10000){timeoutMilliseconds?+=?3000;}}}} } “Task.Delay()的魔力:創建 Task,但并不占用線程,相當于異步版本的Thread.Sleep,且可以在后面編寫繼續體:ContinueWith
”3.總結
3.1 異步編程
通過ContinueWiht繼續體返回Task的特性創建Task,并在后續配置內聯中間件時使用這個Task
使ASP.NET Core的啟動與中間件注冊順滑。
通過TaskCompletionSource可以在稍后開始和結束的任意操作中創建Task,這個Task,可以手動指示操作何時結束(SetResult),何時發生故障(SetException),這兩種狀態都意味著Task完成tcs.Task.IsCompleted,對經常需要等 IO-Bound 類工作比較理想。
總結
以上是生活随笔為你收集整理的【源码解读】Vue与ASP.NET Core WebAPI的集成的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在 ASP.NET Core 中使用
- 下一篇: Dapr微服务应用开发系列3:服务调用构