距离 Node.js 这个东西出来已通过了很久了,感受如今的前端若是不会点 Node.js 就有点太落后于时代啦。我接触它是从去年暑假开始的,当时在写一个比较神奇的东西,就顺便接触了一下。虽然网传 npm 社区不是很好,可是我使用了这么久,以为 Node.js 仍是个很好的工具。本文大概分两部分,前半部分用来向你们介绍 Node.js,后半部分则是用 Node.js 写的一个小项目:一个简单的 WebHook。css
虽然是科普向,但你们仍是须要先熟悉 JavaScript 的基本语法、它的异步思想,以及一些数据库查询语句和命令行操做,此外后面的实例是用的 Coding 为例子,因此还须要了解 Coding 的基本操做。前端
若是你正在使用 Chrome 浏览器,你必定会以为它比其它浏览器要快,其缘由之一是由于 Chrome 有个叫 V8 的东西,能够高效地解析 JavaScript。Node.js 的做者其实一开始打算用 Ruby 来写一个本地的运行平台,可是后来发现 Ruby 性能不够,因而他开始尝试用 V8 引擎,并作了许多修改,最终诞生了 Node.js。node
因此 Node.js 到底是个啥?说白了,无非就是个本地的 JavaScript 的解释器。其实不能说是“解释器”,由于 V8 会将其编译成原生机器码(IA-32,x86-64,ARM,MIPS 等),而且会使用内联缓存等方法来提升性能。据传说,在 V8 的帮助下,JavaScript 的运行效率直逼二进制程序。然而与 V8 相比,Node.js 功能更多,例如直接访问文件系统、处理二进制数据等。mysql
有好多同窗一听到 Node.js,就会联想到这是用来写服务器的。眼界放宽一点吧,刚才不是说了,能够直接访问文件系统、处理二进制数据么?这意味着能够用 JavaScript 的语法来写各类各样的本地工具。其中最著名那些就是前端自动化构建工具了:Webpack、Gulp、Grunt……那么就顺便插播一段前端的故事。git
A long time ago in a galaxy far, far away...
前端的概念无非是 HTML、CSS、JavaScript,当时页面的样式和交互尚未如今那么复杂,因此只须要完成基本的样式显示和数据操做就行了。
As time went by...
各类复杂的页面相继出现,甚至出现了 Angular、React 这样的大工程。为了提升网页的加载速度,前端们不得不在发布前将全部的文件拼合在一块儿并混淆压缩以节省流量和请求数。程序员
上面提到的三款工具,任意一款均可以知足这种需求。当配置好了以后,咱们只要在命令行执行一句 grunt build
,就能够将各类零散的代码文件拼接起来并混淆压缩,甚至还能够对图片进行压缩;执行一句 gulp serve
,就能够直接在本地开启一个小型服务器来预览咱们写的效果。web
其实 Node.js 的程序员几乎不输入 node
命令,他们用的最多的命令是 npm
。因此 NPM 又是个什么东西呢?这又不得不提到两个概念:包、依赖。sql
若是你用过 Linux,确定对这两个概念很熟悉。例如我想装一个 Ruby,那么必须先装 libreadline 和 libruby,由于 Ruby 必须依赖他俩才能运行。为何 Windows 没有依赖的概念呢?由于 Windows 的程序通常在安装的时候会自动帮你装上,固然也有例外,例如运行一个大游戏须要先安装 VC++ 运行库和 DirectX 运行库。数据库
还记得刚才提到的“使用 grunt build
对图片进行压缩”嘛?其实压缩这一步不是 Grunt 作的,而是一个叫 imagemin 的工具作的。若是想安装它,能够从 GitHub 上面下载对应的代码,而后再将这家伙依赖的 36 个项目的代码也下下来,它们是: gulp-imagemin、node-atlas、cropshop……而后再将这些项目的依赖也……npm
坑爹呢!
还好咱们有 NPM,只须要再 npm install -g imagemin
,NPM 就会从指定的源(默认是官方源)中读取 imagemin 的依赖,而后再读取这些依赖里面的依赖……经过拓扑排序生成一个安装序列,而后自动帮你装好全部须要的东西,若是你的指令中带了 -g
,那就是全局安装,执行起来就跟原生的命令行工具同样天然。固然你也能够一条命令就将它们删掉。
一个工具就是一个包。NPM 的全称就是 Node Package Manager。
好久以前个人一个团队有一个用 PHP 写的 Webhook,可是有时候网速很差,执行时间太长,会被 PHP 强行断掉。固然其实能够这样:Web 端只负责接收 Webhook 请求而后存到数据库里,后端再写一个 daemon 不断轮询数据库,看有没有须要 pull / deploy 的项目。然而 JavaScript 是基于单线程事件队列的,能够几乎不占资源地实时监听各类事件,所以我尝试着用 Node.js 来写一个 Webhook 程序。
个人需求很简单:全部须要加入 Webhook 的项目的配置都存放在配置文件中,Webhook 的运行记录存放在数据库中,Web 端监听一个特定端口,只须要提供几个 API 就能够了。
首先咱们新建一个项目目录,而后用 npm init
新建一个项目,填写里面的各类信息,最终生成 package.json
文件。要注意的是,咱们程序的运行方法是 node index.js
,能够为它绑个命令:npm start
。其实咱们还能够为 npm 设定更多命令。
而后就能够在这个目录下写项目啦!配置文件很容易就写出来了:
// 监听的端口
var port = 9091;
// 项目配置
var projects = {
mall: { path: '/data/www', url: 'git@git.coding.net:Click_04/mall.git' }, lib: { path: '/storage', url: 'git@git.coding.net:Click_04/lib.git' }, // 更多的项目... }; // 数据库配置 var db = { host: 'localhost', user: 'root', password: 'root', database: 'webhook' }; module.exports = { projects: projects, port: port, db: db };
其中 module
是 Node.js 模块组织相关的东西,Node.js 几乎遵照了 CommonJS 的标准,然而这个就不在本文的讨论范围以内了。
因而咱们怎么写一个能够监听端口的服务器出来呢?其实很简单,由于 Node.js 自带了 http
模块,咱们只须要这样:
var http = require('http');
var config = require('./config.js');
var server = http.createServer(function (req, res) {
// 接收 POST 数据。若是请求方法不是 POST,那么这个变量最终是空字符串 var POST = ''; req.on('data', function (chunk) { POST += chunk;}); req.on('end', function () { // 执行后端逻辑代码 }); }); server.listen(config.port); console.log("Server runing at port: " + config.port + ".");
其中 http.createServer
的回调函数就是建立完服务器以后须要作的事情,http
的机制是:始终只有一个线程,而后监听 req
的各类事件,例如 data
事件就是正在接收数据,end
事件就是当前请求的数据已经接收完毕了。固然这儿的数据指的是 POST 数据,像 header 这样的东西固然是直接存在 req
变量中的(能够试试 console.log(req)
,这样会将 req
变量输出到终端里)。而后咱们能够经过 res
提供的一些方法输出数据。
下一个问题就是如何链接数据库。Node.js 并无自带这玩意儿,因此咱们必需要手动安装:
npm install --save mysql
选项 --save
表示将这个库添加到 package.json
中,方便后续拿到代码的人直接执行 npm install
安装所有依赖。mysql
这玩意儿是这样用的:
var mysql = require('mysql');
// config 就是上面那个 config
var pool = mysql.createPool(config.db);
pool.getConnection(function (err, conn) {
if (err) throw err; // 接下来能够经过 conn 来干一些事情了 });
最后一个须要解决的问题是如何执行命令行的 git 命令,这个 Node.js 也自带了:
var exec = require('child_process').exec;
exec(cmd_str, function(err, stdout, stderr) {
var status = err ? -1 : 1, cmd_result = err ? stderr : stdout; // 能够获取到错误信息、标准输出和标准错误输出,接下来继续处理吧 });
一切技术问题都扫清了,能够开始理思路了!
首先我分析了一下 Coding 的 Webhook 传过来的数据,首先确定是 JSON 串,其次若是有 zen 属性的话那就是测试请求,若是有 commits 属性的话就是正常的请求。按照 JSON 串的格式,能够获取到我须要的数据并插入到数据库中:
data = (POST == '') ? {} : JSON.parse(POST);
if (data.commits) {
// 获取到数据 var project_name = data.repository.name, trigger_user = data.user.global_key, commit_user = data.commits[0].committer.name, commit_user_email = data.commits[0].committer.email, commit_message = data.commits[0].short_message; if (!config.projects[project_name]) { return; } // 数据库查询 conn.query('INSERT INTO `log` (`project_name`, `trigger_user`, `commit_user`, `commit_user_email`, `commit_message`) VALUES (?, ?, ?, ?, ?)', [project_name, trigger_user, commit_user, commit_user_email, commit_message], function (err, results) { if (err) throw err; // 拼接 git 命令字符串 var cmd_str = 'cd ' + config.projects[project_name].path + '/' + project_name + ' && git pull origin master', log_id = results.insertId; // 执行命令 exec(cmd_str, function(err, stdout, stderr) { var status = err ? -1 : 1, cmd_result = err ? stderr : stdout; // 更新数据库 conn.query('UPDATE `log` SET `status` = ?, cmd_result = ? WHERE `log_id` = ?', [status, cmd_result, log_id], function (err, results) { // 结束对返回数据的写操做 res.end(); }); }); }); }
JSON.parse
是 JavaScript 的一个方法,能够将 JSON 字符串转换为 JSON 对象。咱们只须要在 Coding 上面设置 Webhook 的地址是 http://ip:9091/
或者经过 Nginx 等程序进行端口转发,就能够看到 Webhook 的效果啦!
大部分代码仍是很好理解的,就是那个 res.end
有点别扭。对于大部分语言来讲,执行完了以后是会自动中止向 Response body 写入数据的,而且能够通知浏览器“我写完了,你不用再等了”,然而 Node.js 的 http
并不行,必须手动加上这句话才能够。若是不加,浏览器就会一直等待。其实 Node.js 的一些框架例如 Express,就可让你专心处理后端逻辑,没必要担忧这些细枝末节。
注意到 query -> exec -> query
已经有三层回调了,这是 JavaScript 的一个大坑,固然咱们能够改为 Promise,可是其实本质没太大变化,只是让你写着舒服一点。如何使用异步的思路来写程序也是一个比较好玩的问题,但同时也是比较头疼的问题。关于如何避免掉进回调函数的陷阱里,如今已经有了许多解决方案,可是本文的这个项目很是小,因此并不须要。
其实对于一个 Webhook 来讲,这个功能已经足够了,可是我想干点别的:在网页上直接显示 log,或者显示当前已经加入 Webhook 的所有项目。咱们能够接着上一段代码的 if
来写:
else {
// 处理各类 GET 请求,或者 body 为空的 POST 请求 res.writeHeader(200, {'Content-type': 'application/json'}); // 尝试经过 URL 来判断请求类型 var match = ''; // 显示 log if (req.url == '/log') { conn.query('SELECT * FROM `log` ORDER BY `log_id` DESC LIMIT 30', [], function (err, results) { if (err) throw err; res.write(JSON.stringify(results)); res.end(); }); } // 显示全部加到 Webhook 中的项目信息 else if (req.url == '/projects') { res.write(JSON.stringify(config.projects)); res.end(); } // 手动 pull / clone 一个项目 else if (match = req.url.match(/\/(pull|clone)\/(.+)/i)) { if (!config.projects[match[2]]) { res.end(); return; } conn.query('INSERT INTO `log` (`project_name`) VALUES (?)', [match[2]], function (err, results) { if (err) throw err; var cmd_str = ''; if (match[1] == 'clone') { cmd_str = 'cd ' + config.projects[match[2]].path + ' && git clone ' + config.projects[match[2]].url; } else if (match[1] == 'pull') { cmd_str = 'cd ' + config.projects[match[2]].path + '/' + match[2] + ' && git pull origin master'; } var log_id = results.insertId; exec(cmd_str, function(err, stdout, stderr) { var status = err ? -1 : 1, cmd_result = err ? stderr : stdout; conn.query('UPDATE `log` SET `status` = ?, cmd_result = ? WHERE `log_id` = ?', [status, cmd_result, log_id], function (err, results) {}); }); }); res.end(); } }
经过 res.writeHeader
来输出 header,经过 res.write
来输出一段文本。JSON.stringify
是 JavaScript 自带的一个方法,能够将 JSON 对象转换为字符串。由于是手动触发(Manual),因此只能获取到项目名称,没法显示提交信息(虽然说能够经过 git 命令来获取可是好麻烦),而前文的自动触发是 Coding 发过来的请求,里面附上了完整的信息。
最后我使用了 supervisor 来守护 Node.js 的进程,用 Nginx 作了端口转发,固然这些就不在本文的讨论范围内了。
看一下效果吧,在一个项目中 push 一下,或者手工执行一下 pull / clone,而后从服务器上看 log。为了方便,我写了一个页面,以 AJAX 的形式请求 log,而后将数据以表格方式显示。上个截图:
所有的代码在 这里,欢迎吐槽。