整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

前后端分离实践的架构设计

前后端分离实践的架构设计

后端分离的项目开发策略已经不是什么新鲜东西了,网上介绍这方面的文章非常多。我自己是在14年的时候接触到的,对这种开发策略一直爱不释手,不管新老项目都会首先用前后端分离的思维先去思考一番。从14年到现在在前后分离上面也实践了近3年的时间,项目大大小小的也差不多4,5个吧,但是却从来没有一个是自己觉得很满意的,其中的原由和心酸可能只有自己才能体会了。

前后端分离实践有感

现在到处都是前后端分离的实践。然而一些项目在从一体化 Web 设计转向前后端分离的架构时,仍然会碰见各种各样的问题。由于层出不穷的问题,甚至会有团队质疑,一体化好好的,为什么要前后端分离?

首先看看前后端分离是什么?

“前端”通常指的是,相对来说更接近用户的一端,例如:APP,网页、桌面程序等,在现实开发中大部分情况可以理解为“客户端”;

“后端”相对来说就更泛化了,可以理解为是为前端提供服务的一端。

”分离“顾名思义就是将”前端“和”后端进行分开“,但是这里的分开主要从下面几个纬度进行分离

  1. 架构分离,前端不需要依赖后端架构同时后端也不需要知道前端使用何种架构
  2. 人员分离,前端后端使用的技术相互之间根部不需要相互了解完全可以在做到透明(当然相互了解会更好)
  3. 工作分离,基于项目或者产品的单个功能的横向进行工作分离,任务划分更细
  4. 关注点分离,前端偏向用户,后端偏向系统本身

下面分别是一体式web架构示意图和前后分离式web架构。

一体式 Web 架构示意

一体式 Web 架构示意

为什么要前后端分离

下面讲讲什么时候需要前后端分离,即前后端分离的应用场景。

说起这个问题,我想到了多年前,公司在以 .NET 开发团队为主的基础上扩展了 Java 团队,两个团队虽然是在做不同的产品,但是仍然存在大量重复性的开发,比如用 ASP.NET WebPage 写了组织机构相关的页面,用 JSP 又要再写一遍。在这种情况下,团队就开始思考这样一个方案:如果前端实现与后端技术无关,那页面呈现的部分就可以共用,不同的后端技术只需要实现后端业务逻辑就好。

方案根本要解决的问题是把数据和页面剥离开来。应对这种需求的技术是现成的,前端采用静态网页相关的技术,HTML + CSS + JavaScript,通过 AJAX 技术调用后端提供的业务接口。前后端协商好接口方式通过 HTTP 提供,统一使用 POST 谓词。接口数据结构使用 XML 实现,前端 jQuery 解析 XML 很方便,后端对 XML 的处理工具就更多了如JSON 。

这种架构从本质上来说就是 SOA(面向服务的架构)。当后端不提供页面,只是纯粹的通过 Web API 来提供数据和业务交互能力之后,Web 前端就成了纯粹的客户端角色,与 WinForm、移动终端应用属于同样的角色,可以把它们合在一起,统称为前端。以前的一体化架构需要定制页面来实现 Web 应用,同时又定义一套

WebService/WSDL 来对 WinForm 和移动终端提供服务。转换为新的架构之后,可以统一使用 Web API 形式为所有类型的前端提供服务。至于某些类型的前端对这个 Web API 进行的 RPC 封装,那又是另外一回事了。

通过这样的架构改造,前后端实际就已经分离开了。抛开其它类型的前端不提,这里只讨论 Web 前端和后端。由于分离,Web 前端在开发的时候压根不需要了解后端是用的什么技术,只需要后端提供了什么样的接口可以用来做什么事情就好。前后端分离之后,由于技术和业务都更专注,开发效率也提高了。分离带来的好处渐渐体现出来:

1. 前后职责分离

前端倾向于呈现,着重处理用户体验相关的问题;后端则倾处于业务逻辑、数据处理和持久化等。在设计清晰的情况下,后端只需要以数据为中心对业务处理算法负责,并按约定为前端提供 API 接口;而前端使用这些接口对用户体验负责。

2. 前后技术分离

前端可以不用了解后端技术,也不关心后端具体用什么技术来实现,只需要会 HTML/CSS/JavaScript 就能入手;而后端只需要关心后端开发技术。

3. 前后分离带来了用户用户体验和业务处理解耦

前端可以根据用户不同时期的体验需求迅速改版,后端对此毫无压力。同理,后端进行的业务逻辑升级,数据持久方案变更,只要不影响到接口,前端可以毫不知情。

前后端分离架构

任何技术方案都不是万能的,前后端分离带来了好处,同时也带来矛盾。我们在实践初期,由于前端团队力量相对薄弱,同时按照惯例,所有业务处理几乎都是由后端来设计的,前端处理过程中常常发现接口定义不符合用户操作流程等问题。毕竟后端思维和前端思维还是有所不同——前端思维倾向于用户体验,而后端思维则更倾向于业务的技术实现。

除此之外,由于前后分离本质上是一种 SOA 架构,所以在授权上也需要按 SOA 架构的方式来思考。Cookie/Session 的方式不是特别合适,相对来说,基于 Token 的认证则更适合一些。采用基于 Token 的认证就意味着后端的认证部分需要重写……后端当然不想重写,于是会将皮球踢给前端……于是前端开始报怨(悲剧)……

谁来主导

这些矛盾的出现,归根结底在于设计不够清晰明确。一般在开发过程中,主导者应该是架构师。然而大部分场景中,架构师往往也是开发人员,所以他们的主要技术栈会极大的影响前后端在整个项目中的主次作用。这位骨干处于哪端,开发的便捷性就会向哪端倾斜。这是一个不好的现象。

如果没有良好的流程规范,多数应用产品的开发通常前端接触的到角色会比后端更多。

  1. 前端开发人员会受到产品经理或客户的影响:这个地方应该放个按钮……;
  2. 前端还要与美工对接——这样的设计不好实现,是否可以改成那样?客户要求必须这么操作,但是这个设计做不到;
  3. 前端还要跟后端对接

换句话说,前端可以成为项目沟通的中心,因此前端比后端更合适承担主导的角色。

接口设计

接口分后端服务实现和前端调用两个部分,技术上并不难,因为都是成熟的技术,接口设计才是难点。前面提到前后端会产生一些矛盾。从前端的角度来看,重点关注的是用户体验;而从后端的角度来看,重点关注的是数据完整、有效、安全。解决这些矛盾的着眼点就是接口设计。

接口设计时,其粒度的大小往往代表了前后端工作量的大小:接口粒度太小,前端要处理的事情就多,尤其是对各种异步处理就可能会感到应接不暇;粒度太大,就会出现高耦合,降低灵活性和扩展性,当然这种情况下后端的工作就轻松不了。业务层面的东西涉及到具体的产品,这里不多做讨论。这里主要讨论一点点技术层面的东西。

就形式上来说,Web API 可以定义成 REST,也可以是 RPC,只要前后端商议确定下来就行。更重要的是在输入参数和输出结果上,最好一开始就有相对固定的定义,一般来说取决于前端架构或采用的 UI 框架。

常见请求参数的数据形式如下所示:

  1. 键值对,用于 URL 中的 QueryString 或者 POST 等方法的 Payload
  2. XML/JSON/...,通常用于 POST 等方法的 Payload
  3. ROUTE,由后端路由解析 URL 取得,在 RESTful 中常用

而服务器响应的数据形式就更多了,通常一个完整的响应需要包括状态码、消息、数据三个部分的内容,其中

  • 状态码:HTTP 状态码或响应数据中特定的状态属性
  • 消息:通常是放在响应内容中,作为数据的一部分
  • 数据:根据接口协议,可能是各种格式,当前最流行的是 JSON

我们在实践中使用 JSON 形式,最初定义了下面这种形式

code 主要用于指导前端进行一些特殊的操作,比如 0 表示 API 调用成功,非0 表示调用失败,其中 1 表示需要登录、2 表示未获取授权……对于这个定义,前端拿到响应之后,就可以在应用框架层进行一些常规处理,比如当 code 为 1 的时候,弹出登录窗口请用户在当前页面登录,而当 code 为 2 的时候,则弹出消息提示并后附链接引导用户获取授权。

一开始这样做并没有什么问题,直到前端框架换用了 jQuery EasyUI。以 EasyUI 为例的好多 UI 库都支持为组件配置数据 URL,它会自动通过 AJAX 来获取数据,但对数据结构有要求。如果仍然采用之前设计的响应结构,就需要为组件定义数据过滤器(filter)来处理响应结果,这样做写 filter 以及为组件声明 filter 的工作量也是不小的。为了减少这部分工作量我们决定改一改接口。

新的接口是一种可变结构,正常情况下返回 UI 需要的数据结构,出错的情况则响应一个类型于原定结构的数据结构:

对于新响应数据结构,前端框架只需要判断一下是否存在 error 属性,如果存在,检查其 identity 属性是否为指定的特殊值,然后再使用其 code 和 message 属性处理错误。这个错误判断过程略为复杂一些,但可以由前端应用框架统一处理。

使用 RESTful 风格的接口,部分状态码可以用 HTTP 状态码代替,比如 401 表示需要登录,403 就可以表示没有获得授权,当然,虽然 HTTP 状态码与 RESTful 风格更配,但是非 RESTful 风格也可以使用 HTTP 状态码来代替 error.code。

用户认证

认证方案很多,比如 Cookie/Session 在某些环境下仍然可行、也可以使用基于 Token 和 OAuth 或者 JWT,下面是几种方案的介绍。

a.基于 OAuth 的认证方案

目前各大网站的开放式接口都是 SOA 架构,如果把这些开放式接口看作提供服务方(服务端),而把使用这些开放式接口的应用看作客户端,那么就可以产生这样一种和前后分离对应的关系:

所以,开放式接口广泛使用的 OAuth 方案用于前后分离是可行的,但在具体实施上却并不是那么容易。尤其是在安全性上,由于前端是完全暴露在外的,与 OAuth 通常实施的环境(后端?服务端)相比,要注意的是首次认证不是使用已注册的 AppID 和 AppToken,而是使用用户名和密码。

b.基于 Token/JWT 的认证方案

虽然这个方案放在最后,但这个方案却是目前前后端分离最适合的方案。基于 Token 的认证方案,各种讨论由来已久,而 JWT 是相对较为成熟,也得到多数人认可的一种。从网上可以找到各种技术栈的 JWT 实现,应用起来也比较方便。

前后端分离的测试

前后分离之后,前端的测试将以用户体验测试和集成测试为主,而后端则主要是进行单元测试和 Web API 接口测试。与一体化的 Web 应用相比,多了一层接口测试,这一层测试可以完全自动化,一旦完成测试开发,就能在很大程度上控制住业务处理和数据错误。这样一来,集成测试的工作量会相对单一也容易得多。

前端测试的工作相对来说减轻不了多少,前后分离之后的前端部分承担了原来的集成测试工作。但是在假设 Web API 正确的情况下进行集成测试,工作量是可以减轻不少的,用例可以只关注前端体验性的问题,比如呈现是否正确,跳转是否正确,用户的操作步骤是否符合要求以及提示信息是否准确等等。

对于用户输入有效性验证这部分工作在项目时间紧迫的情况下甚至都可以完全抛给 Web API 去处理。不管是否前后端分离,Web 开发中都有一个共识:永远不要相信前端!既然后端必须保证数据的安全性和有效性,那么前端省略这一步骤并不会对后端造成什么实质性的威胁,最多只是用户体验差一点。但是,如果前后端都要做数据有效性验证,那一定要严格按照文档来进行,不然很容易出现前后端数据验证不一致的情况(这不是前后分离的问题,一体化架构同样存在这个问题)。

小结

总的来说,前后分离所带来的好处还是很明显的。但是具体实施的时候需要一个全新的思考方式,而不是基于原有一体化 Web 开发方式来进行思考。

作者:Alukar

链接:https://www.jianshu.com/p/3c7dafdc3576

来源:简书

在使用了Spring Boot数月之后, 我发觉ASP.NET Core中缺失了对面向切面编程(AOP)的默认支持。

维基百科中针对AOP的定义:

面向切面编程(AOP)是一种编程范例,其旨在通过允许跨领域关注点的分离来提高模块化。它通过“切入点”规范指定要修改的代码,不修改源代码本身的情况下,向现有代码提供额外行为,例如使用日志的方式记录为所有以"set"开头的方法调用记录。使用该方式,你可以向核心业务逻辑中追加一些不太重要的功能(例如日志),而不会使代码混乱。AOP为面向切换的软件开发奠定了基础。

以下是AOP的一些常用场景

  • 日志审计
  • 事务管理
  • 安全

代理模式(Proxy Pattern)也常用于Mocking(例如Moq, NSubstitute等)和延时加载(Lazy Loading)(例如EF Core, NHierante等)

C#中实现AOP

C#中其实已经支持AOP了,你可以快速Google搜索一下,AOP的实现方式有2种 RealProxy 真实代理和 MarshalByRefObject .技术上讲,他们都可以在本地和远程使用,它看起来非常的美好,直到你明白的你的所有目标对象都必须继承 MarshalByRefObject 。仅此一点,就让大部分人不会考虑这种实现方式。

库更好的实现方式

幸运的是,我们在C#中可以使用一种更好的方式创建代理对象,即使用 Castle.DynamicProxy 库。

Castle.DynamicProxy 是一个用于在运行时生成轻量级.NET代理的库。生成代理对象允许你在不修改原始代码的情况下拦截对对象成员的调用,只有virtual对象成员才能被拦截。- Castle Project

使用Castle提供的动态代理,你可以为抽象类、接口(同时提供实现)以及带有virtual方法/属性的普通类创建代理对象。

以下是一个例子,这里我们假设创建了一个处理博客文章的服务应用。

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public bool Disabled { get; set; }
    public DateTime Created { get; set; }
}

public interface IBlogService
{
    void DisablePost(BlogPost post);
    BlogPost GetPost(int id);
}

public class BlogService : IBlogService
{
    public BlogPost GetPost(int id)
    {
        return new BlogPost
        {
            Id=id,
            Title="Test",
            Description="Test",
            Disabled=false,
            Created=DateTime.UtcNow
        };
    }

    public void DisablePost(BlogPost post)
    {
        post.Disabled=true;
    }
}

通常,你会将 BlogService 类注册为 IBlogService 接口的实现,一切都运转的非常正常。但是现在,你希望代理这个接口,当接口中任何方法被调用的时候,做点什么事情。

这里,我们首先创建一个拦截器对象以便拦截方法调用,就像 RealProxy 一样

public class LoggingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine($"正在调用方法 {invocation.TargetType}.{invocation.Method.Name}.");
        invocation.Proceed(); // 执行当前被拦截的方法
    }
}

然后,我们将使用一个代理生成器生成代理对象。

var generator=new ProxyGenerator();
var actual=new BlogService();
var proxiedService=(IBlogService)proxyGenerator.CreateInterfaceProxyWithTarget(typeof(IBlogService), actual, new LoggingInterceptor());
// 使用proxiedService对象和你平常使用IBlogService对象是一样的

现在我们就创建出了一个实现了 IBlogService 接口的代理对象,其中包含了内部实现 BlogService 。当任何一个接口方法被调用的时候, LoggingInterceptor.Intercept 方法就会被调用,当拦截器调用 invocation.Proceed() 方法时,它在 BlogService 类中的具体实现方法就会被调用。

如何在ASP.NET Core中使用Castle实现AOP

在ASP.NET Core中使用Castle实现AOP的实现思路是, 始终使用ASP.NET Core的IOC容器来创建代理服务。虽然Castle项目中包含它自己的IOC容器 Castle Windor , 使得注入代理更加的容易,但是我们暂时不使用它。

这里,我们首先为我们的 LoggingInterceptor 添加一个简单的依赖以展示我们如何使用ASP.NET Core自带的DI来处理依赖问题。因为现实中,你的大部分拦截器都是需要一个或多个依赖项的。

public class LoggingInterceptor : IInterceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;

    public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
    {
        _logger=logger;
    }

    public void Intercept(IInvocation invocation)
    {
        _logger.LogDebug($"Calling method {invocation.TargetType}.{invocation.Method.Name}.");
        invocation.Proceed();
    }
}

第二步,我们在依赖注入容器中注册一个单例的 ProxyGenerator 对象,以及我们即将使用的所有的拦截器对象

services.AddSingleton(new ProxyGenerator());
services.AddScoped<IInterceptor, LoggingInterceptor>();

最后,我们创建一个扩展方法 AddProxiedScoped , 并使用它注册其他所有服务。

public static class ServicesExtensions
{
    public static void AddProxiedScoped<TInterface, TImplementation>(this IServiceCollection services)
        where TInterface : class
        where TImplementation : class, TInterface
    {
        services.AddScoped<TImplementation>();
        services.AddScoped(typeof(TInterface), serviceProvider=>
        {
            var proxyGenerator=serviceProvider.GetRequiredService<ProxyGenerator>();
            var actual=serviceProvider.GetRequiredService<TImplementation>();
            var interceptors=serviceProvider.GetServices<IInterceptor>().ToArray();
            return proxyGenerator.CreateInterfaceProxyWithTarget(typeof(TInterface), actual, interceptors);
        });
    }
}

// In ConfigureServices
services.AddProxiedScoped<IBlogService, BlogService>();

这里,让我们看看它是如何工作的

  1. 我们注册具体实现(例如 BlogService )。这是因为具体实现可能也需要使用依赖注入容器解决依赖问题。
  2. 每当从依赖注入容器中尝试获取接口对象的时候:ProxyGenerator

现在,我们无论何时需要一个 IBlogService 接口对象,都可以通过依赖注入容器得到一个代理对象,这个代理对象会先经过所有的拦截器,然后调用 BlogService 中定义的实际方法。

但是这里,相较与 Spring ,在ASP.NET Core中实现AOP还不够简单直接,但是我们可以轻松将其转换为简单的“框架”,我们可以使用 Castle.DynamicProxy 的一些特定方法,来执行一些更高级的操作。

原文地址: ASPECT ORIENTED PROGRAMMING USING PROXIES IN ASP.NET CORE

https://blog.zhaytam.com/2020/08/18/aspnetcore-dynamic-proxies-for-aop/

原文作者:ZANID HAYTAM

译文地址: 如何在ASP.NET Core中实现面向切面编程(AOP)

https://www.cnblogs.com/lwqlun/p/aop_in_asp_net_core.html

译文作者:Lamond Lu

spose.Words for .NET提供了一套完整的功能,用于在多个.NET应用程序中操作和转换MS Word文档。您可以在桌面或Web应用程序中创建新的或编辑现有的Word文档。

在本文中,将展示如何利用Aspose.Words for .NET的字处理功能,以及如何在ASP.NET MVC中创建基于Web的MS Word编辑器。

Aspose.Words for .NET已升级至V20.4,如果你还没有用过Aspose.Words可以点击文末“了解更多”下载最新版测试。

在ASP.NET MVC中创建MS Word编辑器的步骤

为了演示,将在此应用程序中使用了基于JavaScript的Suneditor WYSIWYG编辑器。您可以使用相同的内容,也可以选择其他任何适合您要求的HTML编辑器。以下是创建ASP.NET Word编辑器的步骤。

在Visual Studio中创建一个新的ASP.NET Core Web应用程序。

选择 Web应用程序(模型-视图-控制器) 模板。

下载所见即所得编辑器的文件,并将其保存在 wwwroot 目录中。

打开NuGet软件包管理器,然后安装Aspose.Words for .NET软件包。

在index.cshtml 视图中添加以下脚本。

 @{   ViewData["Title"]="Word Editor in ASP.NET";   }   <div class="row">   <div class="col-md-12">   <form asp-controller="Home" asp-action="UploadFile" method="post" class="form-inline"   enctype="multipart/form-data">   <br />   <div class="form-group">   <input type="file" name="file" accept=".doc, .docx" class="form-control custom-file-input" />   div>   <div class="form-group">   <button type="submit" class="form-control btn btn-primary">Openbutton>   div>   <div class="form-group" style="position:relative; float :right">   <button type="button" id="download" class="form-control btn btn-success" value="Save and Download">Save and Downloadbutton>   div>   form>   <br />   <form method="post" asp-action="Index" id="formDownload">   <textarea name="editor" id="editor" rows="80" cols="100">   @if (ViewBag.HtmlContent==null)   {   <p>Write something or open an existing Word document. p>   }   else   {   @ViewBag.HtmlContent;   }   textarea>   form>   div>   div>       <link href="~/suneditor/dist/css/suneditor.min.css" rel="stylesheet">       <script src="~/suneditor/dist/suneditor.min.js">script>   <script>   var suneditor=SUNEDITOR.create('editor', {   display: 'block',   width: '100%',   height: '30%',   popupDisplay: 'full',   buttonList: [   ['font', 'fontSize', 'formatBlock'],   ['paragraphStyle', 'blockquote'],   ['bold', 'underline', 'align', 'strike', 'subscript', 'superscript', 'horizontalRule', 'list'],   ['table', 'link', 'image'],   ['align', 'horizontalRule', 'list', 'lineHeight'],   ['codeView']   ],   placeholder: 'Start typing something...'   });   script>   <script>   $(document).ready(function () {   $("#download").click(function () {   suneditor.save();   $("#formDownload").submit();   });   });   script>

在HomeController.cs 控制器中添加以下方法 。

 [HttpPost]   public FileResult Index(string editor)   {   try   {   // Create a unique file name   string fileName=Guid.NewGuid() + ".docx";   // Convert HTML text to byte array   byte[] byteArray=Encoding.UTF8.GetBytes(editor.Contains("") ? editor : "" + editor + "");   // Generate Word document from the HTML   MemoryStream stream=new MemoryStream(byteArray);   Document Document=new Document(stream);   // Create memory stream for the Word file   var outputStream=new MemoryStream();   Document.Save(outputStream, SaveFormat.Docx);   outputStream.Position=0;   // Return generated Word file   return File(outputStream, System.Net.Mime.MediaTypeNames.Application.Rtf, fileName);   }   catch (Exception exp)   {   return null;   }   }   [HttpPost]   public ViewResult UploadFile(IFormFile file)   {   // Set file path   var path=Path.Combine("wwwroot/uploads", file.FileName);   using (var stream=new FileStream(path, FileMode.Create))   {   file.CopyTo(stream);   }   // Load Word document   Document doc=new Document(path);   var outStream=new MemoryStream();   // Set HTML options   HtmlSaveOptions opt=new HtmlSaveOptions();   opt.ExportImagesAsBase64=true;   opt.ExportFontsAsBase64=true;   // Convert Word document to HTML   doc.Save(outStream, opt);   // Read text from stream   outStream.Position=0;   using(StreamReader reader=new StreamReader(outStream))   {   ViewBag.HtmlContent=reader.ReadToEnd();   }   return View("Index");   }

在您喜欢的浏览器中生成并运行该应用程序。

演示

以下是如何在ASP.NET Word编辑器中创建或编辑Word文档的演示。

创建一个Word文档

编辑Word文档

如果您有任何疑问或需求,请随时加入Aspose技术交流群(642018183),我们很高兴为您提供查询和咨询。