【源码解读】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结果的类:

class VueCliServerInfo
{public int Port { get; set; }
}
  • 编写异步代码,启动前端开发服务器

private static async Task<VueCliServerInfo> StartVueCliServerAsync(string sourcePath, string npmScriptName, ILogger logger)
{//省略代码
}

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()配置一个内联中间件,即所有请求都代理至开发服务器

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);//省略部分代码// Proxy all requests to the SPA development serverapplicationBuilder.Use(async (context, next) =>{var didProxyRequest =await SpaProxy.PerformProxyRequest(context, neverTimeOutHttpClient, baseUriTaskFactory(), applicationStoppingToken,proxy404s: true);});
}
  • 所有的后续请求,都会类似 nginx 一样的操作:

public static async Task<bool> PerformProxyRequest(HttpContext context,HttpClient httpClient,Task<Uri> baseUriTask,CancellationToken applicationStoppingToken,bool proxy404s)
{//省略部分代码...//获取task的结果,即开发服务器urivar baseUri = await baseUriTask;//把请求代理至开发服务器//接收开发服务器的响应 给到 context,由asp.net core响应
}

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);

注册OnReceivedLineOnReceivedChunk事件,由读文本流和错误流触发:

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异常信号

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 本地函数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 = () =>{//一直到文本流结束ResolveIfStillPending(() => tcs.SetException(new EndOfStreamException()));};OnReceivedLine += onReceivedLineHandler;OnStreamClosed += onStreamClosedHandler;return tcs.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

app.Use(async (context, next)=>{});

使ASP.NET Core的启动与中间件注册顺滑。

  • 通过TaskCompletionSource可以在稍后开始和结束的任意操作中创建Task,这个Task,可以手动指示操作何时结束(SetResult),何时发生故障(SetException),这两种状态都意味着Task完成tcs.Task.IsCompleted,对经常需要等 IO-Bound 类工作比较理想。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306060.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ios采用什么技术_app软件公司开发宠物别APP采用什么技术?

app软件公司开发宠物别APP采用什么技术&#xff1f;随着经济的发展&#xff0c;人们生活水平的提高&#xff0c;养宠的家庭越来越多&#xff0c;宠物也逐渐成为主人家庭成员的重要组成部分&#xff0c;宠物识别APP在市场上也是很热门的手机软件&#xff0c;那么它是根据什么原理…

Dapr微服务应用开发系列3:服务调用构件块

题记&#xff1a;这篇开始逐一深入介绍各个构件块&#xff0c;从服务调用开始原理所谓服务调用&#xff0c;就是通过这个构件块让你方便的通过HTTP或者gRPC协议同步调用其他服务的方法&#xff0c;这些方法也是通过HTTP或者gRPC来暴露的。而方便的含义在于&#xff0c;你无需担…

Android开发p图软件,媲美大神P图效果 Android软件抠图神手

媲美大神P图效果 Android软件抠图神手2013年02月20日 01:50作者&#xff1a;杨霏霏编辑&#xff1a;杨霏霏文章出处&#xff1a;泡泡网原创分享泡泡网手机频道2月20日 PS的功能大家耳熟能详&#xff0c;其中抠图便是各位PS用户普遍会用到的一个功能。然而手机上抠图大家想过吗&…

腾讯公测云开发低码!实战评测

听说腾讯的新产品『 云开发低码 』即将开放公测了&#xff0c;怀着无比激动的心情&#xff0c;鱼皮立刻去官网申请并成功拿到了公测资格&#xff0c;然后使用它开发了一个小程序&#xff0c;并且通过 2020 Techo Park 开发者大会加深了对这项技术的了解。而就在 2020 年的最后一…

gitee 从 拉取新分支到本地_Hexo博客详细教程(一)| 建立本地站点

点上方蓝字关注我们每天都有好玩的东西等着你博客炫酷效果展示安装Hexo安装Git参考文章&#xff1a;Git实用教程(二) | Git简介及安装详解。安装NodejsNodejs可以从官网( https://nodejs.org/en )下载LTS版本&#xff1a;安装之后检查一下是否正常输出版本信息&#xff1a;安装…

索尼android 怎么截屏快捷键,索尼XZ Premium怎么截屏 2种索尼XZ Premium截图方法

截屏作为手机常用功能之一&#xff0c;我们经常在分享朋友圈或微博的时候经常需要用到屏幕截屏。今天本文主要分享一下索尼XZ Premium怎么截屏&#xff0c;作为一款相对冷门的非国产骁龙835旗舰机&#xff0c;在使用中难免出现一些不太熟悉的问题&#xff0c;下面小编分享2种索…

IdentityServer4 之Client Credentials走起来

前言API裸奔是绝对不允许滴&#xff0c;之前专门针对这块分享了jwt的解决方案(WebApi接口裸奔有风险)&#xff1b;那如果是微服务&#xff0c;又怎么解决呢&#xff1f;每一个服务都加认证授权也可以解决问题&#xff0c;只是显得认证授权这块冗余&#xff0c;重复在搞事情&…

读数据库遇到空就进行不下去_如何保证缓存与数据库的双写一致性?

作者&#xff1a;你是我的海啸来源&#xff1a;https://blog.csdn.net/chang384915878分布式缓存是现在很多分布式应用中必不可少的组件&#xff0c;但是用到了分布式缓存&#xff0c;就可能会涉及到缓存与数据库双存储双写&#xff0c;你只要是双写&#xff0c;就一定会有数据…

Windows Terminal 新手入门

翻译自 Kayla Cinnamon 2020年12月17日的文章《Getting Started with Windows Terminal》[1]安装Windows Terminal&#xff08;Windows 终端&#xff09;有两个不同的版本&#xff1a;Windows Terminal[2] 和 Windows Terminal 预览版[3]。两个版本都可以从 Microsoft Store 和…

5120v2怎么配置web登陆_阿里企业邮箱如何配置和添加到第三个电子邮件客户端中?...

我们常见的第三方邮箱客户端有&#xff1a;Outlook、Thunderbird、Live mail、Web客户端、畅邮&#xff08;DM Pro&#xff09;客户端等&#xff0c;下面用畅邮&#xff08;DM Pro&#xff09;为例。流程一.配置信息企业邮箱POP、SMTP、IMAP地址列表如下&#xff1a;&#xff0…

银河麒麟V10入选2020中国十大科技新闻

日前&#xff0c;中央电视台、中央人民广播电台、中国国际广播电台、中国国际电视台联合评选了“2020十大国内科技新闻”&#xff0c;“银河麒麟操作系统V10”与其他国家科技领域重大成榜上有名。8月13日&#xff0c;银河麒麟操作系统V10发布后&#xff0c;央视新闻、人民日报、…

微型计算机硬件采用什么,微型计算机的硬件系统包括什么?

微型计算机的硬件系统包括cpu、存储器、输入设备、输出设备四大部分。CPU是计算机硬件的核心&#xff0c;控制着整个计算机系统的工作&#xff1b;存储器是计算机中的记忆存储部件&#xff1b;输入设备是计算机与用户或其他设备通信的桥梁&#xff1b;输出设备是计算机硬件系统…

如何在 C# 中使用 反射

C# 中的 反射 常用于在程序的运行时获取 类型 的元数据&#xff0c;可获取的信息包括已加载到进程中的 程序集 和 类型 信息&#xff0c;它和 C 中的 RTTI&#xff08;Runtime Type Information&#xff09; 的作用是差不多的。为了能够使用反射&#xff0c;需要在项目中引用 S…

计算机基础知识教案1,计算机基础知识教案一

计算机基础知识教案一 秦皇岛外国语职业学院教案首页 年 月 日 学科 计算机基础 年级 专一 课题 计算机基础知识 教具 课时分配 2课时 教学目的 1. 了解计算机的发展、特点和应用 2&#xff0e; 掌握计算机的系统组成 3&#xff0e; 掌握各个逻辑部件性能指标和作用 教学重点、…

C# 9 新特性——init only setter

C# 9 新特性——init only setterIntroC# 9 中新支持了 init 关键字&#xff0c;这是一个特殊的 setter&#xff0c;用来指定只能在对象初始化的时候进行赋值&#xff0c;另外支持构造器简化的写法&#xff0c;Target-typed new expression 在已知类型的情况下可以使用 new() 来…

乔安监控云存储_智能运维丨全栈监控,护航云上业务

自2006年至今&#xff0c;云计算商用领域已经历了十余载的发展。云也从概念普及进入到广泛应用阶段&#xff0c;云服务变成了像水电一样的基础服务&#xff0c;已经是行业共识。云发展速度快、成长空间大&#xff0c;监控场景复杂最新Gartner的报告预测到2019年公有云市场将达到…

阿星Plus:基于abp vNext开源一个博客网站

作为微软最早迈向开源的重要软件之一&#xff0c;.NET 5的发布具有重要意义&#xff01;微软希望 .NET Framework 开发者能够迁移他们的代码和应用到 .NET 5.0 上&#xff0c;为明年发布的 .NET 6.0 将 Xamarin 开发者过渡到统一平台奠定基础。这里推荐一个.NET Core 3.1 开源的…

html 保存文件指定路径,78.上传文件及在服务器保存文件到任意路径

上传文件到服务器是一个常用的操作&#xff0c;而在服务器上保存文件就需要多多用心了。因为你不可能只在一个路径里保存文件&#xff0c;所以需要实践一下保存文件到任意位置。当然&#xff0c;前提是你的应用程序有这样的操作权限。首先建立一个main.go文件&#xff0c;作为项…

excel表格中添加combobox_Excel中两个表格对比,找出不同数据

当你有两个Excel工作簿需要对比数据时候&#xff0c;你该怎么做呢&#xff1f;数据少&#xff0c;我们直接用眼睛就可以看到&#xff0c;数据如果太多&#xff0c;那么对比找出不同数据&#xff0c;是一件非常困难的事情。今天&#xff0c;小汪老师就来教大家几招&#xff0c;可…