Lire des fichiers avec node

Projet node

Commençons par créer un projet https://nodejs.org/ :

  1. créez un dossier fichier-node où vous allez mettre les fichiers de votre projet,
  2. dans un terminal et dans ce dossier, initialisez votre projet avec la commande npm init

Vous devriez maintenant avoir un fichier nommé package.json qui contient la configuration minimale d'un projet utilisant node :

{
  "name": "fichier-node",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Nous allons utiliser des bibliothèques pour notre projet, bibliothèques qu'il va falloir importer. Node utilise par défaut un mode d'import nommé commonjs, alors que javascript en utilise une autre basée sur la norme ES6 modules.

Nous allons dire à node que nous allons utiliser la gestion javascript des modules en ajoutant la ligne "type": "module", dans le fichier de configuration package.json, juste en-dessous de la ligne 5. A la fin de cette opération, vous devriez avoir le fichier un fichier nommé package.json qui contient la configuration minimale d'un projet utilisant node :

{
  "name": "fichier-node",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Nous allons utiliser dans toute la suite de ce cours la gestion javascript des modules (es6 modules) et non celle historique de node (commonJS). Si vous cherchez du code sur internet, vous pourrez tout de suite voir de quel type d'import il s'agit :

  • import fs from 'fs'; : import ES6
  • const fs = require('fs'); : import commonJS

Lorsque vous importez des bibliothèques node, il suffit souvent de remplacer une écriture par l'autre pour que tout fonctionne.

Notre projet est prêt à être utilisé.

Code initial

Dans le dossier de votre projet, créez un fichier fichier-texte.txt contenant :

Je suis le contenu du fichier.

Bonjour cher lecteur !

Puis un fichier code.js :

import fs from 'fs';

let contenu = fs.readFileSync("./fichier-texte.txt", {encoding:'utf8'})
console.log(contenu)

En exécutant le code on obtient, comme attendu, le contenu du fichier dans la console :

$ node code.js 
Je suis le contenu du fichier.

Bonjour cher lecteur !

Dossier courant

Le code précédent charge le fichier de façon relative, donc depuis le dossier courant, qui est celui du terminal. Il ne fonctionnera donc plus si on exécute le fichier code.js depuis un autre dossier. Par exemple si on remonte vers le dossier parent avant de l'exécuter :

$ cd ..                    
$ node fichier-node/code.js 
node:fs:453
    return binding.readFileUtf8(path, stringToFlags(options.flag));
                   ^

Error: ENOENT: no such file or directory, open './fichier-texte.txt'
    at Object.readFileSync (node:fs:453:20)
    at file:///Users/fbrucker/fichier-node/code.js:3:18
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: './fichier-texte.txt'
}

Node.js v21.1.0

Le fichier fichier-texte.txt n'est plus dans le dossier courant mais est à ./fichier-node/fichier-texte.txt, node ne trouve pas le fichier,

Une solution pour palier ces problèmes de localisation de fichiers est de se fixer un point de référence, comme la localisation du fichier entrain d'être exécuté. On peut facilement trouver ces valeurs avec node qui définit deux variables :

Comme rien ne peut être simple, ces deux variables dépendant de l'import commonjs et ne sont pas directement disponibles lorsque l'on utilise des modules ES6 (comme nous). Il faut donc recréer ces deux variables, heureusement que c'est simple :

import path from 'path'
import {fileURLToPath} from 'url'

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

console.log(__filename)
console.log(__dirname)

Si vous avez une version très récente de node, il existe une alternative.

On peut ensuite toutes nos lectures de fichier au dossier __dirname en utilisant la fonction path.join qui colle des chemins entre eux.

On obtient alors le code final suivant :

import fs from 'fs';

import path from 'path'
import {fileURLToPath} from 'url'

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

let localisation_fichier = path.join(__dirname,  "./fichier-texte.txt");
let contenu = fs.readFileSync(localisation_fichier, {encoding:'utf8'})
console.log(contenu)

Ce code est portable, il peut être exécuté de n'importe où :

$ node code-final.js 
Je suis le contenu du fichier.

Bonjour cher lecteur !

$ cd ..          
$ node fichier-node/code-final.js 
Je suis le contenu du fichier.

Bonjour cher lecteur !

Lecture Asynchrone

La lecture de fichier précédente (ligne 10) :

let contenu = fs.readFileSync(localisation_fichier, {encoding:'utf8'})

Était synchrone, c'est à dire que le retour de la fonction fs.readFileSync est le contenu du fichier. C'est le comportement attendu de tout programme. On passe à la ligne suivant que si la ligne précédente est terminé.

Ce n'est cependant pas ce qu'il se passe dans la majorité des cas dans le web. En effet la lecture d'un fichier peut prendre temps s'il est :

Et souvent, on peut faire autre chose en attendant son chargement. On procède alors de façon asynchrone : on lance la lecture du fichier puis le programme continue à la ligne suivante. À la fin de la lecture une méthode définie à l'avance est exécutée.

Ce mode de programmation est dit évènementiel. Le code réagit à des évènements en exécutant une fonction déterminée à l'avance.

Code asynchrone

Dans notre cas le code serait le suivant :

import fs from 'fs/promises';

import path from 'path'
import {fileURLToPath} from 'url'

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

let localisation_fichier = path.join(__dirname,  "./fichier-texte.txt");
fs.readFile(localisation_fichier, {encoding:'utf8'}).then(data => (
    console.log(data)
))

Créez un fichier node-asynchrone.js dans le dossier racine de votre projet (le dossier fichier-node) et copiez/collez-y le code précédent.

Exécutez le fichier avec node pour voir que le fichier fichier-texte.txt est bien lu.

Il y a deux différences par rapport au code précédant :

  1. on importe une autre bibliothèque : fs/promises en non plus juste fs
  2. on a jouté une méthode then au fesses de la lecture.

Le paramètre de la méthode then est une fonction à un paramètre qui sera exécutée à la fin de la lecture : lorsque (then) la lecture est terminée.

Vous pouvez vérifier que la lecture du fichier n'est pas immédiate en ajoutant la ligne console.log("coucou !") à la fin du fichier et voir que "coucou !" est affiché avant le contenu du fichier.

Promesses

Le mécanisme à l'œuvre ici est appelé promesse.

Le retour de la fonction fs.readFile est une promesse qui possède une méthode then dont le paramètre est une fonction à un paramètre qui est exécutée à la fin de la promesse si la fonction s'est terminé sans erreur. Le retour est passé en paramètre du paramètre de then

Explicitons ceci modifiant notre code :

import fs from 'fs/promises';

import path from 'path'
import {fileURLToPath} from 'url'

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

let localisation_fichier = path.join(__dirname,  "./fichier-texte.txt");
let promesse = fs.readFile(localisation_fichier, {encoding:'utf8'})

promesse.then(data => {
    console.log("tout s'est bien passé. Le contenu du fichier lu est :");
    console.log(data)
})

Souvent, on lie le tout en un seul grand appel (le retour de then est la promesse) :

promesse
  .then(réponse => {
    // exécuté lorsque la longue fonction s'arrête
    // le paramètre de cette fonction étant de retour de la longue fonction
  })

Le côté sympathique des promesses c'est qu'elle peuvent s'enchaîner si le retour de then est aussi une promesse :

promesse
  .then(response => {
    // then de la promesse

    return une_autre_promesse
  })
  .then(response => {
    // c'est le then de une_autre_promesse

  })

Enfin, comme on l'a vu tout au début souvent on combine la fonction et son retour sous la forme d'une promesse en seule grosse instruction sans déclarer explicitement de promesse :

// ... 

fs.readFile(localisation_fichier, {encoding:'utf8'})
  .then(data => {
    console.log("tout s'est bien passé. Le contenu du fichier lu est :");
    console.log(data)
  })

// ... 

await/async

Il peut cependant parfois être utile d'écrire du code, à l'ancienne, c'est à dire un exécutant ligne à ligne notre code. Il n'y a en effet dans notre cas pas grand intérêt à utiliser du code asynchrone.

Le javascript permet d'utiliser des promesses de façon synchrone en utilisant l'instruction : await. Cette instruction attend que la promesse se termine pour aller à la ligne d'après. Dans notre cas, cela donnerait :

import fs from 'fs/promises';

import path from 'path'
import {fileURLToPath} from 'url'

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

let localisation_fichier = path.join(__dirname,  "./fichier-texte.txt");
let contenu = await fs.readFile(localisation_fichier, {encoding:'utf8'})

On ne peut cependant pas utiliser await partout. On ne peut le faire que dans le corps du programme ou à l'intérieur d'une fonction taguée async.

Dans notre cas, si on voulait déporter la lecture de notre fichier dans une fonction séparée il faudrait l'écrire de cette façon :

async function lire(fichier) {
    let contenu = await fs.readFile(fichier, {encoding:'utf8'});
    return contenu
}

Gestion des erreurs

Les promesses contiennent aussi une gestion des erreurs avec la méthode catch dont le paramètre est une fonction à un paramètre qui est exécutée à la fin de la promesse si la fonction a échoué. Le type d'erreur est passé en paramètre du paramètre de catch

import fs from 'fs/promises';

import path from 'path'
import {fileURLToPath} from 'url'

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

let localisation_fichier = path.join(__dirname,  "./fichier-texte.txt");
fs.readFile(localisation_fichier, {encoding:'utf8'})
  .then(data => {
    console.log("tout s'est bien passé. Le contenu du fichier lu est :");
    console.log(data)
  })
  .catch(erreur => {
    console.log("une erreur :", erreur)
  })