Published on

Nén ảnh bằng Node.js

Khi làm dự án, việc nén ảnh bằng tay sẽ trở nên khó khăn và tốn thời gian. Trong bài viết này, mình sẽ hướng dẫn cách nén ảnh trong Node.js một cách hoàn toàn tự động, dễ dàng và hiệu quả nhất.
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:

bash
npm install sharp

Tạo một file compressImage.js và thêm đoạn mã sau:

js
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:

bash
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:

js
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:

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:

js
... // Đâ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:

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:

js
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

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.

Nguyen Pham — là nhà phát triển và thiết kế giàu kinh nghiệm tập trung vào WordPress, NextJS, Angular. Hãy xem một số dự án chúng tôi đã thực hiện và các sản phẩm nội bộ của chúng tôi.
Liên kết
Made by VueJS and Vercel Cloud· All rights reserved.