In the world of Node.js,
pathandfsThey 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.
| method | Features | return value |
|---|---|---|
path.join() | Simple splicing, no analysis .. or ., / only as delimiter | Relative or absolute path (depending on input) |
path.resolve() | Resolved as an absolute path, encountered / The beginning fragment will be reset to the root directory | Always 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:
resolvealways returnabsolute path, and once encountered/The parameters at the beginning are regarded as restarting from the root directory.joinIt 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);
}
})();
awaitIt is essentially syntactic sugar for Promise, which automatically handles it for us.thenChain, allowing asynchronous process control to return to intuitive "sequential writing".
But please note:awaitcan only beasyncUsed 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
| scene | Recommended plan |
|---|---|
| Path splicing (relative paths) | path.join() |
| Requires absolute path | path.resolve() |
| Get directory name/file name/extension | path.dirname() / path.basename() / path.extname() |
| path normalization | path.normalize() |
| Simple script, startup configuration | fs.readFileSync() |
| Server-side I/O operations | fs.promises.readFile() + async/await |
| Multiple asynchronous tasks are executed sequentially | async/await(Avoid callback hell and long chains) |
| Multiple asynchronous tasks are executed in parallel | Promise.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
.mjsSuffix, using ES Module syntax, can be run directly in the Node.js environment (needs to be turned on"type": "module"or use.mjsextension).