到昨天為止,我們介紹了身為主角的 frontend server 和 web app 的大致結構,接下來幾天就先改開支線任務,進行 API Server 和 database 的開發。如同先前的預告,本次專案在 API Server 會著墨較少,以快速開發完備功能供前端使用為目標,因此開發方式會和 Frontend Server 較為不同。
在這系列文章提到的 API Server 是指連接 database、提供 RESTful HTTP endpoints 的 Node.js server。由這台 Server 提供的 RESTful API 除了會被 Frontend 的 server/client 呼叫外,也能讓 mobile apps 呼叫,因此在架構上 API Server 和 Frontend Server 是獨立運作、佈署的。這樣的職責分割所帶來的 server 結構和以往全部寫成同一台 server 的 Monolithic 結構相比,有著更容易分工開發、易於分別彈性擴展(elastic scaling)等優點,不過也會帶來額外的 communication overhead。
綜合以上所述,API Server 的職責主要有:
在 Node.js ecosystem 有山積一般的 module 能幫忙達成這些目標,不管是對應 MongoDB 的 ODM (註1)套件 Mongoose、自帶優秀 routing 和 middleware 能力的 Express.js 都是常用的 module。這些 module 適合以 bottom-up 的角度逐步打造出自己需要的 API Server,不過這樣的開發速度還不夠快,我們想要更加偷懶快速的解決方案,能在 schema 宣告完成後直接連接 database 並根據 schema 定義的 model 自行生成標準的 RESTful routes。這樣的作法是以 top-down 的角度,一次自動生成出許多 RESTful API,再根據前端需求自行決定是否增減。
以下介紹一些作者曾經使用過的相關套件:
嚴格來說 Keystone.js 不算是快速自動生成 RESTful API 的選擇之一,但它對於快速開發基於 Content Management System (CMS) 的系統而言非常方便,只要做好 model definition 和初始設定,就能自動生成基於 Mongoose 的 ODM 和完整的後台編輯 web app。由於 Keystone 的結構完全基於 Node.js ecosystem 常見的套件(Express.js、Mongoose),開發者可以輕易的將 Keystone app 嵌入(embed)到更大的 Express.js app 內,然後藉由 Keystone API 快速建造自己需要的 API(註2):
import express from 'express';
import keystoneServer from './keystone';
import apiServer from './api';
const app = express();
app.use('/keystone', keystoneServer);
app.use('/api/v1', apiServer);
app.listen(3000);
在 Loopback 官網的標語開宗明義將自己介紹為「Node.js API Framework」;它能夠以 JSON 定義好 server 環境、database 設定、Schema (model) definition 後自動生成整個 server runtime、ORM/ODM、RESTful APIs 甚至 access control list (ACL) 等機制。除了 server side 的 API 建置,Loopback 也提供 Objective-C、Android、Angular.js 等 client side SDK 方便使用者整合 Loopback 的 API,當然我們也能自行撰寫對口串接。
儘管 Loopback 違背了在第二天提到的以 library aggregation 為主的概念,基於本次專案對快速建造 API 的需求來說,Loopback 是相當合適的選擇。接下來會以 Loopback 作為後端開發的主要架構,逐步為專案需求打造 API Server。
開發 Loopback app 最簡單的方式是安裝 Strongloop CLI tool 然後利用 slc
指令生成不同的 Loopback 元件,不過在這次的專案中我們改採取手動設定的作法來靠近檢視 Loopback 專案內的不同元件組成。以下是本次 API Server 的專案架構:
src
|- common
|- config
|- boot
|- index.js
package.json
server.js
單純啟用 babel-register
來轉換 ES6 程式碼。
在 src/common
資料夾內主要擺放 model definition 和 ACL roles 的程式碼。Loopback 會根據 model 的設定檔更新 database 並生成 RESTful API endpoints;ACL roles 則會被當成存取 API 時用來作身分授權(authorization)的 middleware。
顧名思義,在這個資料夾擺放的是設定 Loopback 用的各種 JSON 設定檔,包括 datasource、middleware 等。在命名檔案時,可以加入和環境變數相同的 postfix 進行設定覆寫,例如 config.json
會被所有環境載入,但當 NODE_ENV
為 production
時,config.production.json
檔案中的設定值會覆寫 config.json
中宣告的設定。
除了設定檔以外, src/config/boot
資料夾則是擺放給 loopback-boot
使用的 boostrapping scripts。
Loopback app 本體。由於 Loopback 的 app instance 同樣是基於 Express.js app,這裡可以自行加入需要的 Express middleware。有別於直接呼叫 app.listen()
,先使用 loopback-boot
做 Loopback bootstrapping 讀取 src/config/boot
資料夾內的 bootstrapping scripts:
import path from 'path';
import { promisify, promisifyAll } from 'bluebird';
import bodyParser from 'body-parser';
import loopback from 'loopback';
import boot from 'loopback-boot';
const bootAsync = promisify(boot);
const app = loopback();
promisifyAll(app, { filter: name => name === 'listen' });
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
bootAsync(app, path.join(__dirname, 'config'))
.then(app.listenAsync)
.then(() => {
app.emit('started');
const baseUrl = app.get('url').replace(/\/$/, '');
console.info('API server listening at', baseUrl);
const componentExplorer = app.get('loopback-component-explorer');
if (componentExplorer) {
console.info('REST API Explorer at', componentExplorer.mountPath);
}
});
以上先大致帶完 API Server 的專案架構,細部的 config、modeling、ACL、boot 等部分將於接下來幾天逐一介紹。
註1:相對於對接 SQL database 用的 Object-Relation Mapper (ORM),在 document based 的 noSQL 部分則是稱為 Object-Document Mapper (ODM)
註2:每個 Express app(由 express()
建立的物件)均為 middleware 的子類別實體,因此可以直接被 app.use
作為 middleware 使用。