{"id":14989348,"url":"https://github.com/xiongwilee/gracejs","last_synced_at":"2025-05-14T13:09:11.943Z","repository":{"id":4010247,"uuid":"50917994","full_name":"xiongwilee/Gracejs","owner":"xiongwilee","description":"A Nodejs BFF framework, build with koa2（基于koa2的标准前后端分离框架）","archived":false,"fork":false,"pushed_at":"2025-03-27T08:47:57.000Z","size":9913,"stargazers_count":1396,"open_issues_count":14,"forks_count":237,"subscribers_count":65,"default_branch":"master","last_synced_at":"2025-04-08T04:14:48.927Z","etag":null,"topics":["framework","grace","gracejs","koa","koa-grace","mvc","nodejs","proxy","sfb"],"latest_commit_sha":null,"homepage":"https://grace.wilee.me","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xiongwilee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2016-02-02T12:08:02.000Z","updated_at":"2025-03-27T08:48:01.000Z","dependencies_parsed_at":"2024-01-14T16:22:52.972Z","dependency_job_id":"5737cee5-1efa-4c35-9f08-2d22e261942e","html_url":"https://github.com/xiongwilee/Gracejs","commit_stats":{"total_commits":333,"total_committers":15,"mean_commits":22.2,"dds":0.2582582582582582,"last_synced_commit":"c5ea8bd4d0562a4b2894ad94eea9f6f5f1ff9168"},"previous_names":["xiongwilee/koa-grace"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xiongwilee%2FGracejs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xiongwilee%2FGracejs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xiongwilee%2FGracejs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xiongwilee%2FGracejs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xiongwilee","download_url":"https://codeload.github.com/xiongwilee/Gracejs/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247773719,"owners_count":20993639,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","grace","gracejs","koa","koa-grace","mvc","nodejs","proxy","sfb"],"created_at":"2024-09-24T14:18:10.414Z","updated_at":"2025-04-08T04:14:54.678Z","avatar_url":"https://github.com/xiongwilee.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\u003cimg width=\"30%\" src=\"http://img002.qufenqi.com/products/0e/64/0e648f3e91454d406b3ac8a2941ad15f.png\" /\u003e\u003c/p\u003e\n\n--------------------------------------------------------------------------------\n\n# Gracejs\n\n[Gracejs](https://github.com/xiongwilee/Gracejs)(又称:koa-grace v2)  是全新的基于[koa v2.x](https://github.com/koajs/koa)的MVC+RESTful架构的前后端分离框架。\n\n[![NPM version](https://img.shields.io/npm/v/gracejs.svg)](https://www.npmjs.com/package/gracejs)\n[![Build Status](https://travis-ci.org/xiongwilee/Gracejs.svg?branch=master)](https://travis-ci.org/xiongwilee/Gracejs)\n\n\u003e koa-grace v1.x版本请移步： https://github.com/xiongwilee/Gracejs/tree/v1.0.4\n\n## 一、简介\n\nGracejs是[koa-grace](https://github.com/xiongwilee/Gracejs)的升级版，也可以叫koa-grace v2。\n\n主要特性包括：\n\n1. 支持MVC架构，可以更便捷地生成服务端路由；\n2. 标准的RESTful架构，支持后端接口异步并发，页面性能更优；\n3. 一套Node环境经服务服务多个站点应用，部署更简单；\n4. 优雅的MOCK功能，开发环境模拟数据更流畅；\n5. 完美支持async/await及generator语法，随心所欲；\n6. 更灵活的前端构建选型，想用什么就用什么，随你所愿。\n\n相比于koa-grace v1（以下简称：koa-grace）：**Gracejs完美支持koa v2**，同时做了优化虚拟host匹配和路由匹配的性能、还完善了部分测试用例等诸多升级。当然，如果你正在使用koa-grace也不用担心，我们会把Gracejs中除了支持koa2的性能和功能特性移植到koa-grace的相应中间件中。\n\n这里不再介绍“前后端分离”、“RESTful”、“MVC”等概念，有兴趣可参考[前端团队基于koajs的前后端分离实践](http://feclub.cn/post/content/qudian_koa)一文。\n\nGracejs及前后端分离问题交流群：\n* **微信交流群**： 添加微信 `xiongwilee` 后(备注：城市-职业-姓名)拉你到“Gracejs及前后端分离交流”微信群 \n* **QQ交流群**：368463457 （此群将弃用，可联系添加微信 `xiongwilee` 拉到微信群）\n\n## 二、快速开始\n\n**注意：请确保你的运行环境中Nodejs的版本至少是`v7.6.0`** \n（或者你也可以考虑支持 Nodejs v4.x+ 的[koa-grace v1.x](https://github.com/xiongwilee/Gracejs/tree/v1.0.4)）\n\n### 安装\n\n执行命令：\n```shell\n$ git clone https://github.com/xiongwilee/Gracejs.git\n$ cd Gracejs \u0026\u0026 npm install\n```\n\n### 运行\n\n然后，执行命令：\n```shell\n$ npm run dev\n```\n\n然后访问：[http://127.0.0.1:3000](http://127.0.0.1:3000/) 就可以看到示例了！\n\n## 三、案例说明\n\n这里参考 https://github.com/xiongwilee/Gracejs 中`app/demo`目录下的示例，详解Gracejs的MVC+RESTful架构的实现。\n\n此前也有文章简单介绍过Gracejs的实现（ https://github.com/xiongwilee/Gracejs/wiki ），但考虑到Gracejs的差异性，这里再从**目录结构**、**MVC模型实现**、**proxy机制**这三个关键点做一些比较详细的说明。\n\n### 目录结构\n\nGracejs与koa-grace v1.x版本的目录结构完全一致：\n\n```shell\n.\n├── controller\n│   ├── data.js\n│   ├── defaultCtrl.js\n│   └── home.js\n├── static\n│   ├── css\n│   ├── image\n│   └── js\n└── views\n    └── home.html\n```\n\n其中：\n\n* `controller`用以存放路由及控制器文件\n* `static`用以存放静态文件\n* `views`用以存放模板文件\n\n注意，**这个目录结构是生产环境代码的标准目录结构。在开发环境里你可以任意调整你的目录结构，只要保证编译之后的产出文件以这个路径输出即可**。\n\n如果你对这一点仍有疑问，可以参考: [前端构建-Boilerplate](https://github.com/xiongwilee/Gracejs#boilerplate)\n\n### MVC模型实现\n\n为了满足更多的使用场景，在Gracejs中加入了简单的Mongo数据库的功能。\n\n但准确的说，前后端的分离的Nodejs框架都是VC架构，并没有Model层。因为**前后端分离框架不应该有任何数据库、SESSION存储的职能**。\n\n![mvc](https://raw.githubusercontent.com/xiongwilee/demo/master/photo/mvc-%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB.jpg)\n\n如上图，具体流程如下：\n\n* 第一步，Nodejs server（也就是Gracejs服务）监听到用户请求；\n* 第二步，Gracejs的各个中间件（Middlewares）对请求上下文进行处理；\n* 第三步，根据当前请求的path和method，进入对应的Controller；\n* 第四步，通过http请求以proxy的模式向后端获取数据；\n* 第五步，拼接数据，渲染模板。\n\n这里的第四步，proxy机制，就是Gracejs实现前后端分离的核心部分。\n\n### proxy机制\n\n以实现一个电商应用下的“个人中心”页面为例。假设这个页面的首屏包括：用户基本信息模块、商品及订单模块、消息通知模块。\n\n后端完成服务化架构之后，这三个模块可以解耦，拆分成三个HTTP API接口。这时候就可以通过Gracejs的`this.proxy`方法，去后端异步并发获取三个接口的数据。\n\n如下图：\n\n![proxy](https://raw.githubusercontent.com/xiongwilee/demo/master/photo/proxy-%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB.jpg)\n\n这样有几个好处：\n\n1. 在Nodejs层（服务端）异步并发向后端（服务端）获取数据，可以使HTTP走内网，性能更优；\n2. 后端的接口可以同时提供给客户端，实现接口给Web+APP复用，后端开发成本更低；\n3. 在Nodejs层获取数据后，直接交给页面，不管前端用什么技术栈，可以使首屏体验更佳。\n\n那么，这么做是不是就完美了呢？肯定不是：\n\n1. 后端接口在外网开放之后，如何保证接口安全性？\n2. 如果当前页面请求是GET方法，但我想POST到后端怎么办？\n3. 我想在Controller层重置post参数怎么办？\n4. 后端接口设置cookie如何带给浏览器？\n5. 经过一层Nodejs的代理之后，如何保证SESSION状态不丢失？\n6. 如果当前请求是一个file文件流，又该怎么办呢？\n...\n\n好消息是，这些问题在proxy中间件中都考虑过了。这里不再一一讲解，有兴趣可以看koa-grace-proxy的源码：https://github.com/xiongwilee/Gracejs/middleware/proxy 。\n\n## 四、详细使用手册\n\n在看详细使用手册之前，建议先看一下Gracejs的主文件源码：https://github.com/xiongwilee/Gracejs/blob/master/src/app.js 。\n\n这里不再浪费篇幅贴代码了，其实想说明的就是：**Gracejs是一个个关键中间件的集合**。\n\n所有中间件都在[middleware](https://github.com/xiongwilee/Gracejs/middleware)目录下，配置由`config/main.*.js`管理。\n\n关于配置文件：\n\n1. 配置文件extend的权重为：config/env.json(环境变量) \u003e config/server.json（文件配置） \u003e config/main.*.js \u003e config.js；\n2. 配置生成后保存在Gracejs下的全局作用域`global.config`里，方便读取。\n\n下面介绍几个关键中间件的作用和使用方法。\n\n### vhost——多站点配置\n\n`vhost`在这里可以理解为，一个Gracejs server服务于几个站点。Gracejs支持通过`host`及`host`+`一级path`两种方式的**映射**。所谓的隐射，其实就是一个域名（或者一个域名+一级path）对应一个应用，一个应用对应一个目录。\n\n**注意：考虑到正则的性能问题，vhost不会考虑正则映射**。\n\n参考`config/main.development.js`，可以这么配置vhost：\n\n```javascript\n// vhost配置\nvhost: {\n  '127.0.0.1':'demo',\n  '127.0.0.1/test':'demo_test',\n  'localhost':'blog',\n}\n```\n\n其中，`demo`,`demo_test`,`blog`分别对应`app/`下的三个目录。当然你也可以指定目录路径，在配置文件中修改`path.project`配置即可：\n```javascript\n// 路径相关的配置\npath: {\n  // project\n  project: './app/'\n}\n```\n\n### router——路由及控制器\n\nGracejs中生成路由的方法非常简单，以自带的demo模块为例，进入demo模块的controller目录：`app/demo/controller`。\n\n文件目录如下：\n```\ncontroller\n├── data.js\n├── defaultCtrl.js\n└── home.js\n```\n\n#### 1、 文件路径即路由\n\nrouter中间件会找到模块中所有以`.js`结尾的文件，根据文件路径和module.exports生成路由。\n\n例如，demo模块中的home.js文件：\n```javascript\nexports.index = async function () {\n  await this.bindDefault();\n  await this.render('home', {\n    title: 'Hello , Grace!'\n  });\n}\nexports.hello = function(){\n  this.body = 'hello world!'\n}\n```\n则生成`/home/index`、`/home`、`/home/hello`的路由。需要说明几点：\n\n1. 如果路由是以`/index`结尾的话，Gracejs会\"赠送\"一个去掉`/index`的同样路由；\n2. 如果当前文件是一个依赖，仅仅被其他文件引用；则在文件中配置`exports.__controller__ = false`，该文件就不会生成路由了；参考`defaultCtrl.js`\n3. 这里的控制器函数可以是`await/async`或`generator`函数，也可以是一个普通的函数；Gracejs中推荐使用`await/async`；\n4. 这里的路由文件包裹在一个目录里也是可以的，可以参考：`app/blog`中的controller文件；\n5. 如果当前文件路由就是一个独立的控制器，则`module.exports`返回一个任意函数即可。\n\n最后，如果用户访问的路由查找不到，router会默认查找`/error/404`路由，如果有则渲染`error/404`页（不会重定向到`error/404`），如果没有则返回404。\n \n#### 2、 路由文件使用说明\n\n将demo模块中的home.js扩展一下：\n\n```javascript\nexports.index = async function () {\n    ...\n}\nexports.index.__method__ = 'get';\nexports.index.__regular__ = null;\n```\n\n另外，需要说明以下几点：\n\n* 如果需要配置dashboard/post/list请求为`DELETE`方法，则post.js中声明 `exports.list.__method__ = 'delete'`即可（**不声明默认注入get及post方法**）;\n* 如果要配置更灵活的路由，则中声明`exports.list.__regular__ = '/:id';`即可，然后在控制器中通过`this.params.id`获取ID值，更多相关配置请参看：[koa-router#named-routes](https://github.com/alexmingoia/koa-router#named-routes)\n* 需要注意的是：如果`__regular__`配置为正则表达式的话，则会生成当前控制器默认路由及正则可匹配的路由\n\n当然，如果路由文件中的所有控制器方法都是post方法，您可以在控制器文件最底部加入：`module.exports.__method__ = 'post'`即可，`__regular__`的配置同理。\n\n**注意：一般情况这里不需要额外的配置，为了保证代码美观，没有特殊使用场景的话就不要写`__method__`和`__regular__`配置。**\n\n#### 3、 控制器\n\n将demo模块中的home.js的index方法再扩展一下：\n```javascript\nexports.index = async function () {\n  // 绑定默认控制器方法\n  await this.bindDefault();\n  // 获取数据\n  await this.proxy(...)\n  // 渲染目标引擎\n  await this.render('home', {\n    title: 'Hello , Grace!'\n  });\n}\n```\n\n它就是一个标准的控制器（controller）了。这个控制器的作用域就是当前koa的context，你可以任意使用koa的context的任意方法。\n\n几个关键context属性的使用说明如下：\n\n**koa自带：**\n\n更多koa自带context属性，请查看koajs官网：http://koajs.com/ \n\ncontext属性 | 类型 | 说明\n---------- | ---- | ------------------\n`this.request.href` | `String` | 当前页面完整URL，也可以简写为`this.href`\n`this.request.query` | `object` | get参数，也可以简写为`this.query`\n`this.response.set` | `function` | 设置response头信息，也可以简写为`this.set`\n`this.cookies.set` | `function` | 设置cookie，参考：[cookies](https://github.com/pillarjs/cookies#cookiesset-name--value---options--)\n`this.cookies.get` | `function` | 获取cookie，参考：[cookies](https://github.com/pillarjs/cookies#cookiesget-name--options--)\n\n**Gracejs注入：**\n\ncontext属性 | 类型 | 中间件 | 说明\n---------- | ---- | ----- | ------------------\n`this.bindDefault` | `function` | router | 公共控制器，相当于`require('app/*/controller/defaultCtrl.js')`\n`this.defaultCtrlData` | `object` | views | 模板渲染的全局变量，类似于 Koa `this.state`\n`this.request.body` | `object` | body | post参数，可以直接在this.request.body中获取到post参数\n`this.render` | `function` | views | 模板引擎渲染方法，请参看： 模板引擎- Template engine\n`this.mongo` | `function` | mongo | 数据库操作方法，请参看： 数据库 - Database\n`this.mongoMap` | `function` | mongo | 并行数据库多操作方法，请参看： 数据库 - Database\n`this.proxy` | `function` | proxy | RESTful数据请求方法，请参看：数据代理\n`this.fetch` | `function` | proxy | 从服务器导出文件方法，请参看： 请求代理\n`this.backData` | `Object` | proxy | 默认以Obejct格式存储this.proxy后端返回的JSON数据\n`this.upload` | `function` | xload | 文件上传方法，请参看： 文件上传下载\n`this.download` | `function` | xload | 文件下载方法，请参看： 文件上传下载\n\n#### 4、控制器中异步函数的写法\n\n在控制器中，如果还有其他的异步方法，可以通过Promise来实现。例如：\n\n```javascript\nexports.main = async function() {\n  await ((test) =\u003e {\n    return new Promise((resolve, reject) =\u003e {\n      setTimeout(() =\u003e { resolve(test) }, 3000)\n    });\n  })('测试')\n}\n```\n\n### proxy——数据代理\n\nGracejs支持两种数据代理场景：\n\n1. 单纯的数据代理，任意请求到后端接口，然后返回json数据（也包括文件流请求到后端，后端返回json数据）；\n2. 文件代理，请求后端接口，返回一个文件（例如验证码图片）；\n\n下面逐一介绍两种代理模式的使用方法。\n\n#### 1、 数据代理\n\n数据代理可以在控制器中使用`this.proxy`方法：\n\n```javascript\nthis.proxy(object|string,[opt])\n```\n\n##### 使用方法\n\n`this.proxy` 方法返回的是一个Promise，所以这里你可以根据当前Controller的类型使用`async/await`或者`Generator`实现异步并发。例如：\n\n**async/await：**\n\n```javascript\nexports.demo = async function () {\n  await this.proxy({ /* ... */ })\n}\n```\n\n**Generator：**\n\n```javascript\nexports.demo = function * () {\n  yield this.proxy({ /* ... */ })\n}\n```\n\n为了使语法更简便，可以在执行`this.proxy`之后，直接在上下文中的`backData`字段中获取到数据。例如：\n\n```javascript\nexports.demo = async function () {\n  await this.proxy({\n    userInfo:'github:post:user/login/oauth/access_token?client_id=****',\n    otherInfo:'github:other/info?test=test',\n  })\n  \n  console.log(this.backData);\n  /**\n   *  {\n   *    userInfo : {...},\n   *    otherInfo : {...}\n   *  }\n   */\n}\n```\n\n`Generator`方法亦然。\n\n此外，如果要获取proxy的请求头信息，你可以在proxy方法返回的内容中获取到，例如：\n\n```javascript\nexports.demo = async function (){\n  let res = await this.proxy({\n    userInfo:'github:post:user/login/oauth/access_token?client_id=****',\n    otherInfo:'github:other/info?test=test',\n  });\n  \n  console.log(res);\n  /**\n   *  {\n   *    userInfo : {\n   *      statusCode: {...} // 返回http status code\n   *      request: {...}    // 请求体\n   *      headers: {...}    // 响应头信息\n   *      body: {...}       // 未处理的response body\n   *    },\n   *    otherInfo : {...}\n   *  }\n   */\n}\n```\n\n##### 使用场景一：多个数据请求的代理\n\n可以发现，上文的案例就是多个数据同时请求的代理方案，这里也就是**异步并发**获取数据的实现。使用`this.proxy`方法实现多个数据异步并发请求非常简单：\n\n```javascript\nexports.demo = async function (){\n  await this.proxy({\n    userInfo:'github:post:user/login/oauth/access_token?client_id=****',\n    otherInfo:'github:other/info?test=test',\n  });\n  \n  console.log(this.backData);\n  /**\n   *  {\n   *    userInfo : {...},\n   *    otherInfo : {...}\n   *  }\n   */\n}\n```\n\n然后，proxy的结果会默认注入到上下文的`this.backData`对象中。\n\n\n\n##### 使用场景二：单个数据请求的代理\n\n如果只是为了实现一个接口请求代理，可以这么写：\n\n```javascript\nexports.demo = async function (){\n  await this.proxy('github:post:user/login/oauth/access_token?client_id=****');\n}\n```\n\n这样proxy请求返回的数据体会直接赋值给`this.body`，也就是将这个请求直接返回给客户端。\n\n##### 说明\n\n`github:post:user/login/oauth/access_token?client_id=****`说明如下：\n\n* `github`： 为在`config/main.*.js`的 `api` 对象中进行配置；\n* `post` ： 为数据代理请求的请求方法，该参数可以不传，默认当前用户请求的method；\n* `path`： 后面请求路径中的query参数会覆盖当前页面的请求参数（this.query），将query一同传到请求的接口\n* 你也可以写完整的路径：`{userInfo:'https://api.github.com/user/login?test=test'}`\n\n另外，`this.proxy`的形参说明如下：\n\n 参数名 | 类型 | 默认 | 说明\n ----- | --- | ---- | ----\n `dest` | `Object` | `this.backData` | 指定接收数据的对象，默认为`this.backData`\n `conf` | `Obejct` | `{}` | this.proxy使用[requestjs](https://github.com/request/request)实现，此为传给request的重置配置（你可以在这里设置接口超时时间和http长短链接：`conf: { timeout: 25000 , keepAlive:true}`）,其中keepAlive若为true则会覆盖原请求的长短链接属性，否则对原请求没有影响\n `json` | `Object` | `{}` | 指定json格式的数据，参考：[requestjs json配置](https://github.com/request/request#requestoptions-callback)\n `form` | `Object` | `{}` | 指定application/x-www-form-urlencoded格式的数据，[requestjs form配置](https://github.com/request/request#requestoptions-callback)\n `body` | `Buffer\\|String\\|ReadStream` | 无 | 参考：[requestjs body配置](https://github.com/request/request#requestoptions-callback)\n `query` | `Object` | 无 | 如果配置了query参数，则会替换ctx.query作为proxy参数\n `headers` | `Object` | `{}` | 指定当前请求的headers\n `onBeforeRequest` | `Function` | 无 | 每个请求发送前的回调事件，作用域为当前上下文，有两个形参'requestOpt','proxy name'，可以操作`requestOpt`以修改requestjs的参数\n `onAfterRequest` | `Function` | 无 | 每个请求获取结果后的回调事件，作用域为当前上下文，有两个形参'proxy name','response'，可以操作`response`以修改`this.proxy`的返回结果\n\n如果以上配置中 `json`、`form`、`body` 的参数一个都不传，则默认会将当前客户端请求数据体传给后端接口，推荐使用默认proxy数据的方式；当然了，如果有特殊情况，自行配置数据也无妨。\n\n除了在`this.proxy`的参数中进行配置外，在多个并发请求也可以这么写配置：\n```\nthis.proxy({\n  testInfo: {\n    uri: 'github:other/info?test=test',\n    form: {\n      // formData\n    },\n    headers: {},\n    // ... 其他配置亦可\n  },\n  otherInfo: {\n    // 同上自定义配置\n  }\n})\n```\n这样就可以很灵活地实现接口级别的自定义配置。\n\n关于this.proxy方法还有很多有趣的细节，推荐有兴趣的同学看源码：https://github.com/xiongwilee/Gracejs/middleware/proxy\n\n#### 2、 文件代理\n\n文件代理可以在控制器中使用`this.fetch`方法：\n\n```javascript\nthis.fetch(string)\n```\n\n文件请求代理也很简单，比如如果需要从github代理一个图片请求返回到浏览器中，参考：http://feclub.cn/user/avatar?img=https://avatars.githubusercontent.com/u/1962352?v=3 ， 或者要使用导出文件的功能：\n\n```javascript\nexports.avatar = async function (){\n  await this.fetch(imgUrl);\n}\n```\n\n这里需要注意的是：**在this.fetch方法之后会直接结束response， 不会再往其他中间件执行**。\n\n### views——视图层\n\n#### 1、基本配置\n\n默认的模板引擎为[swig](paularmstrong.github.io/swig/)，但swig作者已经停止维护；你可以在`config/main.*.js`中配置`template`属性想要的模板引擎：\n\n```javascript\n// 模板引擎配置\ntemplate: 'nunjucks'\n```\n\n你还可以根据不同的模块配置不同的模板引擎：\n```javascript\ntemplate: {\n  blog:'swig'\n}\n```\n\n#### 2、基本使用\n\n目前支持的模板引擎列表在这里：[consolidate.js#supported-template-engines](https://github.com/tj/consolidate.js#supported-template-engines)\n\n在控制器中调用`this.render`方法渲染模板引擎：\n\n```javascript\nexports.home = await function () {\n  await this.render('dashboard/site_home',{\n    breads : ['站点管理','通用'],\n    userInfo: this.userInfo,\n    siteInfo: this.siteInfo\n  })\n}\n```\n\n模板文件在模块路径的`/views`目录中。\n\n注意一点：Gracejs渲染模板时，默认会将`main.*.js`中constant配置交给模板数据；这样，如果你想在页面中获取公共配置（比如：CDN的地址）的话就可以在模板数据中的`constant`子中取到。\n\n#### 3、个性化定制\n\n此外，如果需要更个性化的配置，可以在`/view`目录中创建文件`viewsConfig.js`。例如，nunjucks模板引擎添加filter的功能：\n\n```javascript\n/**\n * [nunjucks 个性化定制]\n * 参考：https://github.com/tj/consolidate.js#template-engine-instances\n * \n * @param  {Object} consolidate consolidate对象\n * @param  {Object} config      app.js中的views配置项\n * \n * @return \n */\nmodule.exports = function(consolidate, config) {\n  const nunjucks = require('nunjucks');\n  consolidate.requires.nunjucks = nunjucks.configure(config.root);\n\n  // 注入全局变量\n  consolidate.requires.nunjucks.addGlobal('G', global);\n\n  // 添加foo filter示例\n  consolidate.requires.nunjucks.addFilter('foo', function () {\n    return 'bar';\n  });\n}\n```\n\n以上可参考：`middleware/views/example/views/viewsConfig.js`。\n\n### static——静态文件服务\n\n静态文件的使用非常简单，将`/static/**/`或者`/*/static/*`的静态文件请求代理到了模块路径下的`/static`目录：\n\n```javascript\n// 配置静态文件路由\napp.use(Middles.static(['/static/**/*', '/*/static/**/*'], {\n  dir: config_path_project,\n  maxage: config_site.env == 'production' \u0026\u0026 60 * 60 * 1000\n}));\n```\n\n以案例中`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\n\n注意两点：\n\n1. 静态文件端口和当前路由的端口一致，所以`/static/**/`或者`/*/static/*`形式的路由会是无效的；\n2. 推荐在生产环境中，使用Nginx做静态文件服务，购买CDN托管静态文件；\n\n### mock——Mock数据\n\nMOCK功能的实现其实非常简单，在开发环境中你可以很轻易地使用MOCK数据。\n\n以demo模块为例，首先在`main.development.js`配置文件中添加proxy配置：\n```javascript\n// controller中请求各类数据前缀和域名的键值对\napi: {\n // ...\n demo: 'http://${ip}:${port}/__MOCK__/demo/'\n // ...\n}\n```\n\n然后，在demo模块中添加`mock`文件夹，然后添加`test.json`:\n\n**全局MOCK”模式**\n\n在开发环境中你推荐使用“全局MOCK”模式。首先在`main.development.js`配置文件中添加mock配置`isFullMock`为true：\n```\n  // mock server配置\n  mock: {\n    prefix: '/__MOCK__/',\n    localServer: 'http://${ip}:${port}',\n    isFullMock: true \n  },\n```\n\n然后，不用修改api配置，就直接访问对应的mock数据文件了。例如，在blog模块中的`this.proxy('blogApi:test1/test2')`接口对应的文件则是：\n```\nblog\n├── controller\n├── deploy\n├── mock\n|    └── blogApi // 注意这里的blogApi\n|          └── test1\n|                └── test2.json\n├── model\n├── static\n└── views\n```\n\n**文件结构：**\n```shell\n.\n├── controller\n├── mock\n|     └── test.json\n├── static\n└── views\n```\n**文件内容（就是你想要的请求返回内容）：**\n\n在JSON文件内容中也可以使用注释：\n```javascript\n/*\n * 获取用户信息接口\n */\n{\n    code:0 // 这是code\n}\n```\n\n然后，你可以打开浏览器访问：`http://${ip}:${port}/__MOCK__/demo/test` 验证是否已经返回了test.json里的数据。\n\n最后在你的controller业务代码中就可以通过proxy方法获取mock数据了：\n```javascript\nthis.proxy({\n    test:'demo:test'\n})\n```\n\n**注意：**\n\n* 如果你的mock文件路径是/mock/test/subtest.json 那么proxy路径则是：test/subtest;\n* 强烈建议将mock文件统一为真正的后端请求路径，这样以实现真实路径的mock；\n\n可以参考这个：[Gracejs中的mock功能的示例](https://github.com/xiongwilee/Gracejs/blob/master/app/demo/controller/data.js)\n\n### secure——安全模块\n\n考虑到用户路由完全由Nodejs托管以后，CSRF的问题也得在Nodejs层去防护了。此前写过一片文章：[前后端分离架构下CSRF防御机制](http://feclub.cn/post/content/koa-grace-csrf)，这里就只写使用方法，不再详述原理。\n\n在Gracejs中可以配置：\n```javascript\n// csrf配置\ncsrf: {\n  // 需要进行xsrf防护的模块名称\n  module: []\n}\n```\n\n然后，在业务代码中，获取名为：`grace_token`的cookie，以post或者get参数回传即可。当然，如果你不想污染ajax中的参数对象，你也可以将这个cookie值存到`x-grace-token`头信息中。\n\nGracejs监听到post请求，如果token验证失效，则直接返回错误。\n\n### mongo——简单的数据库\n\n\u003e 请注意：不推荐在生产环境中使用数据库功能\n\n在Gracejs中使用mongoDB非常简单，当然没有做过任何压测，可能存在性能问题。\n\n#### 1、 连接数据库\n\n在配置文件`config/main.*.js`中进行配置：\n\n```javascript\n  // mongo配置\n  mongo: {\n    options:{\n      // mongoose 配置\n    },\n    api:{\n      'blog': 'mongodb://localhost:27017/blog'\n    }\n  },\n```\n\n其中，`mongo.options`配置mongo连接池等信息，`mongo.api`配置站点对应的数据库连接路径。\n\n值得注意的是，**配置好数据库之后，一旦Gracejs server启动mongoose就启动连接，直到Gracejs server关闭**\n\n#### 2、 mongoose的schema配置\n\n依旧以案例`blog`为例，参看`app/blog/model/mongo`目录：\n\n```shell\n└── mongo\n    ├── Category.js\n    ├── Link.js\n    ├── Post.js\n    └── User.js\n```\n\n一个js文件即一个数据库表即相关配置，以`app/blog/model/mongo/Category.js`：\n\n```javascript\n'use strict';\n\n// model名称，即表名\nlet model = 'Category';\n\n// 表结构\nlet schema = [{\n  id: {type: String,unique: true,required: true},\n  name: {type: String,required: true},\n  numb: {type: Number,'default':0}\n}, {\n  autoIndex: true,\n  versionKey: false\n}];\n\n// 静态方法:http://mongoosejs.com/docs/guide.html#statics\nlet statics = {}\n\n// 方法扩展 http://mongoosejs.com/docs/guide.html#methods\nlet methods = {\n  /**\n   * 获取博客分类列表\n   */\n  list: function* () {\n    return this.model('Category').find();\n  }\n}\n\nmodule.exports.model = model;\nmodule.exports.schema = schema;\nmodule.exports.statics = statics;\nmodule.exports.methods = methods;\n```\n\n主要有四个参数：\n\n*  `model` ， 即表名，最好与当前文件同名\n*  `schema` ， 即mongoose schema\n*  `methods` ， 即schema扩展方法，**推荐把数据库元操作都定义在这个对象中**\n*  `statics` ， 即静态操作方法\n\n#### 3、 在控制器中调用数据库\n\n在控制器中使用非常简单，主要通过`this.mongo`,`this.mongoMap`两个方法。\n\n##### 1） `this.mongo(name)` \n\n调用mongoose Entity对象进行数据库CURD操作\n\n**参数说明:**\n\n`@param [string] name` : 在`app/blog/model/mongo`中配置Schema名，\n\n**返回:**\n\n`@return [object]` 一个实例化Schema之后的Mongoose Entity对象，可以通过调用该对象的methods进行数据库操作\n\n**案例**\n\n参考上文中的Category.js的配置，以`app/blog/controller/dashboard/post.js`为例，如果要在博客列表页中获取博客分类数据：\n\n```javascript\n// http://127.0.0.1/dashboard/post/list\nexports.list = async function (){\n  let cates = await this.mongo('Category').list();\n  this.body = cates;\n}\n```\n\n##### 2）`this.mongoMap(option)`\n\n并行多个数据库操作\n\n**参数说明**\n\n`@param [array] option` \n\n`@param [Object] option[].model`  mongoose Entity对象，通过this.mongo(model)获取\n\n`@param [function] option[].fun`  mongoose Entity对象方法\n\n`@param [array] option[].arg`  mongoose Entity对象方法参数\n\n**返回**\n\n`@return [array]` 数据库操作结果，以对应数组的形式返回\n\n**案例**\n\n```javascript\n  let PostModel = this.mongo('Post');\n  let mongoResult = await this.mongoMap([{\n      model: PostModel,\n      fun: PostModel.page,\n      arg: [pageNum]\n    },{\n      model: PostModel,\n      fun:PostModel.count,\n      arg: [pageNum]\n    }]);\n\n  let posts = mongoResult[0];// 获取第一个查询PostModel.page的结果\n  let page = mongoResult[1]; // 获取第二个查询PostModel.count的结果，两者并发执行\n```\n\n### xload——文件上传下载\n\n\u003e 请注意：不推荐在生产环境中使用文件上传下载功能\n\n与数据库功能一样，文件上传下载功能的使用非常简单，但不推荐在生产环境中使用。因为目前仅支持在单台服务器上使用数据库功能，如果多台机器的服务就有问题了。\n\n如果需要在线上使用上传下载功能，你可以使用proxy的方式pipe到后端接口，或者通过上传组件直接将文件上传到后端的接口。\n\n#### 1、文件上传\n\n方法：\n\n```javascript\nthis.upload([opt])\n```\n\n示例：\n```javascript\nexports.aj_upload = async function() {\n  await this.bindDefault();\n\n  let files = await this.upload();\n  let res = {};\n\n  if (!files || files.length \u003c 1) {\n    res.code = 1;\n    res.message = '上传文件失败！';\n    return this.body = res; \n  }\n\n  res.code = 0;\n  res.message = '';\n  res.data = {\n    files: files\n  }\n\n  return this.body = res;\n}\n```\n\n#### 2、文件下载\n\n方法：\n\n```javascript\nthis.download(filename, [opt])\n```\n\n示例：\n```javascript\nexports.download = async function() {\n  await this.download(this.query.file);\n}\n```\n\n### 其他\n\nGracejs中几个核心的中间件都介绍完毕。此外，还有几个中间件不做详细介绍，了解即可：\n\n1. **gzip实现**：使用gzip压缩response中的body；\n2. **http body内容解析**：解析request中的body，存到`this.request.body`字段中；\n3. **简单的session实现**：通过内存或者redis保存session，不推荐在生产环境中使用；生产环境的session服务由后端自行完成。\n\n最后，关于Gracejs的运维部署在这里不再详述，推荐使用[pm2](https://github.com/Unitech/pm2)，**不用担心重启server期间服务不可用**。\n\n## 五、前端构建\n\n到这里，整个前后端服务的搭建都介绍完了。\n\n在介绍如何结合Gracejs进行前端构建之前，先提一下：这种“更彻底”的前后端分离方案相比于基于MVVM框架的单页面应用具体有什么不同呢？\n\n个人认为有以下几点：\n\n1. **运维部署更灵活**\n  基于Nodejs server的服务端构建，服务器的部署可以与后端机器独立出来。而且后端同学就仅仅需要关注接口的实现。\n2. **前端技术栈更统一**\n  比如：PHP部署页面路由，前端通过MVVM框架实现，前端还需要学习PHP语法来实现后端路由。\n3. **前端架构和选型更便捷**\n  比如你可以很容易通过模板引擎完成BigPipe的架构，你也可以从内网异步并发获取首屏数据。\n\n当然Gracejs是只是服务端框架，前端架构如何选型，随你所愿。\n\n### Boilerplate\n\n目前已经有基于Vue和requirejs的boilerplate。\n\n* [gulp-requirejs-boilerplate](https://github.com/xiongwilee/gulp-requirejs-boilerplate) [![gulp-requirejs-boilerplate](https://img.shields.io/github/stars/xiongwilee/gulp-requirejs-boilerplate.svg?label=%E2%98%85)](https://github.com/xiongwilee/gulp-requirejs-boilerplate)  **Requirejs supported.**（by [@xiongwilee](https://github.com/xiongwilee)）\n\n* [grace-vue-webpack-boilerplate](https://github.com/Thunf/grace-vue-webpack-boilerplate) [![grace-vue-webpack-boilerplate](https://img.shields.io/github/stars/Thunf/grace-vue-webpack-boilerplate.svg?label=%E2%98%85)](https://github.com/Thunf/grace-vue-webpack-boilerplate)  **Both Vue@1.x \u0026 Vue@2.x supported.**（by [@thunf](https://github.com/Thunf)）\n\n* [grace-vue2-webpack-boilerplate](https://github.com/haoranw/grace-vue2-webpack-boilerplate) [![grace-vue2-webpack-boilerplate](https://img.shields.io/github/stars/haoranw/grace-vue2-webpack-boilerplate.svg?label=%E2%98%85)](https://github.com/haoranw/grace-vue2-webpack-boilerplate)  Vue@2.x supported.（by [@haoranw](https://github.com/haoranw)）\n\n这里以基于Vue的构建为例。 \n\n### 目录结构\n\n一个完整的依赖基于vue+Gracejs的目录结构推荐使用这种模式：\n\n```shell\n.\n├── app\n│   └── demo\n│         ├── build\n│         ├── controller\n│         ├── mock\n│         ├── static\n│         ├── views\n│         └── vues\n└── server\n    ├── app\n    │    └── demo\n    ├── middleware\n    ├── ...\n```\n\n当然，server（即：Gracejs）允许你配置app目录路径，你可以放到任意你想要的目录里。\n\n这里的demo模块比默认的server下的demo模块多出来两个目录：`build`和`vues`。\n\n### 构建思路\n\n其实，到这里也能猜到如何进行构建了：`build`目录是基于webpack的编译脚本，`vues`目录是所有的.vue的前端业务文件。\n\nwebpack将vues下的vue文件编译之后产出到`server/app/demo/static`下；其他`controller`等没有必要编译的文件，直接使用webpack的复制插件复制到`server/app/demo/`的对应目录下即可。\n\n有兴趣的同学，推荐看`grace-vue-webpack-boilerplate`下的build实现源码；当然，需要对webpack和vue有一定的了解。\n\n欢迎同学们贡献基于`React`、`Angular`的boilerplate，以邮件或者ISSUE的形式通知我们之后，添加到Gracejs的官方文档中。\n\n## 结语\n\n自此，洋洋洒洒1w多字，Gracejs终于介绍完毕；有兴趣的同学去github赏个star呗：https://github.com/xiongwilee/Gracejs 。\n\n最后，欢迎大家提issue、fork；有任何疑问也可以邮件联系：xiongwilee[at]foxmail.com。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxiongwilee%2Fgracejs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxiongwilee%2Fgracejs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxiongwilee%2Fgracejs/lists"}