Créer une page de recherche avec Vue.js et ASP.NET Web API

A l'heure des frameworks front-end conventionnels comme Angular, Riot, Ember ou React, j’ai décidé de m’intéresser à Vue.js. Vue est un framework javascript développé par un ancien développeur de Google, Evan You, qui se veut accessible, versatile et performant.

Le principal avantage de Vue est à mon sens sa capacité d'intégration progressive dans une application existante. Il est également le projet ayant reçu le plus de "likes" sur GitHub en 2016 et 2017.

A titre de démonstration, voici un petit exemple d'un champ de recherche dynamique. Pour la partie back-end, j'ai créé une petite API REST à l'aide des ASP.NET Web API. Mais j'aurais tout aussi bien pu utiliser Node.js, SQLite et Express (voir mes précédents articles à ce sujet).

Création du back-end

1. Créez un nouveau projet de type "Empty MVC application" puis WebAPI dans Visual Studio 2010, 2013, 2015 ou 2017.

2. Implémentez les classes suivantes. Le code est très basique, je ne me suis pas embarrassé d'error handling (mal) ni de sécurité (très mal), le but étant ici de tester Vue.js et non de réaliser un back-end à l'épreuve du feu.



Global.asax.cs

Pour commencer, une petite astuce pour forcer notre API REST à toujours renvoyer du JSON.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Net.Http.Formatting;

namespace VueBackEnd
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode,
    // visit http://go.microsoft.com/?LinkId=9394801

    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            // Force JSON responses on all requests
            GlobalConfiguration.Configuration.Formatters.Clear();
            GlobalConfiguration.Configuration.Formatters.Add(new JsonMediaTypeFormatter());
        }
    }
}

App_Start\WebApiConfig.cs

Dans cette classe de configuration, nous allons définir la route permettant d'invoquer notre API.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace VueBackEnd
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}"
                //,defaults: new { id = RouteParameter.Optional }
            );

            // Add additonal routes here after...
        }
    }
}

Controllers\TrainingController.cs

Le code principal de notre API. Tout d'abord, nous allons définir une petite classe Training pour modéliser une ligne de résultat de notre recherche de formations.

Notre controller étant nommé TrainingController, l'URL générée pour y accéder sera donc api/training.

Nous définissons ensuite une méthode Get qui nécessitera un paramètre supplémentaire de stype string, keyword. A noter que j'ai choisi ici de personnaliser la réponse HTTP renvoyée. En cas d'exception, elle renverra un statut 404 dans le header HTTP ainsi que le message d'erreur.

En cas de succès, elle renverra un objet JSONTrainingResult, contenant une liste de Training.

La requête SQL utilisée dans cet exemple passe par une procédure stockée, qui a été mappée via Linq 2 SQL. A nouveau ce code n'est pas parfait, mais il est parfaitement fonctionnel.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using HelloWebAPI.Models;

namespace MyBackend.Controllers
{
    public class JSONTrainingResult {
        public List<Training> trainings = new List<Training>();  
    }

    public class Training {
        public string id;
        public string title;
        public string tags;
        public int? durationInDays;
        public DateTime? nextSession;
        public string city;
        public string url;
    }

    public class TrainingController : ApiController
    {
        // GET api/training?keyword=autocad
        public HttpResponseMessage Get(string keyword)
        {
            JSONTrainingResult result = new JSONTrainingResult();

            try
            {
                MyBaseDataContext db = new MyBaseDataContext();

                var query = from t in db.SearchTrainingsByKeyword(keyword, "startdate", "fr")
                             select t;

                foreach (var row in query) {
                    Training aTraining = new Training();

                    aTraining.id = row.trainingid.Trim();
                    aTraining.title = row.title;
                    aTraining.tags = row.tags;
                    aTraining.durationInDays = (int?)row.durationInDays;
                    aTraining.nextSession = row.startdate;
                    aTraining.city = row.city;
                    aTraining.url = string.Format("http://monsite/formation/{0}/fr", row.trainingid.Trim());

                    result.trainings.Add(aTraining);
                }

                db.Dispose();

            }
            catch (Exception ex) {
                return Request.CreateResponse(HttpStatusCode.NotFound, ex.Message);
            }

            return Request.CreateResponse(HttpStatusCode.OK, result);
        }
    }
}

Test de l'API REST

Une fois compilé et démarré, notre API peut déjà être testée en local. Tapez l'URL suivante dans le navigateur:

http://localhost:53777/api/training?keyword=blender

Nous obtenons une réponse en format JSON:

{"trainings":[{"id":"08beb933-0378-41a6-851a-72cafea5c12b","title":"Blender","tags":"309=Logiciel;30955=Logiciel graphique","durationInDays":3,"nextSession":null,"city":"Esch-sur-Alzette","url":"http://www.lifelong-learning.lu/Formation/08beb933-0378-41a6-851a-72cafea5c12b/fr"}]}

Vous pouvez également installer l'extension PostMan pour Chrome. Très simple d'utilisation, elle vous permettra également de tester des actions de types POST ou DELETE sur vos API.




Une page de recherche simple avec Vue.js

Passons maintenant à notre page de démonstration. L'API me retournant des dates, j'ai intégré la librairie JavaScript Moment pour formatter les dates, Vue étant dépourvu de fonctionnalité dans ce domaine. Pour plus de détails sur la syntaxe de vue, veuillez vous référencer au site officiel: https://vuejs.org/

Content\Sample1.htm

<!DOCTYPE html>
<html>
<head>
    <title>lifelong-learning.lu REST ASP Demo 1</title>
    <link href="/Content/site.css" rel="stylesheet"/>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>

</head>

<body>
    <div id="app">
        <input type="text" v-model.text="keyword" v-on:keyup.enter="search"/>

        <button @click="search">Search</button>

        <ul>
            <li v-for="training in trainings">
                <a v-bind:href="training.url"><b>{{ training.title }}</b></a>
                <br />
                {{ training.city }}
                <span v-if="training.nextSession">- {{ training.nextSession | moment }}</span>
            </li>
        </ul>

        <span v-if="trainings.length">
        {{ trainings.length }} résultat(s) trouvé(s)
        </span>
    </div>
  
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                trainings: {}, // the trainings collection to be displayed
                keyword: "gdpr"
            },
            methods: {
                search: function() { // Search trainings using the keyword property
                    fetch('/api/training?keyword=' + encodeURIComponent(this.keyword))
                    .then(response => response.json())
                    .then(json => {
                        this.trainings = json.trainings;
                    });
                }
            },
            filters: { // Create a Vue filter to format datetime values
                moment: function (date) {
                    return moment(date).format('DD/MM/YYYY');
                }
            }
        })
    </script>
</body>
</html>


Et voilà le résultat:



Commentaires