Introduction au Responsive Design Server Side (ou RESS)
Il y a quelques années de cela, j'avais rédigé pour le site developpez.com un article sur les fondamentaux du responsive design.
Le principe même du responsive design est de fournir, en se basant sur des outils, un résultat qui est inhérent aux matériaux / supports. Cette phrase peut sembler abscons mais veut dire tout simplement que nous ne nous limitons pas au support afin de réaliser nos oeuvres.
Dans le monde Web, cela se traduit par la réalisation qui ne dépend ni d'un type de navigateur (encore moins d'une version), ni du support (desktop / mobile), ni même de la résolution d'écran ou de son ratio.
Il existe de nombreuses solutions côtés navigateurs pour cela: que ce soit les Media Queries CSS3 afin d’adapter l'affichage en fonction de la résolution, des frameworks comme device.js qui nous indique sur quel support nous sommes (quel orientation également, etc ...). Ce qui va nous permettre encore une fois de soit adapter l'affichage, soit d'appeler le bon plugin JavaScript qui sera adapté en fonction du support.
L'avantage de cela: tout est relié côté frontend. Les serveurs backend n'auront pour rôle uniquement d'héberger simplement l'application Web, voir de fournir des services REST.
L'inconvénient est que nous ne sommes pas assez mobile first.
Qu'est-ce que le mobile first ?
Le mobile first (je vous conseille ce bouquin, il est excellent) a pour principe d'essayer de concevoir d'abord notre application à destination des supports mobiles avant de faire la version desktop (ce qui est largement l'inverse à l'heure d'aujourd'hui).
Quels sont les intérêts ?
Tout d'abord, nous réfléchissons aux fonctionnalités importantes de notre application et surtout aux informations importantes. En effet, nous n'avons pas la même résolution ni la même aisance à naviguer sur un support mobile que sur un support classique. Il faut donc être pragmatique et fournir au mieux les informations / fonctionnalités.
Ensuite, nous devons concevoir une accessibilité adaptés au support mobile. Donc pas de tooltip, pas de survol pour afficher un menu. De même, cela ne sert à rien de fournir un datepicker très sophistiqués si nous ne pouvons même pas appuyer correctement sur une date, car celle-ci s'affichant plus petit que nos doigts (pour information, nous estimons que la taille d'un doigt sur l'écran fait en 45*45 pixels et 60*60 pixels).
Et à la fin, cela va se traduire par de l'optimisation. En effet, je vous laisse lire mon article sur "le constat du Web mobile", mais pour rappel:
- Nous ne sommes pas assez habitués à avoir des performances dégradés de nos navigateurs, et encore moins des connexions réseaux
- Nous ne sommes pas assez habitués aux petites tailles d'écrans
- Nous ne testons très peu en situation "réel"
Du coup, nos applications Web mobiles ne sont pas assez performantes, car souvent conçu d'abord pour un usage desktop. Et c'est d'autant plus gênant, que le pourcentage de personnes n'ayant jamais navigué sur un desktop augmentent de plus en plus (c'est déjà le cas aux Etats-Unis, et cela va s'accroître massivement d'ici 2020).
Ainsi, nous devons réaliser des applications qui puissent fournir un temps de réponses satisfaisantes. Et pour ce faire, nous allons nous appuyer sur le RESS.
Responsive Design Server Side
Le principe est en soit très simple: nous allons faire du responsive design classique (principalement en se basant sur les Media Queries CSS3). Mais le serveur va nous épauler en mettant en place des stratégies d'optimisation.
Ces dernières peuvent être variées, mais consistent principalement:
- A générer un HTML adapté
- A charger un fichier CSS / JavaScript adapté
- A charger des images adaptées
A générer un HTML adapté
En effet, au lieu de charger des portions de pages qui ne s'afficheront pas sur le support mobile, autant ne pas le charger du tout. Cela permet d'économiser de la bande passante, mais aussi en gain de traitement.
En effet, un noeud HTML représente trois instances: une instance HTML, CSS et JavaScript. Même si les éléments sont cachés, le navigateur va les charger en mémoire. De plus, nous pouvons envisager que des éléments soient initialisés en JavaScript (pour afficher un datepicker). Or si la zone est caché, cela a peu de sens.
A charger un fichier CSS / JavaScript adapté
Ici, il faut plutôt voir le fait de charger un CSS ou du côté JavaScript orienté mobile ou non. Par exemple, au lieu de charger le datepicker compliqué avec pleins d'options, chargeons plutôt un datepicker plus simple.
De même, au lieu de charger tout le fichier CSS, chargeons un adapté au mobile. Voir même adapté pour une plateforme mobile.
A charger des images adaptées
Là, nous pouvons envisager de soit chargés des images compressés (à la volée et pré-compressés), soit de charger des images "responsives".
Le principe de ces dernières est d'agrandir l'image par 2, de fortement la compressé, mais de l'afficher avec la taille d'origine. Cela permet d'avoir un résultat d'affichage assez proche de l'origine tout en bénéficiant d'une réduction forte de la bande passante. Egalement, cela peut se faire à la volée ou en pré-compressés.
Pour la génération à la volée, ayez bien en tête que le temps de la génération peut être aussi important que le temps de chargement de l'image originelle. De ce fait, appliquez la génération à la volée uniquement si vous avez des images dynamiques, et / ou très volumineuses. Dans tous les cas, prévoyez un système de cache.
Les outils RESS
Pour nous venir en aide, il existe de nombreuses bibliothèques. Nous avons pour commencer le "WURLF", de Facebook, qui, en analysant les headers HTTP, est capable de nous fournir un grand nombre d'informations (voir même des problèmes de compatibilités HTML5, CSS et JavaScript). Nous avons un peu dans le même principe DeviceAtlas et DetectMobile.
Le problème des deux derniers: ils sont payants (chers même). Et le problème de ces trois solutions: nous devons régulièrement mettre à jour la base de données associés.
Nous allons dans notre illustration à RESS plutôt utiliser "ua-parser" qui se content d'analyser le User-Agent, mais nous donne assez d'informations pour appliquer des stratégies de bases.
Exemple 1: fournir un CSS / JavaScript adapté
Ici, le but est d'afficher un date picker qui soit adapté en fonction du support. Pour ce faire, nous allons écrire un petit service qui va nous faciliter l'usage de "ua-parser":
/** * Device information service * * @see https://www.npmjs.com/package/ua-parser-js * * @module ress/services/ress-device * @export RessDeviceService * @version 1.0.0 * @since 1.0.0 */
'use strict';
// Importsvar UAParser = require('ua-parser-js');
var _ = require('lodash');
// Constants, enums & variablesvar /** * @constant * @private * @type {string} */ USER_AGENT_HEADER_NAME = 'User-Agent',
/** * @enum * @private * @type {string} */ DEVICE_TYPE_ENUM = {
'MOBILE': 'mobile',
'TABLET': 'tablet' },
/** * The service itself ! * * @private * @type {RessDeviceService} */ service;
/** * @memberOf RessDeviceService */service = {
/** * Get all informations around the targeted client * * @method * @static * @param {HttpRequest} req * @returns {UAParser} */ 'getUaInformations': function (req) {
var parser = new UAParser();
parser.setUA(req.get(USER_AGENT_HEADER_NAME));
return parser;
},
/** * Check if we are on a mobile device * * @method * @static * @param {UaParser} parser * @returns {boolean} */ 'isMobileDevice': function (parser) {
var device = parser.getDevice();
return !!(device && (device.type === DEVICE_TYPE_ENUM.MOBILE || device.type === DEVICE_TYPE_ENUM.TABLET));
}
};
/** * Get all information for the targeted client * * @method * @param {HttpRequest} req * @constructor */function RessDeviceService(req) {
var parserInstance = service.getUaInformations(req);
var serviceProperties = _.keys(service);
var propertiesToPartial = _.reject(serviceProperties, function (propertyName) {
return propertyName === 'getUaInformations';
});
propertiesToPartial.forEach(function (propertyName) {
this[propertyName] = _.partial(service[propertyName], parserInstance);
}.bind(this));
}
RessDeviceService.api = service;
module.exports = RessDeviceService;
Maintenant, écrivons le bout de code pour gérer le datepicker côté frontend:
(function () { 'use strict'; // Namespaces window.adaptaters = window.adaptaters || { }; // Define datepicker adaptater /** * @method * @param {HTMLElement} domElement */ window.adaptaters.datepickerAdapater = function (domElement) { var pickerInstance if (typeof Pikaday !== 'undefined') { // So we are on a desktop pickerInstance = new Pikaday({ 'field': domElement }); } else { // We are on a mobile var $input = $(domElement).pickadate(); pickerInstance = $input.pickadate('picker'); } return pickerInstance; // Create a wrapper around the instance } })();
Et maintenant, voici le middleware ExpressJs qui va ni plus ni moins faire une redirection vers le bon fichier CSS / JavaScript:
/** * RESS image middleware * * @module ress/middlewares/index * @export WidgetsMiddleware * @version 1.0.0 * @since 1.0.0 */
'use strict';
// Importsvar path = require('path');
var url = require('url');
var RessDeviceService = require('../services/ress-device');
// Constants, enums & variablesvar /** * @constant * @private * @type {string} */ DATE_PICKER_TOKEN = 'responsive-widget-datepicker';
/** * @method WidgetsMiddleware * @param {string} rootPath * @returns {MiddlewareFunction} */module.exports = function (rootPath) {
return function (req, res, next) {
var urlObject = url.parse(req.originalUrl);
if (urlObject.pathname.indexOf(DATE_PICKER_TOKEN) >= 0) {
var currentRessDeviceService = new RessDeviceService(req);
var filePath = urlObject.pathname .replace('/webapp/', '')
.replace(DATE_PICKER_TOKEN, currentRessDeviceService.isMobileDevice() ? 'picker.date' : 'pikaday');
res.sendFile(path.resolve(path.join(rootPath, filePath)), function (err) {
if (err) {
console.error(err && err.stack ? err.stack : err);
res.status(500);
res.end();
}
});
} else {
next();
}
}
};
Jusqu'ici, c'est plutôt simple.
Exemple 2: fournir une image adaptée
Ici, nous allons utiliser sharp qui est une excellente librairie de manipulation d'images. Faisons d'abord un petit service pour nous faciliter la tâche:
/** * Image manipulation service * * @module ress/services/ress-image * @export RessImageService * @version 1.0.0 * @since 1.0.0 */
'use strict';
// Importsvar sharp = require('sharp');
// Constants, enums & variablesvar /** * @constant * @private * @type {number} */ IDEAL_COMPRESSION_LEVEL = 40,
/** * @constant * @private * @type {number} */ RESPONSIVE_COMPRESSION_LEVEL = 20,
/** * @constant * @private * @type {number} */ RESPONSIVE_IMAGE_RESIZE_RATIO = 2;
/** * @memberOf RessImageService */module.exports = {
/** * @method * @static * @param {string} imageFilePath * @param {stream} outputStream * @returns {Promise.<stream>} */ 'compressImage': function (imageFilePath, outputStream) {
var sharpImage = sharp(imageFilePath);
return sharpImage .metadata()
.then(function (metadata) {
return sharpImage .quality(IDEAL_COMPRESSION_LEVEL)
.resize(metadata.width, metadata.height)
.pipe(outputStream);
});
},
/** * @method * @static * @param {string} imageFilePath * @param {stream} outputStream * @returns {Promise.<stream>} */ 'responsiveImage': function (imageFilePath, outputStream) {
var sharpImage = sharp(imageFilePath);
return sharpImage .metadata()
.then(function (metadata) {
return sharpImage .quality(RESPONSIVE_COMPRESSION_LEVEL)
.resize(metadata.width * RESPONSIVE_IMAGE_RESIZE_RATIO, metadata.height * RESPONSIVE_IMAGE_RESIZE_RATIO)
.pipe(outputStream);
});
}
};
Maintenant, nous allons fournir un middleware qui en fonction si nous sommes sur du mobile, ou fonction d'un token, fera la compression:
/** * RESS image middleware * * @module ress/middlewares/index * @export ImageMiddleware * @version 1.0.0 * @since 1.0.0 */
'use strict';
// Importsvar path = require('path');
var url = require('url');
var RessImageService = require('../services/ress-image');
var RessDeviceService = require('../services/ress-device');
/** * @method ImageMiddleware * @param {string} rootPath * @returns {MiddlewareFunction} */module.exports = function (rootPath) {
return function (req, res, next) {
var currentRessDeviceService = new RessDeviceService(req);
if ((req.query && req.query.imageAction) || currentRessDeviceService.isMobileDevice()) {
var urlObject = url.parse(req.originalUrl);
var filePath = path.resolve(path.join(rootPath, urlObject.pathname.replace(req.baseUrl, '')));
var promise = req.query.action === 'responsive' ?
RessImageService.responsiveImage(filePath, res) :
RessImageService.compressImage(filePath, res);
promise .catch(function (ex) {
console.error(ex);
res.status(500);
res.end();
});
} else {
next();
}
};
};
L'usage dans l'HTML sera plutôt simple:
<!DOCTYPE html>
<html>
<head lang="en">
<title>RESS application example</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<style>
fieldset {
display: inline-block;
}
.plane-image {
height: 320px;
width: 480px;
}
</style>
</head>
<body>
<h1>RESS application example</h1>
<br />
<fieldset>
<legend>Original image</legend>
<img src="./plane.jpg" class="plane-image" />
<!-- <img src="./plane.jpg?imageAction=compressed" class="plane-image" /> <img src="./plane.jpg?imageAction=responsive" class="plane-image" /> --> </fieldset>
</body>
</html>
Ce qui nous donnera les affichages suivants:
Nous voyons que les images sont plutôt proches (même si nous pouvons voir un peu de flou sur les images compressées).
Si nous regardons les résultats, nous voyons que nous avons diviser par 4 la taille des images:
En revanche, dans notre exemple, l'image est plutôt petite et nous faisons le traitement à la volée. Et si nous regardons les temps de latences, l'image originelle se charge plus rapidement.
Cela montre qu'il faut bien réfléchir sur l'usage de cette dernière technique: devons-nous le faire à priori, au moment du chargement, mettre un système de cache ? Etc ...
Commentaires
Enregistrer un commentaire