In the world of Node.js,path and fs They are the two most basic and commonly used built-in modules. One of them manages "paths" and the other manages "files". Almost every back-end project is inseparable from them. Today we will combine practice to clarify the core knowledge points of these two modules at once.


Preface: Directory structure from an engineering perspective

Before we begin, let’s introduce a common engineering directory design:

text

Project root directory /
├── src /               # development directory
│   ├── assets /        # Static resources
│   └── libs /          # Utility function
├── backend /
│   └── path_fs /       # Our sample code directory
└── ...

This structure is very typical in real projects. understand path How the module handles these paths correctly is the first step in writing robust code.


1. Path module: The Swiss Army Knife of path processing

1.1 path.join and path.resolve - seemingly the same, but actually very different

When many people first come into contact with Node.js, they will be confused about path.join and path.resolve Confused - they can both splice paths, but behave fundamentally differently.

methodFeaturesreturn value
path.join()Simple splicing, no analysis .. or ., / only as delimiterRelative or absolute path (depending on input)
path.resolve()Resolved as an absolute path, encountered / The beginning fragment will be reset to the root directoryAlways return absolute path

Code verification:

javascript

import path from 'path';

// join: pure splicing
console.log(path.join('a', 'b', 'c'));  
// output: a/b/c  (exist Windows Above is a\b\c)

// Notice: join in / Does not reset the path, just the delimiter
console.log(path.join(process.cwd(), '/hello', 'world'));
// output: /current working directory/hello/world

// resolve: meet / To start, start from the root directory
console.log(path.resolve(process.cwd(), '/hello', 'world'));
// output: /hello/world  (Ignore the previous process.cwd())

// Purely relative path, resolve Based on the current working directory
console.log(path.resolve('a', 'b', 'c'));
// output: /current working directory/a/b/c

// combine . and .. behavior
console.log(path.resolve('/hello', 'world', './a', 'b'));
// output: /hello/world/a/b

console.log(path.join('/hello', 'world', './a', 'b'));
// output: /hello/world/a/b   (At this time, the two results are the same)

core memory points:

  • resolve always returnabsolute path, and once encountered / The parameters at the beginning are regarded as restarting from the root directory.
  • join It is only responsible for splicing and does not perform absolute path analysis.

1.2 Other commonly used path methods

Apart from join and resolve, path The module also provides a series of practical parsing methods:

javascript

import path from 'path';

// current working directory
console.log(process.cwd());

// 1. dirname: Returns the directory name in the path
console.log(path.dirname('/a/b/c'));       // /a/b
console.log(path.dirname(process.cwd()));  // Previous directory

// 2. basename: Returns the file name, optionally removing the extension
console.log(path.basename('/a/b/c.js'));   // c.js
console.log(path.basename('/a/b/c.js', '.js')); // c
console.log(path.basename('/a/b/c.js', 'js'));  // c.   (Note: Matches at the end will be removed 'js')
console.log(path.basename('/a/b/c.js', 's'));   // c.j

// 3. extname: Get file extension
console.log(path.extname('/a/b/c.js'));    // .js

// 4. normalize: Normalize paths (handle redundant /, .., .)
console.log(path.normalize('/a/b//c/d/e/..')); 
// output: /a/b/c/d

// 5. parse: Parse path into object
console.log(path.parse('/home/user/dir/file.txt'));
// output: 
// {
//   root: '/',
//   dir: '/home/user/dir',
//   base: 'file.txt',
//   ext: '.txt',
//   name: 'file'
// }

These methods are extremely common in daily development, especially when dealing with static resource paths, configuration file loading and other scenarios.


2. fs module: synchronization and asynchronousness of file operations

2.1 Synchronous vs Asynchronous

fs The module provides two sets of APIs: synchronization (*Sync) and asynchronous (callback or Promise). Node.js is single-threaded,Asynchronous non-blocking I/O is its core strength, but synchronous methods also have a place in certain scenarios (such as bootloading configuration).

javascript

import fs from 'fs';

// Synchronous reading - simple and crude, but will block the thread
const syncData = fs.readFileSync('./test.txt', 'utf-8');
console.log(syncData);

// Asynchronous reading - no blocking, put callback into event loop
fs.readFile('./test.txt', 'utf-8', (err, data) => {
    if (!err) {
        console.log(data);
    } else {
        console.log(err);
    }
});
console.log('This line will be executed first');

Synchronous methods are suitable for scripts or initialization phases, but in high-concurrency server applications, asynchronous methods must be used first.


2.2 Callback hell - the nightmare of asynchronous process control

when we needin orderWhen reading multiple files, the problem of callback nesting is exposed:

javascript

// read first file1.txt → Again file2.txt → at last file3.txt
fs.readFile('./file1.txt', 'utf-8', (err, data) => {
    if (!err) {
        console.log('file1.txt The content is', data);
        fs.readFile('./file2.txt', 'utf-8', (err, data) => {
            if (!err) {
                console.log('file2.txt The content is', data);
                fs.readFile('./file3.txt', 'utf-8', (err, data) => {
                    if (!err) {
                        console.log('file3.txt The content is', data);
                    } else {
                        console.log(err);
                    }
                });
            } else {
                console.log(err);
            }
        });
    } else {
        console.log(err);
    }
});

this is famousCallback Hell  ——The code develops horizontally, has poor readability, is difficult to maintain, and has repeated error handling.


2.3 Promise chain calling - the first evolution of elegance

ES6 introduces Promise, which provides a qualitative improvement in asynchronous process control:

javascript

import fs from 'fs/promises';

fs.readFile('./file1.txt', 'utf-8')
    .then(data => {
        console.log('file1.txt The content is', data);
        return fs.readFile('./file2.txt', 'utf-8');
    })
    .then(data => {
        console.log('file2.txt The content is', data);
        return fs.readFile('./file3.txt', 'utf-8');
    })
    .then(data => {
        console.log('file3.txt The content is', data);
    })
    .catch(err => {
        console.log(err);
    });

Compared to callback nesting,then Chained calls allow the code to grow "vertically", with clearer logic and better understanding of semantics. but multiple then The concatenation still seems a bit lengthy.


2.4 async/await - the ultimate syntactic sugar

Introduced by ES8 (ES2017) async/await, allowing asynchronous code to be written like synchronous code,Readability greatly improved.

javascript

import fs from 'fs/promises';

// Execute function immediately (IIFE)
(async () => {
    try {
        const file1Data = await fs.readFile('./file1.txt', 'utf-8');
        console.log('file1.txt The content is', file1Data);
        
        const file2Data = await fs.readFile('./file2.txt', 'utf-8');
        console.log('file2.txt The content is', file2Data);
        
        const file3Data = await fs.readFile('./file3.txt', 'utf-8');
        console.log('file3.txt The content is', file3Data);
    } catch (err) {
        console.log(err);
    }
})();

await It is essentially syntactic sugar for Promise, which automatically handles it for us. then Chain, allowing asynchronous process control to return to intuitive "sequential writing".
But please note:await can only be async Used internally within functions, and it does not turn asynchronous into synchronous, it just makes the code look synchronous.


3. The evolution of asynchronous process control

The evolution path of Node.js asynchronous programming can be clearly sorted out from the comments in the notes:

text

synchronous blocking(readFileSync)
    ↓
Asynchronous non-blocking + callback function(callback)
    ↓
Business is complex → callback hell(Callback Hell)
    ↓
Promise + then Chained calls (resolve nesting, but the chain is slightly longer)
    ↓
async/await(ES8 Syntax sugar, synchronize asynchronous code)

Every evolution is rightreadabilityandmaintainabilitypursuit. Understanding this context can help us choose the most appropriate solution in different scenarios.


4. Summary and best practices

sceneRecommended plan
Path splicing (relative paths)path.join()
Requires absolute pathpath.resolve()
Get directory name/file name/extensionpath.dirname() / path.basename() / path.extname()
path normalizationpath.normalize()
Simple script, startup configurationfs.readFileSync()
Server-side I/O operationsfs.promises.readFile() + async/await
Multiple asynchronous tasks are executed sequentiallyasync/await(Avoid callback hell and long chains)
Multiple asynchronous tasks are executed in parallelPromise.all([...])

write at the end

path and fs They are the cornerstone of the Node.js ecosystem. They seem simple, but they carry all file system operations. by understanding resolve/join The difference and mastering the evolution of asynchronous process control, we can not only write more robust code, but also have a deeper understanding of the design philosophy of Node.js——Non-blocking I/O + event driven.

I hope this note is helpful to you, and you are welcome to share your practical experience in the comment area!

📌 All codes in this article use .mjs Suffix, using ES Module syntax, can be run directly in the Node.js environment (needs to be turned on "type": "module" or use .mjs extension).