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

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

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

Controller 基本应用

Controller 作为 MVC 框架的主力担当,是开发人员接触最多的部分。在开发过程中通常按照需求、业务流程、任务派发等,都是以一个或者多个 Controller 为边界进行划分。

Controller 作为接收用户输入、业务流程处理、响应处理展示的“处理器”,其构成和实现也有非常多的方式方法,以及技巧。

Action 定义

从外部用户(使用者)感知角度来看,最先体现 Controller 的地方就是输入一个 url 所能到达的地方,url 代表着用户输入、流程跳转等动作。例如:

domain.com/home/user/login
domain.com/?m=home&c=user&a=login
domain.com/home/order/detail/id/856
domain.com/?m=home&c=order&a=detail&id=856

thinkjs 要求凡是公开出来可以被访问的方法名都要增加 Action 的后缀,例如 indexAction,来看代码:

// src/home/controller/user.js
export default class extends Base {
  indexAction(){
    //
  }
}

暂且忽略 indexAction 方法内部的实现,想要访问到这个方法的 url 组成规则是 /module/controller/action (注:这是默认路由,可以通过更改路由规则改变 url 的组成方式,后文详述),也就是 /home/user/index

OK,按照自己的需要去组织 Controller 内的 Action 即可,来看代码:

// src/home/controller/user.js
export default class extends Base {
  indexAction(){
    // 访问 url:domain.com/home/user/index
  }
  listAction(){
    // 访问 url:domain.com/home/user/list
  }
  detailAction(){
    // 访问 url:domain.com/home/user/detail
  }
  orderListAction(){
    // 访问 url:domain.com/home/user/order_list
  }
  orderDetailAction(){
    // 访问 url:domain.com/home/user/order_detail
  }
  _getPoints(){
    // private function
  }
  _getBalance(){
    // private function
  }
  // etc...
}

注意第四、第五个方法使用了多个单词的驼峰命名(有强迫症的筒子要开心了~),这种情况下访问 url 就会有些不同了。

另外可以看到,第六和第七个方法不带 Action 后缀,这会被识别为私有方法(ES6 仍然不提供 private 关键字来标记私有方法,因此方法名前使用一个下划线前缀来标识)。

so,就是这么简单,接下来就是考虑怎么去布局 Controller 的方法了。

注:thinkjs 路由默认是强制小写英文字母的,这一点在开发中要注意。

基类与继承链

如果使用 thinkjs module [moduleName] 命令来创建一个模块,那么该模块的 Controller 都会存在一个基类 Base(base.js)。

# 默认生成的代码清单
src/home/
├── config
│   └── config.js
├── controller
│   ├── base.js
│   └── index.js
├── logic
│   └── index.js
└── model
    └── index.js

如果继续使用 thinkjs controller [moduleName/][controllerName] 命令来创建每个 Controller,那么每个 Controller 都会继承此基类。

import Base from './base.js';

这样我们可以迅速建立起两层 Class 的继承链。这个特性你会想到怎么用?没错,用户 Session 的检测和处理,来看代码:

// src/home/controller/base.js
'use strict';
export default class extends think.controller.base {
  init(...args) {
    super.init(...args);
  }
  /**
   * 检测session数据
   * 如果有问题就返回false
   * 如果OK就续命
   * @returns {boolean}
   * @private
   */
  async checkSession() {
    let userSess = await this.session('be_user');
    if (think.isEmpty(userSess)) return false;
    let userToken = userSess['token'];
    if (think.isEmpty(userToken)) return false;
    if (/^[a-z0-9]{128}$/.test(userToken) == false) return false;
    let userExpire = userSess['expire'];
    let now = +(new Date);
    if (now >= userExpire) return false;
    userSess['expire'] = now + this.config('backend.user')['session_life'];
    await this.session('be_user', userSess);
    return true;
  }
}

这样可以把强关联的全部公共业务方法统统放置在这里。之所以说强关联,表示符合下列情况的方法可以考虑放在基类中:

  • 业务相关:与用户业务流程无关的方法不要放在这里(例如日期格式化这种的方法应当放置在全局函数中)
  • 方法调用方:超过一个的(例如 Session 检测方法在后台模块的几乎所有 Controller 子类都会用到)
  • 方法调用次数:超过一次的

关于业务相关性的理解人人不同,这里仅做示例而不是定论,开发人员大可按照自己的理解去划分业务边界,本文主要专注于框架的使用。

前面提到的放置公共业务方法是基类的一种玩法,可是基类还有一种玩法:使用逻辑方法来处理中断或者跳转,来看代码:

// 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 参数');
    }
  }
}

假如 url 中缺少 auth 参数,那么 Class 会初始化失败,提示错误:

{
  "errno": 500,
  "errmsg": "全部 url 必须携带 auth 参数"
}

如果是页面访问,也可以重定向到其他 Controller 处理页面。

表单提交与处理

除了通常的页面切换,Controller 还有一个重要的工作就是处理用户数据,其中以表单提交(以及 AJAX 提交)为重。

GET 提交/访问

thinkjs 提供了 this.get([paramName]) 方法来获取 GET 参数。

let auth = this.get('auth');
console.log(auth); // 打印:xyz

可能有的筒子不喜欢一个一个的写参数名(或者需要对参数排序计算签名),那么 get 方法如果没有入参,则获取到全部 get 参数:

let params = this.get();
console.log(params); // 打印:{ auth: 'xyz' }

POST 提交

thinkjs 提供了 this.post() 方法来获取 POST 参数。

let auth = this.post('auth');
console.log(auth); // 打印:xyz

获取全部 post 参数:

let params = this.post();
console.log(params); // 打印:{ auth: 'xyz' }

上传文件

如果需要接收用户提交的二进制流,需要给 form 元素增加属性 enctype 来指定上传的内容类型 :

<form name="formName" method="POST" enctype="multipart/form-data">
  <input type="file" name="myFile" />
</form>

thinkjs 提供了一个 this.file([fileName]) 方法,这样可以很方便的处理上传文件了(开发人员并不需要自己拼接二进制块,上传文件已经被框架接收,并保存在系统临时目录中,方法返回的只是一个包含相关信息的 Object)。

这是一个简洁明了的 thinkjs 文件上传demo,可以看到其中的工作方法。

不过官网没有说明的是同时上传多个文件的返回数据的结构,试一下就知道!来看代码:

<form name="formName" method="POST" enctype="multipart/form-data">
  <input type="file" name="myFile1" />
  <input type="file" name="myFile2" />
  <input type="submit" name="Submit" />
</form>
let files = think.extend({}, this.file());
console.log(files);
{
  "myFile1": {
    "fieldName": 'myFile1',
    "originalFilename": '查询身份证绑定的公众号.jpg',
    "path": '/data/www/thinkjs_module/runtime/upload/twLYslNHfLzWxFaGR2Rqg_qb.jpg',
    "headers": {
      "content-disposition": 'form-data;name="myFile1";filename="查询身份证绑定的公众号.jpg"',
      "content-type": 'image/jpeg'
    },
    "size": 86411
  },
  "myFile2": {
    "fieldName": 'myFile2',
    "originalFilename": '查询微信号绑定的公众号.jpg',
    "path": '/data/www/thinkjs_module/runtime/upload/EP6KoSAMxlL9vU4uTFviNs7d.jpg',
    "headers": {
      "content-disposition": 'form-data;name="myFile2";filename="查询微信号绑定的公众号.jpg"',
      "content-type": 'image/jpeg'
    },
    "size": 95865
  }
}

实践出真知~ 如果是多个文件上传,服务端 this.file() 返回的数据是以 input.name 为属性的映射关系,处理起来非常方便。

其中 path 属性是到临时文件的位置。如果整个上传业务逻辑正确,应当主动将文件从临时位置中移走,例如移动到 www/static/upload/ 中;如果服务端代码没有将文件移动到其他位置,那么最终框架会自动删除临时文件。

输出到响应

处理完了用户数据,最终需要向客户端浏览器返回内容。返回内容的处理不属于 Controller 的工作范畴(当然你可以用 Controller 也是可以做到的)。这个过程就是 Controller 挑选模版,给定数据(变量),然后统统交给模版引擎来处理。

ThinkJS 默认支持的模版引擎有:ejsjadeswignunjucks,默认模版引擎为 ejs,可以根据需要修改为其他的模版引擎。(来自官网文档)

这个过程简化到两个方法即可完成这一连串工作任务委托:

  • this.assign(dataName, data) 将变量指派给模版引擎,并命名方便模版引擎调用
  • this.display([viewFileName]) 显示模版引擎渲染后的结果

看一下 this.display() 的工作细节:

// src/home/controller/index.js
indexAction() {
  return this.display(); // 系统会去找 view/home/index_index.html 来渲染并输出到响应
}
listAction(){
  return this.display(); // 系统会去找 view/home/index_list.html 来渲染并输出到响应
}

实质上 this.display() 所做的远不止我们看到的这么少,除了输出响应正文(ResponseBody,一堆的 HTML 代码让浏览器去解析),还负责输出合法正确的响应头(ResponseHeader,返回网络响应状态、响应内容类型、响应正文编码等)。看图:

箭头所指就是 ResponseHeader 内容(还包括一个 X-Powered-By 字段,嘿嘿~)。

注:更详细的模版引擎工作方式会另文描述。

输出 JSON 到响应

Controller 默认输出的响应 content-typetext/html 主要用于页面显示。但是以下两种情况下需要 Controller 输出 JSON 格式的响应:

  • 作为 REST API 接口给请求方返回数据
  • 给 AJAX 请求返回数据

thinkjs 提供了 this.successthis.fail 来负责输出标准的 JSON 响应,如前所述,这两个方法同样能够返回正确的响应头(content-typeapplication/json)。

this.success 方法接受一个入参,可以是 Array 也可以是 Object,可根据业务需要自行组织结构和内容,调用之后返回的响应正文是一个统一格式的 JSON,如:

{
  "errno": 0,
  "errmsg": "",
  "data": {
    "id": 234,
    "user": "test"
  }
}

可以看到入參数据被装载在属性 data 中(这个属性名恒定为 data 不可更改)。

this.fail 方法接受两个入參:错误编号和错误文本,两个参数均可根据业务需要自行组织,调用之后返回的响应正文是一个统一格式的 JSON,如:

{
  "errno": 90001,
  "errmsg": "缺少必要参数"
}

这两个方法返回标准的 JSON 响应正文格式,有两个 JSON 属性用于自我描述结果:

  • errno 错误编号,等于 0 表示没有错误,可以读取 data 属性;大于 0 表示出现错误
  • errmsg 错误描述,errno 大于 0 的时候有值,可以用来提示用户

假如觉得 errnoerrmsg 这两个字段名不合适需要修改为其他名字的(比如我习惯使用 errmsg),可以修改 src/common/config/error.js 文件达到目的。

假如不想使用 thinkjs 标准的 JSON 响应正文格式,需要自行定义正文格式,thinkjs 也提供了 this.json 方法,传入一个 Array 或者 Object 参数,方法会自动对参数执行 JSON.stringify 方法转化为格式良好的 JSON 响应正文。

输出 JSONP

Controller 既然可以输出 JSON,那么输出 JSONP 也是没跑了~

thinkjs 提供的方法是 this.jsonp 。callback 的请求参数名默认为 callback。如果需要修改请求参数名,可以通过修改配置 callback_name 来完成。

下面我们简单实现一个 JSONP 业务看看:

  1. 配置 JSONP 的 callback 参数名,此参数为全局有效,定义一次即可
// src/common/config/config.js
export default {
  // jsonp 请求的 callback 参数名,此参数名要告知前端开发人员
  callback_name: 'callbacks'
}
  1. 客户端 js 发起 JSONP 请求(注意 jsonpjsonpCallback 这两个参数的值)
$.ajax({
  url          : '/home/user/ajax_get_user_info',
  dataType     : 'jsonp',
  jsonp        : 'callbacks',
  jsonpCallback: 'myfunc',
  success      : function(res, textStatus) {
    console.log(res);
  }
});
  1. 服务端 获取用户详情数据
async ajaxGetUserInfoAction(){
  if (!this.isAjax()) return this.fail(90001, 'Request must be AJAX');
  let sess      = await this.session('front');
  let userModel = this.model('user');
  let userInfo  = await userModel.field('name,nickname,email').find(sess['id']);
  if (think.isEmpty(userInfo)) return this.fail(10001, 'user is not exists!');
  return this.jsonp(userInfo);
}
  1. 服务端输出的 JSONP 正文
myfunc({"name": "xxuyou", "nickname": "荆秀网", "email": "cap@xxuyou.com"})

未完待续~

上一篇:Node.js 国产 MVC 框架 ThinkJS 开发 config 篇(荆秀网)
下一篇:Node.js 国产 MVC 框架 ThinkJS 开发 controller 篇(续)(荆秀网)

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

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

推荐阅读更多精彩内容