Gracejs
A Nodejs BFF framework, build with koa2(基于koa2的标准前后端分离框架)
Install / Use
/learn @xiongwilee/GracejsREADME
Gracejs
Gracejs(又称:koa-grace v2) 是全新的基于koa v2.x的MVC+RESTful架构的前后端分离框架。
koa-grace v1.x版本请移步: https://github.com/xiongwilee/Gracejs/tree/v1.0.4
一、简介
Gracejs是koa-grace的升级版,也可以叫koa-grace v2。
主要特性包括:
- 支持MVC架构,可以更便捷地生成服务端路由;
- 标准的RESTful架构,支持后端接口异步并发,页面性能更优;
- 一套Node环境经服务服务多个站点应用,部署更简单;
- 优雅的MOCK功能,开发环境模拟数据更流畅;
- 完美支持async/await及generator语法,随心所欲;
- 更灵活的前端构建选型,想用什么就用什么,随你所愿。
相比于koa-grace v1(以下简称:koa-grace):Gracejs完美支持koa v2,同时做了优化虚拟host匹配和路由匹配的性能、还完善了部分测试用例等诸多升级。当然,如果你正在使用koa-grace也不用担心,我们会把Gracejs中除了支持koa2的性能和功能特性移植到koa-grace的相应中间件中。
这里不再介绍“前后端分离”、“RESTful”、“MVC”等概念,有兴趣可参考前端团队基于koajs的前后端分离实践一文。
Gracejs及前后端分离问题交流群:
- 微信交流群: 添加微信
xiongwilee后(备注:城市-职业-姓名)拉你到“Gracejs及前后端分离交流”微信群 - QQ交流群:368463457 (此群将弃用,可联系添加微信
xiongwilee拉到微信群)
二、快速开始
注意:请确保你的运行环境中Nodejs的版本至少是v7.6.0
(或者你也可以考虑支持 Nodejs v4.x+ 的koa-grace v1.x)
安装
执行命令:
$ git clone https://github.com/xiongwilee/Gracejs.git
$ cd Gracejs && npm install
运行
然后,执行命令:
$ npm run dev
然后访问:http://127.0.0.1:3000 就可以看到示例了!
三、案例说明
这里参考 https://github.com/xiongwilee/Gracejs 中app/demo目录下的示例,详解Gracejs的MVC+RESTful架构的实现。
此前也有文章简单介绍过Gracejs的实现( https://github.com/xiongwilee/Gracejs/wiki ),但考虑到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用以存放模板文件
注意,这个目录结构是生产环境代码的标准目录结构。在开发环境里你可以任意调整你的目录结构,只要保证编译之后的产出文件以这个路径输出即可。
如果你对这一点仍有疑问,可以参考: 前端构建-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方法,去后端异步并发获取三个接口的数据。
如下图:

这样有几个好处:
- 在Nodejs层(服务端)异步并发向后端(服务端)获取数据,可以使HTTP走内网,性能更优;
- 后端的接口可以同时提供给客户端,实现接口给Web+APP复用,后端开发成本更低;
- 在Nodejs层获取数据后,直接交给页面,不管前端用什么技术栈,可以使首屏体验更佳。
那么,这么做是不是就完美了呢?肯定不是:
- 后端接口在外网开放之后,如何保证接口安全性?
- 如果当前页面请求是GET方法,但我想POST到后端怎么办?
- 我想在Controller层重置post参数怎么办?
- 后端接口设置cookie如何带给浏览器?
- 经过一层Nodejs的代理之后,如何保证SESSION状态不丢失?
- 如果当前请求是一个file文件流,又该怎么办呢? ...
好消息是,这些问题在proxy中间件中都考虑过了。这里不再一一讲解,有兴趣可以看koa-grace-proxy的源码:https://github.com/xiongwilee/Gracejs/middleware/proxy 。
四、详细使用手册
在看详细使用手册之前,建议先看一下Gracejs的主文件源码:https://github.com/xiongwilee/Gracejs/blob/master/src/app.js 。
这里不再浪费篇幅贴代码了,其实想说明的就是:Gracejs是一个个关键中间件的集合。
所有中间件都在middleware目录下,配置由config/main.*.js管理。
关于配置文件:
- 配置文件extend的权重为:config/env.json(环境变量) > config/server.json(文件配置) > config/main.*.js > config.js;
- 配置生成后保存在Gracejs下的全局作用域
global.config里,方便读取。
下面介绍几个关键中间件的作用和使用方法。
vhost——多站点配置
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/'
}
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的路由。需要说明几点:
- 如果路由是以
/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。
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';即可,然后在控制器中通过this.params.id获取ID值,更多相关配置请参看:koa-router#named-routes - 需要注意的是:如果
__regular__配置为正则表达式的话,则会生成当前控制器默认路由及正则可匹配的路由
当然,如果路由文件中的所有控制器方法都是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', {
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.defaultCtrlData | object | views | 模板渲染的全局变量,类似于 Koa this.state
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支持两种数据代理场景:
- 单纯的数据代理,任意请求到后端接口,然后返回json数据(也包括文件流请求到后端,后端返回json数据);
- 文件代理,请求后端接口,返回一个文件(例如验证码图片);
下面逐一介绍两种代理模式的使用方法。
1、 数据代理
数据代理可以在控制器中使用this.proxy方法:
this.proxy(object|string,[opt])
使用方法
this.proxy 方法返回的是一个Promise,所以这里你可以根据当前Controller的类型使用async/await或者Generator实现异步并发。例如:
async/await:
exports.demo = async function () {
await this.proxy({ /* ... */ })
}
Generator:
exports.demo = function * () {
yield this.proxy({ /* ... */ })
}
为了使语法更简便,可以在执行this.proxy之后,直接在上下文中的backData字段中获取到数据。例如:
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 : {...}
* }
*/
}
Generator方法亦然。
此外,如果要获取proxy的请求头信息,你可以在proxy方法返回的内容中获取到,例如:
exports.demo = async function (){
let res = await this.proxy({
userInfo:'github:post:user/login/oauth/access_token?client_id=****',
otherInfo:'github:other/info?test=test',
});
console.log(res);
/**
* {
* userInfo : {
* statusCode: {...} // 返回http status code
* request: {...} // 请求体
* headers: {...} // 响应头信息
* body: {...} // 未处理的response body
* },
* otherInfo : {...}
* }
*/
}
使用场景一:多个数据请求的代理
可以发现,上文的案例就是多个数据同时请求的代理方案,这里也就是异步并发获取数据的实现。使用this.proxy方法实现多个数据异步并发请求非常简单:
exports.demo = async function (){
await this.proxy({
userInfo:'github:post:user/login/oauth/access_token?cli
Related Skills
node-connect
325.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
80.3kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
325.9kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
80.3kCommit, push, and open a PR
