Gracejs —— 全新的基于koa2的先后端分离框架

Gracejs(又称:koa-grace v2) 是全新的基于koa v2.x的MVC+RESTful架构的先后端分离框架。css

1、简介

Gracejs是koa-grace的升级版,也能够叫koa-grace v2。html

主要特性包括:前端

  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的相应中间件中。vue

这里再也不介绍“先后端分离”、“RESTful”、“MVC”等概念,有兴趣可参考趣店前端团队基于koajs的先后端分离实践一文。node

2、快速开始

注意:请确保你的运行环境中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

3、案例说明

这里参考 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

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/…

4、详细使用手册

在看详细使用手册以前,建议先看一下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
复制代码

一、 文件路径即路由

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官网: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方法还有不少有趣的细节,推荐有兴趣的同窗看源码:github.com/xiongwilee/…

二、 文件代理

文件代理能够在控制器中使用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很是简单,固然没有作过任何压测,可能存在性能问题。

一、 链接数据库

在配置文件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呗:github.com/xiongwilee/…

最后,欢迎你们提issue、fork;有任何疑问也能够邮件联系:xiongwilee[at]foxmail.com。

相关文章
相关标签/搜索