<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【源碼解讀】Vue與ASP.NET Core WebAPI的集成

          共 11493字,需瀏覽 23分鐘

           ·

          2021-01-04 11:50

          在前面博文【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);

          注冊OnReceivedLineOnReceivedChunk事件,由讀文本流和錯誤流觸發(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 類工作比較理想。
          瀏覽 37
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产亚洲欧美精品久久久www | 美女扒开嫩嫩的尿囗让人桶出白浆 | 欧美 日韩 国产在线观看 | 淫秽三级片中文字幕在线免费观看 | 午夜精品一区二区三区在线视频99 |