Peut-on créer une application JavaScript locale sans Electron, ni framework?
Oui, un script local peut lancer le navigateur avec une page HTML comme interface.
Soyons clair, un framework peut être très utile pour réaliser une application JavaScript qui fonctionne localement, en fournissant de nombreuses fonctions utiles. Mais quand on veut réaliser une application très simple, avec un script JavaScript qui utilise une page HTML comme interface, et permet d'interagir avec cet interface, mieux s'affranchir des lourdeurs d'un framework et surtout d'Electron dont les nouvelles versions sont incompatibles avec les précédentes.
Télécharger une application qui doit inclure les 600 Mo de Chromium pour assurer la compatibilité n'est pas pratique. L'idéal serait donc de pouvoir utiliser le navigateur déjà présent sur le bureau pour afficher l'interface HTML et nous allons montrer comment faire dans cette démonstration simple.
Celle-ci permet de charger la liste des fichiers d'un répertoire et d'afficher le contenu d'un fichier en cliquant sur le nom de celui-ci.
Nous avons dans l'article précédent, Comment réaliser un serveur de pages ou d'applications avec Node.js, montré comment un script peut interagir avec une page HTML chargée dans un navigateur.
Pour rendre le fonctionnement semblable à celui d'une application locale usuelle, nous allons ajouter le code qui permet au script de lancer lui-même le navigateur et la page d'interface.
exec('start chrome --app=http://localhost:3000')
L'option "--app" permet d'afficher la page seule par le navigateur Chrome sans l'interface propre au navigateur.
Cette commande fonctionne sur Windows. La démonstration utilise process.platform pour détecter l'OS et s'il s'agit de MacOS ou Linux, passe la commande qui convient à cet OS.
Voici le code complet du serveur local JavaScript:
const http = require('http');
const fs = require('fs');
const path = require('path');
const WebSocket = require('ws');
const { exec } = require('child_process');
const server = http.createServer((req, res) => {
let filePath = req.url === '/' ? './index.html' : `.${req.url}`;
const extname = path.extname(filePath);
const mimeTypes = {
'.html': 'text/html',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
};
const contentType = mimeTypes[extname] || 'application/octet-stream';
fs.readFile(filePath, (error, content) => {
if (error) {
res.writeHead(404);
res.end("Fichier non trouvé");
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
}
});
});
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const data = JSON.parse(message);
if (data.type === 'LIST_DIR') {
const dirPath = data.path || '.';
fs.readdir(dirPath,
{ withFileTypes: true },
(err, files) => {
if (err) return ws.send(
JSON.stringify({ type: 'ERROR', message: err.message })
);
const fileList = files.map(f => ({ name: f.name, isDir: f.isDirectory() }));
//broadcast({ type: 'DIR_CONTENT', , currentPath: });
ws.send(
JSON.stringify({
type: 'DIR',
files: fileList,
currentPath: path.resolve(dirPath)
})
);
});
}
if (data.type === 'READ_FILE') {
fs.readFile(data.path, 'utf8', (err, content) => {
if (err) return ws.send(JSON.stringify({
type: 'ERROR', message: err.message
}));
ws.send(
JSON.stringify({
type: 'FILE',
filecontent:content,
fileName: path.basename(data.path)
})
);
});
};
}); // message
}); // connection
server.listen(3000, () => {
console.log("Start...");
switch(process.platform) {
case "win32":
exec('start chrome --app=http://localhost:3000');
//exec('start chrome http://localhost:3000');
break
case "darwin":
exec('open -a "Google Chrome" http://localhost:3000')
break
case "linux":
exec('google-chrome http://localhost:3000')
break;
}
});
Le nom du fichier HTML d'interface est assigné à la variable filepath au début du script.
Ensuite nous définissons les types MIMES pour les extensions usuelles, sans quoi notre serveur JavaScript ne pourrait charger les fichiers correspondants, même si ces fichiers sont chargés depuis l'interface HTML, comme la feuille de style par exemple.
Le code suivant est le mécanisme WebSocket qui interagit avec la page HTML, tel que déjà décrit dans l'article Premiers pas avec WebSocket et Node.js.
Il utilise la bibliothèque ws. Celle-ci n'est pas incluse dans Node, mais elle a été ajoutée dans l'archive de la démonstration. Si cette extension n'est pas reconnue par votre système, vous pouvez aussi l'installer avec cette commande.
npm install ws
Le code HTML de la démo est plutôt simple:
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>No Electron, no framework - Scriptol.fr/Scriptol.com</title> <link rel="stylesheet" href="style.css"></head>
<body> <div id="explorer"> <h3>File List</h3> <input type="text" id="pathInput" placeholder="." style="width: 80%;"> <button onclick="listDir()">Go</button> <div id="fileList"></div> </div>
<div id="viewer"> <h3 id="fileName"></h3> <hr> <div id="fileContent"></div> </div>
<script>
const ws = new WebSocket('ws://localhost:3000');
let currentPath = '';
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'DIR') {
currentPath = data.currentPath;
const listDiv = document.getElementById('fileList');
document.getElementById('pathInput').value = currentPath;
listDiv.innerHTML = "<p><small>${currentPath}</small></p>";
data.files.forEach(file => {
const el = document.createElement('div');
el.className = file.isDir ? 'dir' : 'file';
el.textContent = (file.isDir ? '📠' : '📄 ') + file.name;
if (!file.isDir) {
el.onclick = () => readFile(file.name);
}
listDiv.appendChild(el);
});
}
if (data.type === 'FILE') {
document.getElementById('fileName').textContent = data.fileName;
document.getElementById('fileContent').textContent = data.filecontent;
}
if (data.type === 'ERROR') {
alert("Erreur: " + data.message);
}
};
function listDir() {
const path = document.getElementById('pathInput').value;
ws.send(JSON.stringify({ type: 'LIST_DIR', path: path }));
}
function readFile(name) {
ws.send(JSON.stringify({ type: 'READ_FILE', path: currentPath + '/' + name }));
}
</script>
</body>
</html>
L'interface est composée de deux fenêtres, la liste des fichiers à gauche, le contenu d'un fichier à droite.
Pour essayer la démonstration, télécharger l'archive
Ouvrir l'archive et transférer le répertoire qu'elle contient, noelectron, sur l'unité de stockage. Ouvrir le répertoire en ligne de commande et taper:
node server.js
Ce qui affiche l'interface. Taper sur go pour afficher le répertoire courant et cliquer sur un nom de fichier pour afficher le contenu à droite.

