1. 程式人生 > 程式設計 >Gracejs —— 全新的基於koa2的前後端分離框架

Gracejs —— 全新的基於koa2的前後端分離框架

Gracejs(又稱:koa-grace v2) 是全新的基於koa v2.x的MVC+RESTful架構的前後端分離框架。

一、簡介

Gracejs是koa-grace的升級版,也可以叫koa-grace v2。

主要特性包括:

  1. 支援MVC架構,可以更便捷地生成服務端路由;
  2. 標準的RESTful架構,支援後端介面非同步併發,頁面效能更優;
  3. 一套Node環境經服務服務多個站點應用,部署更簡單;
  4. 優雅的MOCK功能,開發環境模擬資料更流暢;
  5. 完美支援async/await及generator語法,隨心所欲;
  6. 更靈活的前端構建選型,預設支援Vue及Require.js。

相比於koa-grace v1(以下簡稱:koa-grace):Gracejs完美支援koa v2

,同時做了優化虛擬host匹配和路由匹配的效能、還完善了部分測試用例等諸多升級。當然,如果你正在使用koa-grace也不用擔心,我們會把Gracejs中除了支援koa2的效能和功能特性移植到koa-grace的相應中介軟體中。

這裡不再介紹“前後端分離”、“RESTful”、“MVC”等概念,有興趣可參考趣店前端團隊基於koajs的前後端分離實踐一文。

二、快速開始

注意:請確保你的執行環境中Nodejs的版本至少是v4.0.0,目前需要依賴Babel。(當然26日凌晨nodejs v7已經release,你也可以不依賴Babel,直接通過--harmony_async_await模式啟動。)

安裝

執行命令:

$ git clone -b v2.x https://github.com/xiongwilee/koa-grace.git
$ cd koa-grace && npm install
複製程式碼

執行

然後,執行命令:

$ npm run dev
複製程式碼

然後訪問:http://127.0.0.1:3000 就可以看到示例了!

三、案例說明

這裡參考 github.com/xiongwilee/…app/demo目錄下的示例,詳解Gracejs的MVC+RESTful架構的實現。

此前也有文章簡單介紹過koa-grace的實現( github.com/xiongwilee/… ),但考慮到Gracejs的差異性,這裡再從目錄結構

MVC模型實現proxy機制這三個關鍵點做一些比較詳細的說明。

目錄結構

Gracejs與koa-grace v1.x版本的目錄結構完全一致:

.
├── controller
│   ├── data.js
│   ├── defaultCtrl.js
│   └── home.js
├── static
│   ├── css
│   ├── image
│   └── js
└── views
    └── home.html
複製程式碼

其中:

  • controller用以存放路由及控制器檔案
  • static用以存放靜態檔案
  • views用以存放模板檔案

需要強調的是,這個目錄結構是生產環境程式碼的標準目錄結構。在開發環境裡你可以任意調整你的目錄結構,只要保證編譯之後的產出檔案以這個路徑輸出即可

如果你對這一點仍有疑問,可以參考grace-vue-webpack-boilerplate

MVC模型實現

為了滿足更多的使用場景,在Gracejs中加入了簡單的Mongo資料庫的功能。

但準確的說,前後端的分離的Nodejs框架都是VC架構,並沒有Model層。因為前後端分離框架不應該有任何資料庫、SESSION儲存的職能

如上圖,具體流程如下:

  • 第一步,Nodejs server(也就是Gracejs服務)監聽到使用者請求;
  • 第二步,Gracejs的各個中介軟體(Middlewares)對請求上下文進行處理;
  • 第三步,根據當前請求的path和method,進入對應的Controller;
  • 第四步,通過http請求以proxy的模式向後端獲取資料;
  • 第五步,拼接資料,渲染模板。

這裡的第四步,proxy機制,就是Gracejs實現前後端分離的核心部分。

proxy機制

以實現一個電商應用下的“個人中心”頁面為例。假設這個頁面的首屏包括:使用者基本資訊模組、商品及訂單模組、訊息通知模組。

後端完成服務化架構之後,這三個模組可以解耦,拆分成三個HTTP API介面。這時候就可以通過Gracejs的this.proxy方法,去後端非同步併發獲取三個介面的資料。

如下圖:

這樣有幾個好處:

  1. 在Nodejs層(服務端)非同步併發向後端(服務端)獲取資料,可以使HTTP走內網,效能更優;
  2. 後端的介面可以同時提供給客戶端,實現介面給Web+APP複用,後端開發成本更低;
  3. 在Nodejs層獲取資料後,直接交給頁面,不管前端用什麼技術棧,可以使首屏體驗更佳。

那麼,這麼做是不是就完美了呢?肯定不是:

  1. 後端介面在外網開放之後,如何保證介面安全性?
  2. 如果當前頁面請求是GET方法,但我想POST到後端怎麼辦?
  3. 我想在Controller層重置post引數怎麼辦?
  4. 後端介面設定cookie如何帶給瀏覽器?
  5. 經過一層Nodejs的代理之後,如何保證SESSION狀態不丟失?
  6. 如果當前請求是一個file檔案流,又該怎麼辦呢?
    ...

好訊息是,這些問題在proxy中介軟體中都考慮過了。這裡不再一一講解,有興趣可以看koa-grace-proxy的原始碼:github.com/xiongwilee/…

四、詳細使用手冊

在看詳細使用手冊之前,建議先看一下Gracejs的主檔案原始碼:github.com/xiongwilee/…

這裡不再浪費篇幅貼程式碼了,其實想說明的就是:Gracejs是一個個關鍵中介軟體的集合

所有中介軟體都在middleware目錄下,配置由config/main.*.js管理。

關於配置檔案:

  1. 配置檔案extend關係為:config/server.json的merge欄位 > config/main.*.js > config.js;
  2. 配置生成後儲存在Gracejs下的全域性作用域global.config裡,方便讀取。

下面介紹幾個關鍵中介軟體的作用和使用方法。

vhost——多站點配置

vhost在這裡可以理解為,一個Gracejs server服務於幾個站點。Gracejs支援通過hosthost+一級path兩種方式的對映。所謂的隱射,其實就是一個域名(或者一個域名+一級path)對應一個應用,一個應用對應一個目錄。

注意:考慮到正則的效能問題,vhost不會考慮正則對映

參考config/main.development.js,可以這麼配置vhost:

// vhost配置
vhost: {
  '127.0.0.1':'demo','127.0.0.1/test':'demo_test','localhost':'blog',}
複製程式碼

其中,demo,demo_test,blog分別對應app/下的三個目錄。當然你也可以指定目錄路徑,在配置檔案中修改path.project配置即可:

// 路徑相關的配置
path: {
  // project
  project: './app/'
}
複製程式碼

router——路由及控制器

Gracejs中生成路由的方法非常簡單,以自帶的demo模組為例,進入demo模組的controller目錄:app/demo/controller

檔案目錄如下:

controller
├── data.js
├── defaultCtrl.js
└── home.js
複製程式碼

1、 檔案路徑即路由

router中介軟體會找到模組中所有以.js結尾的檔案,根據檔案路徑和module.exports生成路由。

例如,demo模組中的home.js檔案:

exports.index = async function () {
  await this.bindDefault();
  await this.render('home',{
    title: 'Hello,Grace!'
  });
}
exports.hello = function(){
  this.body = 'hello world!'
}
複製程式碼

則生成/home/index/home/home/hello的路由。需要說明幾點:

  1. 如果路由是以/index結尾的話,Gracejs會"贈送"一個去掉/index的同樣路由;
  2. 如果當前檔案是一個依賴,僅僅被其他檔案引用;則在檔案中配置exports.__controller__ = false,該檔案就不會生成路由了;參考defaultCtrl.js
  3. 這裡的控制器函式可以是await/asyncgenerator函式,也可以是一個普通的函式;Gracejs中推薦使用await/async
  4. 這裡的路由檔案包裹在一個目錄裡也是可以的,可以參考:app/blog中的controller檔案;
  5. 如果當前檔案路由就是一個獨立的控制器,則module.exports返回一個任意函式即可。

最後,如果使用者訪問的路由查詢不到,router會預設查詢/error/404路由,如果有則渲染error/404頁(不會重定向到error/404),如果沒有則返回404。

2、 路由檔案使用說明

將demo模組中的home.js擴充套件一下:

exports.index = async function () {
    ...
}
exports.index.__method__ = 'get';
exports.index.__regular__ = null;
複製程式碼

另外,需要說明以下幾點:

  • 如果需要配置dashboard/post/list請求為DELETE方法,則post.js中宣告 exports.list.__method__ = 'delete'即可(不宣告預設注入get及post方法);
  • 如果要配置更靈活的路由,則中宣告exports.list.__regular__ = '/:id';即可,更多相關配置請參看:koa-router#named-routes

當然,如果路由檔案中的所有控制器方法都是post方法,您可以在控制器檔案最底部加入:module.exports.__method__ = 'post'即可,__regular__的配置同理。

注意:一般情況這裡不需要額外的配置,為了保證程式碼美觀,沒有特殊使用場景的話就不要寫__method____regular__配置。

3、 控制器

將demo模組中的home.js的index方法再擴充套件一下:

exports.index = async function () {
  // 繫結預設控制器方法
  await this.bindDefault();
  // 獲取資料
  await this.proxy(...)
  // 渲染目標引擎
  await this.render('home',Grace!'
  });
}
複製程式碼

它就是一個標準的控制器(controller)了。這個控制器的作用域就是當前koa的context,你可以任意使用koa的context的任意方法。

幾個關鍵context屬性的使用說明如下:

koa自帶:

更多koa自帶context屬性,請檢視koajs官網:koajs.com/

context屬性 型別 說明
this.request.href String 當前頁面完整URL,也可以簡寫為this.href
this.request.query object get引數,也可以簡寫為this.query
this.response.set function 設定response頭資訊,也可以簡寫為this.set
this.cookies.set function 設定cookie,參考:cookies
this.cookies.get function 獲取cookie,參考:cookies

Gracejs注入:

context屬性 型別 中介軟體 說明
this.bindDefault function router 公共控制器,相當於require('app/*/controller/defaultCtrl.js')
this.request.body object body post引數,可以直接在this.request.body中獲取到post引數
this.render function views 模板引擎渲染方法,請參看: 模板引擎- Template engine
this.mongo function mongo 資料庫操作方法,請參看: 資料庫 - Database
this.mongoMap function mongo 並行資料庫多操作方法,請參看: 資料庫 - Database
this.proxy function proxy RESTful資料請求方法,請參看:資料代理
this.fetch function proxy 從伺服器匯出檔案方法,請參看: 請求代理
this.backData Object proxy 預設以Obejct格式儲存this.proxy後端返回的JSON資料
this.upload function xload 檔案上傳方法,請參看: 檔案上傳下載
this.download function xload 檔案下載方法,請參看: 檔案上傳下載

4、控制器中非同步函式的寫法

在控制器中,如果還有其他的非同步方法,可以通過Promise來實現。例如:

exports.main = async function() {
  await ((test) => {
    return new Promise((resolve,reject) => {
      setTimeout(() => { resolve(test) },3000)
    });
  })('測試')
}
複製程式碼

proxy——資料代理

Gracejs支援兩種資料代理場景:

  1. 單純的資料代理,任意請求到後端介面,然後返回json資料(也包括檔案流請求到後端,後端返回json資料);
  2. 檔案代理,請求後端介面,返回一個檔案(例如驗證碼圖片);

下面逐一介紹兩種代理模式的使用方法。

1、 資料代理

資料代理可以在控制器中使用this.proxy方法:

this.proxy(object|string,[opt])
複製程式碼
場景一:多個資料請求的代理

使用this.proxy方法實現多個資料非同步併發請求非常簡單:

exports.demo = async function (){
  await this.proxy({
    userInfo:'github:post:user/login/oauth/access_token?client_id=****',otherInfo:'github:other/info?test=test',});

  console.log(this.backData);
  /**
   *  {
   *    userInfo : {...},*    otherInfo : {...}
   *  }
   */
}
複製程式碼

然後,proxy的結果會預設注入到上下文的this.backData物件中。

場景二:單個資料請求的代理

如果只是為了實現一個介面請求代理,可以這麼寫:

exports.demo = async function (){
  await this.proxy('github:post:user/login/oauth/access_token?client_id=****');
}
複製程式碼
說明

github:post:user/login/oauth/access_token?client_id=****說明如下:

  • github: 為在config/main.*.jsapi 物件中進行配置;
  • post : 為資料代理請求的請求方法,該引數可以不傳,預設為get
  • path: 後面請求路徑中的query引數會覆蓋當前頁面的請求引數(this.query),將query一同傳到請求的介面
  • 你也可以寫完整的路徑:{userInfo:'https://api.github.com/user/login?test=test'}

另外,this.proxy的形參說明如下:

引數名 型別 預設 說明
dest Object this.backData 指定接收資料的物件,預設為this.backData
conf Obejct {} this.proxy使用Request.js實現,此為傳給request的重置配置(你可以在這裡設定介面超時時間:conf: { timeout: 25000 }
form Object {} 指定post方法的post資料,預設為當前頁面的post資料

關於this.proxy方法還有很多有趣的細節,推薦有興趣的同學看原始碼:github.com/xiongwilee/…

2、 檔案代理

檔案代理可以在控制器中使用this.fetch方法:

this.fetch(string)
複製程式碼

檔案請求代理也很簡單,比如如果需要從github代理一個圖片請求返回到瀏覽器中,參考:feclub.cn/user/avatar… , 或者要使用匯出檔案的功能:

exports.avatar = async function (){
  await this.fetch(imgUrl);
}
複製程式碼

這裡需要注意的是:在this.fetch方法之後會直接結束response, 不會再往其他中介軟體執行

views——檢視層

預設的模板引擎為swig,但swig作者已經停止維護;你可以在config/main.*.js中配置template屬性想要的模板引擎:

// 模板引擎配置
template: 'nunjucks'
複製程式碼

你還可以根據不同的模組配置不同的模板引擎:

template: {
  blog:'ejs'
}
複製程式碼

目前支援的模板引擎列表在這裡:consolidate.js#supported-template-engines

在控制器中呼叫this.render方法渲染模板引擎:

exports.home = await function () {
  await this.render('dashboard/site_home',{
    breads : ['站點管理','通用'],userInfo: this.userInfo,siteInfo: this.siteInfo
  })
}
複製程式碼

模板檔案在模組路徑的/views目錄中。

注意一點:Gracejs渲染模板時,預設會將main.*.js中constant配置交給模板資料;這樣,如果你想在頁面中獲取公共配置(比如:CDN的地址)的話就可以在模板資料中的constant子中取到。

static——靜態檔案服務

靜態檔案的使用非常簡單,將/static/**/或者/*/static/*的靜態檔案請求代理到了模組路徑下的/static目錄:

// 配置靜態檔案路由
app.use(Middles.static(['/static/**/*','/*/static/**/*'],{
  dir: config_path_project,maxage: config_site.env == 'production' && 60 * 60 * 1000
}));
複製程式碼

以案例中blog的靜態檔案為例,靜態檔案在blog專案下的路徑為:app/blog/static/image/bg.jpg,則訪問路徑為http://127.0.0.1/blog/static/image/bg.jpg 或者 http://127.0.0.1/static/blog/image/bg.jpg

注意兩點:

  1. 靜態檔案埠和當前路由的埠一致,所以/static/**/或者/*/static/*形式的路由會是無效的;
  2. 推薦在生產環境中,使用Nginx做靜態檔案服務,購買CDN託管靜態檔案;

mock——Mock資料

MOCK功能的實現其實非常簡單,在開發環境中你可以很輕易地使用MOCK資料。

以demo模組為例,首先在main.development.js配置檔案中新增proxy配置:

// controller中請求各類資料字首和域名的鍵值對
api: {
 // ...
 demo: 'http://${ip}:${port}/__MOCK__/demo/'
 // ...
}
複製程式碼

然後,在demo模組中新增mock資料夾,然後新增test.json:

檔案結構:

.
├── controller
├── mock
|     └── test.json
├── static
└── views
複製程式碼

檔案內容(就是你想要的請求返回內容):

在JSON檔案內容中也可以使用註釋:

/*
 * 獲取使用者資訊介面
 */
{
    code:0 // 這是code
}
複製程式碼

然後,你可以開啟瀏覽器訪問:http://${ip}:${port}/__MOCK__/demo/test 驗證是否已經返回了test.json裡的資料。

最後在你的controller業務程式碼中就可以通過proxy方法獲取mock資料了:

this.proxy({
    test:'demo:test'
})
複製程式碼

注意:

  • 如果你的mock檔案路徑是/mock/test/subtest.json 那麼proxy路徑則是:test/subtest;
  • 強烈建議將mock檔案統一為真正的後端請求路徑,這樣以實現真實路徑的mock;

可以參考這個:koa-grace中的mock功能的示例

secure——安全模組

考慮到使用者路由完全由Nodejs託管以後,CSRF的問題也得在Nodejs層去防護了。此前寫過一片文章:前後端分離架構下CSRF防禦機制,這裡就只寫使用方法,不再詳述原理。

在Gracejs中可以配置:

// csrf配置
csrf: {
  // 需要進行xsrf防護的模組名稱
  module: []
}
複製程式碼

然後,在業務程式碼中,獲取名為:grace_token的cookie,以post或者get引數回傳即可。當然,如果你不想汙染ajax中的引數物件,你也可以將這個cookie值存到x-grace-token頭資訊中。

Gracejs監聽到post請求,如果token驗證失效,則直接返回錯誤。

mongo——簡單的資料庫

請注意:不推薦在生產環境中使用資料庫功能

在Gracejs中使用mongoDB非常簡單,當然沒有做過任何壓測,可能存在效能問題。

1、 連線資料庫

在配置檔案config/main.*.js中進行配置:

  // mongo配置
  mongo: {
    options:{
      // mongoose 配置
    },api:{
      'blog': 'mongodb://localhost:27017/blog'
    }
  },複製程式碼

其中,mongo.options配置mongo連線池等資訊,mongo.api配置站點對應的資料庫連線路徑。

值得注意的是,配置好資料庫之後,一旦koa-grace server啟動mongoose就啟動連線,直到koa-grace server關閉

2、 mongoose的schema配置

依舊以案例blog為例,參看app/blog/model/mongo目錄:

└── mongo
    ├── Category.js
    ├── Link.js
    ├── Post.js
    └── User.js
複製程式碼

一個js檔案即一個資料庫表即相關配置,以app/blog/model/mongo/Category.js

'use strict';

// model名稱,即表名
let model = 'Category';

// 表結構
let schema = [{
  id: {type: String,unique: true,required: true},name: {type: String,numb: {type: Number,'default':0}
},{
  autoIndex: true,versionKey: false
}];

// 靜態方法:http://mongoosejs.com/docs/guide.html#statics
let statics = {}

// 方法擴充套件 http://mongoosejs.com/docs/guide.html#methods
let methods = {
  /**
   * 獲取部落格分類列表
   */
  list: function* () {
    return this.model('Category').find();
  }
}

module.exports.model = model;
module.exports.schema = schema;
module.exports.statics = statics;
module.exports.methods = methods;
複製程式碼

主要有四個引數:

  • model , 即表名,最好與當前檔案同名
  • schema , 即mongoose schema
  • methods , 即schema擴充套件方法,推薦把資料庫元操作都定義在這個物件中
  • statics , 即靜態操作方法

3、 在控制器中呼叫資料庫

在控制器中使用非常簡單,主要通過this.mongo,this.mongoMap兩個方法。

1) this.mongo(name)

呼叫mongoose Entity物件進行資料庫CURD操作

引數說明:

@param [string] name : 在app/blog/model/mongo中配置Schema名,

返回:

@return [object] 一個例項化Schema之後的Mongoose Entity物件,可以通過呼叫該物件的methods進行資料庫操作

案例

參考上文中的Category.js的配置,以app/blog/controller/dashboard/post.js為例,如果要在部落格列表頁中獲取部落格分類資料:

// http://127.0.0.1/dashboard/post/list
exports.list = async function (){
  let cates = await this.mongo('Category').list();
  this.body = cates;
}
複製程式碼
2)this.mongoMap(option)

並行多個資料庫操作

引數說明

@param [array] option

@param [Object] option[].model mongoose Entity物件,通過this.mongo(model)獲取

@param [function] option[].fun mongoose Entity物件方法

@param [array] option[].arg mongoose Entity物件方法引數

返回

@return [array] 資料庫操作結果,以對應陣列的形式返回

案例

  let PostModel = this.mongo('Post');
  let mongoResult = await this.mongoMap([{
      model: PostModel,fun: PostModel.page,arg: [pageNum]
    },{
      model: PostModel,fun:PostModel.count,arg: [pageNum]
    }]);

  let posts = mongoResult[0];// 獲取第一個查詢PostModel.page的結果
  let page = mongoResult[1]; // 獲取第二個查詢PostModel.count的結果,兩者併發執行
複製程式碼

xload——檔案上傳下載

請注意:不推薦在生產環境中使用檔案上傳下載功能

與資料庫功能一樣,檔案上傳下載功能的使用非常簡單,但不推薦在生產環境中使用。因為目前僅支援在單臺伺服器上使用資料庫功能,如果多臺機器的服務就有問題了。

如果需要線上上使用上傳下載功能,你可以使用proxy的方式pipe到後端介面,或者通過上傳元件直接將檔案上傳到後端的介面。

1、檔案上傳

方法:

this.upload([opt])
複製程式碼

示例:

exports.aj_upload = async function() {
  await this.bindDefault();

  let files = await this.upload();
  let res = {};

  if (!files || files.length < 1) {
    res.code = 1;
    res.message = '上傳檔案失敗!';
    return this.body = res; 
  }

  res.code = 0;
  res.message = '';
  res.data = {
    files: files
  }

  return this.body = res;
}
複製程式碼

2、檔案下載

方法:

this.download(filename,[opt])
複製程式碼

示例:

exports.download = async function() {
  await this.download(this.query.file);
}
複製程式碼

其他

Gracejs中幾個核心的中介軟體都介紹完畢。此外,還有幾個中介軟體不做詳細介紹,瞭解即可:

  1. gzip實現:使用gzip壓縮response中的body;
  2. http body內容解析:解析request中的body,存到this.request.body欄位中;
  3. 簡單的session實現:通過記憶體或者redis儲存session,不推薦在生產環境中使用;生產環境的session服務由後端自行完成。

最後,關於Gracejs的運維部署在這裡不再詳述,推薦使用pm2不用擔心重啟server期間服務不可用

五、前端構建

到這裡,整個前後端服務的搭建都介紹完了。

在介紹如何結合Gracejs進行前端構建之前,先提一下:這種“更徹底”的前後端分離方案相比於基於MVVM框架的單頁面應用具體有什麼不同呢?

個人認為有以下幾點:

  1. 運維部署更靈活
    基於Nodejs server的服務端構建,伺服器的部署可以與後端機器獨立出來。而且後端同學就僅僅需要關注介面的實現。
  2. 前端技術棧更統一
    比如:PHP部署頁面路由,前端通過MVVM框架實現,前端還需要學習PHP語法來實現後端路由。
  3. 前端架構和選型更便捷
    比如你可以很容易通過模板引擎完成BigPipe的架構,你也可以從內網非同步併發獲取首屏資料。

當然Gracejs是隻是服務端框架,前端架構如何選型,隨你所願。目前已經有基於Vue和requirejs的boilerplate。

這裡以基於Vue的構建為例。

目錄結構

一個完整的依賴基於vue+Gracejs的目錄結構推薦使用這種模式:

.
├── app
│   └── demo
│         ├── build
│         ├── controller
│         ├── mock
│         ├── static
│         ├── views
│         └── vues
└── gracejs
    ├── app
    │    └── demo
    ├── middleware
    ├── ...
複製程式碼

當然,Gracejs允許你配置app目錄路徑,你可以放到任意你想要的目錄裡。

這裡的demo模組比預設的Gracejs下的demo模組多出來兩個目錄:buildvues

構建思路

其實,到這裡也能猜到如何進行構建了:build目錄是基於webpack的編譯指令碼,vues目錄是所有的.vue的前端業務檔案。

webpack將vues下的vue檔案編譯之後產出到gracejs/app/demo/static下;其他controller等沒有必要編譯的檔案,直接使用webpack的複製外掛複製到gracejs/app/demo/的對應目錄下即可。

有興趣的同學,推薦看grace-vue-webpack-boilerplate下的build實現原始碼;當然,需要對webpack和vue有一定的瞭解。

歡迎同學們貢獻基於ReactAngular的boilerplate,以郵件或者ISSUE的形式通知我們之後,新增到gracejs的官方檔案中。

結語

自此,洋洋灑灑1w多字,Gracejs終於介紹完畢;有興趣的同學去github賞個star唄:github.com/xiongwilee/…

最後,歡迎大家提issue、fork;有任何疑問也可以郵件聯絡:xiongwilee[at]foxmail.com。