Gracejs : 全新的基於koa2的先後端分離框架

原文地址:http://feclub.cn/post/content...javascript

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

1、簡介

Gracejs是koa-grace的升級版,也能夠叫koa-grace v2。html

github地址: https://github.com/xiongwilee/koa-grace前端

主要特性包括:vue

  1. 支持MVC架構,能夠更便捷地生成服務端路由;java

  2. 標準的RESTful架構,支持後端接口異步併發,頁面性能更優;node

  3. 一套Node環境經服務服務多個站點應用,部署更簡單;webpack

  4. 優雅的MOCK功能,開發環境模擬數據更流暢;git

  5. 完美支持async/await及generator語法,爲所欲爲;github

  6. 更靈活的前端構建選型,默認支持Vue及Require.js。

相比於koa-grace v1(如下簡稱:koa-grace):Gracejs完美支持koa v2,同時作了優化虛擬host匹配和路由匹配的性能、還完善了部分測試用例等諸多升級。固然,若是你正在使用koa-grace也不用擔憂,咱們會把Gracejs中除了支持koa2的性能和功能特性移植到koa-grace的相應中間件中。

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

2、快速開始

注意:請確保你的運行環境中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 就能夠看到示例了!

3、案例說明

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

此前也有文章簡單介紹過koa-grace的實現( https://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的源碼:https://github.com/xiongwilee...

4、詳細使用手冊

在看詳細使用手冊以前,建議先看一下Gracejs的主文件源碼:https://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

一、 文件路徑即路由

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。

二、 路由文件使用說明

將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__配置。

三、 控制器

將demo模塊中的home.js的index方法再擴展一下:

exports.index = async function () {
  // 綁定默認控制器方法
  await this.bindDefault();
  // 獲取數據
  await this.proxy(...)
  // 渲染目標引擎
  await this.render('home', {
    title: 'Hello , Grace!'
  });
}

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

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

koa自帶:

更多koa自帶context屬性,請查看koajs官網:http://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 文件下載方法,請參看: 文件上傳下載

四、控制器中異步函數的寫法

在控制器中,若是還有其餘的異步方法,能夠經過Promise來實現。例如:

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

proxy——數據代理

Gracejs支持兩種數據代理場景:

  1. 單純的數據代理,任意請求到後端接口,而後返回json數據(也包括文件流請求到後端,後端返回json數據);

  2. 文件代理,請求後端接口,返回一個文件(例如驗證碼圖片);

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

一、 數據代理

數據代理能夠在控制器中使用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方法還有不少有趣的細節,推薦有興趣的同窗看源碼:https://github.com/xiongwilee...

二、 文件代理

文件代理能夠在控制器中使用this.fetch方法:

this.fetch(string)

文件請求代理也很簡單,好比若是須要從github代理一個圖片請求返回到瀏覽器中,參考:http://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/... 或者 http://127.0.0.1/static/blog/...

注意兩點:

  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很是簡單,固然沒有作過任何壓測,可能存在性能問題。

一、 鏈接數據庫

在配置文件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關閉

二、 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,required: true},
  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 , 即靜態操做方法

三、 在控制器中調用數據庫

在控制器中使用很是簡單,主要經過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到後端接口,或者經過上傳組件直接將文件上傳到後端的接口。

一、文件上傳

方法:

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;
}

二、文件下載

方法:

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期間服務不可用

5、前端構建

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

在介紹如何結合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唄:https://github.com/xiongwilee...

最後,歡迎你們提issue、fork;有任何疑問也能夠郵件聯繫:xiongwilee[at]foxmail.com。

相關文章
相關標籤/搜索