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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM