VueJs + MVC código mínimo funcionalidade máxima

Boa tarde


Eu usei o WPF por muitos anos. O padrão MVVM é provavelmente um dos padrões arquiteturais mais convenientes. Eu assumi que o MVC é quase o mesmo. Quando vi o uso do MVC na prática em um novo local de trabalho, fiquei surpreso com a complexidade e ao mesmo tempo a falta de usabilidade elementar. O mais irritante é que a validação ocorre apenas quando o formulário está sobrecarregado. Não há quadros vermelhos destacando o campo em que o erro, mas apenas um alerta com uma lista de erros. Se houver muitos erros, será necessário corrigir alguns erros e salvar para salvar para repetir a validação. O botão Salvar está sempre ativo. Listas vinculadas são realmente implementadas através de js, mas são complicadas e confusas. O modelo, a vista e o controlador estão fortemente acoplados, portanto, teste tudo magnificência muito dificil
Como lidar com isso? Para quem é interessante, pergunto sob kat.


E assim temos:
A construção de formulários MVC de forma clássica não implica outra maneira de interagir com o servidor, sobrecarregando a página inteira, o que não é conveniente para o usuário.
O uso completo de estruturas como Reart, Angular, Vue e a transição para SinglePageApplicatrion tornaria possível a criação de interfaces mais convenientes, mas, infelizmente, em princípio, não é possível na estrutura deste projeto, pois:
- Muitos códigos foram escritos, aceitos e ninguém permitirá que você os refaça.
-Nós somos programadores em C # e não conhecemos js na quantidade certa.


Além disso, as estruturas Reart, Angular, Vue são aprimoradas para escrever lógica complexa no cliente, o que, na minha aparência do WPF, não está correto. Toda a lógica deve estar em um local e este é um objeto de negócios e / ou classe de modelo. A visualização deve exibir apenas o estado do modelo.
Com base no exposto, tentei encontrar uma abordagem que permita obter a funcionalidade máxima com o mínimo de código js. Primeiro de tudo, o código mínimo que precisa ser gravado para gerar e atualizar um campo específico.
Meu pacote VueJs + MVC proposto é assim:


  • O VueJs é usado na versão mais simples com conexão via cdn. Se necessário, os componentes podem ser conectados via CDN.
  • Após o carregamento, o Vue carrega os dados do formulário através do Ajax.
  • Sempre que o formulário é alterado, o Vue envia todas as alterações para o servidor (para campos de texto, você pode configurar que as alterações sejam enviadas quando o foco for perdido).
  • A validação ocorre no servidor através do mecanismo de entidade e os campos inválidos são retornados ao cliente e um sinal de que o estado do modelo foi alterado em relação ao banco de dados.
    -Se a próxima solicitação de validação ocorrer antes da anterior, a solicitação de validação anterior será cancelada.
    O modelo MVC não é usado. A função ViewModel no sentido WPF é borrada aqui entre o vue e o controlador.
    As vantagens dessa implementação sobre a página clássica do Razor:
  • A interface é desenhada usando as ferramentas Vue, projetadas para desenhar interfaces. A principal vantagem.
  • Separando as camadas do ViewModel.
  • erros de validação são exibidos à medida que o formulário é preenchido.
  • teste de conveniência
    Desvantagens:
  • Carga excessiva no servidor com solicitações de validação.
  • A necessidade de conhecer vue e js no mínimo.


    Considero essa abordagem como o modelo inicial para trabalhar com o formulário.
    Em um aplicativo real para um formulário específico, é desejável otimizar:
    1) Envie uma solicitação de validação apenas ao alterar os campos, que devem ser validados no servidor.
    2) A validação é longa, os campos estão cheios, etc. executado no cliente.



Então vamos lá.


No meu exemplo, usei o banco de dados de treinamento Northwind, que baixei com um dos exemplos do Devextreem.
A criação do aplicativo, a conexão da Entidade e a criação do DbContext I serão deixadas nos bastidores. Link para o github com um exemplo no final do artigo.
Crie um novo controlador vazio do MVC 5. Chame-o OrdersController. Até agora, existe um método.


public ActionResult Index() { return View(); } 

Adicione mais um


  public ActionResult Edit() { return View(); } 

Agora você precisa ir para a pasta Views / Orders e adicionar duas páginas Index.cshtml e Edit.cshtml
Uma observação importante de que uma página cshtml funcionaria sem um modelo deve ser adicionada à parte superior da página herdada System.Web.Mvc.WebViewPage.
Supõe-se que Index.cshtml contenha uma tabela da qual uma linha destacada irá para a página de edição. Por enquanto, basta criar links que levarão à página de edição.


 @inherits System.Web.Mvc.WebViewPage <table > @foreach (var item in ViewBag.Orders) { <tr><td><a href="Edit?id=@item.OrderID">@item.OrderID</a></td></tr> } </table> 

Agora eu quero implementar a edição de um objeto existente.


A primeira coisa a fazer é descrever um método no controlador que retornaria uma descrição do objeto para o cliente Json por identificador.


  [HttpGet] public ActionResult GetById(int id) { var order = _db.Orders.Find(id);//  string orderStr = JsonConvert.SerializeObject(order);//  return Content(orderStr, "application/json");// } 

Você pode verificar se tudo funciona digitando no navegador (o número da porta é naturalmente seu) http: // localhost: 63164 / Orders / GetById? Id = 10501
Você deve obter algo assim no navegador


 { "OrderID": 10501, "CustomerID": "BLAUS", "EmployeeID": 9, "OrderDate": "1997-04-09T00:00:00", "RequiredDate": "1997-05-07T00:00:00", "ShippedDate": "1997-04-16T00:00:00", "ShipVia": 3, "Freight": 8.85, "ShipName": "Blauer See Delikatessen", "ShipAddress": "Forsterstr. 57", "ShipCity": "Mannheim", "ShipRegion": null, "ShipPostalCode": "68306", "ShipCountry": "Germany" } 

Bem e (ou) escrevendo um teste simples. No entanto, vamos deixar o teste além do escopo deste artigo.


  [Test] public void OrderControllerGetByIdTest() { var bdContext = new Northwind(); var id = bdContext.Orders.First().OrderID; //    var orderController = new OrdersController(); var json = orderController.GetById(id) as ContentResult; var res = JsonConvert.DeserializeObject(json.Content,typeof(Order)) as Order; Assert.AreEqual(id, res.OrderID); } 

Em seguida, você precisa criar um formulário do Vue.


 @inherits System.Web.Mvc.WebViewPage <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> </title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <h1>A  </h1> <table > <tr v-for="(item,i) in order"> @*      *@ <td> {{i}}</td> <td> <input type="text" v-model="order[i]"/> </td> </tr> </table> </div> <script> new Vue({ el: "#app", data: { order: { OrderID: 10501, CustomerID: "BLAUS", EmployeeID: 9, OrderDate: "1997-04-09T00:00:00", RequiredDate: "1997-05-07T00:00:00", ShippedDate: "1997-04-16T00:00:00", ShipVia: 3, Freight: 8.85, ShipName: "Blauer See Delikatessen", ShipAddress: "Forsterstr. 57", ShipCity: "Mannheim", ShipRegion: null, ShipPostalCode: "68306", ShipCountry: "Germany" } } }); </script> </body> </html> 

Se tudo for feito corretamente, o protótipo do formulário futuro deverá ser exibido no navegador.



Como podemos ver, o Vue exibiu todos os campos exatamente como o modelo. Mas os dados no modelo ainda são estáticos e a primeira coisa a fazer é implementar o carregamento de dados do banco de dados através do método acabado de escrever.
Para fazer isso, adicione o método fetchOrder () e chame-o na seção montada:


  new Vue({ el: "#app", data: { id: @ViewBag.Id, order: { OrderID: 0, CustomerID: "", EmployeeID: 0, OrderDate: "", RequiredDate: "", ShippedDate: "", ShipVia: 0, Freight: 0, ShipName: "0", ShipAddress: "", ShipCity: "", ShipRegion: null, ShipPostalCode: "", ShipCountry: "" }, }, methods: { //  fetchOrder() { var path = "../Orders/GetById?key=" + this.id; console.log(path); this.fetchJson(path, json => this.order = json); }, //    fetch fetchJson(path, collback) { try { fetch(path, { mode: 'cors' }) .then(response => response.json()) .then(function(json) { collback(json); } ); } catch (ex) { alert(ex); } } }, mounted: function() { this.fetchOrder(); } }); 

Bem, como o identificador do objeto agora deve vir do controlador, no controlador, você precisa passar o identificador para o objeto dinâmico do ViewBag para que ele possa ser obtido no modo de exibição.


  public ActionResult SimpleEdit(int id = 0) { ViewBag.Id = id; return View(); } 

Isso é suficiente para ler os dados no momento da inicialização.
É hora de personalizar o formulário.
Para não sobrecarregar o artigo, deduzi um mínimo de campos. Sugiro que os iniciantes descubram como trabalhar com listas vinculadas.


  <table > <tr> <td> </td> <td > <input type="number" v-model="order.Freight" /> </td> </tr> <tr> <td>  </td> <td> <input type="text" v-model="order.ShipCountry" /> </td> </tr> <tr> <td> </td> <td> <input type="text" v-model="order.ShipCity" /> </td> </tr> <tr> <td> </td> <td> <input type="text" v-model="order.ShipAddress" /> </td> </tr> </table> 

Os campos ShipCountry e ShipAddress são os melhores candidatos para listas vinculadas.
Aqui estão os métodos do controlador. Como você pode ver, tudo é bem simples, toda a filtragem é feita usando o Linq.


  /// <summary> ///    c     ///       ,    /// </summary> /// <param name="country"></param> /// <param name="region"></param> /// <returns></returns> [HttpGet] public ActionResult AvaiableCityList( string country,string region=null) { var avaiableCity = _db.Orders.Where(c => ((c.ShipRegion == region) || region == null)&& (c.ShipCountry == country) || country == null).Select(a => a.ShipCity).Distinct(); var jsonStr = JsonConvert.SerializeObject(avaiableCity); return Content(jsonStr, "application/json"); } /// <summary> ///    c   ///    ,    /// </summary> /// <param name="region"></param> /// <returns></returns> [HttpGet] public ActionResult AvaiableCountrys(string region=null) { var resList = _db.Orders.Where(c => (c.ShipRegion == region)||region==null).Select(c => c.ShipCountry).Distinct(); var json = JsonConvert.SerializeObject(resList); return Content(json, "application/json"); } 

Mas no código View foi adicionado significativamente mais.
Além das funções dos países e cidades, é necessário adicionar um relógio que monitora as alterações do objeto; infelizmente, o valor antigo do objeto vue complexo não salva, portanto, você deve salvá-lo manualmente, para o qual criei o método saveOldOrderValue: enquanto salvo apenas o país nele. Isso permite reler a lista de cidades somente quando o país muda. Caso contrário, o código é o mesmo, eu acho. No exemplo, mostrei apenas uma lista vinculada de nível único (por esse princípio não é difícil fazer o aninhamento de nenhum nível).


 @inherits System.Web.Mvc.WebViewPage <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> </title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <table> <tr> <td>C </td> <td> <input type="number" v-model="order.Freight" /> </td> </tr> <tr> <td>  </td> <td> <select v-model="order.ShipCountry" class="input"> <option v-for="item in AvaialbeCountrys" :v-key="item">{{item}} </option> </select> </td> </tr> <tr> <td> </td> <td> <select v-model="order.ShipCity" > <option v-for="city in AvaialbeCitys" :v-key="city">{{city}} </option> </select> </td> </tr> <tr> <td> </td> <td> <input type="text" v-model="order.ShipAddress" /> </td> </tr> </table> </div> <script> new Vue({ el: "#app", data: { id: @ViewBag.Id, order: { OrderID: 0, CustomerID: "", EmployeeID: 0, OrderDate: "", RequiredDate: "", ShippedDate: "", ShipVia: 0, Freight: 0, ShipName: "0", ShipAddress: "", ShipCity: "", ShipRegion: null, ShipPostalCode: "", ShipCountry: "" }, oldOrder: { ShipCountry: "" }, AvaialbeCitys: [], AvaialbeCountrys: [] }, methods: { //  fetchOrder() { var path = "../Orders/GetById?Id=" + this.id; this.fetchJson(path, json => this.order = json); }, fetchCityList() { //     var country = this.order.ShipCountry; if (country == null || country === "") { country = ''; } var path = "../Orders/AvaiableCityList?country=" + country; this.fetchJson(path, json => {this.AvaialbeCitys = json;}); }, fetchCountrys() { var path = "../Orders/AvaiableCountrys"; this.fetchJson(path,jsonResult => {this.AvaialbeCountrys = jsonResult;}); }, //    fetch fetchJson(path, collback) { try { fetch(path, { mode: 'cors' }) .then(response => response.json()) .then(function(json) { collback(json); } ); } catch (ex) { alert(ex); } }, saveOldOrderValue:function(){ this.oldOrder.ShipCountry = this.order.ShipCountry; } }, watch: { order: { handler: function (after) { if (this.oldOrder.ShipCountry !== after.ShipCountry)//    { this.fetchCityList();//       } this.saveOldOrderValue(); }, deep: true } }, mounted: function () { this.fetchCountrys();//   //    ,      this.fetchOrder();//  this.saveOldOrderValue();//   } }); </script> </body> </html> 

Um tópico separado é Validação. Do ponto de vista da otimização de velocidade, é claro, você precisa fazer a validação no cliente. Mas isso levará à duplicação de código, por isso estou mostrando um exemplo com validação no nível da Entidade (como deveria ser ideal). Ao mesmo tempo, o código mínimo, a validação em si ocorre muito rapidamente e também de forma assíncrona. Como a prática demonstrou, mesmo com uma Internet muito lenta, tudo funciona mais do que o normal.
Os problemas só surgem se o texto for digitado rapidamente em um campo de texto e a velocidade de digitação for de 260 caracteres por minuto. A opção de otimização mais simples para os campos de texto é definir o modelo v de atualização lenta .lazy = "order.ShipAddress"; a validação ocorrerá quando o foco for alterado. Uma opção mais avançada é atrasar a validação desses campos + se a próxima solicitação de validação for chamada antes de receber uma resposta e ignorar o processamento da solicitação anterior.
Os métodos para processar a validação no controle foram os seguintes.


  [HttpGet] public ActionResult Validate(int id, string json) { var order = _db.Orders.Find(id); JsonConvert.PopulateObject(json, order); var errorsD = GetErrorsJsArrey(); return Content(errorsD.ToString(), "application/json"); } private String GetErrorsAndChanged() { var changed= _db.ChangeTracker.HasChanges(); var errors = _db.GetValidationErrors(); return GetErrorsAndChanged(errors,changed); } private static string GetErrorsAndChanged(IEnumerable<DbEntityValidationResult> errors,bool changed) { dynamic dynamic = new ExpandoObject(); dynamic.IsChanged = changed;//  IsChanged var errProperty = new Dictionary<string, object>();//      dynamic.Errors = new DynObject(errProperty);//        foreach (DbEntityValidationResult validationError in errors)//   { foreach (DbValidationError err in validationError.ValidationErrors)//   { errProperty.Add(err.PropertyName,err.ErrorMessage); } } var json = JsonConvert.SerializeObject(dynamic); return json; } 

     DynObject 

  public sealed class DynObject : DynamicObject { private readonly Dictionary<string, object> _properties; public DynObject(Dictionary<string, object> properties) { _properties = properties; } public override IEnumerable<string> GetDynamicMemberNames() { return _properties.Keys; } public override bool TryGetMember(GetMemberBinder binder, out object result) { if (_properties.ContainsKey(binder.Name)) { result = _properties[binder.Name]; return true; } else { result = null; return false; } } public override bool TrySetMember(SetMemberBinder binder, object value) { if (_properties.ContainsKey(binder.Name)) { _properties[binder.Name] = value; return true; } else { return false; } } } 

Bastante detalhado, mas esse código foi escrito uma vez para todo o aplicativo e não requer ajuste para um objeto ou campo específico. Como resultado do método que trabalha no cliente, o objeto json com as propriedades IsChanded e Errors. Naturalmente, essas propriedades precisam ser criadas em nosso Vue e preenchidas a cada alteração do objeto.
Para obter erros de validação, você precisa definir essa validação em algum lugar. É hora de adicionar alguns atributos de validação à nossa descrição do objeto Entidade do pedido.


  [MinLength(10)] [StringLength(60)] public string ShipAddress { get; set; } [CheckCityAttribute(" ShipCity   ")] public string ShipCity { get; set; } 

MinLength e StringLength são atributos padrão, mas para ShipCity eu criei um atributo personalizado


  /// <summary> /// Custom Attribute Example /// </summary> [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class CheckCityAttribute : ValidationAttribute { public CheckCityAttribute(string message) { this.ErrorMessage = message; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { ValidationResult result = ValidationResult.Success; string[] memberNames = new string[] { validationContext.MemberName }; string val = value?.ToString(); Northwind _db = new Northwind(); Order order = (Order)validationContext.ObjectInstance; bool exsist = _db.Orders.FirstOrDefault(o => o.ShipCity == val && o.ShipCountry == order.ShipCountry)!=null; if (!exsist) { result = new ValidationResult(string.Format(this.ErrorMessage,order.ShipCity , val), memberNames); } return result; } } 

No entanto, vamos deixar o tópico Validação de entidade também fora do escopo deste artigo.
Para exibir erros, você precisa adicionar um link ao Css e modificar ligeiramente o formulário.
É assim que nosso formulário modificado deve ficar agora:


 @inherits System.Web.Mvc.WebViewPage <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> id=@ViewBag.Id</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <link rel="stylesheet" type="text/css" href="~/Content/vueError.css" /> </head> <body> <div id="app"> <table> <tr> <td> </td> <td class="tooltip"> <input type="number" v-model="order.Freight" v-bind:class="{error:!errors.Freight==''} " class="input" /> <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span> </td> </tr> <tr> <td>  </td> <td> <select v-model="order.ShipCountry" class="input"> <option v-for="item in AvaialbeCountrys" :v-key="item">{{item}} </option> </select> </td> </tr> <tr> <td> </td> <td class="tooltip"> <select v-model="order.ShipCity" v-bind:class="{error:!errors.ShipCity==''}" class="input"> <option v-for="city in AvaialbeCitys" :v-key="city">{{city}} </option> </select> <span v-if="!errors.ShipCity==''" class="tooltiptext">{{errors.ShipCity}}</span> </td> </tr> <tr> <td> </td> <td class="tooltip"> <input type="text" v-model.lazy="order.ShipAddress" v-bind:class="{error:!errors.ShipAddress=='' }" class="input" /> <span v-if="!errors.ShipAddress==''" class="tooltiptext">{{errors.ShipAddress}}</span> </td> </tr> <tr> <td> </td> <td> <button v-on:click="Save()" :disabled="IsChanged===false" || hasError class="alignRight">Save</button> </td> </tr> </table> </div> <script> new Vue({ el: "#app", data: { id: @ViewBag.Id, order: { OrderID: 0, CustomerID: "", EmployeeID: 0, OrderDate: "", RequiredDate: "", ShippedDate: "", ShipVia: 0, Freight: 0, ShipName: "0", ShipAddress: "", ShipCity: "", ShipRegion: null, ShipPostalCode: "", ShipCountry: "" }, oldOrder: { ShipCountry: "" }, errors: { OrderID: null, CustomerID: null, EmployeeID: null, OrderDate: null, RequiredDate: null, ShippedDate: null, ShipVia: null, Freight: null, ShipName: null, ShipAddress: null, ShipCity: null, ShipRegion: null, ShipPostalCode: null, ShipCountry: null }, IsChanged: false, AvaialbeCitys: [], AvaialbeCountrys: [] }, computed : { hasError: function () { for (var err in this.errors) { var error = this.errors[err]; if (error !== '' || null) return true; } return false; } }, methods: { //  fetchOrder() { var path = "../Orders/GetById?Id=" + this.id; this.fetchJson(path, json => this.order = json); }, fetchCityList() { //     var country = this.order.ShipCountry; if (country == null || country === "") { country = ''; } var path = "../Orders/AvaiableCityList?country=" + country; this.fetchJson(path, json => {this.AvaialbeCitys = json;}); }, fetchCountrys() { var path = "../Orders/AvaiableCountrys"; this.fetchJson(path,jsonResult => {this.AvaialbeCountrys = jsonResult;}); }, //    fetch Validate() {this.Action("Validate");}, Save() {this.Action("Save");}, Action(action) { var myJSON = JSON.stringify(this.order); var path = "../Orders/" + action + "?id=" + this.id + "&json=" + myJSON; this.fetchJson(path, jsonResult => { this.errors = jsonResult.Errors; this.IsChanged = jsonResult.IsChanged; }); }, fetchJson(path, collback) { try { fetch(path, { mode: 'cors' }) .then(response => response.json()) .then(function(json) { collback(json); } ); } catch (ex) { alert(ex); } }, saveOldOrderValue:function(){ this.oldOrder.ShipCountry = this.order.ShipCountry; } }, watch: { order: { handler: function (after) { this.IsChanged=true; if (this.oldOrder.ShipCountry !== after.ShipCountry)//    { this.fetchCityList();//       } this.saveOldOrderValue(); this.Validate(); }, deep: true } }, mounted: function () { this.fetchCountrys();//   //    ,      this.fetchOrder();//  this.saveOldOrderValue();//   } }); </script> </body> </html> 

Parece CSS


 .tooltip { position: relative; display: inline-block; border-bottom: 1px dotted black; } .tooltip .tooltiptext { visibility: hidden; width: 120px; background-color: #555; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; bottom: 125%; left: 50%; margin-left: -60px; opacity: 0; transition: opacity 0.3s; } .tooltip .tooltiptext::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #555 transparent transparent transparent; } .tooltip:hover .tooltiptext { visibility: visible; opacity: 1; } .error { color: red; border-color: red; border-style: double; } .input { width: 200px ; } .alignRight { float: right } 

E aqui está o resultado do trabalho.



Para entender como a validação funciona, vamos examinar com atenção a marcação que descreve um campo:


 <td class="tooltip"> <input type="number" **v-model="order.Freight" v-bind:class="{error:!errors.Freight==''} " **class="input" /> <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span> </td> 

Aqui estão dois pontos importantes:


Esta parte da marcação conecta o estilo responsável pelo quadro vermelho ao redor do elemento v-bind: class = "{error :! Errors.Freight == ''} aqui o vue conecta a classe css por condição.


E aqui está esta janela pop-up mostrada quando o cursor do mouse está sobre um elemento:


  <span v-if="errors.Freight!==''" class="tooltiptext">{{errors.Freight}}</span> 

além disso, o elemento pai do elemento deve conter o atributo class = "tooltip".


Na última versão, o botão salvar é adicionado configurado para que estivesse disponível apenas se fosse possível salvar.
Para simplificar a marcação necessária para a validação, proponho escrever o componente mais simples que levaria toda a validação em si.


 Vue.component('error-aborder', { props: { error: String }, template: '<div class="tooltip" >' + '<div v-bind:class="{error:!error==\'\' }" >' + '<slot>test</slot>' + '</div>' + '<p class="tooltiptext" v-if="!error==\'\'" >{{error}}</p>' + '</div>' }); 

agora a marcação parece mais limpa.


  <error-aborder v-bind:error="errors.Freight"> <input type="number" v-model="order.Freight" class="input" /> </error-aborder> 

O desenvolvimento se resume a organizar os campos em um formulário, configurar a validação no Entyty e criar listas. Se as listas são estáticas e não são grandes, elas podem ser completamente definidas no código.


A parte C # do código é bem testada. Os próximos planos lidam com o teste Vue.


Era tudo o que eu queria contar.
Eu apreciaria muito as críticas construtivas.


Aqui está o link para o código fonte .


No exemplo, o formulário é chamado SimpleEdit e contém a versão mais recente. Qualquer pessoa interessada em opções preliminares pode passar por confirmações.
No exemplo, implementei a otimização: abortando a solicitação de validação se, sem aguardar a resposta da validação, causar a validação pela segunda vez.

Source: https://habr.com/ru/post/pt455367/


All Articles