一直被吐槽前端编译速度慢,没办法,文件多了,固然慢啦,并且目前公司使用的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服务是同一个,否则找不到对应的进程实例。
整体来讲,代码写的很粗糙,由于开发任务重,实在是没办法抽出太多时间来完善。
代码就不放出来了, 由于代码原本就很简单,这里更重要是记录一下本身一些心得和成果,在日常看似重复并且枯燥的任务中,细心一点,其实能够发现不少能够优化的地方,天天学习和进步一点点,才能从菜鸟成长为大牛。