博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【源码解读】Vue与ASP.NET Core WebAPI的集成
阅读量:4035 次
发布时间:2019-05-24

本文共 12878 字,大约阅读时间需要 42 分钟。

在前面博文【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
 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
> 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);                           });}
  • 所有的后续请求,都会类似 nginx 一样的操作:

public static async Task
 PerformProxyRequest(    HttpContext context,    HttpClient httpClient,    Task
 baseUriTask,    CancellationToken applicationStoppingToken,    bool proxy404s){    //省略部分代码...    //获取task的结果,即开发服务器uri    var 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
 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
 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
(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
 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
 WaitForMatch(Regex regex)    {        var tcs = new TaskCompletionSource
();        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
 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://akudi.baihongyu.com/

你可能感兴趣的文章
微视linux 进程的当前目录
查看>>
慢慢欣赏linux 页面回收续
查看>>
微视linux内核 x86_32内核启动
查看>>
链接脚本
查看>>
微视linux 内存水线划分
查看>>
微视linux 文件系统之打开文件返回只读
查看>>
微视linux 文件系统之打开文件create模式
查看>>
微视linux bash返回值
查看>>
慢慢欣赏linux ext3文件系统 直接块和间接块
查看>>
好用的博客和网站
查看>>
创建特定文件
查看>>
linux内核版本号
查看>>
mips指令学习
查看>>
慢慢欣赏linux ext3文件系统 获取日志块
查看>>
linux kernel 内存越界和泄漏的工具
查看>>
慢慢欣赏linux cgroup
查看>>
微视linux tmpfs文件系统
查看>>
慢慢欣赏linux 内存被改写
查看>>
慢慢欣赏linux make menuconfig流程
查看>>
慢慢欣赏linux 内核保留内存
查看>>