Gracejs : 全新的基于 koa2 的前后端分离框架
Posted SegmentFault
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Gracejs : 全新的基于 koa2 的前后端分离框架相关的知识,希望对你有一定的参考价值。
Gracejs(又称:koa-grace v2) 是全新的基于 koa v2.x 的 MVC+RESTful 架构的前后端分离框架。
一、简介
Gracejs 是 koa-grace 的升级版,也可以叫 koa-grace v2。
主要特性包括:
支持 MVC 架构,可以更便捷地生成服务端路由;
标准的 RESTful 架构,支持后端接口异步并发,页面性能更优;
一套 Node 环境经服务服务多个站点应用,部署更简单;
优雅的 MOCK 功能,开发环境模拟数据更流畅;
完美支持 async/await 及 generator 语法,随心所欲;
更灵活的前端构建选型,默认支持Vue及Require.js。
相比于 koa-grace v1(以下简称:koa-grace):Gracejs 完美支持 koa v2,同时做了优化虚拟 host 匹配和路由匹配的性能、还完善了部分测试用例等诸多升级。当然,如果你正在使用 koa-grace 也不用担心,我们会把 Gracejs 中除了支持 koa2 的性能和功能特性移植到 koa-grace 的相应中间件中。
这里不再介绍“前后端分离”、“RESTful”、“MVC”等概念,有兴趣可参考一文。
二、快速开始
注意:请确保你的运行环境中 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 就可以看到示例了!
三、案例说明
这里参考 https://github.com/xiongwilee/koa-grace/tree/v2.x 中app/demo
目录下的示例,详解 Gracejs 的 MVC+RESTful 架构的实现。
此前也有文章简单介绍过 koa-grace 的实现( https://github.com/xiongwilee/koa-grace/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
用以存放模板文件
需要强调的是,这个目录结构是生产环境代码的标准目录结构。在开发环境里你可以任意调整你的目录结构,只要保证编译之后的产出文件以这个路径输出即可。
如果你对这一点仍有疑问,可以参考 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
方法,去后端异步并发获取三个接口的数据。
如下图:
这样有几个好处:
在 Nodejs 层(服务端)异步并发向后端(服务端)获取数据,可以使 HTTP 走内网,性能更优;
后端的接口可以同时提供给客户端,实现接口给 Web+APP 复用,后端开发成本更低;
在 Nodejs 层获取数据后,直接交给页面,不管前端用什么技术栈,可以使首屏体验更佳。
那么,这么做是不是就完美了呢?肯定不是:
后端接口在外网开放之后,如何保证接口安全性?
如果当前页面请求是 GET 方法,但我想 POST 到后端怎么办?
我想在 Controller 层重置 post 参数怎么办?
后端接口设置 cookie 如何带给浏览器?
经过一层 Nodejs 的代理之后,如何保证 SESSION 状态不丢失?
如果当前请求是一个 file 文件流,又该怎么办呢?
…
好消息是,这些问题在 proxy 中间件中都考虑过了。这里不再一一讲解,有兴趣可以看 koa-grace-proxy 的源码:https://github.com/xiongwilee/koa-grace/tree/v2.x/middleware/proxy 。
四、详细使用手册
在看详细使用手册之前,建议先看一下 Gracejs 的主文件源码:https://github.com/xiongwilee/koa-grace/blob/v2.x/src/app.js 。
这里不再浪费篇幅贴代码了,其实想说明的就是:Gracejs 是一个个关键中间件的集合。
所有中间件都在middleware目录下,配置由config/main.*.js
管理。
关于配置文件:
配置文件 extend 关系为:config/server.json 的 merge 字段 > 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';
即可,更多相关配置请参看:koa-router#named-routes
当然,如果路由文件中的所有控制器方法都是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/
Gracejs 注入:
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
方法实现多个数据异步并发请求非常简单:
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
的形参说明如下:
关于this.proxy方法还有很多有趣的细节,推荐有兴趣的同学看源码:https://github.com/xiongwilee/koa-grace/tree/v2.x/middleware/proxy
2、 文件代理
文件代理可以在控制器中使用this.fetch
方法:
this.fetch(string)
文件请求代理也很简单,比如如果需要从 github 代理一个图片请求返回到浏览器中,参考:http://feclub.cn/user/avatar?img=https://avatars.githubusercontent.com/u/1962352?v=3 , 或者要使用导出文件的功能:
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
目录中。
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
注意两点:
静态文件端口和当前路由的端口一致,所以
/static/**/
或者/*/static/*
形式的路由会是无效的;推荐在生产环境中,使用 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 非常简单,当然没有做过任何压测,可能存在性能问题。
1、 连接数据库
在配置文件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 关闭
2、 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 schemamethods
, 即 schema 扩展方法,推荐把数据库元操作都定义在这个对象中statics
, 即静态操作方法
3、 在控制器中调用数据库
在控制器中使用非常简单,主要通过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 到后端接口,或者通过上传组件直接将文件上传到后端的接口。
1、文件上传
方法:
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;
}
2、文件下载
方法:
this.download(filename, [opt])
示例:
exports.download = async function() {
await this.download(this.query.file);
}
其他
Gracejs 中几个核心的中间件都介绍完毕。此外,还有几个中间件不做详细介绍,了解即可:
gzip 实现:使用gzip压缩response中的body;
http body 内容解析:解析request中的body,存到
this.request.body
字段中;简单的 session 实现:通过内存或者 redis 保存 session,不推荐在生产环境中使用;生产环境的 session 服务由后端自行完成。
最后,关于 Gracejs 的运维部署在这里不再详述,推荐使用 pm2,不用担心重启 server 期间服务不可用。
五、前端构建
到这里,整个前后端服务的搭建都介绍完了。
在介绍如何结合 Gracejs 进行前端构建之前,先提一下:这种“更彻底”的前后端分离方案相比于基于 MVVM 框架的单页面应用具体有什么不同呢?
个人认为有以下几点:
运维部署更灵活
基于 Nodejs server 的服务端构建,服务器的部署可以与后端机器独立出来。而且后端同学就仅仅需要关注接口的实现。前端技术栈更统一
比如:php 部署页面路由,前端通过 MVVM 框架实现,前端还需要学习 PHP 语法来实现后端路由。前端架构和选型更便捷
比如你可以很容易通过模板引擎完成 BigPipe 的架构,你也可以从内网异步并发获取首屏数据。
当然 Gracejs 是只是服务端框架,前端架构如何选型,随你所愿。目前已经有基于 Vue 和 requirejs 的 boilerplate。
Vue: https://github.com/Thunf/grace-vue-webpack-boilerplate (by @thunf)
requirejs: https://github.com/xiongwilee/gulp-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 呗:https://github.com/xiongwilee/koa-grace 。
最后,欢迎大家提 issue、fork;有任何疑问也可以邮件联系:xiongwilee[at]foxmail.com。
-EOF-
【SFDC 来啦】四年沉淀,SegmentFault 首次全国性技术大会——SegmentFault Developer Conference 来啦!「北京站 - Security」限量超值票为数不多,抓紧购买。
以上是关于Gracejs : 全新的基于 koa2 的前后端分离框架的主要内容,如果未能解决你的问题,请参考以下文章