Table of Contents
Détection de navigateur et Design Patterns créatifs en JavaScript
Dans le monde du développement web moderne, deux défis majeurs se présentent constamment aux développeurs JavaScript : assurer la compatibilité cross-browser et structurer le code de manière maintenable et évolutive. Ces deux problématiques, bien que distinctes, partagent un objectif commun : créer des applications robustes et professionnelles.
La détection de navigateur a considérablement évolué depuis les débuts du web. À l’époque des “guerres des navigateurs”, les développeurs devaient jongler avec des implémentations radicalement différentes de JavaScript. Aujourd’hui, bien que les navigateurs modernes soient beaucoup plus standardisés, des différences subtiles persistent, notamment pour les nouvelles APIs et fonctionnalités expérimentales.
Les design patterns créatifs, quant à eux, constituent le fondement d’une architecture logicielle solide. Ces patterns, issus du célèbre livre “Gang of Four”, ont été adaptés au paradigme JavaScript et sont devenus essentiels pour tout développeur souhaitant écrire du code professionnel. Ils permettent de résoudre des problèmes récurrents de création d’objets tout en favorisant la réutilisabilité et la testabilité du code.
Dans cet article approfondi, nous explorerons les techniques modernes de détection de navigateur, puis nous plongerons dans les patterns créatifs les plus utilisés : Singleton, Factory, Builder et Prototype. Chaque concept sera illustré par des exemples pratiques en JavaScript pur, directement applicables dans vos projets.
Détection de navigateur : approches et bonnes pratiques
La détection de navigateur est essentielle pour garantir que votre application offre une expérience optimale sur toutes les plateformes. Cependant, toutes les approches ne se valent pas. Examinons les différentes méthodes disponibles, de la plus recommandée à la moins conseillée.
Feature Detection : la méthode recommandée
La feature detection (détection de fonctionnalités) est la méthode privilégiée par la communauté JavaScript moderne. Plutôt que de demander “Quel navigateur utilises-tu ?”, on demande “Supportes-tu cette fonctionnalité ?”. Cette approche est plus fiable et future-proof.
Détection native en JavaScript
// Vérifier le support de localStorage
function supportsLocalStorage() {
try {
var storage = window.localStorage;
var testKey = '__storage_test__';
storage.setItem(testKey, testKey);
storage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
// Vérifier le support de Fetch API
function supportsFetch() {
return 'fetch' in window;
}
// Vérifier le support des Promises
function supportsPromises() {
return typeof Promise !== 'undefined' &&
Promise.toString().indexOf('[native code]') !== -1;
}
// Vérifier le support de Service Workers
function supportsServiceWorkers() {
return 'serviceWorker' in navigator;
}
// Vérifier le support de WebGL
function supportsWebGL() {
try {
var canvas = document.createElement('canvas');
return !!(
window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
);
} catch (e) {
return false;
}
}
// Utilisation
if (supportsLocalStorage()) {
localStorage.setItem('user', JSON.stringify({ name: 'John' }));
} else {
// Fallback avec cookies ou autre mécanisme
console.log('localStorage non supporté, utilisation du fallback');
}
Utilisation de Modernizr
Modernizr est une bibliothèque populaire qui automatise la détection de fonctionnalités et ajoute des classes CSS au document.
// Installation: npm install modernizr
// Ou inclusion via CDN
// Détection de multiples fonctionnalités
if (Modernizr.flexbox) {
console.log('Flexbox est supporté');
}
if (Modernizr.webgl) {
initWebGLApplication();
} else {
initCanvasFallback();
}
// Chargement conditionnel de polyfills
Modernizr.on('fetch', function(result) {
if (!result) {
// Charger le polyfill fetch
var script = document.createElement('script');
script.src = 'https://cdn.polyfill.io/v3/polyfill.min.js?features=fetch';
document.head.appendChild(script);
}
});
// Exemple de configuration personnalisée Modernizr
// modernizr-config.json
var modernizrConfig = {
"minify": true,
"options": ["setClasses"],
"feature-detects": [
"css/flexbox",
"css/grid",
"storage/localstorage",
"serviceworker"
]
};
CSS @supports pour la détection côté styles
La règle CSS @supports permet de détecter les fonctionnalités directement dans les feuilles de style.
/* Détection de Grid Layout */
@supports (display: grid) {
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
}
/* Fallback pour navigateurs sans Grid */
@supports not (display: grid) {
.container {
display: flex;
flex-wrap: wrap;
}
.container > * {
flex: 1 1 250px;
margin: 0.5rem;
}
}
/* Détection de backdrop-filter */
@supports (backdrop-filter: blur(10px)) {
.modal-overlay {
backdrop-filter: blur(10px);
background-color: rgba(0, 0, 0, 0.5);
}
}
/* Détection de position: sticky */
@supports (position: sticky) {
.header {
position: sticky;
top: 0;
}
}
// Équivalent JavaScript de @supports
function cssSupports(property, value) {
if ('CSS' in window && 'supports' in window.CSS) {
return window.CSS.supports(property, value);
}
// Fallback pour anciens navigateurs
var element = document.createElement('div');
element.style[property] = value;
return element.style[property] === value;
}
// Utilisation
if (cssSupports('display', 'grid')) {
document.body.classList.add('grid-supported');
}
if (cssSupports('backdrop-filter', 'blur(10px)')) {
document.body.classList.add('backdrop-supported');
}
Tableau des APIs modernes de détection
| Fonctionnalité | Méthode de détection | Support navigateur |
|---|---|---|
| Fetch API | 'fetch' in window | Chrome 42+, Firefox 39+, Safari 10.1+ |
| Service Workers | 'serviceWorker' in navigator | Chrome 40+, Firefox 44+, Safari 11.1+ |
| Web Components | 'customElements' in window | Chrome 54+, Firefox 63+, Safari 10.1+ |
| Intersection Observer | 'IntersectionObserver' in window | Chrome 51+, Firefox 55+, Safari 12.1+ |
| ResizeObserver | 'ResizeObserver' in window | Chrome 64+, Firefox 69+, Safari 13.1+ |
| WebAssembly | typeof WebAssembly === 'object' | Chrome 57+, Firefox 52+, Safari 11+ |
| CSS Grid | CSS.supports('display', 'grid') | Chrome 57+, Firefox 52+, Safari 10.1+ |
| ES6 Modules | 'noModule' in document.createElement('script') | Chrome 61+, Firefox 60+, Safari 11+ |
User Agent Parsing : quand et comment l’utiliser
Bien que déconseillé comme méthode principale, le parsing du User Agent reste parfois nécessaire pour des cas spécifiques comme l’analytics ou les workarounds de bugs connus.
// Parser de User Agent complet
var BrowserDetector = (function() {
var ua = navigator.userAgent;
var browserInfo = {
name: 'Unknown',
version: 'Unknown',
os: 'Unknown',
mobile: false
};
// Détection du système d'exploitation
function detectOS() {
if (ua.indexOf('Windows') !== -1) return 'Windows';
if (ua.indexOf('Mac') !== -1) return 'macOS';
if (ua.indexOf('Linux') !== -1) return 'Linux';
if (ua.indexOf('Android') !== -1) return 'Android';
if (ua.indexOf('iOS') !== -1 || ua.indexOf('iPhone') !== -1 || ua.indexOf('iPad') !== -1) return 'iOS';
return 'Unknown';
}
// Détection du navigateur et version
function detectBrowser() {
var match;
// Edge (Chromium)
if ((match = ua.match(/Edg\/(\d+)/))) {
return { name: 'Edge', version: match[1] };
}
// Chrome
if ((match = ua.match(/Chrome\/(\d+)/) ) && ua.indexOf('Edg') === -1) {
return { name: 'Chrome', version: match[1] };
}
// Firefox
if ((match = ua.match(/Firefox\/(\d+)/))) {
return { name: 'Firefox', version: match[1] };
}
// Safari
if ((match = ua.match(/Version\/(\d+).*Safari/))) {
return { name: 'Safari', version: match[1] };
}
// Opera
if ((match = ua.match(/OPR\/(\d+)/))) {
return { name: 'Opera', version: match[1] };
}
// Internet Explorer
if ((match = ua.match(/MSIE\s(\d+)/)) || (match = ua.match(/Trident.*rv:(\d+)/))) {
return { name: 'IE', version: match[1] };
}
return { name: 'Unknown', version: 'Unknown' };
}
// Détection mobile
function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
}
var browser = detectBrowser();
browserInfo.name = browser.name;
browserInfo.version = browser.version;
browserInfo.os = detectOS();
browserInfo.mobile = isMobile();
return {
info: browserInfo,
isChrome: function() { return browserInfo.name === 'Chrome'; },
isFirefox: function() { return browserInfo.name === 'Firefox'; },
isSafari: function() { return browserInfo.name === 'Safari'; },
isEdge: function() { return browserInfo.name === 'Edge'; },
isIE: function() { return browserInfo.name === 'IE'; },
isMobile: function() { return browserInfo.mobile; },
versionAtLeast: function(minVersion) {
return parseInt(browserInfo.version) >= minVersion;
}
};
})();
// Utilisation
console.log('Navigateur:', BrowserDetector.info.name);
console.log('Version:', BrowserDetector.info.version);
console.log('OS:', BrowserDetector.info.os);
console.log('Mobile:', BrowserDetector.info.mobile);
if (BrowserDetector.isChrome() && BrowserDetector.versionAtLeast(90)) {
console.log('Chrome moderne détecté');
}
Pourquoi éviter le UA sniffing comme méthode principale
- Fragile : Les User Agents peuvent être facilement modifiés ou falsifiés
- Non maintenable : Chaque nouveau navigateur ou version nécessite une mise à jour
- Imprécis : Ne reflète pas les capacités réelles du navigateur
- Obsolète : Les navigateurs modernes convergent vers des standards communs
// MAUVAISE PRATIQUE - À éviter
if (navigator.userAgent.indexOf('Chrome') !== -1) {
// Code spécifique Chrome - FRAGILE !
}
// BONNE PRATIQUE - Feature detection
if ('IntersectionObserver' in window) {
// Utiliser IntersectionObserver
var observer = new IntersectionObserver(callback, options);
} else {
// Fallback avec scroll events
window.addEventListener('scroll', handleScrollFallback);
}
Singleton Pattern : garantir une instance unique
Le Singleton est l’un des patterns créatifs les plus utilisés. Il garantit qu’une classe n’a qu’une seule instance et fournit un point d’accès global à cette instance.
Cas d’utilisation typiques
- Gestionnaire de configuration : Une seule source de vérité pour les paramètres
- Pool de connexions : Contrôler le nombre de connexions à une base de données
- Logger : Un seul point d’entrée pour la journalisation
- Cache : Un cache partagé entre tous les modules
- État global : Store Redux-like simplifié
Implémentation avec closure
// Singleton classique avec closure
var ConfigurationManager = (function() {
var instance = null;
var config = {};
function createInstance() {
return {
get: function(key) {
return config[key];
},
set: function(key, value) {
config[key] = value;
return this;
},
getAll: function() {
return Object.assign({}, config);
},
load: function(newConfig) {
config = Object.assign({}, config, newConfig);
return this;
},
reset: function() {
config = {};
return this;
}
};
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// Utilisation
var config1 = ConfigurationManager.getInstance();
config1.set('apiUrl', 'https://api.example.com')
.set('timeout', 5000)
.set('debug', true);
var config2 = ConfigurationManager.getInstance();
console.log(config2.get('apiUrl')); // 'https://api.example.com'
console.log(config1 === config2); // true - même instance
Implémentation avec classe ES6
// Singleton avec classe ES6
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}
this.logs = [];
this.level = 'info';
this.levels = { debug: 0, info: 1, warn: 2, error: 3 };
Logger.instance = this;
}
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
setLevel(level) {
if (this.levels.hasOwnProperty(level)) {
this.level = level;
}
return this;
}
_shouldLog(level) {
return this.levels[level] >= this.levels[this.level];
}
_formatMessage(level, message, data) {
var timestamp = new Date().toISOString();
return {
timestamp: timestamp,
level: level.toUpperCase(),
message: message,
data: data || null
};
}
_log(level, message, data) {
if (this._shouldLog(level)) {
var logEntry = this._formatMessage(level, message, data);
this.logs.push(logEntry);
console[level]('[' + logEntry.timestamp + '] [' + logEntry.level + ']', message, data || '');
}
return this;
}
debug(message, data) { return this._log('debug', message, data); }
info(message, data) { return this._log('info', message, data); }
warn(message, data) { return this._log('warn', message, data); }
error(message, data) { return this._log('error', message, data); }
getLogs(level) {
if (level) {
return this.logs.filter(function(log) {
return log.level === level.toUpperCase();
});
}
return this.logs.slice();
}
clear() {
this.logs = [];
return this;
}
}
// Utilisation
var logger1 = Logger.getInstance();
logger1.setLevel('debug');
logger1.info('Application démarrée');
logger1.debug('Chargement des modules', { modules: ['auth', 'api', 'ui'] });
var logger2 = new Logger(); // Retourne la même instance
console.log(logger1 === logger2); // true
Module Pattern : Singleton naturel
En JavaScript, les modules ES6 sont naturellement des singletons. Chaque module n’est évalué qu’une seule fois.
// cache.js - Module singleton naturel
var cache = {};
var maxSize = 100;
var accessOrder = [];
function get(key) {
if (cache.hasOwnProperty(key)) {
// Mettre à jour l'ordre d'accès (LRU)
var index = accessOrder.indexOf(key);
if (index > -1) {
accessOrder.splice(index, 1);
}
accessOrder.push(key);
return cache[key].value;
}
return undefined;
}
function set(key, value, ttl) {
// Vérifier la taille du cache
if (!cache.hasOwnProperty(key) && Object.keys(cache).length >= maxSize) {
// Supprimer l'élément le moins récemment utilisé
var oldestKey = accessOrder.shift();
delete cache[oldestKey];
}
cache[key] = {
value: value,
expiry: ttl ? Date.now() + ttl : null
};
var index = accessOrder.indexOf(key);
if (index > -1) {
accessOrder.splice(index, 1);
}
accessOrder.push(key);
return true;
}
function has(key) {
if (!cache.hasOwnProperty(key)) {
return false;
}
// Vérifier l'expiration
if (cache[key].expiry && Date.now() > cache[key].expiry) {
remove(key);
return false;
}
return true;
}
function remove(key) {
if (cache.hasOwnProperty(key)) {
delete cache[key];
var index = accessOrder.indexOf(key);
if (index > -1) {
accessOrder.splice(index, 1);
}
return true;
}
return false;
}
function clear() {
cache = {};
accessOrder = [];
}
function size() {
return Object.keys(cache).length;
}
function setMaxSize(newSize) {
maxSize = newSize;
// Purger si nécessaire
while (Object.keys(cache).length > maxSize) {
var oldestKey = accessOrder.shift();
delete cache[oldestKey];
}
}
// Export du module (singleton naturel)
var Cache = {
get: get,
set: set,
has: has,
remove: remove,
clear: clear,
size: size,
setMaxSize: setMaxSize
};
// Utilisation
Cache.set('user:123', { name: 'John', email: 'john@example.com' }, 60000); // TTL 60s
Cache.set('settings', { theme: 'dark', language: 'fr' });
console.log(Cache.get('user:123')); // { name: 'John', ... }
console.log(Cache.has('settings')); // true
console.log(Cache.size()); // 2
Factory Pattern : création flexible d’objets
Le Factory Pattern permet de créer des objets sans spécifier leur classe exacte. Il encapsule la logique de création et favorise le découplage.
Simple Factory
La Simple Factory est une classe ou fonction qui crée des objets basés sur un paramètre.
// Simple Factory pour les notifications
function NotificationFactory() {}
NotificationFactory.create = function(type, options) {
var notification;
switch (type) {
case 'email':
notification = new EmailNotification(options);
break;
case 'sms':
notification = new SMSNotification(options);
break;
case 'push':
notification = new PushNotification(options);
break;
case 'slack':
notification = new SlackNotification(options);
break;
default:
throw new Error('Type de notification inconnu: ' + type);
}
return notification;
};
// Classes de notification
function EmailNotification(options) {
this.to = options.to;
this.subject = options.subject || 'Notification';
this.body = options.body;
}
EmailNotification.prototype.send = function() {
console.log('Envoi email à ' + this.to + ': ' + this.subject);
// Logique d'envoi email
return Promise.resolve({ success: true, type: 'email' });
};
function SMSNotification(options) {
this.phone = options.phone;
this.message = options.message;
}
SMSNotification.prototype.send = function() {
console.log('Envoi SMS à ' + this.phone);
// Logique d'envoi SMS
return Promise.resolve({ success: true, type: 'sms' });
};
function PushNotification(options) {
this.token = options.token;
this.title = options.title;
this.body = options.body;
this.data = options.data || {};
}
PushNotification.prototype.send = function() {
console.log('Envoi push notification: ' + this.title);
// Logique d'envoi push
return Promise.resolve({ success: true, type: 'push' });
};
function SlackNotification(options) {
this.webhook = options.webhook;
this.channel = options.channel;
this.message = options.message;
}
SlackNotification.prototype.send = function() {
console.log('Envoi message Slack sur ' + this.channel);
// Logique d'envoi Slack
return Promise.resolve({ success: true, type: 'slack' });
};
// Utilisation
var emailNotif = NotificationFactory.create('email', {
to: 'user@example.com',
subject: 'Bienvenue',
body: 'Merci de votre inscription!'
});
var smsNotif = NotificationFactory.create('sms', {
phone: '+33612345678',
message: 'Code de vérification: 123456'
});
emailNotif.send();
smsNotif.send();
Factory Method
Le Factory Method définit une interface pour créer un objet, mais laisse les sous-classes décider quelle classe instancier.
// Factory Method pour les composants UI
function UIComponent(options) {
this.id = options.id || 'component-' + Date.now();
this.className = options.className || '';
this.element = null;
}
UIComponent.prototype.render = function() {
throw new Error('La méthode render() doit être implémentée');
};
UIComponent.prototype.mount = function(container) {
this.element = this.render();
container.appendChild(this.element);
return this;
};
UIComponent.prototype.unmount = function() {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
return this;
};
// Factory Method - à surcharger
UIComponent.prototype.createElement = function() {
return document.createElement('div');
};
// Implémentation Button
function ButtonComponent(options) {
UIComponent.call(this, options);
this.text = options.text || 'Button';
this.onClick = options.onClick || function() {};
this.variant = options.variant || 'primary';
}
ButtonComponent.prototype = Object.create(UIComponent.prototype);
ButtonComponent.prototype.constructor = ButtonComponent;
ButtonComponent.prototype.render = function() {
var button = document.createElement('button');
button.id = this.id;
button.className = 'btn btn-' + this.variant + ' ' + this.className;
button.textContent = this.text;
button.addEventListener('click', this.onClick);
return button;
};
// Implémentation Input
function InputComponent(options) {
UIComponent.call(this, options);
this.type = options.type || 'text';
this.placeholder = options.placeholder || '';
this.value = options.value || '';
this.onChange = options.onChange || function() {};
}
InputComponent.prototype = Object.create(UIComponent.prototype);
InputComponent.prototype.constructor = InputComponent;
InputComponent.prototype.render = function() {
var wrapper = document.createElement('div');
wrapper.className = 'input-wrapper ' + this.className;
var input = document.createElement('input');
input.id = this.id;
input.type = this.type;
input.placeholder = this.placeholder;
input.value = this.value;
input.addEventListener('input', this.onChange);
wrapper.appendChild(input);
return wrapper;
};
// Implémentation Modal
function ModalComponent(options) {
UIComponent.call(this, options);
this.title = options.title || '';
this.content = options.content || '';
this.onClose = options.onClose || function() {};
}
ModalComponent.prototype = Object.create(UIComponent.prototype);
ModalComponent.prototype.constructor = ModalComponent;
ModalComponent.prototype.render = function() {
var self = this;
var overlay = document.createElement('div');
overlay.className = 'modal-overlay ' + this.className;
overlay.id = this.id;
var modal = document.createElement('div');
modal.className = 'modal';
var header = document.createElement('div');
header.className = 'modal-header';
header.innerHTML = '<h3>' + this.title + '</h3>';
var closeBtn = document.createElement('button');
closeBtn.className = 'modal-close';
closeBtn.textContent = '×';
closeBtn.addEventListener('click', function() {
self.close();
});
header.appendChild(closeBtn);
var body = document.createElement('div');
body.className = 'modal-body';
body.innerHTML = this.content;
modal.appendChild(header);
modal.appendChild(body);
overlay.appendChild(modal);
overlay.addEventListener('click', function(e) {
if (e.target === overlay) {
self.close();
}
});
return overlay;
};
ModalComponent.prototype.close = function() {
this.onClose();
this.unmount();
};
// Factory pour créer les composants
var ComponentFactory = {
create: function(type, options) {
switch (type) {
case 'button': return new ButtonComponent(options);
case 'input': return new InputComponent(options);
case 'modal': return new ModalComponent(options);
default: throw new Error('Type de composant inconnu: ' + type);
}
}
};
// Utilisation
var loginButton = ComponentFactory.create('button', {
text: 'Se connecter',
variant: 'primary',
onClick: function() { console.log('Login clicked'); }
});
var emailInput = ComponentFactory.create('input', {
type: 'email',
placeholder: 'Votre email',
onChange: function(e) { console.log('Email:', e.target.value); }
});
var welcomeModal = ComponentFactory.create('modal', {
title: 'Bienvenue !',
content: '<p>Merci de votre visite.</p>',
onClose: function() { console.log('Modal fermée'); }
});
Abstract Factory
L’Abstract Factory fournit une interface pour créer des familles d’objets liés sans spécifier leurs classes concrètes.
// Abstract Factory pour les thèmes UI
function UIThemeFactory() {}
UIThemeFactory.prototype.createButton = function() {
throw new Error('Méthode abstraite');
};
UIThemeFactory.prototype.createInput = function() {
throw new Error('Méthode abstraite');
};
UIThemeFactory.prototype.createCard = function() {
throw new Error('Méthode abstraite');
};
// Thème Light
function LightThemeFactory() {
UIThemeFactory.call(this);
}
LightThemeFactory.prototype = Object.create(UIThemeFactory.prototype);
LightThemeFactory.prototype.constructor = LightThemeFactory;
LightThemeFactory.prototype.createButton = function(options) {
return {
type: 'button',
theme: 'light',
styles: {
backgroundColor: '#ffffff',
color: '#333333',
border: '1px solid #cccccc',
borderRadius: '4px',
padding: '8px 16px'
},
text: options.text,
render: function() {
var btn = document.createElement('button');
Object.assign(btn.style, this.styles);
btn.textContent = this.text;
return btn;
}
};
};
LightThemeFactory.prototype.createInput = function(options) {
return {
type: 'input',
theme: 'light',
styles: {
backgroundColor: '#ffffff',
color: '#333333',
border: '1px solid #cccccc',
borderRadius: '4px',
padding: '8px 12px'
},
placeholder: options.placeholder,
render: function() {
var input = document.createElement('input');
Object.assign(input.style, this.styles);
input.placeholder = this.placeholder;
return input;
}
};
};
LightThemeFactory.prototype.createCard = function(options) {
return {
type: 'card',
theme: 'light',
styles: {
backgroundColor: '#ffffff',
color: '#333333',
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '16px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
},
title: options.title,
content: options.content,
render: function() {
var card = document.createElement('div');
Object.assign(card.style, this.styles);
card.innerHTML = '<h3>' + this.title + '</h3><p>' + this.content + '</p>';
return card;
}
};
};
// Thème Dark
function DarkThemeFactory() {
UIThemeFactory.call(this);
}
DarkThemeFactory.prototype = Object.create(UIThemeFactory.prototype);
DarkThemeFactory.prototype.constructor = DarkThemeFactory;
DarkThemeFactory.prototype.createButton = function(options) {
return {
type: 'button',
theme: 'dark',
styles: {
backgroundColor: '#333333',
color: '#ffffff',
border: '1px solid #555555',
borderRadius: '4px',
padding: '8px 16px'
},
text: options.text,
render: function() {
var btn = document.createElement('button');
Object.assign(btn.style, this.styles);
btn.textContent = this.text;
return btn;
}
};
};
DarkThemeFactory.prototype.createInput = function(options) {
return {
type: 'input',
theme: 'dark',
styles: {
backgroundColor: '#2d2d2d',
color: '#ffffff',
border: '1px solid #555555',
borderRadius: '4px',
padding: '8px 12px'
},
placeholder: options.placeholder,
render: function() {
var input = document.createElement('input');
Object.assign(input.style, this.styles);
input.placeholder = this.placeholder;
return input;
}
};
};
DarkThemeFactory.prototype.createCard = function(options) {
return {
type: 'card',
theme: 'dark',
styles: {
backgroundColor: '#1e1e1e',
color: '#ffffff',
border: '1px solid #333333',
borderRadius: '8px',
padding: '16px',
boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
},
title: options.title,
content: options.content,
render: function() {
var card = document.createElement('div');
Object.assign(card.style, this.styles);
card.innerHTML = '<h3>' + this.title + '</h3><p>' + this.content + '</p>';
return card;
}
};
};
// Gestionnaire de thème
var ThemeManager = (function() {
var currentFactory = null;
return {
setTheme: function(theme) {
switch (theme) {
case 'light':
currentFactory = new LightThemeFactory();
break;
case 'dark':
currentFactory = new DarkThemeFactory();
break;
default:
throw new Error('Thème inconnu: ' + theme);
}
},
getFactory: function() {
if (!currentFactory) {
this.setTheme('light'); // Thème par défaut
}
return currentFactory;
}
};
})();
// Utilisation
ThemeManager.setTheme('dark');
var factory = ThemeManager.getFactory();
var darkButton = factory.createButton({ text: 'Valider' });
var darkInput = factory.createInput({ placeholder: 'Rechercher...' });
var darkCard = factory.createCard({ title: 'Dashboard', content: 'Bienvenue sur votre tableau de bord.' });
Autres Patterns Créatifs
Builder Pattern
Le Builder Pattern sépare la construction d’un objet complexe de sa représentation, permettant de créer différentes représentations avec le même processus de construction.
// Builder pour les requêtes HTTP
function HttpRequestBuilder() {
this.request = {
method: 'GET',
url: '',
headers: {},
body: null,
timeout: 30000,
credentials: 'same-origin'
};
}
HttpRequestBuilder.prototype.setMethod = function(method) {
this.request.method = method.toUpperCase();
return this;
};
HttpRequestBuilder.prototype.setUrl = function(url) {
this.request.url = url;
return this;
};
HttpRequestBuilder.prototype.addHeader = function(key, value) {
this.request.headers[key] = value;
return this;
};
HttpRequestBuilder.prototype.setBody = function(body) {
this.request.body = body;
return this;
};
HttpRequestBuilder.prototype.setJson = function(data) {
this.request.headers['Content-Type'] = 'application/json';
this.request.body = JSON.stringify(data);
return this;
};
HttpRequestBuilder.prototype.setTimeout = function(timeout) {
this.request.timeout = timeout;
return this;
};
HttpRequestBuilder.prototype.withCredentials = function() {
this.request.credentials = 'include';
return this;
};
HttpRequestBuilder.prototype.withAuth = function(token) {
this.request.headers['Authorization'] = 'Bearer ' + token;
return this;
};
HttpRequestBuilder.prototype.build = function() {
if (!this.request.url) {
throw new Error('URL est requise');
}
return Object.assign({}, this.request);
};
HttpRequestBuilder.prototype.execute = function() {
var config = this.build();
return fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.body,
credentials: config.credentials,
signal: AbortSignal.timeout(config.timeout)
});
};
// Utilisation
var getUserRequest = new HttpRequestBuilder()
.setMethod('GET')
.setUrl('https://api.example.com/users/123')
.withAuth('jwt-token-here')
.addHeader('Accept', 'application/json')
.setTimeout(10000)
.build();
console.log(getUserRequest);
// Exécution directe
new HttpRequestBuilder()
.setMethod('POST')
.setUrl('https://api.example.com/users')
.setJson({ name: 'John', email: 'john@example.com' })
.withAuth('jwt-token-here')
.execute()
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log('Utilisateur créé:', data);
})
.catch(function(error) {
console.error('Erreur:', error);
});
// Builder pour la construction de formulaires
function FormBuilder(formId) {
this.form = document.createElement('form');
this.form.id = formId;
this.fields = [];
}
FormBuilder.prototype.addTextField = function(name, label, options) {
options = options || {};
this.fields.push({
type: 'text',
name: name,
label: label,
placeholder: options.placeholder || '',
required: options.required || false,
validation: options.validation || null
});
return this;
};
FormBuilder.prototype.addEmailField = function(name, label, options) {
options = options || {};
this.fields.push({
type: 'email',
name: name,
label: label,
placeholder: options.placeholder || '',
required: options.required || false
});
return this;
};
FormBuilder.prototype.addPasswordField = function(name, label, options) {
options = options || {};
this.fields.push({
type: 'password',
name: name,
label: label,
minLength: options.minLength || 8,
required: options.required || false
});
return this;
};
FormBuilder.prototype.addSelectField = function(name, label, options, selectOptions) {
options = options || {};
this.fields.push({
type: 'select',
name: name,
label: label,
options: selectOptions || [],
required: options.required || false
});
return this;
};
FormBuilder.prototype.addSubmitButton = function(text) {
this.submitText = text || 'Envoyer';
return this;
};
FormBuilder.prototype.onSubmit = function(callback) {
this.submitCallback = callback;
return this;
};
FormBuilder.prototype.build = function() {
var self = this;
this.fields.forEach(function(field) {
var wrapper = document.createElement('div');
wrapper.className = 'form-field';
var label = document.createElement('label');
label.textContent = field.label;
label.setAttribute('for', field.name);
wrapper.appendChild(label);
var input;
if (field.type === 'select') {
input = document.createElement('select');
field.options.forEach(function(opt) {
var option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.label;
input.appendChild(option);
});
} else {
input = document.createElement('input');
input.type = field.type;
input.placeholder = field.placeholder || '';
}
input.name = field.name;
input.id = field.name;
if (field.required) input.required = true;
wrapper.appendChild(input);
self.form.appendChild(wrapper);
});
if (this.submitText) {
var button = document.createElement('button');
button.type = 'submit';
button.textContent = this.submitText;
this.form.appendChild(button);
}
if (this.submitCallback) {
this.form.addEventListener('submit', function(e) {
e.preventDefault();
var formData = new FormData(self.form);
var data = {};
formData.forEach(function(value, key) {
data[key] = value;
});
self.submitCallback(data);
});
}
return this.form;
};
// Utilisation du FormBuilder
var loginForm = new FormBuilder('login-form')
.addEmailField('email', 'Adresse email', { required: true, placeholder: 'you@example.com' })
.addPasswordField('password', 'Mot de passe', { required: true, minLength: 8 })
.addSubmitButton('Se connecter')
.onSubmit(function(data) {
console.log('Formulaire soumis:', data);
})
.build();
Prototype Pattern
Le Prototype Pattern crée de nouveaux objets en clonant un prototype existant plutôt qu’en utilisant un constructeur.
// Prototype Pattern avec Object.create
var VehiclePrototype = {
type: 'vehicle',
wheels: 4,
engine: null,
init: function(options) {
this.brand = options.brand || 'Unknown';
this.model = options.model || 'Unknown';
this.year = options.year || new Date().getFullYear();
this.color = options.color || 'white';
return this;
},
clone: function() {
var cloned = Object.create(Object.getPrototypeOf(this));
return Object.assign(cloned, this);
},
getInfo: function() {
return this.brand + ' ' + this.model + ' (' + this.year + ') - ' + this.color;
},
start: function() {
console.log(this.brand + ' ' + this.model + ' démarre...');
},
stop: function() {
console.log(this.brand + ' ' + this.model + ' s\'arrête...');
}
};
// Création de prototypes spécialisés
var CarPrototype = Object.create(VehiclePrototype);
CarPrototype.type = 'car';
CarPrototype.doors = 4;
CarPrototype.openTrunk = function() {
console.log('Coffre ouvert');
};
var MotorcyclePrototype = Object.create(VehiclePrototype);
MotorcyclePrototype.type = 'motorcycle';
MotorcyclePrototype.wheels = 2;
MotorcyclePrototype.hasSidecar = false;
// Utilisation
var myCar = Object.create(CarPrototype).init({
brand: 'Toyota',
model: 'Corolla',
year: 2023,
color: 'blue'
});
var myBike = Object.create(MotorcyclePrototype).init({
brand: 'Honda',
model: 'CB500',
year: 2022,
color: 'red'
});
console.log(myCar.getInfo()); // Toyota Corolla (2023) - blue
console.log(myBike.getInfo()); // Honda CB500 (2022) - red
// Clonage
var carClone = myCar.clone();
carClone.color = 'green';
console.log(myCar.color); // blue (non affecté)
console.log(carClone.color); // green
// Registry de prototypes
var PrototypeRegistry = (function() {
var prototypes = {};
return {
register: function(name, prototype) {
prototypes[name] = prototype;
},
unregister: function(name) {
delete prototypes[name];
},
create: function(name, options) {
var prototype = prototypes[name];
if (!prototype) {
throw new Error('Prototype non trouvé: ' + name);
}
return Object.create(prototype).init(options);
},
clone: function(name) {
var prototype = prototypes[name];
if (!prototype) {
throw new Error('Prototype non trouvé: ' + name);
}
return prototype.clone();
},
list: function() {
return Object.keys(prototypes);
}
};
})();
// Enregistrement des prototypes
PrototypeRegistry.register('car', CarPrototype);
PrototypeRegistry.register('motorcycle', MotorcyclePrototype);
// Création via le registry
var sedan = PrototypeRegistry.create('car', {
brand: 'Honda',
model: 'Civic',
year: 2024,
color: 'silver'
});
console.log(sedan.getInfo()); // Honda Civic (2024) - silver
Bonnes Pratiques
Pour la détection de navigateur
- Privilégiez toujours la feature detection - Testez les capacités, pas les noms de navigateurs
- Utilisez des polyfills - Ajoutez les fonctionnalités manquantes plutôt que de les contourner
- Progressive enhancement - Construisez une base fonctionnelle, puis enrichissez
- Testez sur de vrais appareils - Les émulateurs ne reproduisent pas tous les comportements
- Documentez vos workarounds - Expliquez pourquoi un UA sniffing est nécessaire si vous l’utilisez
Pour les design patterns
- Ne sur-architecturez pas - Un pattern doit résoudre un problème, pas en créer
- Singleton avec modération - L’état global peut rendre le code difficile à tester
- Factory pour la flexibilité - Utilisez-la quand les types d’objets peuvent évoluer
- Builder pour les objets complexes - Plus de 3-4 paramètres ? Pensez Builder
- Prototype pour les performances - Cloner est souvent plus rapide que construire
// Exemple de bonne pratique : combiner patterns
var ApplicationBuilder = (function() {
var instance = null; // Singleton
function Application() {
this.config = {};
this.modules = [];
this.logger = Logger.getInstance(); // Réutilisation du singleton Logger
}
Application.prototype.configure = function(config) {
this.config = Object.assign({}, this.config, config);
return this;
};
Application.prototype.addModule = function(name, factory) {
this.modules.push({ name: name, factory: factory }); // Factory pattern
return this;
};
Application.prototype.build = function() {
var self = this;
this.modules.forEach(function(mod) {
self[mod.name] = mod.factory(self.config);
});
this.logger.info('Application construite avec ' + this.modules.length + ' modules');
return this;
};
return {
getInstance: function() {
if (!instance) {
instance = new Application();
}
return instance;
}
};
})();
Pièges Courants
Détection de navigateur
| Piège | Problème | Solution |
|---|---|---|
| UA sniffing exclusif | Navigateurs non reconnus exclus | Feature detection en complément |
| Cache du UA | Le UA peut changer (mises à jour) | Détecter à chaque chargement si critique |
| Oublier les bots | Crawlers ont des UAs différents | Gérer les cas “inconnus” gracieusement |
| Tests incomplets | Ne tester que Chrome/Firefox | Automatiser les tests cross-browser |
Design Patterns
| Piège | Problème | Solution |
|---|---|---|
| Singleton partout | Couplage fort, tests difficiles | Injection de dépendances |
| Factory trop générique | Switch case énorme | Abstract Factory ou Strategy |
| Builder sans reset | Réutilisation problématique | Méthode reset() ou nouvelle instance |
| Prototype mutation | Modifications affectent tous les clones | Deep clone pour objets imbriqués |
// Piège : Singleton qui empêche les tests
// MAUVAIS
var Database = (function() {
var instance = new RealDatabase(); // Connexion immédiate !
return { getInstance: function() { return instance; } };
})();
// MIEUX : Lazy initialization + injection possible
var Database = (function() {
var instance = null;
var DatabaseClass = RealDatabase; // Peut être remplacé pour les tests
return {
setImplementation: function(impl) { DatabaseClass = impl; },
getInstance: function() {
if (!instance) { instance = new DatabaseClass(); }
return instance;
},
resetForTesting: function() { instance = null; }
};
})();
// En test
Database.setImplementation(MockDatabase);
Database.resetForTesting();
Conclusion et Récapitulatif
La maîtrise de la détection de navigateur et des design patterns créatifs est essentielle pour tout développeur JavaScript souhaitant créer des applications robustes et maintenables.
Tableau récapitulatif des patterns
| Pattern | Cas d’usage | Avantages | Inconvénients |
|---|---|---|---|
| Singleton | Configuration, Cache, Logger | Instance unique garantie, accès global | Couplage fort, tests difficiles |
| Simple Factory | Création basée sur un type | Simple, centralisée | Switch case peut grandir |
| Factory Method | Création déléguée aux sous-classes | Extensible, découplé | Plus de classes à maintenir |
| Abstract Factory | Familles d’objets liés | Cohérence garantie, thèmes | Complexité accrue |
| Builder | Objets avec nombreux paramètres | Lisible, flexible | Verbeux pour objets simples |
| Prototype | Clonage d’objets configurés | Performance, flexibilité | Attention aux références |
Points clés à retenir
- Feature detection > UA sniffing - Testez les capacités, pas les navigateurs
- Progressive enhancement - Base fonctionnelle + enrichissements
- Patterns = outils - Utilisez-les pour résoudre des problèmes spécifiques
- Testabilité - Concevez vos singletons et factories pour être testables
- KISS - Keep It Simple, Stupid - Ne sur-architecturez pas
Ces techniques vous permettront de créer des applications JavaScript professionnelles, compatibles avec une large gamme de navigateurs et structurées de manière à faciliter la maintenance et l’évolution du code.
In-Article Ad
Dev Mode
Tags
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Définir des getters et des setters en ES6 : Le guide complet
Voici une proposition de meta description : "Apprenez à manipuler les dates en JavaScript avec des getters et setters. Découvrez comment améliorer la sécurité
Manipulation des dates en JavaScript : UTC, conversion et formatage
Guide complet sur les dates JavaScript : conversion en chaine, creation de dates UTC, methodes setUTC et bonnes pratiques pour eviter les problemes de fuseaux.
La comparaison équivalente en JavaScript : une analyse appro
Here's a compelling meta description that summarizes the main value proposition, includes a subtle call-to-action, and meets the 150-160 character requirement: