一、情景描述
对已有MPA项目源代码source下的Html、Js及Css文件代码进行压缩,并将压缩代码生成于dist目录。
二、代码实现
在source同级目录下新建compress.js:
const { log } = require('console');
let fs = require('graceful-fs');
let path = require('path');
let UglifyJS = require("uglify-js");
let minify = require('html-minifier').minify;
let CleanCSS = require('clean-css');
let distDir = path.join(__dirname, './dist'); //压缩后的目录
let sourceDir = path.join(__dirname, './source'); //项目源代码
let showCompress = true;
let minifyJS = showCompress ? {
compress: {
warnings: false,
drop_debugger: true,
drop_console: true
}
} : true;//配置压缩js,showCompress为true时压缩代码并去除console,debugger控制台提示,正式发布上线可开启,否则只压缩js
deleteFolder(distDir); //清除打包后的目录
// 清除目录
function deleteFolder(paths) {
var files = [];
if (fs.existsSync(paths)) {
files = fs.readdirSync(paths);
files.forEach(function (file, index) {
var curPath = paths + "/" + file;
if (fs.statSync(curPath).isDirectory()) { // recurse
deleteFolder(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(paths);
}
}
var copyFile = function (srcPath, tarPath, cb) {
var rs = fs.createReadStream(srcPath)
rs.on('error', function (err) {
if (err) {
console.log('read error', srcPath)
}
cb && cb(err)
})
var ws = fs.createWriteStream(tarPath)
ws.on('error', function (err) {
if (err) {
console.log('write error', tarPath)
}
cb && cb(err)
})
ws.on('close', function (ex) {
cb && cb(ex)
})
rs.pipe(ws)
}
var copyFolder = function (srcDir, tarDir, cb) {
fs.readdir(srcDir, function (err, files) {
var count = 0
var checkEnd = function () {
++count == files.length && cb && cb()
}
if (err) {
checkEnd()
return
}
files.forEach(function (file) {
var srcPath = path.join(srcDir, file)
var tarPath = path.join(tarDir, file)
fs.stat(srcPath, function (err, stats) {
if (stats.isDirectory()) {
console.log('mkdir', tarPath)
fs.mkdir(tarPath, function (err) {
if (err) {
console.log(err)
return
}
copyFolder(srcPath, tarPath, checkEnd)
})
} else {
copyFile(srcPath, tarPath, checkEnd)
}
})
})
//为空时直接回调
files.length === 0 && cb && cb()
})
}
//创建dist目录
function creatDir(dir) {
return new Promise((resolve, reject) => {
fs.mkdir(dir, function (err) {
if (err) {
reject(err);
} else {
console.log("目录创建成功。");
resolve()
}
});
})
}
/**
* 文件遍历方法
* @param filePath 需要遍历的文件路径
*/
function fileDisplay(filePath) {
//根据文件路径读取文件,返回文件列表
fs.readdir(filePath, function (err, files) {
if (err) {
console.warn(err)
} else {
//遍历读取到的文件列表
files.forEach(function (filename) {
//获取当前文件的绝对路径
var filedir = path.join(filePath, filename);
//根据文件路径获取文件信息,返回一个fs.Stats对象
fs.stat(filedir, function (eror, stats) {
if (eror) {
console.warn('获取文件stats失败');
} else {
var isFile = stats.isFile(); //是文件
var isDir = stats.isDirectory(); //是文件夹
if (isFile && /\.htm/.test(filedir)) { //压缩.htm或.html文件
miniHtml(filedir)
}
if (isFile && /\.js$/.test(filedir)) { //压缩js文件
miniJs(filedir)
}
if (isFile && /\.css$/.test(filedir)) { //压缩css文件
miniCss(filedir)
}
if (isDir) {
fileDisplay(filedir); //递归,如果是文件夹,就继续遍历该文件夹下面的文件
}
}
})
});
}
});
}
function miniCss(filedir){
console.log(filedir)
fs.readFile(filedir, 'utf8', function (err, data) {
if (err) {
throw err;
}
let options = { /* options */ };
let result = new CleanCSS(options).minify(data);
if(result.warnings.length>0){//压缩错误处理
console.log(filedir,result.warnings);
}else{
fs.writeFile(filedir,result.styles, function () {
console.log(filedir,'success');
});
}
});
}
function miniJs(filedir){
fs.readFile(filedir, 'utf8', function (err, data) {
if (err) {
throw err;
}
let result = UglifyJS.minify(data);
if (!result.code) {//压缩错误处理
console.log(filedir, result.error);
} else {
fs.writeFile(filedir,result.code , function () {
console.log(filedir,'success');
});
}
});
}
function miniHtml(filedir){
fs.readFile(filedir, 'utf8', function (err, data) {
if (err) {
throw err;
}
fs.writeFile(filedir, minify(data, { //主要压缩配置
processScripts: ['text/html'],
collapseWhitespace: true,
minifyJS: minifyJS,
minifyCSS: true,
removeComments: true, //删除注释
removeCommentsFromCDATA: true, //从脚本和样式删除的注释
continueOnParseError: true
}), function () {
console.log(filedir,'success');
});
});
}
// 开始压缩
creatDir(distDir).then(() => {
copyFolder(sourceDir, distDir, function (err) {
if (err) {
return
}
console.log('代码复制完成');
fileDisplay(distDir);
});
}).catch((err) => {
});
考虑到浏览器缓存问题比较常见,对于Html文件内引入的js及css链接考虑携带上对应文件的MD5戳:
const { log } = require('console');
let fs = require('graceful-fs');
let path = require('path');
let UglifyJS = require("uglify-js");
let minify = require('html-minifier').minify;
let CleanCSS = require('clean-css');
let md5 = require('blueimp-md5')
let distDir = path.join(__dirname, './dist'); //压缩后的目录
let sourceDir = path.join(__dirname, './source'); //项目源代码
let showCompress = true;
let minifyJS = showCompress ? {
compress: {
warnings: false,
drop_debugger: true,
drop_console: true
}
} : true;//配置压缩js,showCompress为true时压缩代码并去除console,debugger控制台提示,正式发布上线可开启,否则只压缩js
deleteFolder(distDir); //清除打包后的目录
// 清除目录
function deleteFolder(paths) {
var files = [];
if (fs.existsSync(paths)) {
files = fs.readdirSync(paths);
files.forEach(function (file, index) {
var curPath = paths + "/" + file;
if (fs.statSync(curPath).isDirectory()) { // recurse
deleteFolder(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(paths);
}
}
var copyFile = function (srcPath, tarPath, cb) {
var rs = fs.createReadStream(srcPath)
rs.on('error', function (err) {
if (err) {
console.log('read error', srcPath)
}
cb && cb(err)
})
var ws = fs.createWriteStream(tarPath)
ws.on('error', function (err) {
if (err) {
console.log('write error', tarPath)
}
cb && cb(err)
})
ws.on('close', function (ex) {
cb && cb(ex)
})
rs.pipe(ws)
}
var copyFolder = function (srcDir, tarDir, cb) {
fs.readdir(srcDir, function (err, files) {
var count = 0
var checkEnd = function () {
++count == files.length && cb && cb()
}
if (err) {
checkEnd()
return
}
files.forEach(function (file) {
var srcPath = path.join(srcDir, file)
var tarPath = path.join(tarDir, file)
fs.stat(srcPath, function (err, stats) {
if (stats.isDirectory()) {
console.log('mkdir', tarPath)
fs.mkdir(tarPath, function (err) {
if (err) {
console.log(err)
return
}
copyFolder(srcPath, tarPath, checkEnd)
})
} else {
copyFile(srcPath, tarPath, checkEnd)
}
})
})
//为空时直接回调
files.length === 0 && cb && cb()
})
}
//创建dist目录
function creatDir(dir) {
return new Promise((resolve, reject) => {
fs.mkdir(dir, function (err) {
if (err) {
reject(err);
} else {
console.log("目录创建成功。");
resolve()
}
});
})
}
/**
* 文件遍历方法
* @param filePath 需要遍历的文件路径
*/
function fileDisplay(filePath) {
//根据文件路径读取文件,返回文件列表
fs.readdir(filePath, function (err, files) {
if (err) {
console.warn(err)
} else {
//遍历读取到的文件列表
files.forEach(function (filename) {
//获取当前文件的绝对路径
var filedir = path.join(filePath, filename);
//根据文件路径获取文件信息,返回一个fs.Stats对象
fs.stat(filedir, function (eror, stats) {
if (eror) {
console.warn('获取文件stats失败');
} else {
var isFile = stats.isFile(); //是文件
var isDir = stats.isDirectory(); //是文件夹
if (isFile && /\.htm/.test(filedir)) { //压缩.htm或.html文件
miniHtml(filedir)
}
if (isFile && /\.js$/.test(filedir)) { //压缩js文件
miniJs(filedir)
}
if (isFile && /\.css$/.test(filedir)) { //压缩css文件
miniCss(filedir)
}
if (isDir) {
fileDisplay(filedir); //递归,如果是文件夹,就继续遍历该文件夹下面的文件
}
}
})
});
}
});
}
function replacePath(content,exp,md5str) {
var matchs = content.matchAll(exp)
var arr = Array.from(matchs)
arr.forEach(o=> {
content = content.replace(o[1], o[1].indexOf('?')>=0?`${o[1]}&${md5str}`:`${o[1]}?${md5str}`)
})
return content
}
function appendMD5Str(data) {
const scriptExp = /<script(?:(?:[^>])*(?:src="([^"]*))+[^>]*)?>(.*?)<\/script\s*>/ig
const linkExp = /<link(?:(?:[^>])*(?:href="([^"]*))[^>]*)+>/ig;
const md5str = md5(data)
let content = replacePath(data, scriptExp, md5str)
content = replacePath(content, linkExp, md5str)
return content
}
function miniCss(filedir){
console.log(filedir)
fs.readFile(filedir, 'utf8', function (err, data) {
if (err) {
throw err;
}
let options = { /* options */ };
let result = new CleanCSS(options).minify(data);
if(result.warnings.length>0){//压缩错误处理
console.log(filedir,result.warnings);
}else{
fs.writeFile(filedir,result.styles, function () {
console.log(filedir,'success');
});
}
});
}
function miniJs(filedir){
fs.readFile(filedir, 'utf8', function (err, data) {
if (err) {
throw err;
}
let result = UglifyJS.minify(data);
if (!result.code) {//压缩错误处理
console.log(filedir, result.error);
} else {
fs.writeFile(filedir,result.code , function () {
console.log(filedir,'success');
});
}
});
}
function miniHtml(filedir){
fs.readFile(filedir, 'utf8', function (err, data) {
if (err) {
throw err;
}
fs.writeFile(filedir, minify(appendMD5Str(data), { //主要压缩配置
processScripts: ['text/html'],
collapseWhitespace: true,
minifyJS: minifyJS,
minifyCSS: true,
removeComments: true, //删除注释
removeCommentsFromCDATA: true, //从脚本和样式删除的注释
continueOnParseError: true
}), function () {
console.log(filedir,'success');
});
});
}
// 开始压缩
creatDir(distDir).then(() => {
copyFolder(sourceDir, distDir, function (err) {
if (err) {
return
}
console.log('代码复制完成');
fileDisplay(distDir);
});
}).catch((err) => {
});
三、运行压缩
在compress.js文件所在目录运行:
node ./compress.js
四、问题记录
1、在使用 var fs=require('fs')
时,由于读取文件时打开的文件太多导致异常:
解决方案:
将 var fs=require('fs')
改用 const fs = require('graceful-fs');