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。