「譯」如何使用 NodeJS 構建基於 RPC 的 API 系統


如何使用 NodeJS 構建基於 RPC 的 API 系統

API 在它存在的很長時間內都不斷地侵蝕着咱們的開發工做。不管是構建僅供其餘微服務訪問的微服務仍是構建對外暴露的服務,你都須要開發 API。javascript

目前,大多數 API 都基於 REST 規範,REST 規範通俗易懂,而且創建在 HTTP 協議之上。 可是在很大程度上,REST 可能並不適合你。許多公司好比 Uber,facebook,Google,netflix 等都構建了本身的服務間內部通訊協議,這裏的關鍵問題在於什麼時候作,而不是應不該該作。java

假設你想使用傳統的 RPC 方式,可是你仍然想經過 http 格式傳遞 json 數據,這時要怎麼經過 node.js 來實現呢?請繼續閱讀本文。node

閱讀本教程前應確保如下兩點linux

  • 你至少應該具有 Node.js 的實戰經驗
  • 爲了得到 ES6 支持,須要安裝 Node.js v4.0.0 以上版本。

設計原則

在本教程中,咱們將爲 API 設置以下兩個約束:git

  • 保持簡單(沒有外部包裝和複雜的操做)
  • API 和接口文檔,應該一同編寫

如今開始

本教程的完整源代碼能夠在 Github 上找到,所以你能夠 clone 下來方便查看。 首先,咱們須要首先定義類型以及將對它們進行操做的方法(這些將是經過 API 調用的相同方法)。github

建立一個新目錄,並在新目錄中建立兩個文件,types.jsmethods.js。 若是你正在使用 linux 或 mac 終端,能夠鍵入如下命令。express

mkdir noderpc && cd noderpc
touch types.js methods.js
複製代碼

types.js 文件中,輸入如下內容。npm

'use strict';

let types = {
    user: {
        description:'the details of the user',
        props: {
            name:['string', 'required'],
            age: ['number'],
            email: ['string', 'required'],
            password: ['string', 'required']
        }
    },
    task: {
        description:'a task entered by the user to do at a later time',
        props: {
            userid: ['number', 'required'],
            content: ['string', 'require'],
            expire: ['date', 'required']
        }
    }
}

module.exports = types;
複製代碼

乍一看很簡單,用一個 key-value 對象來保存咱們的類型,key 是類型的名稱,value 是它的定義。該定義包括描述(是一段可讀文本,主要用於生成文檔),在 props 中描述了各個屬性,這樣設計主要用於文檔生成和驗證,最後經過 module.exports 暴露出來。json

methods.js 有如下內容。api

'use strict';

let db = require('./db');

let methods = {
    createUser: {
        description: `creates a new user, and returns the details of the new user`,
        params: ['user:the user object'],
        returns: ['user'],
        exec(userObj) {
            return new Promise((resolve) => {
                if (typeof (userObj) !== 'object') {
                    throw new Error('was expecting an object!');
                }
                // you would usually do some validations here
                // and check for required fields

                // attach an id the save to db
                let _userObj = JSON.parse(JSON.stringify(userObj));
                _userObj.id = (Math.random() * 10000000) | 0; // binary or, converts the number into a 32 bit integer
                resolve(db.users.save(userObj));
            });
        }
    },

    fetchUser: {
        description: `fetches the user of the given id`,
        params: ['id:the id of the user were looking for'],
        returns: ['user'],
        exec(userObj) {
            return new Promise((resolve) => {
                if (typeof (userObj) !== 'object') {
                    throw new Error('was expecting an object!');
                }
                // you would usually do some validations here
                // and check for required fields

                // fetch
                resolve(db.users.fetch(userObj.id) || {});
            });
        }
    },

     fetchAllUsers: {
        released:false;
        description: `fetches the entire list of users`,
        params: [],
        returns: ['userscollection'],
        exec() {
            return new Promise((resolve) => {
                // fetch
                resolve(db.users.fetchAll() || {});
            });
        }
    },

};

module.exports = methods;
複製代碼

能夠看到,它和類型模塊的設計很是相似,但主要區別在於每一個方法定義中都包含一個名爲 exec 的函數,它返回一個 Promise。 這個函數暴露了這個方法的功能,雖然其餘屬性也暴露給了用戶,但這必須經過 API 抽象。

db.js

咱們的 API 須要在某處存儲數據,可是在本教程中,咱們不但願經過沒必要要的 npm install 使教程複雜化,咱們建立一個很是簡單、原生的內存中鍵值存儲,由於它的數據結構由你本身設計,因此你能夠隨時改變數據的存儲方式。

db.js 中包含如下內容。

'use strict';

let users = {};
let tasks = {};

// we are saving everything inmemory for now
let db = {
    users: proc(users),
    tasks: proc(tasks)
}

function clone(obj) {
    // a simple way to deep clone an object in javascript
    return JSON.parse(JSON.stringify(obj));
}

// a generalised function to handle CRUD operations
function proc(container) {
    return {
        save(obj) {
            // in JS, objects are passed by reference
            // so to avoid interfering with the original data
            // we deep clone the object, to get our own reference
            let _obj = clone(obj);

            if (!_obj.id) {
                // assign a random number as ID if none exists
                _obj.id = (Math.random() * 10000000) | 0;
            }

            container[_obj.id.toString()] = _obj;
            return clone(_obj);
        },
        fetch(id) {
            // deep clone this so that nobody modifies the db by mistake from outside
            return clone(container[id.toString()]);
        },
        fetchAll() {
            let _bunch = [];
            for (let item in container) {
                _bunch.push(clone(container[item]));
            }
            return _bunch;
        },
        unset(id) {
            delete container[id];
        }
    }
}

module.exports = db;
複製代碼

其中比較重要是 proc 函數。經過獲取一個對象,並將其包裝在一個帶有一組函數的閉包中,方便在該對象上添加,編輯和刪除值。若是你對閉包不夠了解,應該提早閱讀關於 JavaScript 閉包的內容。

因此,咱們如今基本上已經完成了程序功能,咱們能夠存儲和檢索數據,而且能夠實現對這些數據進行操做,咱們如今須要作的是經過網絡公開這個功能。 所以,最後一部分是實現 HTTP 服務。

這是咱們大多數人但願使用express的地方,但咱們不但願這樣,因此咱們將使用隨節點一塊兒提供的http模塊,並圍繞它實現一個很是簡單的路由表。

正如預期的那樣,咱們繼續建立 server.js 文件。在這個文件中咱們把全部內容關聯在一塊兒,以下所示。

'use strict';

let http = require('http');
let url = require('url');
let methods = require('./methods');
let types = require('./types');

let server = http.createServer(requestListener);
const PORT = process.env.PORT || 9090;
複製代碼

文件的開頭部分引入咱們所須要的內容,使用 http.createServer 來建立一個 HTTP 服務。requestListener 是一個回調函數,咱們稍後定義它。 而且咱們肯定下來服務器將偵聽的端口。

在這段代碼以後咱們來定義路由表,它規定了咱們的應用程序將響應的不一樣 URL 路徑。

// we'll use a very very very simple routing mechanism
// don't do something like this in production, ok technically you can...
// probably could even be faster than using a routing library :-D

let routes = {
    // this is the rpc endpoint
    // every operation request will come through here
    '/rpc': function (body) {
        return new Promise((resolve, reject) => {
            if (!body) {
                throw new (`rpc request was expecting some data...!`);
            }
            let _json = JSON.parse(body); // might throw error
            let keys = Object.keys(_json);
            let promiseArr = [];

            for (let key of keys) {
                if (methods[key] && typeof (methods[key].exec) === 'function') {
                    let execPromise = methods[key].exec.call(null, _json[key]);
                    if (!(execPromise instanceof Promise)) {
                        throw new Error(`exec on ${key} did not return a promise`);
                    }
                    promiseArr.push(execPromise);
                } else {
                    let execPromise = Promise.resolve({
                        error: 'method not defined'
                    })
                    promiseArr.push(execPromise);
                }
            }

            Promise.all(promiseArr).then(iter => {
                console.log(iter);
                let response = {};
                iter.forEach((val, index) => {
                    response[keys[index]] = val;
                });

                resolve(response);
            }).catch(err => {
                reject(err);
            });
        });
    },

    // this is our docs endpoint
    // through this the clients should know
    // what methods and datatypes are available
    '/describe': function () {
        // load the type descriptions
        return new Promise(resolve => {
            let type = {};
            let method = {};

            // set types
            type = types;

            //set methods
            for(let m in methods) {
                let _m = JSON.parse(JSON.stringify(methods[m]));
                method[m] = _m;
            }

            resolve({
                types: type,
                methods: method
            });
        });
    }
};
複製代碼

這是整個程序中很是重要的一部分,由於它提供了實際的接口。 咱們有一組 endpoint,每一個 endpoint 都對應一個處理函數,在路徑匹配時被調用。根據設計原則每一個處理函數都必須返回一個 Promise。

RPC endpoint 獲取一個包含請求內容的 json 對象,而後將每一個請求解析爲 methods.js 文件中的對應方法,調用該方法的 exec 函數,並將結果返回,或者拋出錯誤。

describe endpoint 掃描方法和類型的描述,並將該信息返回給調用者。讓使用 API 的開發者可以輕鬆地知道如何使用它。

如今讓咱們添加咱們以前討論過的函數 requestListener,而後就能夠啓動服務。

// request Listener
// this is what we'll feed into http.createServer
function requestListener(request, response) {
    let reqUrl = `http://${request.headers.host}${request.url}`;
    let parseUrl = url.parse(reqUrl, true);
    let pathname = parseUrl.pathname;

    // we're doing everything json
    response.setHeader('Content-Type', 'application/json');

    // buffer for incoming data
    let buf = null;

    // listen for incoming data
    request.on('data', data => {
        if (buf === null) {
            buf = data;
        } else {
            buf = buf + data;
        }
    });

    // on end proceed with compute
    request.on('end', () => {
        let body = buf !== null ? buf.toString() : null;

        if (routes[pathname]) {
            let compute = routes[pathname].call(null, body);

            if (!(compute instanceof Promise)) {
                // we're kinda expecting compute to be a promise
                // so if it isn't, just avoid it

                response.statusCode = 500;
                response.end('oops! server error!');
                console.warn(`whatever I got from rpc wasn't a Promise!`);
            } else {
                compute.then(res => {
                    response.end(JSON.stringify(res))
                }).catch(err => {
                    console.error(err);
                    response.statusCode = 500;
                    response.end('oops! server error!');
                });
            }

        } else {
            response.statusCode = 404;
            response.end(`oops! ${pathname} not found here`)
        }
    })
}

// now we can start up the server
server.listen(PORT);
複製代碼

每當有新請求時調用此函數並等待拿到數據,以後查看路徑,並根據路徑匹配到路由表上的對應處理方法。而後使用 server.listen 啓動服務。

如今咱們能夠在目錄下運行 node server.js 來啓動服務,而後使用 postman 或你熟悉的 API 調試工具,向 http://localhost{PORT}/rpc 發送請求,請求體中包含如下 JSON 內容。

{
    "createUser": {
        "name":"alloys mila",
        "age":24
    }
}
複製代碼

server 將會根據你提交的請求建立一個新用戶並返回響應結果。一個基於 RPC、文檔完善的 API 系統已經搭建完成了。

注意,咱們還沒有對本教程接口進行任何參數驗證,你在調用測試的時候必須手動保證數據正確性。

相關文章
相關標籤/搜索