Gracejs(又稱:koa-grace v2) 是全新的基於koa v2.x的MVC+RESTful架構的先後端分離框架。css
Gracejs是koa-grace的升級版,也能夠叫koa-grace v2。html
主要特性包括:前端
相比於koa-grace v1(如下簡稱:koa-grace):Gracejs完美支持koa v2,同時作了優化虛擬host匹配和路由匹配的性能、還完善了部分測試用例等諸多升級。固然,若是你正在使用koa-grace也不用擔憂,咱們會把Gracejs中除了支持koa2的性能和功能特性移植到koa-grace的相應中間件中。vue
這裏再也不介紹「先後端分離」、「RESTful」、「MVC」等概念,有興趣可參考趣店前端團隊基於koajs的先後端分離實踐一文。node
注意:請確保你的運行環境中Nodejs的版本至少是v4.0.0
,目前須要依賴Babel。(固然26日凌晨nodejs v7
已經release,你也能夠不依賴Babel,直接經過--harmony_async_await
模式啓動。)webpack
執行命令:git
$ git clone -b v2.x https://github.com/xiongwilee/koa-grace.git $ cd koa-grace && npm install 複製代碼
而後,執行命令:github
$ npm run dev
複製代碼
而後訪問:http://127.0.0.1:3000 就能夠看到示例了!web
這裏參考 github.com/xiongwilee/… 中app/demo
目錄下的示例,詳解Gracejs的MVC+RESTful架構的實現。ajax
此前也有文章簡單介紹過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。
爲了知足更多的使用場景,在Gracejs中加入了簡單的Mongo數據庫的功能。
但準確的說,先後端的分離的Nodejs框架都是VC架構,並無Model層。由於先後端分離框架不該該有任何數據庫、SESSION存儲的職能。
如上圖,具體流程以下:
這裏的第四步,proxy機制,就是Gracejs實現先後端分離的核心部分。
以實現一個電商應用下的「我的中心」頁面爲例。假設這個頁面的首屏包括:用戶基本信息模塊、商品及訂單模塊、消息通知模塊。
後端完成服務化架構以後,這三個模塊能夠解耦,拆分紅三個HTTP API接口。這時候就能夠經過Gracejs的this.proxy
方法,去後端異步併發獲取三個接口的數據。
以下圖:
這樣有幾個好處:
那麼,這麼作是否是就完美了呢?確定不是:
好消息是,這些問題在proxy中間件中都考慮過了。這裏再也不一一講解,有興趣能夠看koa-grace-proxy的源碼:github.com/xiongwilee/… 。
在看詳細使用手冊以前,建議先看一下Gracejs的主文件源碼:github.com/xiongwilee/… 。
這裏再也不浪費篇幅貼代碼了,其實想說明的就是:Gracejs是一個個關鍵中間件的集合。
全部中間件都在middleware目錄下,配置由config/main.*.js
管理。
關於配置文件:
global.config
裏,方便讀取。下面介紹幾個關鍵中間件的做用和使用方法。
vhost
在這裏能夠理解爲,一個Gracejs server服務於幾個站點。Gracejs支持經過host
及host
+一級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/' } 複製代碼
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
的路由。須要說明幾點:
/index
結尾的話,Gracejs會"贈送"一個去掉/index
的一樣路由;exports.__controller__ = false
,該文件就不會生成路由了;參考defaultCtrl.js
await/async
或generator
函數,也能夠是一個普通的函數;Gracejs中推薦使用await/async
;app/blog
中的controller文件;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; 複製代碼
另外,須要說明如下幾點:
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官網: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) }); })('測試') } 複製代碼
Gracejs支持兩種數據代理場景:
下面逐一介紹兩種代理模式的使用方法。
數據代理能夠在控制器中使用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.*.js
的 api
對象中進行配置;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/…
文件代理能夠在控制器中使用this.fetch
方法:
this.fetch(string)
複製代碼
文件請求代理也很簡單,好比若是須要從github代理一個圖片請求返回到瀏覽器中,參考:feclub.cn/user/avatar… , 或者要使用導出文件的功能:
exports.avatar = async function (){ await this.fetch(imgUrl); } 複製代碼
這裏須要注意的是:在this.fetch方法以後會直接結束response, 不會再往其餘中間件執行。
默認的模板引擎爲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
目錄:
// 配置靜態文件路由 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
注意兩點:
/static/**/
或者/*/static/*
形式的路由會是無效的;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' }) 複製代碼
注意:
能夠參考這個:koa-grace中的mock功能的示例
考慮到用戶路由徹底由Nodejs託管之後,CSRF的問題也得在Nodejs層去防禦了。此前寫過一片文章:先後端分離架構下CSRF防護機制,這裏就只寫使用方法,再也不詳述原理。
在Gracejs中能夠配置:
// csrf配置
csrf: {
// 須要進行xsrf防禦的模塊名稱
module: []
}
複製代碼
而後,在業務代碼中,獲取名爲:grace_token
的cookie,以post或者get參數回傳便可。固然,若是你不想污染ajax中的參數對象,你也能夠將這個cookie值存到x-grace-token
頭信息中。
Gracejs監聽到post請求,若是token驗證失效,則直接返回錯誤。
請注意:不推薦在生產環境中使用數據庫功能
在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關閉
依舊以案例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 schemamethods
, 即schema擴展方法,推薦把數據庫元操做都定義在這個對象中statics
, 即靜態操做方法在控制器中使用很是簡單,主要經過this.mongo
,this.mongoMap
兩個方法。
調用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; } 複製代碼
並行多個數據庫操做
參數說明
@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的結果,二者併發執行 複製代碼
請注意:不推薦在生產環境中使用文件上傳下載功能
與數據庫功能同樣,文件上傳下載功能的使用很是簡單,但不推薦在生產環境中使用。由於目前僅支持在單臺服務器上使用數據庫功能,若是多臺機器的服務就有問題了。
若是須要在線上使用上傳下載功能,你可使用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中幾個核心的中間件都介紹完畢。此外,還有幾個中間件不作詳細介紹,瞭解便可:
this.request.body
字段中;最後,關於Gracejs的運維部署在這裏再也不詳述,推薦使用pm2,不用擔憂重啓server期間服務不可用。
到這裏,整個先後端服務的搭建都介紹完了。
在介紹如何結合Gracejs進行前端構建以前,先提一下:這種「更完全」的先後端分離方案相比於基於MVVM框架的單頁面應用具體有什麼不一樣呢?
我的認爲有如下幾點:
固然Gracejs是隻是服務端框架,前端架構如何選型,隨你所願。目前已經有基於Vue和requirejs的boilerplate。
這裏以基於Vue的構建爲例。
一個完整的依賴基於vue+Gracejs的目錄結構推薦使用這種模式:
.
├── app
│ └── demo
│ ├── build
│ ├── controller
│ ├── mock
│ ├── static
│ ├── views
│ └── vues
└── gracejs
├── app
│ └── demo
├── middleware
├── ...
複製代碼
固然,Gracejs容許你配置app目錄路徑,你能夠放到任意你想要的目錄裏。
這裏的demo模塊比默認的Gracejs下的demo模塊多出來兩個目錄:build
和vues
。
其實,到這裏也能猜到如何進行構建了:build
目錄是基於webpack的編譯腳本,vues
目錄是全部的.vue的前端業務文件。
webpack將vues下的vue文件編譯以後產出到gracejs/app/demo/static
下;其餘controller
等沒有必要編譯的文件,直接使用webpack的複製插件複製到gracejs/app/demo/
的對應目錄下便可。
有興趣的同窗,推薦看grace-vue-webpack-boilerplate
下的build實現源碼;固然,須要對webpack和vue有必定的瞭解。
歡迎同窗們貢獻基於React
、Angular
的boilerplate,以郵件或者ISSUE的形式通知咱們以後,添加到gracejs的官方文檔中。
自此,洋洋灑灑1w多字,Gracejs終於介紹完畢;有興趣的同窗去github賞個star唄:github.com/xiongwilee/… 。
最後,歡迎你們提issue、fork;有任何疑問也能夠郵件聯繫:xiongwilee[at]foxmail.com。