Node.JS开源:koa-grace - 基于koa的标准前后端分离框架
KathyFreund
8年前
<p style="text-align:center"><img src="https://simg.open-open.com/show/0e648f3e91454d406b3ac8a2941ad15f.png"></p> <p>koa-grace v1.x版本请移步: <a href="/misc/goto?guid=4959737812995474101" rel="nofollow,noindex">https://github.com/xiongwilee/koa-grace/tree/master</a></p> <h2>koa-grace v2</h2> <p>Gracejs (又称:koa-grace v2) 是全新的基于 koa v2.x 的MVC+RESTful架构的前后端分离框架。</p> <h2>一、简介</h2> <p>Gracejs是 koa-grace 的升级版,也可以叫koa-grace v2。</p> <p>主要特性包括:</p> <ol> <li>支持MVC架构,可以更便捷地生成服务端路由;</li> <li>标准的RESTful架构,支持后端接口异步并发,页面性能更优;</li> <li>一套Node环境经服务服务多个站点应用,部署更简单;</li> <li>优雅的MOCK功能,开发环境模拟数据更流畅;</li> <li>完美支持async/await及generator语法,随心所欲;</li> <li>更灵活的前端构建选型,想用什么就用什么,随你所愿。</li> </ol> <p>相比于koa-grace v1(以下简称:koa-grace): <strong>Gracejs完美支持koa v2</strong> ,同时做了优化虚拟host匹配和路由匹配的性能、还完善了部分测试用例等诸多升级。当然,如果你正在使用koa-grace也不用担心,我们会把Gracejs中除了支持koa2的性能和功能特性移植到koa-grace的相应中间件中。</p> <h2>二、快速开始</h2> <p>注意:请确保你的运行环境中Nodejs的版本至少是 v4.0.0 ,目前需要依赖Babel。 (当然26日凌晨 nodejs v7 已经release,你也可以不依赖Babel,直接通过 --harmony_async_await 模式启动。)</p> <h3>安装</h3> <p>执行命令:</p> <pre> <code class="language-javascript">$ git clone -b v2.x https://github.com/xiongwilee/koa-grace.git $ cd koa-grace && npm install</code></pre> <h3>运行</h3> <p>然后,执行命令:</p> <pre> <code class="language-javascript">$ npm run dev</code></pre> <p>然后访问: <a href="/misc/goto?guid=4959617512231524450" rel="nofollow,noindex">http://127.0.0.1:3000</a> 就可以看到示例了!</p> <h2>三、案例说明</h2> <p>这里参考 <a href="/misc/goto?guid=4959737813137327507" rel="nofollow,noindex">https://github.com/xiongwilee/koa-grace/tree/v2.x</a> 中 app/demo 目录下的示例,详解Gracejs的MVC+RESTful架构的实现。</p> <p>此前也有文章简单介绍过koa-grace的实现( <a href="/misc/goto?guid=4959737813223253373" rel="nofollow,noindex">https://github.com/xiongwilee/koa-grace/wiki</a> ),但考虑到Gracejs的差异性,这里再从 <strong>目录结构</strong> 、 <strong>MVC模型实现</strong> 、 <strong>proxy机制</strong> 这三个关键点做一些比较详细的说明。</p> <h3>目录结构</h3> <p>Gracejs与koa-grace v1.x版本的目录结构完全一致:</p> <pre> <code class="language-javascript">. ├── controller │ ├── data.js │ ├── defaultCtrl.js │ └── home.js ├── static │ ├── css │ ├── image │ └── js └── views └── home.html</code></pre> <p>其中:</p> <ul> <li>controller 用以存放路由及控制器文件</li> <li>static 用以存放静态文件</li> <li>views 用以存放模板文件</li> </ul> <p>需要强调的是, 这个目录结构是生产环境代码的标准目录结构。在开发环境里你可以任意调整你的目录结构,只要保证编译之后的产出文件以这个路径输出即可 。</p> <h3>MVC模型实现</h3> <p>为了满足更多的使用场景,在Gracejs中加入了简单的Mongo数据库的功能。</p> <p>但准确的说,前后端的分离的Nodejs框架都是VC架构,并没有Model层。因为 <strong>前后端分离框架不应该有任何数据库、SESSION存储的职能</strong> 。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/93f656596c49dc97a2c1ed6ea50103e8.jpg"></p> <p>如上图,具体流程如下:</p> <ul> <li>第一步,Nodejs server(也就是Gracejs服务)监听到用户请求;</li> <li>第二步,Gracejs的各个中间件(Middlewares)对请求上下文进行处理;</li> <li>第三步,根据当前请求的path和method,进入对应的Controller;</li> <li>第四步,通过http请求以proxy的模式向后端获取数据;</li> <li>第五步,拼接数据,渲染模板。</li> </ul> <p>这里的第四步,proxy机制,就是Gracejs实现前后端分离的核心部分。</p> <h3>proxy机制</h3> <p>以实现一个电商应用下的“个人中心”页面为例。假设这个页面的首屏包括:用户基本信息模块、商品及订单模块、消息通知模块。</p> <p>后端完成服务化架构之后,这三个模块可以解耦,拆分成三个HTTP API接口。这时候就可以通过Gracejs的 this.proxy 方法,去后端异步并发获取三个接口的数据。</p> <p>如下图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/5042ac72d61735ccc8896aed0b7752ce.jpg"></p> <p>这样有几个好处:</p> <ol> <li>在Nodejs层(服务端)异步并发向后端(服务端)获取数据,可以使HTTP走内网,性能更优;</li> <li>后端的接口可以同时提供给客户端,实现接口给Web+APP复用,后端开发成本更低;</li> <li>在Nodejs层获取数据后,直接交给页面,不管前端用什么技术栈,可以使首屏体验更佳。</li> </ol> <p>那么,这么做是不是就完美了呢?肯定不是:</p> <ol> <li>后端接口在外网开放之后,如何保证接口安全性?</li> <li>如果当前页面请求是GET方法,但我想POST到后端怎么办?</li> <li>我想在Controller层重置post参数怎么办?</li> <li>后端接口设置cookie如何带给浏览器?</li> <li>经过一层Nodejs的代理之后,如何保证SESSION状态不丢失?</li> <li>如果当前请求是一个file文件流,又该怎么办呢? ...</li> </ol> <p>好消息是,这些问题在proxy中间件中都考虑过了。这里不再一一讲解,有兴趣可以看koa-grace-proxy的源码: <a href="/misc/goto?guid=4959737813314836656" rel="nofollow,noindex">https://github.com/xiongwilee/koa-grace/tree/v2.x/middleware/proxy</a> 。</p> <h2>四、详细使用手册</h2> <p>在看详细使用手册之前,建议先看一下Gracejs的主文件源码: <a href="/misc/goto?guid=4959737813400051847" rel="nofollow,noindex">https://github.com/xiongwilee/koa-grace/blob/v2.x/src/app.js</a> 。</p> <p>这里不再浪费篇幅贴代码了,其实想说明的就是: <strong>Gracejs是一个个关键中间件的集合</strong> 。</p> <p>所有中间件都在 middleware 目录下,配置由 config/main.*.js 管理。</p> <p>关于配置文件:</p> <ol> <li>配置文件extend关系为:config/server.json的merge字段 > config/main.*.js > config.js;</li> <li>配置生成后保存在Gracejs下的全局作用域 global.config 里,方便读取。</li> </ol> <p>下面介绍几个关键中间件的作用和使用方法。</p> <h3>vhost——多站点配置</h3> <p>vhost 在这里可以理解为,一个Gracejs server服务于几个站点。Gracejs支持通过 host 及 host + 一级path 两种方式的 <strong>映射</strong> 。所谓的隐射,其实就是一个域名(或者一个域名+一级path)对应一个应用,一个应用对应一个目录。</p> <p>注意:考虑到正则的性能问题,vhost不会考虑正则映射。</p> <p>参考 config/main.development.js ,可以这么配置vhost:</p> <pre> <code class="language-javascript">// vhost配置 vhost: { '127.0.0.1':'demo', '127.0.0.1/test':'demo_test', 'localhost':'blog', }</code></pre> <p>其中, demo , demo_test , blog 分别对应 app/ 下的三个目录。当然你也可以指定目录路径,在配置文件中修改 path.project 配置即可:</p> <pre> <code class="language-javascript">// 路径相关的配置 path: { // project project: './app/' }</code></pre> <h3>router——路由及控制器</h3> <p>Gracejs中生成路由的方法非常简单,以自带的demo模块为例,进入demo模块的controller目录: app/demo/controller 。</p> <p>文件目录如下:</p> <pre> <code class="language-javascript">controller ├── data.js ├── defaultCtrl.js └── home.js</code></pre> <p>1、 文件路径即路由</p> <p>router中间件会找到模块中所有以 .js 结尾的文件,根据文件路径和module.exports生成路由。</p> <p>例如,demo模块中的home.js文件:</p> <pre> <code class="language-javascript">exports.index = async function () { await this.bindDefault(); await this.render('home', { title: 'Hello , Grace!' }); } exports.hello = function(){ this.body = 'hello world!' }</code></pre> <p>则生成 /home/index 、 /home 、 /home/hello 的路由。需要说明几点:</p> <ol> <li>如果路由是以 /index 结尾的话,Gracejs会"赠送"一个去掉 /index 的同样路由;</li> <li>如果当前文件是一个依赖,仅仅被其他文件引用;则在文件中配置 exports.__controller__ = false ,该文件就不会生成路由了;参考 defaultCtrl.js</li> <li>这里的控制器函数可以是 await/async 或 generator 函数,也可以是一个普通的函数;Gracejs中推荐使用 await/async ;</li> <li>这里的路由文件包裹在一个目录里也是可以的,可以参考: app/blog 中的controller文件;</li> <li>如果当前文件路由就是一个独立的控制器,则 module.exports 返回一个任意函数即可。</li> </ol> <p>最后,如果用户访问的路由查找不到,router会默认查找 /error/404 路由,如果有则渲染 error/404 页(不会重定向到 error/404 ),如果没有则返回404。</p> <p>2、 路由文件使用说明</p> <p>将demo模块中的home.js扩展一下:</p> <pre> <code class="language-javascript">exports.index = async function () { ... } exports.index.__method__ = 'get'; exports.index.__regular__ = null;</code></pre> <p>另外,需要说明以下几点:</p> <ul> <li>如果需要配置dashboard/post/list请求为 DELETE 方法,则post.js中声明 exports.list.__method__ = 'delete' 即可( <strong>不声明默认注入get及post方法</strong> );</li> <li>如果要配置更灵活的路由,则中声明 exports.list.__regular__ = '/:id'; 即可</li> <li>需要注意的是:如果 __regular__ 配置为正则表达式的话,则会生成当前控制器默认路由及正则可匹配的路由</li> </ul> <p>当然,如果路由文件中的所有控制器方法都是post方法,您可以在控制器文件最底部加入: module.exports.__method__ = 'post' 即可, __regular__ 的配置同理。</p> <p>注意:一般情况这里不需要额外的配置,为了保证代码美观,没有特殊使用场景的话就不要写 __method__ 和 __regular__ 配置。</p> <p>3、 控制器</p> <p>将demo模块中的home.js的index方法再扩展一下:</p> <pre> <code class="language-javascript">exports.index = async function () { // 绑定默认控制器方法 await this.bindDefault(); // 获取数据 await this.proxy(...) // 渲染目标引擎 await this.render('home', { title: 'Hello , Grace!' }); }</code></pre> <p>它就是一个标准的控制器(controller)了。这个控制器的作用域就是当前koa的context,你可以任意使用koa的context的任意方法。</p> <p>几个关键context属性的使用说明如下:</p> <p>koa自带:</p> <p>更多koa自带context属性,请查看koajs官网: <a href="/misc/goto?guid=4958827254298637858" rel="nofollow,noindex">http://koajs.com/</a></p> <table> <thead> <tr> <th>context属性</th> <th>类型</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>this.request.href</td> <td>String</td> <td>当前页面完整URL,也可以简写为 this.href</td> </tr> <tr> <td>this.request.query</td> <td>object</td> <td>get参数,也可以简写为 this.query</td> </tr> <tr> <td>this.response.set</td> <td>function</td> <td>设置response头信息,也可以简写为 this.set</td> </tr> <tr> <td>this.cookies.set</td> <td>function</td> <td>设置cookie,参考: <a href="/misc/goto?guid=4959737813516571154" rel="nofollow,noindex">cookies</a></td> </tr> <tr> <td>this.cookies.get</td> <td>function</td> <td>获取cookie,参考: <a href="/misc/goto?guid=4959737813598522701" rel="nofollow,noindex">cookies</a></td> </tr> </tbody> </table> <p>Gracejs注入:</p> <table> <thead> <tr> <th>context属性</th> <th>类型</th> <th>中间件</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>this.bindDefault</td> <td>function</td> <td>router</td> <td>公共控制器,相当于 require('app/*/controller/defaultCtrl.js')</td> </tr> <tr> <td>this.request.body</td> <td>object</td> <td>body</td> <td>post参数,可以直接在this.request.body中获取到post参数</td> </tr> <tr> <td>this.render</td> <td>function</td> <td>views</td> <td>模板引擎渲染方法,请参看: 模板引擎- Template engine</td> </tr> <tr> <td>this.mongo</td> <td>function</td> <td>mongo</td> <td>数据库操作方法,请参看: 数据库 - Database</td> </tr> <tr> <td>this.mongoMap</td> <td>function</td> <td>mongo</td> <td>并行数据库多操作方法,请参看: 数据库 - Database</td> </tr> <tr> <td>this.proxy</td> <td>function</td> <td>proxy</td> <td>RESTful数据请求方法,请参看:数据代理</td> </tr> <tr> <td>this.fetch</td> <td>function</td> <td>proxy</td> <td>从服务器导出文件方法,请参看: 请求代理</td> </tr> <tr> <td>this.backData</td> <td>Object</td> <td>proxy</td> <td>默认以Obejct格式存储this.proxy后端返回的JSON数据</td> </tr> <tr> <td>this.upload</td> <td>function</td> <td>xload</td> <td>文件上传方法,请参看: 文件上传下载</td> </tr> <tr> <td>this.download</td> <td>function</td> <td>xload</td> <td>文件下载方法,请参看: 文件上传下载</td> </tr> </tbody> </table> <p>4、控制器中异步函数的写法</p> <p>在控制器中,如果还有其他的异步方法,可以通过Promise来实现。例如:</p> <pre> <code class="language-javascript">exports.main = async function() { await ((test) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(test) }, 3000) }); })('测试') }</code></pre> <h3>proxy——数据代理</h3> <p>Gracejs支持两种数据代理场景:</p> <ol> <li>单纯的数据代理,任意请求到后端接口,然后返回json数据(也包括文件流请求到后端,后端返回json数据);</li> <li>文件代理,请求后端接口,返回一个文件(例如验证码图片);</li> </ol> <p>下面逐一介绍两种代理模式的使用方法。</p> <p>1、 数据代理</p> <p>数据代理可以在控制器中使用 this.proxy 方法:</p> <pre> <code class="language-javascript">this.proxy(object|string,[opt])</code></pre> <p>使用方法</p> <p>this.proxy 方法返回的是一个Promise,所以这里你可以根据当前Controller的类型使用 async/await 或者 Generator 实现异步并发。例如:</p> <p>async/await:</p> <pre> <code class="language-javascript">exports.demo = async function () { await this.proxy({ /* ... */ }) }</code></pre> <p>Generator:</p> <pre> <code class="language-javascript">exports.demo = function * () { yield this.proxy({ /* ... */ }) }</code></pre> <p>为了使语法更简便,可以在执行 this.proxy 之后,直接在上下文中的 backData 字段中获取到数据。例如:</p> <pre> <code class="language-javascript">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 : {...} * } */ }</code></pre> <p>Generator 方法亦然。</p> <p>此外,如果要获取proxy的请求头信息,你可以在proxy方法返回的内容中获取到,例如:</p> <pre> <code class="language-javascript">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 : { * headers: {...} // 头信息 * body: {...} // 未处理的response body * ... // ... * }, * otherInfo : {...} * } */ }</code></pre> <p>使用场景一:多个数据请求的代理</p> <p>可以发现,上文的案例就是多个数据同时请求的代理方案,这里也就是 <strong>异步并发</strong> 获取数据的实现。使用 this.proxy 方法实现多个数据异步并发请求非常简单:</p> <pre> <code class="language-javascript">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 : {...} * } */ }</code></pre> <p>然后,proxy的结果会默认注入到上下文的 this.backData 对象中。</p> <p>使用场景二:单个数据请求的代理</p> <p>如果只是为了实现一个接口请求代理,可以这么写:</p> <pre> <code class="language-javascript">exports.demo = async function (){ await this.proxy('github:post:user/login/oauth/access_token?client_id=****'); }</code></pre> <p>这样proxy请求返回的数据体会直接赋值给 this.body ,也就是将这个请求直接返回给客户端。</p> <p>说明</p> <p>github:post:user/login/oauth/access_token?client_id=**** 说明如下:</p> <ul> <li>github : 为在 config/main.*.js 的 api 对象中进行配置;</li> <li>post : 为数据代理请求的请求方法,该参数可以不传,默认为 get</li> <li>path : 后面请求路径中的query参数会覆盖当前页面的请求参数(this.query),将query一同传到请求的接口</li> <li>你也可以写完整的路径: {userInfo:'https://api.github.com/user/login?test=test'}</li> </ul> <p>另外, this.proxy 的形参说明如下:</p> <table> <thead> <tr> <th>参数名</th> <th>类型</th> <th>默认</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>dest</td> <td>Object</td> <td>this.backData</td> <td>指定接收数据的对象,默认为 this.backData</td> </tr> <tr> <td>conf</td> <td>Obejct</td> <td>{}</td> <td>this.proxy使用 <a href="/misc/goto?guid=4959643065847412898" rel="nofollow,noindex">Request.js</a> 实现,此为传给request的重置配置(你可以在这里设置接口超时时间: conf: { timeout: 25000 } )</td> </tr> <tr> <td>form</td> <td>Object</td> <td>{}</td> <td>指定post方法的post数据,默认为当前页面的post数据</td> </tr> </tbody> </table> <p>关于this.proxy方法还有很多有趣的细节,推荐有兴趣的同学看源码: <a href="/misc/goto?guid=4959737813314836656" rel="nofollow,noindex">https://github.com/xiongwilee/koa-grace/tree/v2.x/middleware/proxy</a></p> <p>2、 文件代理</p> <p>文件代理可以在控制器中使用 this.fetch 方法:</p> <pre> <code class="language-javascript">this.fetch(string)</code></pre> <p>文件请求代理也很简单,比如如果需要从github代理一个图片请求返回到浏览器中,参考: <a href="/misc/goto?guid=4959737813728967772" rel="nofollow,noindex">http://feclub.cn/user/avatar?img=https://avatars.githubusercontent.com/u/1962352?v=3</a> , 或者要使用导出文件的功能:</p> <pre> <code class="language-javascript">exports.avatar = async function (){ await this.fetch(imgUrl); }</code></pre> <p>这里需要注意的是: 在this.fetch方法之后会直接结束response, 不会再往其他中间件执行 。</p> <h3>views——视图层</h3> <p>默认的模板引擎为 swig ,但swig作者已经停止维护;你可以在 config/main.*.js 中配置 template 属性想要的模板引擎:</p> <pre> <code class="language-javascript">// 模板引擎配置 template: 'nunjucks'</code></pre> <p>你还可以根据不同的模块配置不同的模板引擎:</p> <pre> <code class="language-javascript">template: { blog:'ejs' }</code></pre> <p>目前支持的模板引擎列表在这里: <a href="/misc/goto?guid=4959666989330667396" rel="nofollow,noindex">consolidate.js#supported-template-engines</a></p> <p>在控制器中调用 this.render 方法渲染模板引擎:</p> <pre> <code class="language-javascript">exports.home = await function () { await this.render('dashboard/site_home',{ breads : ['站点管理','通用'], userInfo: this.userInfo, siteInfo: this.siteInfo }) }</code></pre> <p>模板文件在模块路径的 /views 目录中。</p> <p>注意一点:Gracejs渲染模板时,默认会将 main.*.js 中constant配置交给模板数据;这样,如果你想在页面中获取公共配置(比如:CDN的地址)的话就可以在模板数据中的 constant 子中取到。</p> <h3>static——静态文件服务</h3> <p>静态文件的使用非常简单,将 /static/**/ 或者 /*/static/* 的静态文件请求代理到了模块路径下的 /static 目录:</p> <pre> <code class="language-javascript">// 配置静态文件路由 app.use(Middles.static(['/static/**/*', '/*/static/**/*'], { dir: config_path_project, maxage: config_site.env == 'production' && 60 * 60 * 1000 }));</code></pre> <p>以案例中 blog 的静态文件为例,静态文件在blog项目下的路径为: app/blog/static/image/bg.jpg ,则访问路径为 <a href="/misc/goto?guid=4959737813845855722" rel="nofollow,noindex">http://127.0.0.1/blog/static/image/bg.jpg</a> 或者 <a href="/misc/goto?guid=4959737813929451900" rel="nofollow,noindex">http://127.0.0.1/static/blog/image/bg.jpg</a></p> <p>注意两点:</p> <ol> <li>静态文件端口和当前路由的端口一致,所以 /static/**/ 或者 /*/static/* 形式的路由会是无效的;</li> <li>推荐在生产环境中,使用Nginx做静态文件服务,购买CDN托管静态文件;</li> </ol> <h3>mock——Mock数据</h3> <p>MOCK功能的实现其实非常简单,在开发环境中你可以很轻易地使用MOCK数据。</p> <p>以demo模块为例,首先在 main.development.js 配置文件中添加proxy配置:</p> <pre> <code class="language-javascript">// controller中请求各类数据前缀和域名的键值对 api: { // ... demo: 'http://${ip}:${port}/__MOCK__/demo/' // ... }</code></pre> <p>然后,在demo模块中添加 mock 文件夹,然后添加 test.json :</p> <p>文件结构:</p> <pre> <code class="language-javascript">. ├── controller ├── mock | └── test.json ├── static └── views</code></pre> <p>文件内容(就是你想要的请求返回内容):</p> <p>在JSON文件内容中也可以使用注释:</p> <pre> <code class="language-javascript">/* * 获取用户信息接口 */ { code:0 // 这是code }</code></pre> <p>然后,你可以打开浏览器访问: http://${ip}:${port}/__MOCK__/demo/test 验证是否已经返回了test.json里的数据。</p> <p>最后在你的controller业务代码中就可以通过proxy方法获取mock数据了:</p> <pre> <code class="language-javascript">this.proxy({ test:'demo:test' })</code></pre> <p>注意:</p> <ul> <li>如果你的mock文件路径是/mock/test/subtest.json 那么proxy路径则是:test/subtest;</li> <li>强烈建议将mock文件统一为真正的后端请求路径,这样以实现真实路径的mock;</li> </ul> <p>可以参考这个: <a href="/misc/goto?guid=4959737814014335921" rel="nofollow,noindex">koa-grace中的mock功能的示例</a></p> <h3>secure——安全模块</h3> <p>考虑到用户路由完全由Nodejs托管以后,CSRF的问题也得在Nodejs层去防护了。此前写过一片文章: <a href="/misc/goto?guid=4959737814098196564" rel="nofollow,noindex">前后端分离架构下CSRF防御机制</a> ,这里就只写使用方法,不再详述原理。</p> <p>在Gracejs中可以配置:</p> <pre> <code class="language-javascript">// csrf配置 csrf: { // 需要进行xsrf防护的模块名称 module: [] }</code></pre> <p>然后,在业务代码中,获取名为: grace_token 的cookie,以post或者get参数回传即可。当然,如果你不想污染ajax中的参数对象,你也可以将这个cookie值存到 x-grace-token 头信息中。</p> <p>Gracejs监听到post请求,如果token验证失效,则直接返回错误。</p> <h3>mongo——简单的数据库</h3> <p>请注意:不推荐在生产环境中使用数据库功能</p> <p>在Gracejs中使用mongoDB非常简单,当然没有做过任何压测,可能存在性能问题。</p> <p>1、 连接数据库</p> <p>在配置文件 config/main.*.js 中进行配置:</p> <pre> <code class="language-javascript">// mongo配置 mongo: { options:{ // mongoose 配置 }, api:{ 'blog': 'mongodb://localhost:27017/blog' } },</code></pre> <p>其中, mongo.options 配置mongo连接池等信息, mongo.api 配置站点对应的数据库连接路径。</p> <p>值得注意的是, 配置好数据库之后,一旦koa-grace server启动mongoose就启动连接,直到koa-grace server关闭</p> <p>2、 mongoose的schema配置</p> <p>依旧以案例 blog 为例,参看 app/blog/model/mongo 目录:</p> <pre> <code class="language-javascript">└── mongo ├── Category.js ├── Link.js ├── Post.js └── User.js</code></pre> <p>一个js文件即一个数据库表即相关配置,以 app/blog/model/mongo/Category.js :</p> <pre> <code class="language-javascript">'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;</code></pre> <p>主要有四个参数:</p> <ul> <li>model , 即表名,最好与当前文件同名</li> <li>schema , 即mongoose schema</li> <li>methods , 即schema扩展方法, <strong>推荐把数据库元操作都定义在这个对象中</strong></li> <li>statics , 即静态操作方法</li> </ul> <p>3、 在控制器中调用数据库</p> <p>在控制器中使用非常简单,主要通过 this.mongo , this.mongoMap 两个方法。</p> <p>1) this.mongo(name)</p> <p>调用mongoose Entity对象进行数据库CURD操作</p> <p>参数说明:</p> <p>@param [string] name : 在 app/blog/model/mongo 中配置Schema名,</p> <p>返回:</p> <p>@return [object] 一个实例化Schema之后的Mongoose Entity对象,可以通过调用该对象的methods进行数据库操作</p> <p>案例</p> <p>参考上文中的Category.js的配置,以 app/blog/controller/dashboard/post.js 为例,如果要在博客列表页中获取博客分类数据:</p> <pre> <code class="language-javascript">// http://127.0.0.1/dashboard/post/list exports.list = async function (){ let cates = await this.mongo('Category').list(); this.body = cates; }</code></pre> <p>2) this.mongoMap(option)</p> <p>并行多个数据库操作</p> <p>参数说明</p> <p>@param [array] option</p> <p>@param [Object] option[].model mongoose Entity对象,通过this.mongo(model)获取</p> <p>@param [function] option[].fun mongoose Entity对象方法</p> <p>@param [array] option[].arg mongoose Entity对象方法参数</p> <p>返回</p> <p>@return [array] 数据库操作结果,以对应数组的形式返回</p> <p>案例</p> <pre> <code class="language-javascript">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的结果,两者并发执行</code></pre> <h3>xload——文件上传下载</h3> <p>请注意:不推荐在生产环境中使用文件上传下载功能</p> <p>与数据库功能一样,文件上传下载功能的使用非常简单,但不推荐在生产环境中使用。因为目前仅支持在单台服务器上使用数据库功能,如果多台机器的服务就有问题了。</p> <p>如果需要在线上使用上传下载功能,你可以使用proxy的方式pipe到后端接口,或者通过上传组件直接将文件上传到后端的接口。</p> <p>1、文件上传</p> <p>方法:</p> <pre> <code class="language-javascript">this.upload([opt])</code></pre> <p>示例:</p> <pre> <code class="language-javascript">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; }</code></pre> <p>2、文件下载</p> <p>方法:</p> <pre> <code class="language-javascript">this.download(filename, [opt])</code></pre> <p>示例:</p> <pre> <code class="language-javascript">exports.download = async function() { await this.download(this.query.file); }</code></pre> <h3>其他</h3> <p>Gracejs中几个核心的中间件都介绍完毕。此外,还有几个中间件不做详细介绍,了解即可:</p> <ol> <li><strong>gzip实现</strong> :使用gzip压缩response中的body;</li> <li><strong>http body内容解析</strong> :解析request中的body,存到 this.request.body 字段中;</li> <li><strong>简单的session实现</strong> :通过内存或者redis保存session,不推荐在生产环境中使用;生产环境的session服务由后端自行完成。</li> </ol> <p>最后,关于Gracejs的运维部署在这里不再详述,推荐使用 pm2 , <strong>不用担心重启server期间服务不可用</strong> 。</p> <h2>五、前端构建</h2> <p>到这里,整个前后端服务的搭建都介绍完了。</p> <p>在介绍如何结合Gracejs进行前端构建之前,先提一下:这种“更彻底”的前后端分离方案相比于基于MVVM框架的单页面应用具体有什么不同呢?</p> <p>个人认为有以下几点:</p> <ol> <li><strong>运维部署更灵活</strong> 基于Nodejs server的服务端构建,服务器的部署可以与后端机器独立出来。而且后端同学就仅仅需要关注接口的实现。</li> <li><strong>前端技术栈更统一</strong> 比如:PHP部署页面路由,前端通过MVVM框架实现,前端还需要学习PHP语法来实现后端路由。</li> <li><strong>前端架构和选型更便捷</strong> 比如你可以很容易通过模板引擎完成BigPipe的架构,你也可以从内网异步并发获取首屏数据。</li> </ol> <p>当然Gracejs是只是服务端框架,前端架构如何选型,随你所愿。</p> <h3>Boilerplate</h3> <p>目前已经有基于Vue和requirejs的boilerplate。</p> <ul> <li> <p><a href="/misc/goto?guid=4959737814188638020" rel="nofollow,noindex">gulp-requirejs-boilerplate</a> <strong>Requirejs supported.</strong> (by <a href="/misc/goto?guid=4959737814270027684" rel="nofollow,noindex">@xiongwilee</a> )</p> </li> <li> <p><a href="/misc/goto?guid=4959737814348012066" rel="nofollow,noindex">grace-vue-webpack-boilerplate</a> <strong> Both <a href="/misc/goto?guid=4959737814442098516" rel="nofollow,noindex">Vue@1.x</a> & <a href="/misc/goto?guid=4959737814515147538" rel="nofollow,noindex">Vue@2.x</a> supported. </strong> (by <a href="/misc/goto?guid=4959737814598463981" rel="nofollow,noindex">@thunf</a> )</p> </li> <li> <p><a href="/misc/goto?guid=4959737814693361642" rel="nofollow,noindex">grace-vue2-webpack-boilerplate</a> <a href="/misc/goto?guid=4959737814515147538" rel="nofollow,noindex">Vue@2.x</a> supported.(by <a href="/misc/goto?guid=4959737814781121999" rel="nofollow,noindex">@haoranw</a> )</p> </li> </ul> <p>这里以基于Vue的构建为例。</p> <h3>目录结构</h3> <p>一个完整的依赖基于vue+Gracejs的目录结构推荐使用这种模式:</p> <pre> <code class="language-javascript">. ├── app │ └── demo │ ├── build │ ├── controller │ ├── mock │ ├── static │ ├── views │ └── vues └── server ├── app │ └── demo ├── middleware ├── ...</code></pre> <p>当然,server(即:Gracejs)允许你配置app目录路径,你可以放到任意你想要的目录里。</p> <p>这里的demo模块比默认的server下的demo模块多出来两个目录: build 和 vues 。</p> <h3>构建思路</h3> <p>其实,到这里也能猜到如何进行构建了: build 目录是基于webpack的编译脚本, vues 目录是所有的.vue的前端业务文件。</p> <p>webpack将vues下的vue文件编译之后产出到 server/app/demo/static 下;其他 controller 等没有必要编译的文件,直接使用webpack的复制插件复制到 server/app/demo/ 的对应目录下即可。</p> <p>有兴趣的同学,推荐看 grace-vue-webpack-boilerplate 下的build实现源码;当然,需要对webpack和vue有一定的了解。</p> <p>欢迎同学们贡献基于 React 、 Angular 的boilerplate,以邮件或者ISSUE的形式通知我们之后,添加到Gracejs的官方文档中。</p> <h2>结语</h2> <p>自此,洋洋洒洒1w多字,Gracejs终于介绍完毕;有兴趣的同学去github赏个star呗: <a href="/misc/goto?guid=4959666987520863974" rel="nofollow,noindex">https://github.com/xiongwilee/koa-grace</a> 。</p> <p>最后,欢迎大家提issue、fork;有任何疑问也可以邮件联系:xiongwilee[at]foxmail.com。</p> <p> </p> <p> </p>