Février 2016

Volume 31, numéro 2

Cet article a fait l'objet d'une traduction automatique.

ASP.NET - Amélioration progressive avec ASP.NET et React

Par Graham Mendick

La remise de contenu Web fiable est un jeu de hasard. Les éléments aléatoires sont la vitesse du réseau de l'utilisateur et les fonctionnalités du navigateur. L'amélioration progressive ou PE, est une technique de développement qui prend en compte ce caractère imprévisible. La pierre angulaire de PE est rendu côté serveur du code HTML. Il est le seul moyen d'optimiser vos chances de succès dans le jeu de remise de contenu. Pour les utilisateurs avec un navigateur moderne, vous pouvez combiner sur JavaScript pour améliorer l'expérience.

Avec l'avènement des bibliothèques de liaison de données telles que AngularJS et Knockout, l'Application une seule Page (SPA) fourni dans son propre. SPA est l'antithesis de PE car, par le client de rendu HTML, il ignore la nature imprévisible du Web. Visiteurs sur les réseaux lents sont confrontés à temps de chargement tandis que les utilisateurs et les moteurs de recherche avec des navigateurs moindres recevront alors peut-être pas de contenu du tout. Mais même ces problèmes n'ont pas émoussés l'attrait de l'application à page unique.

L'application SPA terminé l'amélioration progressive. Il était trop difficile de transformer une application serveur rendu SPA via JavaScript amélioration.

Heureusement, il s'avère que PE n'est pas vraiment mort. Il a simplement été mise en veille et une bibliothèque JavaScript appelée réagissent a simplement contribué et il réveillés. Réagissent fournit le meilleur des deux mondes, car il peut s'exécuter sur le serveur et sur le client. Vous pouvez démarrer avec une application serveur rendu et, en temps réel d'un commutateur, lui donner vie en tant que client rendu.

Le projet TodoMVC (todomvc.com) offre la même tâche SPA avec différentes bibliothèques de liaison de données JavaScript pour vous aider à choisir. Il s'agit d'un projet réussi, mais les implémentations d'être affectées par le client-rendu uniquement. Dans cet article, je vais vous donner ce droit en créant une version Couper comme une amélioration progressive SPA à l'aide de réagir et ASP.NET. Cet article se concentre sur les fonctionnalités en lecture seule, donc vous serez en mesure d'afficher une liste des todos et les filtrer pour afficher les actifs ou de le celles terminée.

Rendu sur le serveur

L'ancienne approche PE, je serait créer une application ASP.NET MVC à l'aide de Razor pour afficher la liste des tâches sur le serveur. Si j'ai décidé de l'améliorer dans une application à page unique, je serais retournez à, je devrais ré-implémenter la logique de rendu en JavaScript. Mon approche nouvelle PE, je vais créer une application ASP.NET MVC à l'aide de réagir au lieu de Razor pour afficher la liste des tâches sur le serveur. De cette façon, il peut servir de code de rendu de client.

Je vais commencer par créer un nouveau projet ASP.NET MVC appelé TodoMVC. À part la couche View, le code est ordinaires, le dossier Modèles conserve un TodoRepository qui renvoie un IEnumerable de todos, et dans le HomeController est une méthode qui appelle dans le référentiel d'Index. À ce stade, les choses commencent à un aspect un peu différent. Au lieu de transmettre la liste de tâches à l'affichage Razor, je vais la transmettre à réagissent pour produire le code HTML sur le serveur.

Pour exécuter JavaScript sur le serveur, vous devez Node.js, que vous pouvez télécharger à partir de nodejs.org. Node.js est fourni avec son propre gestionnaire de package appelé npm. Je vais installer réagissent à l'aide de npm comme j'utiliserais NuGet pour installer un package de .NET. Je vais ouvrir une invite de commandes, cd dans mon dossier de projet TodoMVC et exécutez la commande « npm install réagir ».

Ensuite, je vais créer un fichier appelé app.jsx dans le dossier de Scripts (j'expliquerai l'extension .jsx sous peu). Ce fichier contiendra la logique de rendu réagissent, prendre la place de la vue Razor dans un projet ASP.NET MVC standard. Node.js utilise un système de chargement des modules, charger le module réagissent, je vais ajouter un besoin d'instruction au début de app.jsx :

var React = require('react');

Une interface utilisateur de réagir est constituée de composants. Chaque composant possède une fonction de rendu qui transforme les données d'entrée en HTML. Les données d'entrée sont transmises comme propriétés. À l'intérieur de app.jsx, je vais créer un composant de liste qui prend le todos et les place dans une liste non triée, avec le titre de chaque tâche représentée sous la forme d'un élément de liste :

var List = React.createClass({
  render: function () {
    var todos = this.props.todos.map(function (todo) {
      return <li key={todo.Id}>{todo.Title}</li>;
    });
    return <ul>{todos}</ul>;
  }
});

Le fichier a une extension .jsx parce que le code de réaction est un mélange de JavaScript et une syntaxe de type HTML appelé JSX. Je souhaite exécuter ce code sur le serveur, mais ne comprend pas Node.js JSX, donc vous devez d'abord convertir le fichier JavaScript. Conversion de JSX en JavaScript est appelé transpiling et un exemple transpiler est Babel. Je pourrais coller le contenu de mon app.jsx le transpiler Babel en ligne (babeljs.io/repl) et créer un fichier app.js à partir de la sortie de transpiled. Mais il est plus judicieux pour automatiser cette étape app.jsx a été modifiée relativement souvent.

Je vais utiliser les choses pour automatiser la conversion de app.jsx en app.js. Gulp est un exécuteur de tâches JavaScript qui est fourni avec un large éventail de plug-ins pour vous aider à transformer des fichiers source. Ensuite je vais écrire une tâche de choses que faisceaux de JavaScript pour le navigateur. Pour l'instant, j'ai besoin d'une tâche qui passe app.jsx via la transpiler Babel afin qu'il puisse être utilisé dans Node.js sur le serveur. Allons installer Gulp et le plug-in de Babel de npm en exécutant :

npm install gulp gulp-babel babel-preset-react

Comme vous pouvez le voir, en séparant les noms de packages avec des espaces, je peux installer plusieurs packages en une seule commande. Je créer un gulpfile.js dans le dossier de projet TodoMVC et j'ajoute la tâche transpile à celui-ci :

var babel = require('gulp-babel');
gulp.task('transpile', function(){
  return gulp.src('Scripts/app.jsx')
    .pipe(babel({ presets: ['react'] }))
    .pipe(gulp.dest('Scripts/'))
});

La tâche est constituée de trois étapes. Tout d'abord, les choses reçoit le fichier source app.jsx. Ensuite, le fichier est redirigé via la transpiler Babel. Pour finir, le fichier app.js de sortie est enregistré dans le dossier Scripts. Pour que la tâche exécutable, je vais utiliser le bloc-notes pour créer un fichier package.json dans le dossier de projet TodoMVC avec une écriture de scripts en pointant dessus :

{
  "scripts": {
    "transpile": "gulp transpile"
  }
}

À partir de la ligne de commande j'exécute la tâche transpile à l'aide de « npm exécuté transpile ». Cela génère un fichier de app.js qui peut exécuter à l'intérieur de Node.js car le JSX a été remplacé par JavaScript.

Parce que j'utilise réagissent comme la couche de vue, je veux passer le todos à partir du contrôleur dans le composant de liste et avoir le code HTML renvoyé. Dans Node.js, le code dans le fichier app.js est privé et peut uniquement être rendu public en les exportant explicitement. Allez exporter une fonction getList de app.jsx pour le composant de liste peut être créé en externe, sans oublier d'exécuter la tâche transpile afin que app.js est mis à jour :

function getList(todos) {
  return <List todos={todos} />;
}
exports.getList = getList;

Le HomeController est en c# et la fonction getList dans JavaScript. Pour appeler dans cette limite, je vais utiliser Edge.js (tjanczuk.github.io/edge), qui est disponible à partir de NuGet en exécutant Install-Package Edge.js. Edge.js attend que vous lui passez une chaîne c# contenant le code Node.js qui retourne une fonction avec deux paramètres. Le premier paramètre conserve les données passées à partir de c# et le deuxième paramètre est un rappel utilisé pour retourner les données de JavaScript vers c#. Après l'exécution de « npm install réagissent-dom » pour afficher les capacités de rendu du serveur de réaction, je vais utiliser Edge.js pour créer une fonction qui retourne HTML du composant liste à partir du tableau todos passé :

private static Func<object, Task<object>> render = Edge.Func(@"
  var app = require('../../Scripts/app.js');
  var ReactDOMServer = require('react-dom/server');
  return function (todos, callback) {
    var list = app.getList(todos);
    callback(null, ReactDOMServer.renderToString(list));
  }
");

À partir du code Node.js, Edge.js crée une Func c# que j'affecte à une variable appelée « afficher » dans le HomeController. Rendu d'appel avec une liste de todos renvoie le code HTML. Je vais ajouter cet appel dans la méthode Index, à l'aide du modèle async/await appelle Edge.js étant asynchrone :

public async Task<ActionResult> Index()
{
  var todos = new TodoRepository().Todos.ToList();
  ViewBag.List = (string) await render(todos);
  return View();
}

J'ai ajouté le code HTML renvoyé à ViewBag dynamique afin d'accéder à partir de la vue Razor. Bien que réagir fait tout le travail, j'ai encore besoin d'une seule ligne de Razor pour envoyer le code HTML au navigateur et effectuer le rendu du serveur :

<div id="content">@Html.Raw(ViewBag.List)</div>

Cette nouvelle approche de l'amélioration progressive peut sembler plus de travail par rapport à l'ancienne approche. Mais n'oubliez pas que cette nouvelle approche, le code de rendu de serveur devient le code de rendu client. Il n'est pas l'effort en double requis par l'ancienne approche lors de la conversion de l'application serveur rendu dans une application à page unique.

Filtrage sur le serveur

Le todos doit être filtrables afin que soit actifs ou terminés celles qui peuvent être affichés. Filtrage signifie que les liens hypertexte et liens hypertexte signifient le routage. J'ai simplement remplacer Razor réagissent, un convertisseur de JavaScript qui fonctionne sur le client et le serveur. Ensuite, je vais appliquer le même traitement pour le routage. Plutôt que d'utiliser la solution de routage qui est fourni avec ASP.NET MVC, je vais remplacer par le routeur de Navigation (grahammendick.github.io/navigation), un routeur JavaScript qui fonctionne sur le client et le serveur.

J'exécute « npm install navigation » mettre dans le routeur. Vous pouvez considérer le routeur de Navigation comme une machine à états, où chacun d'eux représente une vue différente dans votre application. Dans app.jsx, je configure le routeur avec un état qui représente la vue de la « liste » todo. Je vais attribuer cet état un itinéraire avec un paramètre facultatif « filtre » afin que les filtrages URL recherche like « / active » et « / terminée » :

var Navigation = require('navigation');
var config = [
  { key: 'todoMVC', initial: 'list', states: [
    { key: 'list', route: '{filter?}' }]
  }
];
Navigation.StateInfoConfig.build(config);

L'ancienne approche PE, vous devez placer la logique de filtrage dans le contrôleur. Avec la nouvelle approche, la logique de filtrage réside dans le code de réagir afin de pouvoir le réutiliser sur le client lorsque vous l'activez dans une application à page unique. Le composant de liste prend dans le filtre et vérifier par rapport à l'état terminé d'une tâche pour déterminer les éléments de liste à afficher :

var filter = this.props.filter;
var todoFilter = function(todo){
  return !filter || (filter === 'active' && !todo.Completed)
    || (filter === 'completed' && todo.Completed);
}
var todos = this.props.todos.filter(todoFilter).map(function(todo) {
  return <li key={todo.Id}>{todo.Title}</li>;
});

Je vais modifier le code HTML renvoyé par le composant de liste pour inclure les liens hypertexte filtre ci-dessous la liste des tâches filtrées :

<div>
  <ul>{todos}</ul>
  <ul>
    <li><a href="/">All</a></li>
    <li><a href="/active">Active</a></li>
    <li><a href="/completed">Completed</a></li>
  </ul>
</div>

La fonction exportée « getList » nécessite un paramètre supplémentaire afin qu'il peut passer la nouvelle propriété de filtre dans le composant de liste. Il s'agit de la dernière modification de app.jsx pour prendre en charge le filtrage, il est judicieux de réexécuter la tâche de transpile Gulp pour générer une nouvelle app.js.

function getList(todos, filter) {
  return <List todos={todos} filter={filter} />;
}

Le filtre sélectionné doit être extrait à partir de l'URL. Il se peut que vous pouvez être tenté d'inscrire un itinéraire ASP.NET MVC afin que le filtre a été passé dans le contrôleur. Mais cela serait dupliquer l'itinéraire déjà configuré sur le routeur de Navigation. Au lieu de cela, je vais utiliser le routeur de Navigation pour extraire le paramètre de filtre. Tout d'abord, allez supprimer toutes les mentionner de paramètres d'itinéraire à partir de la classe c# RouteConfig.

routes.MapRoute(
  name: "Default",
   url: "{*url}",
  defaults: new { controller = "Home", action = "Index" }
);

Le routeur de Navigation a une fonction navigateLink pour l'analyse des URL. Vous transmettre une URL et stocke les données extraites dans un objet StateContext. Vous pouvez accéder à ces données en utilisant le nom du paramètre itinéraire comme clé :

Navigation.StateController.navigateLink('/completed');
var filter = Navigation.StateContext.data.filter;

Je vais Branchez ce code d'extraction du paramètre itinéraire rendu Edge.js fonctionnent pour le filtre peut être récupéré à partir de l'URL actuelle et transmis à la fonction getList. Mais le code JavaScript sur le serveur ne peut pas accéder à l'URL de la requête actuelle, il faut donc en être transmis à partir de c#, ainsi que les todos, via le premier paramètre de la fonction :

return function (data, callback) {
  Navigation.StateController.navigateLink(data.Url);
  var filter = Navigation.StateContext.data.filter;
  var list = app.getList(data.Todos, filter);
  callback(null, ReactDOMServer.renderToString(list));
}

La modification correspondante à la méthode Index du HomeController consiste à transmettre un objet dans l'appel de rendu qui contient l'URL de la demande de côté serveur et la liste des tâches.

var data = new {
  Url = Request.Url.PathAndQuery,
  Todos = todos
};
ViewBag.List = (string) await render(data);

Avec le filtrage en place, la phase côté serveur de la build est terminée. À partir de garanties de rendu du serveur, la liste des tâches est visible par tous les navigateurs et moteurs de recherche. Le plan est d'améliorer l'expérience des navigateurs modernes en filtrant la liste des tâches sur le client. Le routeur de Navigation gérer l'historique du navigateur et garantir une liste filtrée de client signet.

Rendu sur le Client

Si j'avais construit l'interface utilisateur avec Razor, je serais ne proche maintenant SPA fin ligne que lorsque je me suis fixés. Devoir dupliquer la logique de rendu en JavaScript est la raison pour laquelle la vieille école PE sont plus aussi favorisés. Mais, avec réagissent, il est à l'opposé, car je peux réutiliser tout mon code app.js sur le client. Comme j'ai utilisé réagissent à rendre le composant de liste HTML sur le serveur, je vais l'utiliser pour rendre ce même composant au DOM sur le client.

Pour afficher le composant de liste sur le client, je dois accéder à la todos. Je vais en faire le todos disponible en leur envoyant vers le bas dans une variable JavaScript dans le cadre du rendu du serveur. En ajoutant la liste todo à ViewBag dans le HomeController, je peux les sérialiser dans un tableau JavaScript à l'intérieur de la vue Razor :

<script>
  var todos = 
    @Html.Raw(new JavaScriptSerializer().Serialize(ViewBag.Todos));
</script>

Je vais créer un fichier client.js dans le dossier de Scripts pour contenir la logique de rendu client. Ce code recherche le même que le code Node.js que j'ai passé dans Edge.js pour gérer le rendu du serveur, mais ajustée de façon à prendre en compte les différences dans l'environnement. Ainsi, l'URL provenant de l'objet du navigateur emplacement, plutôt que la demande de côté serveur et réagir rend le composant de liste dans la balise div de contenu, plutôt qu'à une chaîne HTML :

var app = require('./app.js');
var ReactDOM = require('react-dom');
var Navigation = require('navigation');
Navigation.StateController.navigateLink(location.pathname);
var filter = Navigation.StateContext.data.filter;
var list = app.getList(todos, filter);
ReactDOM.render(list, document.getElementById('content'));

Je vais ajouter une ligne à app.jsx qui indique le routeur Navigation que j'utilise l'historique HTML5 plutôt que la valeur par défaut de l'historique de hachage. Si vous n'avez pas faire cela, la fonction navigateLink serait considérer que l'URL a changé et mettre à jour le hachage de navigateur pour correspondre à :

Navigation.settings.historyManager = 
  new Navigation.HTML5HistoryManager();

Si je peux ajouter une référence de script client.js directement à la vue Razor, ce serait la fin des modifications nécessaires pour le rendu client. Malheureusement, il n'est pas assez simple, car le requièrent des instructions à l'intérieur de client.js font partie du système de chargement des modules Node.js et ne sont pas reconnues par les navigateurs. Je vais utiliser un plug-in de choses appelé browserify pour créer une tâche qui client.js faisceaux et tous ses modules requis dans un seul fichier JavaScript, je peux alors ajouter à la vue Razor. J'exécute « npm install browserify vinyle-source-stream » à placer dans le plug-in :

var browserify = require('browserify');
var source = require('vinyl-source-stream');
gulp.task('bundle', ['transpile'], function(){
  return browserify('Scripts/client.js')
    .bundle()
    .pipe(source('bundle.js'))
    .pipe(gulp.dest('Scripts/'))
});

Je ne veux pas la tâche de faisceau à exécuter, sauf si elle inclut les dernières modifications apportées à app.jsx. Pour vous assurer que la tâche transpile toujours s'exécute en premier, j'ai a une dépendance de la tâche de regroupement. Vous pouvez voir que le deuxième paramètre d'une tâche de choses répertorie ses dépendances. Je vais ajouter une entrée à la section scripts de package.json pour la tâche de regroupement. Exécute la commande « npm exécutez bundle » crée bundle.js et je vais ajouter une référence de script en pointant dessus vers le bas de la vue Razor :

<script src="~/Scripts/bundle.js"></script>

Par le code HTML pour le rendu du serveur, j'ai créé une application qui démarre plus rapides que celles à todomvc.com car ils ne peut pas afficher un contenu jusqu'à ce que leur JavaScript charge et exécute. De même, une fois le code JavaScript dans mon application, un client rendu s'exécute. En revanche, cela ne met à jour le DOM du tout, mais permet de réagir à attacher au contenu rendu de serveur afin que le filtrage de la liste des tâches suivantes permettre être traitée sur le client.

Filtrage sur le Client

Si vous faisiez PE la méthode traditionnelle, vous pouvez implémenter le filtrage sur le client en basculant les noms de classe pour contrôler la visibilité des éléments todo. Mais sans un routeur JavaScript pour vous aider, il est très facile à rompre l'historique du navigateur. Si vous oubliez du mettre à jour l'URL, la liste filtrée ne sera pas signet. En procédant PE la façon moderne, j'ai déjà le routeur de Navigation en cours d'exécution sur le client de conserver l'historique du navigateur.

Pour mettre à jour l'URL lorsque l'utilisateur clique sur un lien hypertexte de filtre, je dois intercepter l'événement click et transmettre les href du lien hypertexte en fonction de navigateLink du routeur. Une réaction est plug-in pour le routeur de Navigation qui va gérer cela pour moi, autant de créer les liens hypertexte de la manière prescrite. Par exemple, au lieu d'écrire < un href = "/ active" > Directory < /a >, je dois utiliser le composant RefreshLink réagir fournit le plug-in :

var RefreshLink = require('navigation-react').RefreshLink;
<RefreshLink toData={{filter: 'active'}}>Active</RefreshLink>

Après l'exécution de « npm install navigation-réaction » mettre dans le plug-in, je mettrai à jour le composant de liste dans app.jsx en remplaçant les trois liens hypertexte de filtre par leurs équivalents RefreshLink.

Pour synchroniser l'interface utilisateur et l'URL, j'ai filtrer la liste des tâches chaque fois que l'URL est modifiée, pas uniquement lorsqu'un lien hypertexte filtre clique sur, mais également lorsque l'utilisateur appuie sur le bouton précédent du navigateur. Au lieu d'ajouter des écouteurs d'événements distinct, je peux ajouter un seul écouteur vers le routeur de Navigation qui sera appelé navigation de n'importe quel moment se produit. Cet écouteur de navigation doit être attaché à l'état « liste » que j'ai créé dans le cadre de la configuration du routeur. Tout d'abord, j'accède cet état à partir du routeur de Navigation en utilisant les touches de la configuration :

var todoMVC = Navigation.StateInfoConfig.dialogs.todoMVC;
var listState = todoMVC.states.list;

Un écouteur de navigation est une fonction affectée à la propriété « cible » de l'état. Chaque fois que l'URL est modifiée, le routeur de Navigation appeler cette fonction et transmettre les données extraites de l'URL. Je vais Remplacez le code dans client.js avec un écouteur de navigation qui le rend à nouveau le composant de liste dans la balise div « content » en utilisant le nouveau filtre. Réagir s'occupera du reste, mettre à jour le DOM pour afficher les todos qui vient d'être filtrés :

listState.navigated = function(data){
  var list = app.getList(todos, data.filter);
  ReactDOM.render(list, document.getElementById('content'));
}

Pour implémenter le filtrage, j'ai supprimé accidentellement le code à partir de client.js qui a déclenché le rendu initial du client. Je vais restaurer cette fonctionnalité en ajoutant un appel à « Navigation.start » en bas de client.js. Cela passe effectivement l'URL du navigateur actuel en fonction du routeur navigateLink, qui déclenche l'écouteur de navigation et effectue le rendu du client. Je vais réexécutez la tâche de regroupement pour importer les modifications les plus récentes dans app.js et bundle.js.

La nouvelle approche PE est alchemy de modernes. Il trouve métal d'une application serveur rendu en or SPA. Mais il prend un type spécial de métal pour la transformation à utiliser, qui est construit à partir de bibliothèques JavaScript qui s'exécutent aussi bien sur le serveur et dans le navigateur : Réagir et le routeur de Navigation à la place de Razor et ASP.NET MVC routage. Il s'agit de la nouvelle chimique pour le Web.

Couper la moutarde

L'objectif de PE est une application qui fonctionne dans tous les navigateurs, tout en offrant une expérience améliorée pour les navigateurs modernes. Toutefois, dans la construction de cette expérience améliorée, j'ai arrêté la liste des tâches de fonctionner dans des navigateurs plus anciens. La conversion de SPA s'appuie sur l'API d'historique HTML5, Internet Explorer 9, par exemple, pas en charge.

PE n'est pas sur la même expérience à tous les navigateurs de l'offre. La liste de tâches ne doit être un SPA dans Internet Explorer 9. Dans les navigateurs qui ne prennent pas en charge l'historique HTML5, je peux revenir à l'application serveur rendu. Je vais modifier l'affichage Razor pour charger dynamiquement bundle.js, donc il n'est plus envoyé aux navigateurs qui prennent en charge l'historique HTML5 :

if (window.history && window.history.pushState) {
  var script = document.createElement('script');
  script.src = "/Scripts/bundle.js";
  document.body.appendChild(script);
}

Cette vérification est appelée « couper la moutarde » car uniquement ces navigateurs répondant aux exigences sont considérées comme dignes de recevoir le code JavaScript. Le résultat final est l'équivalent de Web de l'illusion optique où la même image peut soit ressembler un lapin ou un canard. Examinez la liste des tâches via un navigateur moderne et il s'agit d'une application à page unique, mais squint dessus à l'aide d'un ancien navigateur et il est une application client-serveur traditionnelles.


Graham Mendickestime que dans un site Web accessible à tous et est motivé par les nouvelles possibilités pour l'amélioration progressive qui a ouvert isomorphes JavaScript. Il est l'auteur du routeur Navigation JavaScript, lequel il espère va aider les gens à aller isomorphes. Vous pouvez le contacter sur Twitter : @grahammendick.

Merci à l'experte technique Microsoft suivante d'avoir relu cet article : Steve Sanderson
Steve Sanderson est un développeur Web de l'équipe ASP.NET chez Microsoft. Son actuel s'efforce de ASP.NET formidable pour les développeurs créant des applications riches de JavaScript.