Node.js 国产 MVC 框架 ThinkJS 开发 controller 篇(续)

原创:荆秀网 网页即时推送 https://xxuyou.com | 转载请注明出处
链接:https://blog.xxuyou.com/nodejs-thinkjs-study-controller-2/

本系列教程以 ThinkJS v2.x 版本(官网)为例进行介绍,教程以实际操作为主。

本篇继续讲解 Controller 的使用。

构造方法

如果想要在对象实例化的时候做点事情,构造方法是最好的选择。ES6 提供的构造方法是 constructor

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
http://es6.ruanyifeng.com/#docs/class#constructor-方法
ECMAScript 6 入门 作者:阮一峰

init 与 constructor

thinkjs 强大的地方在于,我们不仅可以规规矩矩的 export default class 自己声明 Class ,还提供了动态创建 Class 的方法:think.controller

但是 thinkjs 动态创建的 Class 没有 constructor,而是提供了一个 init 作为构造方法的替代方法,该方法的使用方式与 constructor 一致。

上一篇文章(Node.js 国产 MVC 框架 ThinkJS 开发 controller 篇 基类与继承链部分)中也有 init 方法的使用示例,再看代码:

// src/home/controller/base.js
'use strict';
export default class extends think.controller.base {
  init(...args) {
    super.init(...args);
    // 要求全部 url 必须携带 auth 参数
    let auth = this.get('auth');
    if (think.isEmpty(auth)) {
      return this.error(500, '全部 url 必须携带 auth 参数');
    }
  }
}

当然这并不是表示不能使用 constructor 方法了,假如你是像我一样习惯使用 export default class 自己声明 Class 的筒子,还是可以用回标准的 constructor 方法的。

thinkjs 动态创建 Class 的方法参见官方文档,这里不再赘述。

魔术方法

thinkjs 实现了几个很有用的魔术方法,为开发提供了极大的便利,手动点赞~

__before 前置操作

顾名思义,前置操作会抢先在 Controller 中具体的 Action 执行之前执行,就是“在 xxx 之前执行”的意思。来看代码:

// src/home/controller/user.js
'use strict';
export default class extends think.controller.base {
  __before() {
    console.log('this is __before().');
  }
  indexAction() {
    console.log('this is indexAction().');
    return this.end();
  }
}
// 访问 /home/user/index 的执行结果如下:
// this is __before().
// this is indexAction().

那么可能有人会说:看上去 __beforeinit 是一样的用途嘛。老规矩,来看代码:

// src/home/controller/user.js
'use strict';
export default class extends think.controller.base {
  init(...args) {
    super.init(...args);
    console.log('this is init().');
  }
  __before() {
    console.log('this is __before().');
  }
  indexAction() {
    console.log('this is indexAction().');
    return this.end();
  }
}
// 访问 /home/user/index 的执行结果如下:
// this is init().
// this is __before().
// this is indexAction().

看到了吗?执行还是有先后顺序的,再来个复杂一点的:

// src/home/controller/base.js
'use strict';
export default class extends think.controller.base {
  init(...args) {
    super.init(...args);
    console.log('this is base.init().');
  }
}
// src/home/controller/user.js
'use strict';
export default class extends think.controller.base {
  init(...args) {
    super.init(...args);
    console.log('this is user.init().');
  }
  __before() {
    console.log('this is user.__before().');
  }
  indexAction() {
    console.log('this is user.indexAction().');
    return this.end();
  }
}
// 访问 /home/user/index 的执行结果如下:
// this is base.init().
// this is user.init().
// this is user.__before().
// this is user.indexAction().

好吧,你会说“意料之中”~

__after 后置操作

明白了前置操作,后置操作也不难理解,看代码:

// src/home/controller/user.js
'use strict';
export default class extends think.controller.base {
  init(...args) {
    super.init(...args);
    console.log('this is init().');
  }
  __before() {
    console.log('this is __before().');
  }
  __after() {
    console.log('this is __after().');
  }
  indexAction() {
    console.log('this is indexAction().');
    return this.end();
  }
}
// 访问 /home/user/index 的执行结果如下:
// this is init().
// this is __before().
// this is indexAction().

咦?貌似有地方不对。。。__after 没执行。

这当然不是 __after 写在 indexAction 上面导致的!修改代码:

// src/home/controller/user.js
'use strict';
export default class extends think.controller.base {
  init(...args) {
    super.init(...args);
    console.log('this is init().');
  }
  __before() {
    console.log('this is __before().');
  }
  __after() {
    console.log('this is __after().');
    return this.end();
  }
  indexAction() {
    console.log('this is indexAction().');
  }
}
// 访问 /home/user/index 的执行结果如下:
// this is init().
// this is __before().
// this is indexAction().
// this is __after().

这次 OK 了,和预期的结果一致。

我知道细心的你已经注意到有句代码 return this.end()indexAction 移动到 __after 里面了。

this.end() 内部执行了 Node.js HTTP response.end() 操作,表示整个响应流结束了,因此如果想要启用 __after 的话,这句代码就要放在 __after 里面运行。

__call 空操作

这个魔术方法有点特殊,它不像前两个魔术方法一样用于在某个流程节点打点运行,而是分担了 init 的部分职责:用于检测当某个 Controller 被访问的 Action 并未定义的情况下,由 __call 来接手运行。

// src/home/controller/user.js
'use strict';
export default class extends think.controller.base {
  init(...args) {
    super.init(...args);
    console.log('this is init().');
  }
  __call() {
    console.log(this.http.action + 'Action is not exists.');
    return this.end();
  }
  indexAction() {
    console.log('this is indexAction().');
    return this.end();
  }
}
// 访问 /home/user/test 的执行结果如下:
// this is init().
// testAction is not exists.

可以看到当访问的 testAction 不存在时,框架会运行 __call 来进行处理,我们的处理是记录错误并结束响应输出。

示例代码是将 __call 放在了二级子类中,通常是放在基类中,可以管控全部子类的非法访问处理。

提示:本方法只能用于捕捉 Action 不存在的情况,但是假如 Controller 不存在,会直接触发 404 错误(被框架接管)而无法干涉。
如要捕捉 Controller 不存在的情况,需要扩展框架的错误类,另文描述。

外部调用方式

thinkjs 官网 API 中有实例化另外一个 Controller 的接口,但是并没有说明这个具体有什么用途:

//实例化 home 模块下 user controller
let instance = think.controller('user', http, 'home');

那么通常这个方法可以用来实例化兄弟层级 Controller ,或者获取数据、或者触发一个业务流程等,来看代码:

// src/home/controller/user.js 增加
_getPoints() {
  return 8000;
}

// src/home/controller/index.js
let instance = think.controller('user', this.http, 'home');
let points = instance._getPoints();
console.log(points); // 打印:8000
instance.indexAction(); // 与直接执行 /home/user/index 是一样的效果
instance.testAction(); // 报错 [Error] TypeError: instance.testAction is not a function

可见是 thinkjs 提供了一个按需实例化某个 Controller 并运行其方法的途径。

乍看上去这个方式与 this.redirect 运行结果非常接近(除了不会触发 __call 的魔术方法以外),那么 thinkjs 提供这个方式有什么用呢?来看代码:

// src/home/controller/util.js
'use strict';
export default class extends think.controller.base {
  calcGPSDistance(lat, lng){
    // 计算 GPS 两点直线距离
    return distance;
  }
  calcBaiduDistance(lat, lng){
    // 计算 百度大地坐标 两点直线距离
    return distance;
  }
  calcSosoDistance(lat, lng){
    // 计算 Soso坐标 两点直线距离
    return distance;
  }
}

这是一个助手 Controller,一个“隐身”的 Controller,从 url 是无法直接访问到的,因为它的所有方法名均没有 Action 后缀。

这个场景下,运行时实例化 Controller 并操作其方法的方式就派上用场了。

内置 http 对象

控制器在实例化时,会将 http 传递进去。该 http 对象是 ThinkJS 对 req 和 res 重新包装的一个对象,而非 Node.js 内置的 http 对象。
Action 里如果想获取该对象,可以通过 this.http 来获取。
https://thinkjs.org/zh-cn/doc/2.2/controller.html#toc-efc
thinkjs 官网

扩展应用:增加一个 n 秒后自动跳转的过渡页功能

thinkjs 框架并没有给我们准备这样一个过渡页面的功能,那么我们可以自己实现一个来练练手,上代码:

// src/common/controller/complete.js
'use strict';
export default class extends think.controller.base {
  /**
   * 显示中转页面
   *
   * 调用方式:
   * let complete = think.controller('complete', this.http, 'common');
   * return complete.display('应用新增成功!', '/', 5);
   *
   * @param msg 提示文字,支持 HTML
   * @param url 后续自动跳转的目标地址
   * @param delay 停留秒数
   * @returns {think.Promise}
   */
  display(msg, url='', delay=3) {
    let tpl = 'common/complete/200';
    let opt = think.extend({}, {type: 'base', file_depr: '_', content_type: 'text/html'});
    this.fetch(tpl, {}, opt).then(content => {
      content = content.replace(/COMPLETE_MESSAGE/g, msg);
      if (url) {
        content = content.replace(/TARGET_URL/g, url);
        content = content.replace(/WAIT_SECONDS/g, delay);
      };
      this.type(opt['content_type']);
      return this.end(content);
    }).catch(function(err){
      return this.end('');
    });
  }
}
<!-- view/common/complete_200.html -->
<!DOCTYPE html>
<html>
<head>
    <title>正在跳转 - 荆秀网</title>
</head>
<body>
<div class="header">
    <div class="wrap">
        <div class="logo"><a href="/"><img src="/static/img/logo.png" alt="XxuYou" width="60"></a></div>
        <div class="headr">&nbsp;</div>
    </div>
</div>
<div class="wrap">
    <div style="margin-top:20px;height:100px;background:url(/static/img/200.gif) top center no-repeat;"></div>
    <h1>COMPLETE_MESSAGE</h1>
    <div class="error-msg"><pre>提示:页面将在 <span id="_count">WAIT_SECONDS</span> 秒后重定向到 <a href="TARGET_URL">TARGET_URL</a></pre></div>
    <input type="hidden" id="_target_url" value="TARGET_URL" />
    <input type="hidden" id="_wait_seconds" value="WAIT_SECONDS" />
</div>
<script type="text/javascript">
    var thisLoad = function () {
        var _target_url = document.getElementById('_target_url').value;
        var _wait_seconds = document.getElementById('_wait_seconds').value;
        if (_target_url == '') return false;
        if (/^\d+$/.test(_wait_seconds) == false || _wait_seconds < 1 || _wait_seconds >= 3600) {
            try {
                document.location.replace(_target_url);
            } catch(e) {};
        } else {
            thisCount(_wait_seconds);
            window.setTimeout(function () {
                try {
                    document.location.replace(_target_url);
                } catch(e) {};
            }, _wait_seconds*1000);
        };
        return true;
    };
    var thisCount = function (cnt) {
        if (cnt < 0) return false;
        document.getElementById('_count').innerHTML = cnt;
        window.setTimeout(function () {
            thisCount(--cnt);
        }, 1000);
    };
    window.attachEvent ? window.attachEvent('onload', thisLoad) : window.addEventListener('load', thisLoad);
</script>
</body>
</html>
// Controller 内调用方式
indexAction() {
  // 业务流程。。。
  let complete = think.controller('complete', this.http, 'common');
  return complete.display('操作成功!', '/', 5);
}

以上新增的 src/common/controller/complete.js 是一个非侵入式的成功业务处理页面,其内部运行与兄弟 Controller src/common/controller/error.js 类似。

以上新增的 view/common/complete_200.html 则是相关的过渡页面的模版。其中存在三个占位符(分别对应 display 方法的入參):

  • COMPLETE_MESSAGE 用于操作成功的文字提示内容
  • TARGET_URL 用于稍后会自动进入的目标 url
  • WAIT_SECONDS 用于页面过渡时间,单位是秒

实现原理其实非常简单,阅读一下两个新增的代码就能明白。

扩展应用:快速构建 REST API

其实这部分因为太简答,我本来是不想写的。不过考虑到教程的完整性,还是写一下比较好。

REST 的概念介绍这里不再赘述,有兴趣的可以自行搜索。

thinkjs 的官网说到:

自动生成 REST API,而无需写任何的代码。

此言不虚,创建 Controller 时只要增加一个参数(thinkjs controller [name] --rest),即可生成一个能够操作数据库表的 REST API。

当然操作的约定也还是有的:

  • GET /ticket #获取ticket列表
  • GET /ticket/12 #查看某个具体的ticket
  • POST /ticket #新建一个ticket
  • PUT /ticket/12 #更新ticket 12
  • DELETE /ticket/12 #删除ticekt 12

遵从了上述操作约定,的确是可以直接操作数据库表内的数据了。

只是这样的 API 只不过是一个“裸奔”状态的 API,还是不能直接投入使用。因为它不仅要求请求方熟悉所有的数据表结构,还要依赖请求方来维护多表数据之间的关联性,更不用提什么操作路径的映射、字段映射、返回数据的映射等等问题了。

就算 thinkjs 还提供了字段过滤、自定义 GET/POST/PUT/DELETE 方法来进行更多的定制,那么最终的结果很可能是在当前的 API 外面再包裹一层能够提供操作路径映射、鉴权令牌发放和识别、字段映射、关联数据维护等等。

当然作为一个开发框架,thinkjs 确实已经做的够多了,足够优秀了,因此我们还是需要像构建一个应用系统那样去完整构建一个可供实施的 REST API 的生产模型。

扩展应用:控制器分层(多级)

Controller 开篇我们就讲到了多级控制器的用法,现在是实践的时候了~

thinkjs controller home/group/article
# 打印结果
#  create : src/home/controller/group
#  create : src/home/controller/group/article.js
#  create : src/home/logic/group
#  create : src/home/logic/group/article.js

可以看到自动在 controller 下面建立了分级文件夹 group ,并且新的 controller 文件也放置在 group 下面了。

但是不幸的的是:thinkjs 忽略了当存在分级时,新的 controller 内部对于基类 Base 的继承路径没有修正。

我们来修改 src/home/controller/group/article.js 代码:

import Base from './base.js';
// 修改为
import Base from './../base.js';

处理完了基类继承的路径问题,其他就没什么了。仅仅是多了一层文件夹而已,相应的需要去调整访问的 url:

domain.com/home/controller/group/article

按照实际情况调整你的 Nginx Rewrite 规则,或者 thinkjs 的 route 规则,即可,是不是很简单~

done~

上一篇:Node.js 国产 MVC 框架 ThinkJS 开发 controller 篇
下一篇:Node.js 国产 MVC 框架 ThinkJS 开发 logic 篇

原创:荆秀网 网页即时推送 https://xxuyou.com | 转载请注明出处
链接:https://blog.xxuyou.com/nodejs-thinkjs-study-controller-2/

posted @ 2017-07-15 22:21  荆秀  阅读(738)  评论(0编辑  收藏  举报