1. path module - path processing
path It is a built-in module of Node.js, specially used to handle file paths. Anywhere in the project that deals with paths is inseparable from it.
1.1 path.join() and path.resolve() The difference
Both methods can spell paths, but the way of doing things is different.
path.join(): Use each incoming segment to / Splice them together, and then normalize the result (eliminate redundant ., .. and consecutive slashes). It has a special rule: if a certain segment is an absolute path (ending with / beginning), the previously assembled part will be discarded, and the assembly will start again from this absolute path segment.
path.resolve(): Like typing in the terminal cd The command is the same, "enter" the directory section by section, and finally output an absolute path. If no absolute path segment is encountered throughout the process, the current working directory (cwd) is used as the starting point.
import path from 'path';
// join — Splicing + normalization
path.join('a', 'b', 'c'); // 'a/b/c'
path.join(process.cwd(), '/hello', 'world'); // '/hello/world'
// ↑ /hello It is an absolute path, spelled out in front. cwd discarded from /hello restart
// resolve — picture cd The same analysis is performed piece by piece, and the absolute path is always returned.
path.resolve('a', 'b', 'c'); // '/current working directory/a/b/c'
path.resolve('/hello', 'world', './a', 'b'); // '/hello/world/a/b'
path.resolve('/hello', 'world', '../a', 'b'); // '/hello/a/b'(.. Back one level)
// Comparison: When the first parameter is an absolute path, the results are the same.(readme important conclusions in)
path.join('/hello', 'world', './a', 'b'); // '/hello/world/a/b'
path.join('/hello', 'world', '../a', 'b'); // '/hello/a/b'
// ↑ join Bundle .. are also processed, so the result is the same as resolve same
When do the results differ? When all parameters are relative paths:
path.resolve('a', 'b'); // '/current working directory/a/b' — resolve Made it up cwd
path.join('a', 'b'); // 'a/b' — join Just work hard and don’t make up the absolute path.
Simply remember:join After you finish fighting, return it.resolve Be sure to give an absolute path after spelling.
How to use it in actual projects:
Project root directory / → process.cwd() or __dirname
├── src/ → path.resolve(__dirname, 'src')
│ ├── assets/ → path.resolve(__dirname, 'src/assets') Static resource directory
│ └── libs/ → path.resolve(__dirname, 'src/libs') Tool function directory
└── dist/ → Build output directory
use path.resolve(__dirname, 'src') To locate the directory, no matter which path the script is executed from, you can get the correct absolute path.
1.2 Other commonly used path methods
import path, { normalize } from 'path';
// normalize Both can pass path.normalize() Call, can also be used directly as a named export
// dirname — Get directory name
path.dirname('/a/b/c.js'); // '/a/b'
path.dirname(process.cwd()); // Previous directory
// basename — Get the file name, the second parameter can specify the suffix to be removed
path.basename('a/b/c.js'); // 'c.js'
path.basename('a/b/c.js', '.js'); // 'c'(Removed .js suffix)
path.basename('a/b/c.js', 'js'); // 'c.'(Note: only removed 'js' Two characters, the dot is still there)
path.basename('a/b/c.js', 's'); // 'c.j'(Only removed the last one 's')
// basename When removing the suffix, the end of the string is simply matched and deleted, and will not be processed automatically. . Number
// extname — Get the extension (including dots))
path.extname('/a/b/c.js'); // '.js'
path.extname('/a/b/.gitignore'); // ''(Files starting with dot do not count as extensions)
path.extname('/a/b/index.html'); // '.html'
// normalize — put in the path . and .. and clean up excess slashes
normalize('a/b//c/d/e/..'); // 'a/b/c/d'
// parse — Split the path into an object
path.parse('/home/user/dir/file.txt');
// { root: '/', dir: '/home/user/dir', base: 'file.txt', ext: '.txt', name: 'file' }
// isAbsolute — Determine whether a path is an absolute path
path.isAbsolute('/a/b/c'); // true(POSIX)
path.isAbsolute('a/b/c'); // false
// relative — Calculate relative paths from one path to another
path.relative('/a/b/c', '/a/b/d/e'); // '../d/e'
path.relative('/a/b/c', '/a/b/c/d'); // 'd'
// sep — The delimiter of the current platform (this value is taken when used, not a function call))
path.sep; // Linux/Mac Yes '/', Windows Yes '\'
// delimiter — environment variables PATH separator
path.delimiter; // Linux/Mac Yes ':', Windows Yes ';'
2. fs module - file system operations
fs It is the core module used by Node.js to read and write files. Node.js itself uses C++ to write the underlying fs and path modules, and wraps a layer of V8 engine to run JavaScript.
2.1 Synchronous reading and asynchronous reading
import fs from 'fs';
// Synchronous reading - the code stops here waiting for the result, and all subsequent codes are blocked
const data = fs.readFileSync('./test.txt', 'utf-8');
console.log(data);
// Asynchronous reading - no blockage, get the result through callback function after reading
fs.readFile('./test.txt', 'utf-8', (err, data) => {
// Node Convention: the first parameter of the callback is always err
if (err) {
console.log(err);
return;
}
console.log(data);
});
Why does Node.js use asynchronous?
JavaScript is single-threaded. If you read a large file synchronously, the entire program will have to stop and wait, and nothing else can be done. The asynchronous read returns immediately after it is initiated. After the file is finished reading, the results are sent back through the event loop, and other requests can be processed in the intervening time. This is also the secret why Node.js can support high concurrency - one server can do the work of several servers in other languages.
There is no such thing as a file system in the front-end environment, so reading and writing files is the unique ability of Node.js. Both synchronous and asynchronous methods can be chosen, but in most scenarios, asynchronous should be used.
2.2 The evolution of asynchronous process control
This is the most important line to understand in Node.js asynchronous programming:
synchronous(block the road) → asynchronous(Not blocking the road) → callback(callback)
→ callback hell(Nested too deeply) → Promise.then(chain call)
→ async/await(ES8 Syntactic sugar)
The first stage: callback - I fell into callback hell as I wrote.
// Requirements: read in order file1 → file2 → file3
fs.readFile('./file1.txt', 'utf-8', (err, data) => {
console.log('file1:', data);
fs.readFile('./file2.txt', 'utf-8', (err, data) => {
console.log('file2:', data);
fs.readFile('./file3.txt', 'utf-8', (err, data) => {
console.log('file3:', data);
// More to read file4, file5 If...if you try it layer by layer, people will be confused.
});
});
});
The trouble here is: each step depends on the result of the previous step, and the next step can only be initiated in the callback of the previous step. When there are too many files, the code is indented into a pyramid, commonly known as "callback hell".
The second stage: Promise + then — chain call
import fs from 'fs/promises'; // Replace Promise version fs
fs.readFile('./file1.txt', 'utf-8')
.then(data => {
console.log('file1:', data);
return fs.readFile('./file2.txt', 'utf-8'); // return promise, Continue the chain
})
.then(data => {
console.log('file2:', data);
return fs.readFile('./file3.txt', 'utf-8');
})
.then(data => {
console.log('file3:', data);
})
.catch(err => {
console.log('something went wrong:', err);
});
The nesting is flattened, and the code is indented from left to right instead of expanded from top to bottom, making it much more comfortable to read.catch You can catch all the mistakes in one place.
There is still a little regret: every .then() You have to write everything return, there are too many steps and it’s a bit long-winded.
Phase 3: async/await — asynchronous code that looks like sync
import fs from 'fs/promises';
(async () => {
const file1 = await fs.readFile('./file1.txt', 'utf-8');
console.log('file1:', file1);
const file2 = await fs.readFile('./file2.txt', 'utf-8');
console.log('file2:', file2);
const file3 = await fs.readFile('./file3.txt', 'utf-8');
console.log('file3:', file3);
})();
The code looks almost the same as the synchronous writing method. It is very intuitive to read line by line from top to bottom. Here are a few things to clarify:
async/awaitIt is syntactic sugar, and the bottom layer still runs Promise. It just makes writing more comfortable and does not change the execution mechanism.awaitHelped us with the "wait for the results to be obtained before proceeding" thing, so we don't have to write it ourselves..then()chain.- The code behind await enters the microtask queue (microtask).
setTimeoutThe kind that enters is a macrotask (macrotask), and the two teams are different. - The code reads like synchronous, but the thread is not blocked. This is what "asynchronous code is written synchronously" means.
3. Summary
path How to take the tube path:join Splicing + normalization,resolve The absolute path must be returned after spelling. Used in engineering resolve It is more stable to locate the directory because the absolute path is always obtained.
fs Regardless of how files are read and written: use asynchronous if you can. No blockage is the advantage of Node. Asynchronous writing has evolved from callback to Promise and then to async/await. Each step is to reduce hair loss.
async/await makes asynchronous code much more readable, but you have to know that behind it is still Promise plus event loop.