如何同时或依次 minify/compress 数以千计的 JS 文件(包括一些大文件)而不会使控制台崩溃?

How to minify/compress thousands of JS files - including some large ones - at the same time or sequentially without crashing the console?

上下文

我目前正在重构a demo,我有一个包含 196 MB 的 src 文件夹。大约 142 MB 由两个二进制文件组成。

其余2137个文件中约有2000个(约46MB)由JavaScript个文件组成,其中大部分属于两大框架的官方完整发行版。最大的 JavaScript 文件大约有 23MB。它是最初用 C++ 编写并编译的原始代码 - emscripten - to asm.

我想编写一个 Node.js 脚本,将我的所有文件从 src 路径复制到 dist 路径并缩小每个 JS 或 CSS 文件一路上的相遇。不幸的是,涉及的 JS 文件的数量 and/or 似乎破坏了我的脚本。


让我们来看看我采取的步骤...

步骤 1

我开始编写一个小型构建脚本,将所有数据从我的 src 文件夹复制到我的 dist 文件夹。得知这个过程在几秒钟内完成,我感到很惊讶。

下面是我为这个脚本编写的代码。请注意,您需要节点 8 才能 运行 该代码。

const util = require('util');
const fs = require('fs');
const path = require('path');

const mkdir = util.promisify(require('mkdirp'));
const rmdir = util.promisify(require('rimraf'));
const ncp = util.promisify(require('ncp').ncp);
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const stat = util.promisify(fs.stat);

const moveFrom = path.join(__dirname,"../scr");
const moveTo = path.join(__dirname,"../dist");

var copyFile = function(source, target) {
    return new Promise(function(resolve,reject){
        const rd = fs.createReadStream(source);
        rd.on('error', function(error){
            reject(error);
        });
        const wr = fs.createWriteStream(target);
        wr.on('error', function(error){
            reject(error);
        });
        wr.on('close', function(){
            resolve();
        });
        rd.pipe(wr);
    });
};

var copy = function(source, target) {
    stat(source)
    .then(function(stat){
        if(stat.isFile()) {
            console.log("Copying file %s", source);
            switch (path.extname(target)) {
                default:
                    return copyFile(source, target);
            }
        } else if( stat.isDirectory() ) {
            return build(source, target);
        }
    }).catch(function(error){
        console.error(error);
    });
};

var build = function(source, target) {
    readdir(source)
    .then(function(list) {
        return rmdir(target).then(function(){
            return list;
        });
    })
    .then(function(list) {
        return mkdir(target).then(function(){
            return list;
        });
    }).then(function(list) {
        list.forEach(function(item, index) {
            copy(path.join(source, item), path.join(target, item));
        });
    }).catch(function(error){
        console.error(error);
    })
};

build(moveFrom, moveTo);

第 2 步

为了在遇到 CSS 文件时缩小它们,我添加了 CSS 缩小。

为此,我对我的代码进行了以下修改。

首先,我添加了这个功能:

var uglifyCSS = function(source, target) {
    readFile(source, "utf8")
    .then(function(content){
        return writeFile(target, require('ycssmin').cssmin(content), "utf8");
    }).catch(function(error){
        console.error(error);
    });
}

然后,我修改了我的复制功能,像这样:

var copy = function(source, target) {
    stat(source)
    .then(function(stat){
        if(stat.isFile()) {
            console.log("Copying file %s", source);
            switch (path.extname(target)) {
            case ".css":
                return uglifyCSS(source, target);
            default:
                return copyFile(source, target);
            }
        } else if( stat.isDirectory() ) {
            return build(source, target);
        }
    }).catch(function(error){
        console.error(error);
    });
};

到目前为止,还不错。现阶段一切还 运行 顺利。

步骤 3

然后,我做了同样的事情来缩小我的 JS。

所以我又添加了一个新函数:

var uglifyJS = function(source, target) {
    readFile(source, "utf8")
    .then(function(content){
        return writeFile(target, require('uglify-js').minify(content).code, "utf8");
    }).catch(function(error){
        console.error(error);
    });
}

然后,我又修改了我的复制功能:

var copy = function(source, target) {
    stat(source)
    .then(function(stat){
        if(stat.isFile()) {
            console.log("Copying file %s", source);
            switch (path.extname(target)) {
            case ".css":
                return uglifyCSS(source, target);
            case ".js":
                return uglifyJS(source, target);
            default:
                return copyFile(source, target);
            }
        } else if( stat.isDirectory() ) {
            return build(source, target);
        }
    }).catch(function(error){
        console.error(error);
    });
};

问题

在这里,事情出了问题。随着进程不断遇到越来越多的 JS 文件,它不断变慢,直到进程似乎完全停止。

似乎启动了太多并行进程并不断消耗越来越多的内存,直到没有更多内存剩余并且进程静静地结束。我尝试了除 UglifyJS 之外的其他压缩器,我对所有压缩器都遇到了同样的问题。所以问题似乎不是 UglifyJS 特有的。

有解决此问题的想法吗?

这是完整的代码:

const util = require('util');
const fs = require('fs');
const path = require('path');

const mkdir = util.promisify(require('mkdirp'));
const rmdir = util.promisify(require('rimraf'));
const ncp = util.promisify(require('ncp').ncp);
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const stat = util.promisify(fs.stat);

const moveFrom = path.join(__dirname,"../scr");
const moveTo = path.join(__dirname,"../dist");

var copyFile = function(source, target) {
    return new Promise(function(resolve,reject){
        const rd = fs.createReadStream(source);
        rd.on('error', function(error){
            reject(error);
        });
        const wr = fs.createWriteStream(target);
        wr.on('error', function(error){
            reject(error);
        });
        wr.on('close', function(){
            resolve();
        });
        rd.pipe(wr);
    });
};

var uglifyCSS = function(source, target) {
    readFile(source, "utf8")
    .then(function(content){
        return writeFile(target, require('ycssmin').cssmin(content), "utf8");
    }).catch(function(error){
        console.error(error);
    });
}

var uglifyJS = function(source, target) {
    readFile(source, "utf8")
    .then(function(content){
        return writeFile(target, require('uglify-js').minify(content).code, "utf8");
    }).catch(function(error){
        console.error(error);
    });
}

var copy = function(source, target) {
    stat(source)
    .then(function(stat){
        if(stat.isFile()) {
            console.log("Copying file %s", source);
            switch (path.extname(target)) {
                    case ".css":
                        return uglifyCSS(source, target);
                            case ".js":
                                return uglifyJS(source, target);
                default:
                    return copyFile(source, target);
            }
        } else if( stat.isDirectory() ) {
            return build(source, target);
        }
    }).catch(function(error){
        console.error(error);
    });
};

var build = function(source, target) {
    readdir(source)
    .then(function(list) {
        return rmdir(target).then(function(){
            return list;
        });
    })
    .then(function(list) {
        return mkdir(target).then(function(){
            return list;
        });
    }).then(function(list) {
        list.forEach(function(item, index) {
            copy(path.join(source, item), path.join(target, item));
        });
    }).catch(function(error){
        console.error(error);
    })
};

build(moveFrom, moveTo);

轻松解决:您的整个问题是您对并行化没有限制:

list.forEach(function(item, index) {
        copy(path.join(source, item), path.join(target, item));
});

您同步分派异步操作。这意味着他们会立即 return,无需您等待。您要么需要使操作顺序进行,要么为操作设置一个界限 运行ning。这将生成一个函数列表:

const copyOperations = list.map((item) => {
        return copy(path.join(source, item), path.join(target, item));
});

然后按顺序运行:

const initialValue = Promise.resolve();
copyOperations.reduce((accumulatedPromise, nextFn) => {
    return accumulatedPromise.then(nextFn);
}, initialValue);

现在,如果您想等待所有这些完成,您需要 return 一个承诺,因此代码的复制部分将如下所示:

.then(function(list) {
    const copyOperations = list.map((item) => {
            return copy(path.join(source, item), path.join(target, item));
    });

    const allOperations = copyOperations.reduce((accumulatedPromise, nextFn) => {
        return accumulatedPromise.then(nextFn);
    }, Promise.resolve());

    return allOperations; 
})

当然,这只会一次复制一个文件,如果您需要同时完成更多操作,则需要更高级的机制。尝试 this promise pooling mechanism,您可以在其中设置阈值,例如 require('os').cpus().length;

使用 ES6 生成器的有界并行化示例

把上面的then函数体替换成这个

const PromisePool = require('es6-promise-pool')
const maxProcesses = require('os').cpus().length;

const copyOperations = list.map((item) => {
        return copy(path.join(source, item), path.join(target, item));
});

const promiseGenerator = function *(){
    copyOperations.forEach( operation => yield operation );
}

var pool = new PromisePool(promiseGenerator(), maxProcesses)

return pool.start()
  .then(function () {
    console.log('Complete')
  });

Oligofren的建议似乎没有帮助。然而,删除 23 MB 的 JS 文件确实解决了这个问题。所以看起来问题不是大量文件(正如我所怀疑的那样),而是文件太大,NodeJs 无法处理。我想尝试使用 NodeJs 的内存设置(例如 node --stack-size)可以解决这个问题。

无论如何,虽然我仍然需要一个解决方案来让一切正常工作而不删除 23 MB 的文件,但我想现在必须从要处理的文件中删除这个文件。无论如何,这几乎只是我正在研究的概念验证。