距離 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,而後將數據以表格方式顯示。上個截圖:
所有的代碼在 這裏,歡迎吐槽。