一直被吐槽前端編譯速度慢,沒辦法,文件多了,固然慢啦,並且目前公司使用的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
使用語法分析工具來找出修改文件對應的入口文件,使用webpack從新打包指定的入口文件。webpack
在服務器上直接運行一個webapck -w
實例,主動監聽文件變化,而後打包。git
可是上面兩個方法都有嚴重的缺陷:web
首先我暫時沒精力去研究babel-ast
等分析抽象語法樹的工具,其次,在生產模式中,若是使用了CommonsChunkPlugin
,若是入口文件數量變更,極可能會影響最終生成的common.js
,這種方法是不可取的。shell
webapck -w
實例對於package.json
和webapck.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(); } });
最後寫了一個頁面,方便控制
第一個版本我部署在測試服務器後,效果明顯,每次打包從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 }, (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
這裏要注意的一點是,若是你是非root用戶,記得執行的時候添加sudo;Jenkins的執行任務的用戶要和你啓動pm2服務是同一個,否則找不到對應的進程實例。
整體來講,代碼寫的很粗糙,由於開發任務重,實在是沒辦法抽出太多時間來完善。
代碼就不放出來了, 由於代碼原本就很簡單,這裏更重要是記錄一下本身一些心得和成果,在日常看似重複並且枯燥的任務中,細心一點,其實能夠發現不少能夠優化的地方,天天學習和進步一點點,才能從菜鳥成長爲大牛。