使用 Node.js 實現簡單的 Webhook



距離 Node.js 這個東西出來已經過了好久了,感覺現在的前端如果不會點 Node.js 就有點太落后於時代啦。我接觸它是從去年暑假開始的,當時在寫一個比較神奇的東西,就順便接觸了一下。雖然網傳 npm 社區不是很好,但是我使用了這么久,覺得 Node.js 還是個很好的工具。本文大概分兩部分,前半部分用來向大家介紹 Node.js,后半部分則是用 Node.js 寫的一個小項目:一個簡單的 WebHook。

雖然是科普向,但大家還是需要先熟悉 JavaScript 的基本語法、它的異步思想,以及一些數據庫查詢語句和命令行操作,此外后面的實例是用的 Coding 為例子,所以還需要了解 Coding 的基本操作。

Node.js 究竟是什么?

如果你正在使用 Chrome 瀏覽器,你一定會覺得它比其它瀏覽器要快,其原因之一是因為 Chrome 有個叫 V8 的東西,可以高效地解析 JavaScript。Node.js 的作者其實一開始打算用 Ruby 來寫一個本地的運行平台,但是后來發現 Ruby 性能不夠,於是他開始嘗試用 V8 引擎,並做了許多修改,最終誕生了 Node.js。

所以 Node.js 究竟是個啥?說白了,無非就是個本地的 JavaScript 的解釋器。其實不能說是“解釋器”,因為 V8 會將其編譯成原生機器碼(IA-32,x86-64,ARM,MIPS 等),並且會使用內聯緩存等方法來提高性能。據傳說,在 V8 的幫助下,JavaScript 的運行效率直逼二進制程序。然而與 V8 相比,Node.js 功能更多,例如直接訪問文件系統、處理二進制數據等。

有好多同學一聽到 Node.js,就會聯想到這是用來寫服務器的。眼界放寬一點吧,剛才不是說了,可以直接訪問文件系統、處理二進制數據么?這意味着可以用 JavaScript 的語法來寫各種各樣的本地工具。其中最著名那些就是前端自動化構建工具了:Webpack、Gulp、Grunt……那么就順便插播一段前端的故事。

A long time ago in a galaxy far, far away...

前端的概念無非是 HTML、CSS、JavaScript,當時頁面的樣式和交互還沒有現在那么復雜,所以只需要完成基本的樣式顯示和數據操作就好了。

As time went by...

各種復雜的頁面相繼出現,甚至出現了 Angular、React 這樣的大工程。為了提高網頁的加載速度,前端們不得不在發布前將所有的文件拼合在一起並混淆壓縮以節省流量和請求數。

上面提到的三款工具,任意一款都可以滿足這種需求。當配置好了之后,我們只要在命令行執行一句 grunt build,就可以將各種零散的代碼文件拼接起來並混淆壓縮,甚至還可以對圖片進行壓縮;執行一句 gulp serve,就可以直接在本地開啟一個小型服務器來預覽我們寫的效果。

Node.js 的好幫手:NPM

其實 Node.js 的程序員幾乎不輸入 node 命令,他們用的最多的命令是 npm。所以 NPM 又是個什么東西呢?這又不得不提到兩個概念:包、依賴。

如果你用過 Linux,肯定對這兩個概念很熟悉。例如我想裝一個 Ruby,那么必須先裝 libreadline 和 libruby,因為 Ruby 必須依賴他倆才能運行。為什么 Windows 沒有依賴的概念呢?因為 Windows 的程序一般在安裝的時候會自動幫你裝上,當然也有例外,例如運行一個大游戲需要先安裝 VC++ 運行庫和 DirectX 運行庫。

還記得剛才提到的“使用 grunt build 對圖片進行壓縮”嘛?其實壓縮這一步不是 Grunt 做的,而是一個叫 imagemin 的工具做的。如果想安裝它,可以從 GitHub 上面下載對應的代碼,然后再將這家伙依賴的 36 個項目的代碼也下下來,它們是: gulp-imagemin、node-atlas、cropshop……然后再將這些項目的依賴也……

坑爹呢!

還好我們有 NPM,只需要再 npm install -g imagemin,NPM 就會從指定的源(默認是官方源)中讀取 imagemin 的依賴,然后再讀取這些依賴里面的依賴……通過拓撲排序生成一個安裝序列,然后自動幫你裝好所有需要的東西,如果你的指令中帶了 -g,那就是全局安裝,執行起來就跟原生的命令行工具一樣自然。當然你也可以一條命令就將它們刪掉。

一個工具就是一個包。NPM 的全稱就是 Node Package Manager。

寫一個 Node.js 程序

很久之前我的一個團隊有一個用 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,然后將數據以表格方式顯示。上個截圖:

1

全部的代碼在 這里,歡迎吐槽。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM