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="/">![](/static/img/logo.png)</a></div>
        <div class="headr"> </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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 原创:荆秀网 网页即时推送 https://xxuyou.com | 转载请注明出处链接:https://blog...
    832116839b8a阅读 1,478评论 0 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399
  • 刚刚看完毛姆的《面纱》,这本书是微信好友赠我的微信读书中的一本,看完后被他深深地震撼,说不出的感觉,毛姆的书总是对...
    艾米姜阅读 524评论 0 0
  • 开学第一课,今天和女儿一块观看了课程,被课堂上的老师们狠狠地感动了。她们为了实现理想的那种坚持不懈,不论病痛始终如...
    幸福_216b阅读 54评论 0 0