基於node.js、mariaDB實現RESTful API === ###### tags: `RESTful` node.js是近幾年開始流行的後端語言,使用node.js可以取代傳統的php、apache組合,並且可以直接以javaScript處理後端邏輯與伺服器建置等工作,統一前後端程式撰寫語法。node.js與php不同之處為,php為阻塞式I/O,前一條命令執行完成才執行下一條命令,node.js為非阻塞式I/O,所有命令同時執行,並且使用回呼函數來確定命令已經執行完畢。 :::success :bookmark: **目錄** :::spoiler [TOC] ::: ## 使用套件介紹 node.js背後擁有巨大的套件庫,我們可以利用node.js同捆的[npm套件管理器](https://hackmd.io/3G8cb72ZTs6ces_BLAKz6Q),下載數以萬計的javaScript套件,透過他人已經開發完成的套件,來縮短專案的撰寫時間,開發出更加成熟的專案。 ### express ``` npm install express ``` 使用express可以快速搭建功能完整的網站伺服器,使用express可以幫助我們替請求方法以及URLs定義路由。基於express我們可以快速搭建起功能完整的RESTful API。 [官方網站](https://expressjs.com/zh-tw/) ### body-parser ``` npm install body-parser ``` body-parser用於處理從客戶端傳輸而來的request,可以直接獲得POST、PUT中的body內容,解析json、urlencoded、text等多種檔案格式。 ### mariadb ``` npm install mariadb ``` mariadb官方所提供的連線庫,可以處理所有與mariadb資料庫溝通的需求。 ### sequelize ``` npm install sequelize@next ``` sequelize為[ORM框架](https://zh.wikipedia.org/wiki/%E5%AF%B9%E8%B1%A1%E5%85%B3%E7%B3%BB%E6%98%A0%E5%B0%84)(Object Relationship Model),我們可以使用物件導向的方式操作資料庫中的表,所有資料庫的表都會映射到sequelize的物件之中,可以便利地操作查詢。 在這個筆記中,我們使用的是BETA 5的版本(sequelize@next),因為sequelize從第五版開始支援mariadb。當配上上述的mariadb官方連線庫,即可建立ORM風格的程式。 sequelize操作的方法與屬性請參閱[官方手冊](https://)或是[這篇文章](https://segmentfault.com/a/1190000003987871#articleHeader24)。 ## 資料庫與API設計 在開始建立起第一個RESTful API前,我們得先規劃資料庫以及API架構。 假設今天我們有一個留言板的需求,必須滿足留言板的新增、修改、移除與查閱等功能。且為了凸顯RESTful設計風格,在接下來的設計中我們將暫時忽略帳戶權限的功能,以完成API為優先要件。若能理解整體運作,再整合帳戶權限等功能進入程式,並不是太大問題。 ### 資料表設計 ```sql CREATE TABLE `blog`.`borad` ( `id` INT(11) NOT NULL AUTO_INCREMENT , `title` VARCHAR(255) NOT NULL , `author` VARCHAR(255) NOT NULL , `content` VARCHAR(255) NOT NULL , `addTime` DATE NOT NULL , `modifyTime` DATE NOT NULL , PRIMARY KEY (`id`) )ENGINE=InnoDB; ``` ### HTML動詞設計 |名稱(API Name)| 路由(Route) |HTML動詞(Verb)| 描述(Description) | | -------- | -------- | -------- | -------- | |Create MSG| /api/borad | POST | 新增一則留言 | |MSG List| /api/borad | GET | 獲取留言列表 | |Get Single MSG| /api/borad/:id | GET | 獲取一則內容 | |Edit MSG| /api/borad/:id | PUT | 編輯內容,更新時間 | |Delete MSG| /api/borad/:id | DELETE | 刪除一筆留言 | ### 回傳結構 ``` HTML API : Create MSG URL : http://localhost/api/borad METHOD : POST PARAMETERS : title : String author : String content : String RETURN : STATUS : 200 OK JSON : { "id" : xx "title" : "xxxxx", "author" : "xxxxx", "content" : "xxxxx", "addTime" : "yyyy-mm-ddThh:mm:ssZ", "modifyTime" : "yyyy-mm-ddThh:mm:ssZ" } API : MSG List URL : http://localhost/api/borad METHOD : GET RETURN : STATUS : 200 OK JSON : [ { "id" : xx "title" : "xxxxx", "author" : "xxxxx", "addTime" : "yyyy-mm-ddThh:mm:ssZ", "modifyTime" : "yyyy-mm-ddThh:mm:ssZ" }, { "id" : xx "title" : "xxxxx", "author" : "xxxxx", "addTime" : "yyyy-mm-ddThh:mm:ssZ", "modifyTime" : "yyyy-mm-ddThh:mm:ssZ" }, ......(省略) ] API : Get Single MSG URL : http://localhost/api/borad/:id METHOD : GET RETURN : STATUS : 200 OK JSON : { "id" : xx "title" : "xxxxx", "author" : "xxxxx", "content" : "xxxxx", "addTime" : "yyyy-mm-ddThh:mm:ssZ", "modifyTime" : "yyyy-mm-ddThh:mm:ssZ" } API : Edit MSG URL : http://localhost/api/borad/:id METHOD : PATCH PARAMETERS : name : String content : String RETURN : STATUS : 200 OK JSON : { "id" : xx "title" : "xxxxx", "author" : "xxxxx", "content" : "xxxxx", "addTime" : "yyyy-mm-ddThh:mm:ssZ", "modifyTime" : "yyyy-mm-ddThh:mm:ssZ" } API : Delete MSG URL : https://localhost/api/borad/:newsId METHOD : Delete RETURN : STATUS : 200 OK ``` ### Model-Controller-Route 借艦於目前主流的開發架構MVC,我們在規劃API程式時,也可以將程式碼做一定的切割,但因為API重於伺服器回傳以及溝通,並沒有view的概念。於是我們將Route(路由)獨立出來,增加程式簡潔以及可維護性。 ![](https://i.imgur.com/94xS59H.png) 1. 所有請求進入伺服器時都會經過Route所設定的規則。 2. Route會分析請求方法為何,並將程式分配到對應的Controller。 * express : 負責解析不同的請求方法以及路由設定。 3. Controller將頁面解析的結果經過判斷以及處理過傳送給Model。 * body-parser : 負責解析頁面傳來的資訊(POST or PUT)。 5. Model接收到Controller的動作後執行查詢。 * mariadb : 與資料庫進行連接。 * sequelize : 允許我們以物件導向的方式存取資料庫內容。 6. DB回傳對應資訊給予Model 7. Model將資訊回傳給Controller,包含查詢成功或失敗。 8. 將正確結果回傳給請求方,或者是錯誤失敗的狀態及訊息。 ## 程式實作 在完成了程式規劃後,將會開始程式的實現,在接下來的說明中,將不會對基本的node.js語法進行解釋。需要具備基本的javaScript與node.js等知識,便能完成整個程式設計。 ### 資料夾與檔案結構 ``` REST-API ├─config │ └─config.json ├─controllers │ ├─borad.js │ └─index.js ├─models │ ├─borad.js │ └─index.js ├─routes │ ├─borad.js │ └─index.js └─index.js ``` ### config/config.json ```json { "development": { "username": "root", "password": "", "database": "blog", "host": "127.0.0.1", "port": 3306, "dialect": "mariadb" } } ``` 定義資料庫連線方法與帳號密碼,日後可以分成開發中的資料庫與實際上線的資料庫。 ### models/borad.js ```javascript=1 module.exports = (sequelize, DataTypes) => { const borad = sequelize.define('borad', { id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, title: { type: DataTypes.STRING, allowNull: false, }, author: { type: DataTypes.STRING, allowNull: false, }, content: { type: DataTypes.INTEGER, allowNull: false, }, addTime:{ type:DataTypes.DATE }, modifyTime:{ type:DataTypes.DATE } },{ timestamps:false, freezeTableName:true }); return borad; }; ``` 將實際資料表的結構映射進sequelize套件中,注意欄位的規則必須與資料庫相同,否則將會在日後執行查詢時發生錯誤。 在這個文件中,將會return整個設定好的物件。 sequelize操作的方法與屬性請參閱[官方手冊](https://)或是[這篇文章](https://segmentfault.com/a/1190000003987871#articleHeader24)。 ### models/index.js ```javascript=1 //讀取檔案套件 const fs = require('fs'); //處理文件路徑套件 const path = require('path'); //資料庫連接套件 const Sequelize = require('sequelize'); //設定預設文件路徑,module.filename將會回傳該原始碼所在的文件夾 const basename = path.basename(module.filename); //目前系統環境為開發中,若有設定環境變數,將可以讓程式自適應環境,切換所要連線的伺服器。 const env = /*process.env.NODE_ENV || */'development'; //設定檔載入 const config = require(`${__dirname}/../config/config.json`)[env]; const db = {}; //初始化資料庫連線 let sequelize = new Sequelize( config.database, config.username, config.password, config ); //將資料夾內的.js檔案依序實例,並註冊在同一物件之中。 fs.readdirSync(__dirname) .filter((file) => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')) .forEach((file) => { const model = sequelize.import(path.join(__dirname, file)); db[model.name] = model; }); Object.keys(db).forEach((modelName) => { if (db[modelName].associate) { db[modelName].associate(db); } }); db.sequelize = sequelize; db.Sequelize = Sequelize; module.exports = db; ``` ### controllers/borad.js ```javascript=1 //載入相對應的model const Borad = require('../models/index').borad; module.exports = { //創建 create(req, res) { return Borad .create({ title: req.body.title, author: req.body.author, content: req.body.content, addTime: new Date(new Date(new Date).valueOf() + 8 * 60 * 60 * 1000), modifyTime: new Date(new Date(new Date).valueOf() + 8 * 60 * 60 * 1000) }) .then((borad) => res.status(200).send(borad)) .catch((error) => res.status(400).send(error)); }, //列表項 list(req, res) { return Borad .findAll({ attributes : ['id','title','author','addTime','modifyTime'], order: [ ['id', 'DESC'] ], }) .then((borad) => res.status(200).send(borad)) .catch((error) => res.status(400).send(error)); }, //取得某項 retrieve(req, res) { return Borad .findByPk(req.params.id, { }) .then((borad) => { if (!borad) { return res.status(404).send({ message: '找不到這筆留言', }); } return res.status(200).send(borad); }) .catch((error) => res.status(400).send(error)); }, //更新 update(req, res) { return Borad .findByPk(req.params.id, { }) .then(borad => { if (!borad) { return res.status(404).send({ message: '找不到這筆留言', }); } return borad .update({ title: req.body.title || borad.title, author: req.body.author || borad.author, content: req.body.content || borad.content, modifyTime: new Date(new Date(new Date).valueOf() + 8 * 60 * 60 * 1000) }) .then(() => res.status(200).send(borad)) .catch((error) => res.status(400).send(error)); }) .catch((error) => res.status(400).send(error)); }, //刪除 destroy(req, res) { return Borad .findByPk(req.params.id) .then(borad => { if (!borad) { return res.status(400).send({ message: '找不到這筆留言', }); } return borad .destroy() .then(() => res.status(204).send()) .catch((error) => res.status(400).send(error)); }) .catch((error) => res.status(400).send(error)); } }; ``` 在Controller中,定義每一個動作,對應我們所規劃的行為。這些對應行為的設定方法可以參考上面所提到的sequelize手冊。 ### controllers/index.js ```javascript=1 //非同步讀取檔案套件 const fs = require('fs'); //處理文件路徑套件 const path = require('path'); const allControllers = {}; //設定預設文件路徑,module.filename將會回傳該文件夾中的預設文件index.js const basename = path.basename(module.filename); //依序讀取目錄中檔案,並且一一引入至同一物件中 fs.readdirSync(__dirname) .filter((file) => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')) .forEach((file) => { const fileName = file.replace('.js',''); const controller = require('./'+fileName); allControllers[fileName] = controller; }); //判斷非同步是否完成,完成後放入物件中。 Object.keys(allControllers).forEach((controllerName) => { if (allControllers[controllerName].associate) { allControllers[controllerName].associate(allControllers); } }); const borad = require('./borad'); module.exports = allControllers; ``` ### routes/borad.js ```javascript=1 var express = require('express'); var router = express.Router(); const boradController = require('../controllers/index').borad;   /* GET home page. */ router.get('/', function(req, res, next) { boradController.list(req,res) });   router.get('/:id', function(req, res, next) { boradController.retrieve(req,res) });   router.post('/', function(req, res, next) { console.log(req.body); boradController.create(req,res) });   router.delete('/:id', function(req, res, next) { boradController.destroy(req,res) });     router.put('/:id', function(req, res, next) { boradController.update(req,res) });   module.exports = router; ``` 根據所規劃的URL以及動詞,將請求導向到正確的controller。 ### routes/index.js ```javascript=1 //非同步讀取檔案套件 const fs = require('fs'); //處理文件路徑套件 const path = require('path'); const allRoutes = {}; //設定預設文件路徑,module.filename將會回傳該文件夾中的預設文件index.js const basename = path.basename(module.filename); //依序讀取目錄中檔案,並且一一引入至同一物件中 fs.readdirSync(__dirname) .filter((file) => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')) .forEach((file) => { const fileName = file.replace('.js',''); const route = require('./'+fileName); allRoutes[fileName] = route; }); //判斷非同步是否完成,完成後放入物件中。 Object.keys(allRoutes).forEach((routeName) => { if (allRoutes[routeName].associate) { allRoutes[routeName].associate(allRoutes); } }); module.exports = allRoutes; ``` ### index.js ```javascript=1 var express = require('express'); var app = express(); var bodyParser = require('body-parser'); const routers = require('./routes'); // 替app設定中介層為bodyParser // 通過以下設定,在路由處理request,可以直接獲得body的部分 app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); //獲取express Router物件 var topRouter = express.Router().get('/', function(req, res) { res.json({ message: 'Hi,this is restFul API' }); }); //建立起第一層的router app.use('/api', topRouter); //註冊已經設計好的API for(var key in routers) { app.use('/api/'+key, routers[key]); } app.listen(3000); console.log('伺服器運作中'); ``` 由根目錄的index.js將伺服器建立起來,負責所有路由的載入與設定,並監聽3000 port。 ``` cd [yourDisk]:\rest-api node index.js ``` 在CMD中執行程式建立起整個伺服器。