【小實驗】使用webpack & pm2搭建編譯服務器

使用webpack & pm2搭建編譯服務器

背景

一直被吐槽前端編譯速度慢,沒辦法,文件多了,固然慢啦,並且目前公司使用的Jenkins做爲持續集成工具,其中編譯&重啓Nodejs服務器命令以下:html

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種思路node

  1. 使用語法分析工具來找出修改文件對應的入口文件,使用webpack從新打包指定的入口文件。webpack

  2. 在服務器上直接運行一個webapck -w實例,主動監聽文件變化,而後打包。git

可是上面兩個方法都有嚴重的缺陷:web

  1. 首先我暫時沒精力去研究babel-ast等分析抽象語法樹的工具,其次,在生產模式中,若是使用了CommonsChunkPlugin,若是入口文件數量變更,極可能會影響最終生成的common.js,這種方法是不可取的。shell

  2. webapck -w實例對於package.jsonwebapck.config.js的變更是沒法監聽到的,並且我在項目中把HtmlWebpackPlugin的配置抽離出來了,webpack也沒辦法監聽到,當這些配置文件被修改的時候,須要將整個編譯進程重啓。npm

最後解決辦法就是用一個進程控制webpack編譯進程,而且利用webpack天生支持的增量編譯功能,能夠大幅提升後面的構建速度。json

嘗試

這裏利用到了webpack做爲Nodejs模塊時的API,文檔在這裏gulp

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,
                error: null,
                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();
    }


});

最後寫了一個頁面,方便控制

clipboard.png

第一個版本我部署在測試服務器後,效果明顯,每次打包從10多分鐘縮減到了4-5秒,不再用和測試人員說:改好啦,等編譯完成,大概10分鐘左右。。。

clipboard.png

第二版

後來項目作了比較大的變更,有三個webpack.config.js並行編譯,初版是將全部的webpack.config.js合併成一個單獨的config,再一塊兒打包,效率比較低,所以第二版作了改變,每一個webpack.config.js被分配到獨立的編譯進程中去,各自監聽文件變更,這樣能夠更加精確的編譯js文件了。

這裏和初版的區別以下

  • webpack是以watch模式啓動的,也就是說,若是新增了包,或者配置文件修改了,該進程在嘗試增量編譯的時候會報錯,這時候依賴與父進程重啓本身。

總體的架構以下:

clipboard.png

每一個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
}, (err, 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'),
            env: {
                NODE_ENV: process.env.NODE_ENV
            },
            env_production: {
                NODE_ENV: 'production'
            },
        },
        {
            name: 'h5',
            script: getScript('h5.js'),
            env: {
                NODE_ENV: process.env.NODE_ENV
            },
            env_production: {
                NODE_ENV: 'production'
            },
        }, {
            name: 'server',
            script: getScript('server.js'),
            env: {
                NODE_ENV: process.env.NODE_ENV
            },
            env_production: {
                NODE_ENV: 'production'
            },
        },]

    /**
     * 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, reject) {
        exec('cnpm i', {maxBuffer: 1024 * 2048}, (err, sto, 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', (err, ret) => {
                console.log(ret);
            })
        )
    });

    gulp.watch(h5Conf, () => {
        console.log('h5 前端配置文件修改。。。重啓編譯進程');

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

    gulp.watch(merchantConf, () => {
        console.log('merchant-pc 前端配置文件修改。。。重啓編譯進程');

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

    gulp.watch(packageConf, () => {
        console.log('package.json 配置文件修改。。。重啓全部編譯進程');

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


}

這樣,能夠直接在shell中控制每一個進程了,更加方便

pm2 ls

clipboard.png

這裏要注意的一點是,若是你是非root用戶,記得執行的時候添加sudo;Jenkins的執行任務的用戶要和你啓動pm2服務是同一個,否則找不到對應的進程實例。

總結

整體來講,代碼寫的很粗糙,由於開發任務重,實在是沒辦法抽出太多時間來完善。

代碼就不放出來了, 由於代碼原本就很簡單,這裏更重要是記錄一下本身一些心得和成果,在日常看似重複並且枯燥的任務中,細心一點,其實能夠發現不少能夠優化的地方,天天學習和進步一點點,才能從菜鳥成長爲大牛。

相關文章
相關標籤/搜索