Middleware express pour servir les applications à page unique avec des URL pushState et des performances accrues

Linux : Windows : Général :
Serve-SPA se comporte comme le middleware express.static. Cependant, si une URL pushState est demandée, Serve-SPA ne renvoie pas de 404 mais sert à la place la page SPA correspondante.
Supposons que ce SPA très simple soit servi à partir de ce dossier :
spa
|-- index.html
|-- app.js
Dans un premier temps, un visiteur charge généralement votre SPA via l'url de base http://localhost:3000/
. Ainsi, index.html est servi et app.js est également chargé. Ensuite, le visiteur navigue dans votre SPA qui met à jour l'URL à l'aide de pushState et arrive par exemple à http://localhost:3000/profile/me
. Le visiteur peut désormais ajouter cette page à ses favoris et la rouvrir le lendemain. express.static enverrait un 404 pour cette URL car le dossier servi ne contient pas de dossier « profil » contenant un fichier « moi ». Serve-SPA, cependant, reconnaît http://localhost:3000/profile/me
comme une URL pushState et recherche dans la structure des dossiers une page qui correspond le mieux à l'URL donnée, c'est-à-dire http://localhost:3000/
.
Tout ce que vous devez faire pour activer la prise en charge des URL pushState est de renommer vos fichiers index.html
en index.htmlt
(avec un t ). C'est à dire :
spa
|-- index.htmlt <-- Just renamed and pushState urls are supported
|-- app.js
Voici comment un SPA standard (utilisant Angular.js dans ce cas) est chargé :
Le temps nécessaire à l'utilisateur pour voir la page est considérablement augmenté par les deux requêtes AJAX "list.html" qui est un modèle HTML et "projets" qui sont les données JSON utilisées pour remplir la page.
Avec Serve-SPA, vous pouvez facilement intégrer le modèle/les données dans le index.htmlt
afin que les appels AJAX soient ignorés et que la page soit rendue immédiatement :
Serve-SPA apporte la puissance des modèles de lodash à vos fichiers index.htmlt
. L'exemple ci-dessus pour le SPA standard utilise cette page HTML :
<!doctype html >
< html ng-app =" project " >
< head >
< title > Pure Angular.js SPA </ title >
< script src =" /bower_components/angular/angular.min.js " > </ script >
< script src =" /bower_components/angular-resource/angular-resource.min.js " > </ script >
< script src =" /bower_components/angular-route/angular-route.min.js " > </ script >
< link rel =" stylesheet " href =" /bower_components/bootstrap/dist/css/bootstrap.min.css " >
< script src =" /scripts/app.js " > </ script >
< base href =" / " >
</ head >
< body >
< div class =" container " >
< h1 > JavaScript Projects </ h1 >
< div ng-view > </ div >
</ div >
</ body >
</ html >
Si le visiteur demande http://localhost:3000/
ce SPA a besoin du modèle list.html
pour s'afficher. Pour ignorer l'appel AJAX autrement nécessaire, le modèle doit être intégré. Cependant, étant donné que les autres URL pushState n'ont pas besoin de ce modèle, il ne doit être intégré que si http://localhost:3000/
est demandé. Cela peut être accompli avec l'ajout suivant à index.htmlt
:
<body>
+ <% if (req.path === '/') { %>
+ <script type="text/ng-template" id="partials/list.html">
+ <%= require('fs').readFileSync('app/partials/list.html') %>
+ </script>
+ <% } %>
<div class="container">
<h1>JavaScript Projects</h1>
<div ng-view></div>
</div>
</body>
Il existe des moyens (par exemple en utilisant compose.js
) d'implémenter cela de manière plus propre et non bloquante, mais vous voyez l'idée.
Si vous servez déjà votre SPA avec le middleware express.static, vous pourrez le servir avec Serve-SPA à la place :
// Just replace:
app . use ( express . static ( appDir ) ) ;
// with:
serveSpa ( app , appDir ) ;
Ensuite, vous renommez vos fichiers index.html
en index.htmlt
.
app
|-- blog
| |-- img
| | |-- ...
| |
- | |-- index.html
+ | |-- index.htmlt
| |-- blog.css
| |-- blog.js
|
- |-- index.html
+ |-- index.htmlt
|-- main.css
|-- main.js
Cela vous offre la prise en charge des URL pushState et une fonctionnalité de création de modèles pour intégrer des modèles HTML et des données JSON dans le HTML servi pour chaque requête. Si vous avez besoin, par exemple, de récupérer les données de votre base de données au préalable, vous pouvez ajouter un fichier compose.js
à côté pour ce faire :
app
|-- blog
| |-- img
| | |-- ...
| |
+ | |-- compose.js // May load latest article headlines from the database
| |-- index.htmlt // May inline article headline so the browser spares an AJAX call
| |-- blog.css
| |-- blog.js
|
|-- index.htmlt
|-- main.css
|-- main.js
BTW, Serve-SPA ne fait aucune hypothèse sur la façon dont votre SPA est implémenté côté client. Toute implémentation doit être capable de fonctionner avec les modifications qui doivent être apportées côté serveur.
Consultez le référentiel Serve-SPA Demos qui vise à fournir des versions régulières et migrées de SPA pour les frameworks SPA bien connus. Pour voir par exemple comment migrer un SPA basé sur Angular.js, faites une différence entre son implémentation régulière et la version précomposée qui tire pleinement parti de Serve-SPA.
Le module pour node.js est installé via npm :
npm install serve-spa --save
Serve-SPA dépend d'une version vaguement définie de serve-static. Si vous souhaitez installer une version spécifique, veuillez installer serve-static au préalable.
var serveSpa = require ( 'serve-spa' ) ;
serveSpa ( appOrRouter , rootPath , options ) ;
appOrRouter
doit être extrait de var app = express();
ou var router = express.Router();
selon l'endroit où vous souhaitez monter Serve-SPA. L'utilisation d'un routeur permet de monter le SPA sur une URL spécifique. Par exemple, le SPA est monté sur http://localhost:3000/app/
si vous utilisez app.use('/app', router);
.rootPath
est le chemin du système de fichiers vers les ressources SPA. Le paramètre est identique à celui utilisé avec app.use(express.static(rootPath))
. De plus, rootPath
peut être un tableau de chemins permettant d'utiliser plusieurs racines.options
est un objet qui autorise les attributs suivants :options.staticSettings
est transmis au middleware serve-static utilisé en interne. BTW, ce sont les mêmes options qui sont transmises à express.static(rootPath, staticSettings)
. Voir la documentation des options de serve-static pour plus de détails.options.beforeAll
prend un middleware qui est exécuté avant le rendu d'un modèle. Utilisez-le pour effectuer des tâches générales requises pour tous les modèles. Si vous avez besoin d'effectuer des tâches pour un modèle particulier, utilisez plutôt un fichier compose.js.options.templateSettings
est transmis au _.template(template, templateSettings)
de lodash. Voir la documentation de _.templateSettings
pour plus de détails.options.require
prend une fonction require particulière pour être utilisée dans le rendu du modèle. Si l'option n'est pas donnée, Serve-SPA fournit sa propre fonction require lors du rendu d'un modèle. Cette fonction require peut avoir une hiérarchie de recherche de chemin de module différente. Ainsi, l'option permet de transmettre une autre fonction require qui a une hiérarchie de recherche de chemin de module préférée.Le modèle est un fichier HTML qui peut contenir des mix-ins JavaScript :
<!doctype html >
< html >
< head >
< title > Example template </ title >
</ head >
< body >
Today is: < %= (new Date()).toString() % > < br />
< ul >
< % _.forEach(['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], function (day, i) { % >
< li < %= (new Date()).getDay() === i ? ' style="color: red;"' : '' % > >
< %= day % >
</ li >
< % }); % >
</ ul >
</ body >
</ html >
Cela produit :
Today is: Tue Oct 20 2015 16:09:40 GMT+0200 (CEST) < br />
< ul >
< li > Sunday </ li >
< li > Monday </ li >
< li style =" color: red; " > Tuesday </ li >
< li > Wednesday </ li >
< li > Thursday </ li >
< li > Friday </ li >
< li > Saturday </ li >
</ ul >
Dans le modèle, les variables suivantes sont disponibles :
_
: La bibliothèque lodashrequire
: fonction require du nœudrequest
et req
: L'objet de requête fourni par Expressresponse
et res
: L'objet de réponse fourni par Express Si le rendu échoue, une erreur est transmise au prochain middleware de gestion des erreurs. Vous pouvez enregistrer votre propre middleware de gestion des erreurs comme ceci :
// First Serve-SPA
serveSpa ( app , rootPath ) ;
// Then this middleware with the additional err parameter
app . use ( function ( err , req , res , next ) {
// If headers were already sent it gets complicated...
if ( res . headersSent ) {
return next ( err ) ; // ...just leave it to Express' default error handling middleware.
}
// Do your error handling:
console . error ( err ) ;
res . redirect ( '/guru-meditation/' ) ;
} ) ;
Placez un fichier compose.js
dans le même dossier que votre index.htmlt
et il sera exécuté juste avant le rendu du modèle. Cela permet par exemple de récupérer des données de la base de données pour les utiliser lors du rendu du modèle.
compose.js
doit exporter un middleware :
var db = require ( '../lib/db.js' ) ;
module . exports = function ( req , res , next ) {
db . loadData ( { for : 'me' } , function ( err , data ) {
if ( err ) {
return next ( err ) ;
}
req . data = data ; // You can access the data through req.data in the template now.
next ( ) ;
} ) ;
} ;
Le middleware exporté via compose.js
est utilisé comme un middleware classique. Ainsi, la gestion des erreurs fonctionne comme d'habitude.
Un middleware peut être transmis via options.beforeAll
qui est exécuté pour toutes les requêtes aboutissant au rendu d'un modèle. Si seul un fichier statique est demandé, ce middleware n'est pas exécuté.
L’ordre global d’exécution est le suivant :
Le middleware fourni est utilisé comme un middleware classique. Ainsi, la gestion des erreurs fonctionne comme d'habitude.
Comme express.static(...)
Serve-SPA ne traite que les requêtes GET et HEAD. Par défaut, pour une requête HEAD, ni les middlewares beforeAll et compose.js ne sont exécutés ni le modèle index.htmlt n'est rendu. Cependant, l'exécution des middlewares peut être explicitement activée en ajoutant callForHEAD = true
à la fonction middleware :
function middlewareForGETandHEAD ( req , res , next ) {
res . set ( 'my-header' , 'yay' ) ;
if ( req . method === 'GET' ) {
db . load ( ... ) ;
}
}
middlewareForGETandHEAD . callForHEAD = true ;
Le README Serve-Static explique comment utiliser plusieurs racines comme ceci :
app . use ( serveStatic ( path . join ( __dirname , '/public-optimized' ) ) ) ;
app . use ( serveStatic ( path . join ( __dirname , '/public' ) ) ) ;
Avec Serve-SPA, cependant, utiliser plusieurs middlewares comme celui-ci n'est plus possible. Parce que gérer correctement les URL pushState le rend plus complexe.
Pour fournir la même prise en charge de plusieurs racines, Serve-SPA utilise un ensemble de chemins racine. Le code suivant est équivalent à l'exemple Serve-Static ci-dessus :
serveSpa ( app , [
path . join ( __dirname , '/public-optimized' ) ,
path . join ( __dirname , '/public' )
] ) ;
Une URL n'est identifiée comme URL pushState que si aucune ressource statique dans tous les chemins donnés ne correspond à l'URL. Et une URL pushState est appliquée au SPA qui correspond le mieux, comme d'habitude – comme si les répertoires donnés étaient fusionnés.
Pour configurer votre environnement de développement pour Serve-SPA :
cd
dans le dossier principal,npm install
,npm install gulp -g
si vous n'avez pas encore installé gulp globalement, etgulp dev
. (Ou exécutez node ./node_modules/.bin/gulp dev
si vous ne souhaitez pas installer gulp globalement.) gulp dev
surveille tous les fichiers sources et si vous enregistrez certaines modifications, il peluchera le code et exécutera tous les tests. Le rapport de couverture des tests peut être consulté à partir de ./coverage/lcov-report/index.html
.
Si vous souhaitez déboguer un test, vous devez utiliser gulp test-without-coverage
pour exécuter tous les tests sans masquer le code par l'instrumentation de couverture de test.
minimatch
comme conseillé par Node Security Platformexpress.Router()
dans compose.jsexpress.Router()
Si vous n'avez jamais entendu parler de la licence ISC, elle est fonctionnellement équivalente à la licence MIT.
Voir le fichier LICENSE pour plus de détails.