- Published on
Nén ảnh bằng Node.js

- Authors
- Name
- Nguyen Pham
Tổng quan
Để tối ưu hóa website, việc nén ảnh là một trong những bước quan trọng. Có nhiều cách để nén ảnh chẳng hạn như dùng tinypng.com hoặc sử dụng các công cụ như Photoshop, GIMP, ... Tuy nhiên, trong thực tế để nén ảnh một cách tự động khi build chúng ta có thể sử dụng Node.js.
Cài đặt
Đầu tiên, chúng ta cần cài đặt thư viện sau:
npm install sharp
Tạo một file compressImage.js
và thêm đoạn mã sau:
const sharp = require('sharp');
const fs = require('node:fs');
console.log('Start compressing images...');
Bây giờ chạy thử file compressImage.js
bằng lệnh sau:
node compressImage.js
Tạo thư mục chứa ảnh
Bây giờ update file compressImage.js
để tạo thư mục chứa ảnh cần nén:
const sharp = require('sharp');
const fs = require('node:fs');
... // Đây là đoạn mã ở trên
const inputDirectory = './src';
const outputDirectory = './tmp';
try {
if (!fs.existsSync(outputDirectory)) {
fs.mkdirSync(outputDirectory);
}
} catch (err) {
console.error(err);
}
Ý nghĩa của đoạn mã trên là nếu thư mục tmp
chưa tồn tại thì sẽ tạo mới nó. Nhiệm vụ tiếp theo là đọc tất cả các file ảnh trong thư mục src
và nén chúng sau đó lưu vào thư mục tmp
.
Tiếp tục update file compressImage.js
:
... // Đây là đoạn mã ở trên
const quality = 80;
const maxWidth = 4000;
const maxHeight = 4000;
fs.readdirSync(inputDirectory).forEach((file) => {
const filePath = `${inputDirectory}/${file}`;
if (file.match(/\.(jpg|jpeg)$/)) {
sharp(filePath)
.resize(
Math.min(maxWidth, maxHeight),
Math.min(maxWidth, maxHeight), {
fit: 'inside'
}
)
.jpeg({
quality: quality
})
.toFile(`${outputDirectory}/${file}`);
}
if (file.match(/\.(png)$/)) {
sharp(filePath)
.resize(
Math.min(maxWidth, maxHeight),
Math.min(maxWidth, maxHeight), {
fit: 'inside'
}
)
.png({
quality: quality,
compressionLevel: 5,
})
.toFile(`${outputDirectory}/${file}`);
}
});
Đoạn mã trên sẽ đọc tất cả các file ảnh trong thư mục src
và nén chúng với chất lượng 80
và kích thước tối đa là 4000x4000
. Kết quả sẽ được lưu vào thư mục tmp
.
Vậy còn các file trong thư mục con thì sao?
Đừng lo, chúng ta có thể update đoạn mã trên để nén cả các file trong thư mục con:
... // Đây là đoạn mã ở trên
fs.readdirSync(inputDirectory).forEach((file) => {
const filePath = `${inputDirectory}/${file}`;
... // Đây là đoạn mã ở trên
if (fs.lstatSync(filePath).isDirectory()) {
const outputDirectoryPath = `${outputDirectory}/${file}`;
if (!fs.existsSync(outputDirectoryPath)) {
fs.mkdirSync(outputDirectoryPath);
}
fs.readdirSync(filePath).forEach((subFile) => {
const subFilePath = `${filePath}/${subFile}`;
if (subFile.match(/\.(jpg|jpeg)$/)) {
sharp(subFilePath)
.resize(
Math.min(maxWidth, maxHeight),
Math.min(maxWidth, maxHeight), {
fit: 'inside'
}
)
.jpeg({
quality: quality
})
.toFile(`${outputDirectoryPath}/${subFile}`);
}
if (subFile.match(/\.(png)$/)) {
sharp(subFilePath)
.resize(
Math.min(maxWidth, maxHeight),
Math.min(maxWidth, maxHeight), {
fit: 'inside'
}
)
.png({
quality: quality,
compressionLevel: 5,
})
.toFile(`${outputDirectoryPath}/${subFile}`);
}
});
}
});
Copy file từ tmp
sang src
Cuối cùng, chúng ta cần copy tất cả các file trong thư mục tmp
sang thư mục src
. Tiếp tục update file compressImage.js
:
... // Đây là đoạn mã ở trên
fs.readdirSync(outputDirectory).forEach((file) => {
const filePath = `${outputDirectory}/${file}`;
if (fs.lstatSync(filePath).isDirectory()) {
const inputDirectoryPath = `${inputDirectory}/${file}`;
fs.readdirSync(filePath).forEach((subFile) => {
const subFilePath = `${filePath}/${subFile}`;
if (subFile.match(/\.(jpg|jpeg|png)$/)) {
const fileStats = fs.statSync(subFilePath);
const publicFileStats = fs.statSync(`${inputDirectoryPath}/${subFile}`);
if (fileStats.size > publicFileStats.size) {
return;
} else {
fs.copyFileSync(subFilePath, `${inputDirectoryPath}/${subFile}`);
}
}
});
}
});
// Delete the output directory
fs.rmSync(outputDirectory, {
recursive: true
});
OK rồi, giờ chạy lại file compressImage.js
và xem kết quả nhé!
Thu gọn
Dưới đây là toàn bộ đoạn mã của file compressImage.js
đã được viết ngắn gọn:
const sharp = require("sharp"),
fs = require("node:fs"),
inputDirectory = "./dist",
outputDirectory = "./tmp",
quality = 80,
maxWidth = 4e3,
maxHeight = 4e3;
try {
fs.existsSync(outputDirectory) || fs.mkdirSync(outputDirectory)
} catch (t) {
console.error(t)
}
fs.readdirSync(inputDirectory).forEach(t => {
let i = `${inputDirectory}/${t}`;
if (fs.lstatSync(i).isDirectory()) {
let r = `${outputDirectory}/${t}`;
fs.existsSync(r) || fs.mkdirSync(r), fs.readdirSync(i).forEach(t => {
let e = `${i}/${t}`;
t.match(/\.(jpg|jpeg)$/) && sharp(e).resize(Math.min(maxWidth, maxHeight), {
fit: "inside"
}).jpeg({
quality: quality
}).toFile(`${r}/${t}`), t.match(/\.(png)$/) && sharp(e).resize(Math.min(maxWidth, maxHeight), {
fit: "inside"
}).png({
quality: quality,
compressionLevel: 5
}).toFile(`${r}/${t}`)
})
}
t.match(/\.(jpg|jpeg)$/) && sharp(i).resize(Math.min(maxWidth, maxHeight), {
fit: "inside"
}).jpeg({
quality: quality
}).toFile(`${outputDirectory}/${t}`), t.match(/\.(png)$/) && sharp(i).resize(Math.min(maxWidth, maxHeight), {
fit: "inside"
}).png({
quality: quality,
compressionLevel: 5
}).toFile(`${outputDirectory}/${t}`)
}), fs.readdirSync(outputDirectory).forEach(t => {
let i = `${outputDirectory}/${t}`;
if (fs.lstatSync(i).isDirectory()) {
let r = `${inputDirectory}/${t}`;
fs.readdirSync(i).forEach(t => {
let e = `${i}/${t}`;
if (t.match(/\.(jpg|jpeg|png)$/)) {
let s = fs.statSync(e),
c = fs.statSync(`${r}/${t}`);
if (s.size > c.size) return;
fs.copyFileSync(e, `${r}/${t}`)
}
})
}
}), fs.rmSync(outputDirectory, {
recursive: !0
});

Nguyen Pham
Làm việc tại phòng thí nghiệm MADE, Texas, USA. Là một người đam mê với công nghệ và thích chia sẻ kiến thức với mọi người.