【小实验】使用webpack & pm2搭建编译服务器

使用webpack & pm2搭建编译服务器

背景

一直被吐槽前端编译速度慢,没办法,文件多了,当然慢啦,而且目前公司使用的Jenkins作为持续集成工具,其中编译&重启Nodejs服务器命令如下:

npm i;npm run dist-h5;npm run dist-pc; kill -9 `netstat -tlnp|grep 3000|awk '"'"'{print $7}'"'"' | awk -F "/" '"'"'{print $1}'"'"'`; nohup npm run start > /data/home/home.log 2>&1 &

由于历史原因,PC项目和H5项目还有nodejs中间层服务是放在一个项目里面的,因此每次git提交到服务器的时候,都会全量打包一次,所以问题就在于,没有工具可以帮我们精确的编译修改的文件,加上运维也不是很熟nodejs,想来想去,于是自己手动撸一个加快编译的小工具,来逐步代替Jenkins的打包功能。

思路

初步想了想,大体上有以下2种思路

  1. 使用语法分析工具来找出修改文件对应的入口文件,使用webpack重新打包指定的入口文件。

  2. 在服务器上直接运行一个webapck -w实例,主动监听文件变化,然后打包。

但是上面两个方法都有严重的缺陷:

  1. 首先我暂时没精力去研究babel-ast等分析抽象语法树的工具,其次,在生产模式中,如果使用了CommonsChunkPlugin,如果入口文件数量变动,很可能会影响最终生成的common.js,这种方法是不可取的。

  2. webapck -w实例对于package.jsonwebapck.config.js的变动是无法监听到的,而且我在项目中把HtmlWebpackPlugin的配置抽离出来了,webpack也没办法监听到,当这些配置文件被修改的时候,需要将整个编译进程重启。

最后解决办法就是用一个进程控制webpack编译进程,并且利用webpack天生支持的增量编译功能,可以大幅提高后面的构建速度。

尝试

这里利用到了webpack作为Nodejs模块时的API,文档在这里

const webpack = require("webpack");

const compiler = webpack({
  // Configuration Object
});

compiler.run((err,stats) => {
  // ...
});

然后我搭建了一个koa server,每次Jenkins执行打包命令就curl我们的server的rest api,server先检测是否需要重启编译进程、安装新的npm包,再执行全量或者增量编译的任务。

第一版

webpack编译进程代码如下:

/**
 * Created by chenchen on 2017/4/20.
 *
 * 编译进程,被主进程fork运行
 */



const _ = require("lodash");


// =======================================

const gulp = require("gulp");

const fs = require("fs");
const path = require("path");
const chalk = require("chalk");

// ==========================================

const webpack = require("webpack");

let compiler = null;
let isBusy = false;
let currentFullCompileTask = null;

let logCount = 0;
let logArr = new Array(20);


const watchFiles = ['../app/h5/src/webpack.config.js','../app/pc/src/webpack.config.js','../app/pc/src/config/dll.config.js','../package.json'].map(f => path.resolve(__dirname,f));


process.send(`webpack 编译进程启动! pid ==> ${process.pid}`);
watchConfFiles();
watchDog();
fullCompile();

process.on('message',msg => {

    let {taskID} = msg;
    if (taskID) {
        console.log(chalk.gray(`【receive Task】 ${taskID}`))
    }

    switch (msg.type) {
        case 'increment':
            compiler.run((err,stats) => {
                if (err) {
                    console.error(err.stack || err);
                    if (err.details) {
                        console.error(err.details);
                    }
                    return;
                }
                let retObj = {taskID};
                const info = stats.toJson();

                if (stats.hasErrors()) {
                    console.error(info.errors);
                    retObj.error = info.errors;
                }
                retObj.result = outputStatsInfo(stats);
                process.send(retObj);
            });
            break;
        case 'full':

            let p = null;

            //
            if (isBusy) {
                p = currentFullCompileTask;
            } else {
                p = fullCompile();
            }

            p.then(stats => {

                if (typeof stats === 'string') {
                    process.send({
                        taskID,error: stats,result: null
                    });
                } else {
                    process.send({
                        taskID,error: null,result: outputStatsInfo(stats)
                    });
                }

            }).catch(e => {
                process.send({
                    taskID,error: e.message,result: null
                });
            });


            break;
        case 'status':
            process.send({
                taskID,result: {
                    isBusy,resUsage: logArr
                }
            });
            break;
        default:
            console.log('未知指令');
            break;
    }

});


function requireWithoutCache(filename) {

    delete require.cache[path.resolve(__dirname,filename)];

    return require(filename);
}


function outputStatsInfo(stats) {
    return stats.toString({
        colors: false,// children: false,modules: false,chunk: false,source: false,chunkModules: false,chunkOrigins: false,})
}

/**
 * 全量编译
 * @return {Promise}
 */
function fullCompile() {

    isBusy = true;

    let h5Conf = requireWithoutCache("../app/h5/src/webpack.config.js");

    let pcConf = requireWithoutCache("../app/pc/src/webpack.config.js");


    console.log('start full compile');

    currentFullCompileTask = new Promise((resolve,reject) => {

        compiler = webpack([...pcConf,...h5Conf]);

        // compiler = webpack(pcConf);


        compiler.run((err,stats) => {

            isBusy = false;

            console.log('full compile done');


            if (err)return resolve(err.message);

            console.log(stats.toString("minimal"));

            resolve(stats);
        });
    });

    return currentFullCompileTask;
}


// =========================================


function cnpmInstallPackage() {
    var {exec} = require("child_process");
    return new Promise(function (resolve,reject) {
        exec('cnpm i',{maxBuffer: 1024 * 2048},(err,sto,ster) => {
            if (err)return reject(err);

            resolve(sto.toString());
        })
    });
}


function watchConfFiles() {


    console.log('监听webpack配置文件。。。');
    gulp.watch(watchFiles,e => {
        console.log(e);
        console.log('config file changed,reRuning...');
        if (e.path.match(/package\.json$/)) {
            cnpmInstallPackage().catch(e => {
                console.log(e);
                return -1;
            });
        }


        fullCompile();
    });
}


function watchDog() {
    function run() {
        logArr[logCount % 20] = {
            memoryUsage: process.memoryUsage(),cpuUsage: process.cpuUsage(),time: Date.now()
        };
        logCount++;
    }

    setInterval(run,3000);
}

这个js文件执行的任务有3个

  • 启动webpack编译任务

  • 监听webpack.config.js等文件变动,重启编译任务

  • 每隔200ms收集自己进程占用的系统资源

这里有几个要注意的地方

  • 由于require有缓存机制,因此当重新启动编译任务前,需要清除缓存从而拿到最新的配置文件,可以调用下面的函数来require最新的文件内容。

function requireWithoutCache(filename) {

    delete require.cache[path.resolve(__dirname,filename)];

    return require(filename);
}
  • 编译进程和主进程通过message来通讯,而且大部分是异步任务,因此要构建一套简单的任务收发系统,下面是控制进程创建一个任务的代码:

/**
     * @description 创建一个任务
     * @param {string | null} [id] 任务ID,可以不填
     * @param {string} type  任务类型
     * @param {number} timeout
     * @return {Promise<TaskResult>} taskResult
     */
    createBuildTask(id,type = 'increment',timeout = 180000) {

        let taskID = id || `${type}-${Date.now() + Math.random() * 1000}`;


        return new Promise((resolve,reject) => {


            this.taskObj[taskID] = resolve;

            setTimeout(reject.bind(null,'编译任务超时'),timeout);

            this.webpackProcess.send({taskID,type});

        });


    }

在koa server端,我们只需要判断querystring,即可执行编译任务,下面是server的部分代码

app.use((ctx,next) => {


    let {action} = ctx.query;

    switch (action) {
        case 'full':

            return buildProc.createBuildTask(null,'full').then(e => {
                ctx.body = e;
            });
        case 'increment':

            return buildProc.createBuildTask().then(e => {
                ctx.body = e;
            });

        case 'reset':

            buildProc.reset();

            ctx.body = 'success';

            return next();
        case 'sys-info':
            return ctx.body = {
                uptime: process.uptime(),version: process.version,serverPid: process.pid,webpackProcessPid: buildProc.webpackProcess.pid
            };

        case 'build-status':

            return buildProc.createBuildTask(null,'status').then(ret => {
                return ctx.body = ret.result;
            });

        default:

            ctx.body = fs.readFileSync(path.join(__dirname,'./views/index.html')).toString();

            return next();
    }


});

最后写了一个页面,方便控制

第一个版本我部署在测试服务器后,效果明显,每次打包从10多分钟缩减到了4-5秒,再也不用和测试人员说:改好啦,等编译完成,大概10分钟左右。。。

第二版

后来项目做了比较大的变动,有三个webpack.config.js并行编译,第一版是将所有的webpack.config.js合并成一个单独的config,再一起打包,效率比较低,因此第二版做了改变,每个webpack.config.js被分配到独立的编译进程中去,各自监听文件变动,这样可以更加精确的编译js文件了。

这里和第一版的区别如下

  • webpack是以watch模式启动的,也就是说,如果新增了包,或者配置文件修改了,该进程在尝试增量编译的时候会报错,这时候依赖与父进程重启自己。

整体的架构如下:

每个webpack编译进程的代码如下

const path = require("path");
const webpack = require("webpack");

let pcConf = require("../../app/pc/front/webpack.config");

const compiler = webpack(pcConf);

const watching = compiler.watch({
    poll: 1000
},stats) => {

    if (err) {
        console.error(err);
        return process.exit(0);
    }
    console.log(stats.toString('minimal'))
});

为了方便管理,我使用了pm2来帮我控制所有的进程,我创建了一个ecosystem.config.js文件,代码如下

const path = require("path");

function getScript(s) {

    return path.join(__dirname,'./proc/',s)

}


module.exports = {

    /**
     * Application configuration section
     * http://pm2.keymetrics.io/docs/usage/application-declaration/
     */
    apps: [

        // First application
        {
            name: 'pc',script: getScript('pc.js'),env: {
                NODE_ENV: process.env.NODE_ENV
            },env_production: {
                NODE_ENV: 'production'
            },},{
            name: 'merchant-pc',script: getScript('merchant-pc.js'),{
            name: 'h5',script: getScript('h5.js'),{
            name: 'server',script: getScript('server.js'),]

    /**
     * Deployment section
     * http://pm2.keymetrics.io/docs/usage/deployment/
     */
};

这样在controller-server中也使用pm2来启动编译进程,而不用自己fork了。

function startPm2() {
    const cfg = require("./ecosystem.config");
    return new Promise(function (resolve,reject) {
        pm2.start(cfg,err => {
            if (err) {
                console.error(err);
                return process.exit(0)
            }
            resolve()
        })
    });
}

controller-server端核心代码如下

app.listen(PORT,_ => {
    console.log(`taskServer is running on ${PORT}`);


    pm2.connect(function (err) {
        if (err) {
            console.error(err);
            process.exit(2);
        }

        startPm2().then(() => {

            console.log('pm2 started... init watch dog');
            watchDog();

            return listProc();

        }).then(list => {
            list.forEach(proc => {

                console.log(proc.name);

            })
        })

    });
});

function cnpmInstallPackage() {
    var {exec} = require("child_process");
    return new Promise(function (resolve,ster) => {
            if (err)return reject(err);

            resolve(sto.toString());
        })
    });
}

function watchDog() {
    let merchantConf = require.resolve("../app/merchant-pc/front/webpack.config");
    let pcConf = require.resolve("../app/pc/front/webpack.config");
    let h5Conf = require.resolve("../app/h5/front/webpack.config");
    let packageConf = require.resolve("../package.json");

    gulp.watch(pcConf,() => {
        console.log('pc 前端配置文件修改。。。重启编译进程');

        cnpmInstallPackage().then(() => pm2.restart('pc',ret) => {
                console.log(ret);
            })
        )
    });

    gulp.watch(h5Conf,() => {
        console.log('h5 前端配置文件修改。。。重启编译进程');

        cnpmInstallPackage().then(() => pm2.restart('h5',ret) => {
                console.log(ret);
            })
        )
    });

    gulp.watch(merchantConf,() => {
        console.log('merchant-pc 前端配置文件修改。。。重启编译进程');

        cnpmInstallPackage().then(() => pm2.restart('merchant-pc',ret) => {
                console.log(ret);
            })
        )
    });

    gulp.watch(packageConf,() => {
        console.log('package.json 配置文件修改。。。重启所有编译进程');

        cnpmInstallPackage().then(() => pm2.restart('all',ret) => {
                console.log(ret);
            })
        )
    });


}

这样,可以直接在shell中控制每个进程了,更加方便

pm2 ls

这里要注意的一点是,如果你是非root用户,记得执行的时候添加sudo;Jenkins的执行任务的用户要和你启动pm2服务是同一个,不然找不到对应的进程实例。

总结

总体来说,代码写的很粗糙,因为开发任务重,实在是没办法抽出太多时间来完善。

代码就不放出来了, 因为代码本来就很简单,这里更重要是记录一下自己一些心得和成果,在平常看似重复而且枯燥的任务中,细心一点,其实可以发现很多可以优化的地方,每天学习和进步一点点,才能从菜鸟成长为大牛。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


这篇文章主要介绍“基于nodejs的ssh2怎么实现自动化部署”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“基于nodejs...
本文小编为大家详细介绍“nodejs怎么实现目录不存在自动创建”,内容详细,步骤清晰,细节处理妥当,希望这篇“nodejs怎么实现目录不存在自动创建”文章能帮助大...
这篇“如何把nodejs数据传到前端”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这...
本文小编为大家详细介绍“nodejs如何实现定时删除文件”,内容详细,步骤清晰,细节处理妥当,希望这篇“nodejs如何实现定时删除文件”文章能帮助大家解决疑惑...
这篇文章主要讲解了“nodejs安装模块卡住不动怎么解决”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来...
今天小编给大家分享一下如何检测nodejs有没有安装成功的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文...
本篇内容主要讲解“怎么安装Node.js的旧版本”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎...
这篇“node中的Express框架怎么安装使用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家...
这篇文章主要介绍“nodejs如何实现搜索引擎”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“nodejs如何实现搜索引擎...
这篇文章主要介绍“nodejs中间层如何设置”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“nodejs中间层如何设置”文...
这篇文章主要介绍“nodejs多线程怎么实现”,在日常操作中,相信很多人在nodejs多线程怎么实现问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法...
这篇文章主要讲解了“nodejs怎么分布式”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“nodejs怎么分布式”...
本篇内容介绍了“nodejs字符串怎么转换为数组”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情...
这篇文章主要介绍了nodejs如何运行在php服务器的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇nodejs如何运行在php服务器文章都...
本篇内容主要讲解“nodejs单线程如何处理事件”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“nodejs单线程如何...
这篇文章主要介绍“nodejs怎么安装ws模块”,在日常操作中,相信很多人在nodejs怎么安装ws模块问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法...
本篇内容介绍了“怎么打包nodejs代码”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!
本文小编为大家详细介绍“nodejs接收到的汉字乱码怎么解决”,内容详细,步骤清晰,细节处理妥当,希望这篇“nodejs接收到的汉字乱码怎么解决”文章能帮助大家解...
这篇“nodejs怎么同步删除文件”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇...
今天小编给大家分享一下nodejs怎么设置淘宝镜像的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希