原創(chuàng):荊秀網(wǎng) 網(wǎng)頁即時(shí)推送 https://xxuyou.com | 轉(zhuǎn)載請(qǐng)注明出處
鏈接:https://blog.xxuyou.com/nodejs-thinkjs-study-controller/
本系列教程以 ThinkJS v2.x 版本(官網(wǎng))為例進(jìn)行介紹,教程以實(shí)際操作為主。
Controller 基本應(yīng)用
Controller 作為 MVC 框架的主力擔(dān)當(dāng),是開發(fā)人員接觸最多的部分。在開發(fā)過程中通常按照需求、業(yè)務(wù)流程、任務(wù)派發(fā)等,都是以一個(gè)或者多個(gè) Controller 為邊界進(jìn)行劃分。
Controller 作為接收用戶輸入、業(yè)務(wù)流程處理、響應(yīng)處理展示的“處理器”,其構(gòu)成和實(shí)現(xiàn)也有非常多的方式方法,以及技巧。
Action 定義
從外部用戶(使用者)感知角度來看,最先體現(xiàn) Controller 的地方就是輸入一個(gè) url 所能到達(dá)的地方,url 代表著用戶輸入、流程跳轉(zhuǎn)等動(dòng)作。例如:
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 方法內(nèi)部的實(shí)現(xiàn),想要訪問到這個(gè)方法的 url 組成規(guī)則是 /module/controller/action (注:這是默認(rèn)路由,可以通過更改路由規(guī)則改變 url 的組成方式,后文詳述),也就是 /home/user/index 。
OK,按照自己的需要去組織 Controller 內(nèi)的 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...
}
注意第四、第五個(gè)方法使用了多個(gè)單詞的駝峰命名(有強(qiáng)迫癥的筒子要開心了~),這種情況下訪問 url 就會(huì)有些不同了。
另外可以看到,第六和第七個(gè)方法不帶 Action 后綴,這會(huì)被識(shí)別為私有方法(ES6 仍然不提供 private 關(guān)鍵字來標(biāo)記私有方法,因此方法名前使用一個(gè)下劃線前綴來標(biāo)識(shí))。
so,就是這么簡單,接下來就是考慮怎么去布局 Controller 的方法了。
注:thinkjs 路由默認(rèn)是強(qiáng)制小寫英文字母的,這一點(diǎn)在開發(fā)中要注意。
基類與繼承鏈
如果使用 thinkjs module [moduleName] 命令來創(chuàng)建一個(gè)模塊,那么該模塊的 Controller 都會(huì)存在一個(gè)基類 Base(base.js)。
# 默認(rèn)生成的代碼清單
src/home/
├── config
│ └── config.js
├── controller
│ ├── base.js
│ └── index.js
├── logic
│ └── index.js
└── model
└── index.js
如果繼續(xù)使用 thinkjs controller [moduleName/][controllerName] 命令來創(chuàng)建每個(gè) Controller,那么每個(gè) Controller 都會(huì)繼承此基類。
import Base from './base.js';
這樣我們可以迅速建立起兩層 Class 的繼承鏈。這個(gè)特性你會(huì)想到怎么用?沒錯(cuò),用戶 Session 的檢測和處理,來看代碼:
// src/home/controller/base.js
'use strict';
export default class extends think.controller.base {
init(...args) {
super.init(...args);
}
/**
* 檢測session數(shù)據(jù)
* 如果有問題就返回false
* 如果OK就續(xù)命
* @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;
}
}
這樣可以把強(qiáng)關(guān)聯(lián)的全部公共業(yè)務(wù)方法統(tǒng)統(tǒng)放置在這里。之所以說強(qiáng)關(guān)聯(lián),表示符合下列情況的方法可以考慮放在基類中:
- 業(yè)務(wù)相關(guān):與用戶業(yè)務(wù)流程無關(guān)的方法不要放在這里(例如日期格式化這種的方法應(yīng)當(dāng)放置在全局函數(shù)中)
- 方法調(diào)用方:超過一個(gè)的(例如 Session 檢測方法在后臺(tái)模塊的幾乎所有 Controller 子類都會(huì)用到)
- 方法調(diào)用次數(shù):超過一次的
關(guān)于業(yè)務(wù)相關(guān)性的理解人人不同,這里僅做示例而不是定論,開發(fā)人員大可按照自己的理解去劃分業(yè)務(wù)邊界,本文主要專注于框架的使用。
前面提到的放置公共業(yè)務(wù)方法是基類的一種玩法,可是基類還有一種玩法:使用邏輯方法來處理中斷或者跳轉(zhuǎn),來看代碼:
// src/home/controller/base.js
'use strict';
export default class extends think.controller.base {
init(...args) {
super.init(...args);
// 要求全部 url 必須攜帶 auth 參數(shù)
let auth = this.get('auth');
if (think.isEmpty(auth)) {
return this.error(500, '全部 url 必須攜帶 auth 參數(shù)');
}
}
}
假如 url 中缺少 auth 參數(shù),那么 Class 會(huì)初始化失敗,提示錯(cuò)誤:
{
"errno": 500,
"errmsg": "全部 url 必須攜帶 auth 參數(shù)"
}
如果是頁面訪問,也可以重定向到其他 Controller 處理頁面。
表單提交與處理
除了通常的頁面切換,Controller 還有一個(gè)重要的工作就是處理用戶數(shù)據(jù),其中以表單提交(以及 AJAX 提交)為重。
GET 提交/訪問
thinkjs 提供了 this.get([paramName]) 方法來獲取 GET 參數(shù)。
let auth = this.get('auth');
console.log(auth); // 打?。簒yz
可能有的筒子不喜歡一個(gè)一個(gè)的寫參數(shù)名(或者需要對(duì)參數(shù)排序計(jì)算簽名),那么 get 方法如果沒有入?yún)?,則獲取到全部 get 參數(shù):
let params = this.get();
console.log(params); // 打印:{ auth: 'xyz' }
POST 提交
thinkjs 提供了 this.post() 方法來獲取 POST 參數(shù)。
let auth = this.post('auth');
console.log(auth); // 打印:xyz
獲取全部 post 參數(shù):
let params = this.post();
console.log(params); // 打?。簕 auth: 'xyz' }
上傳文件
如果需要接收用戶提交的二進(jìn)制流,需要給 form 元素增加屬性 enctype 來指定上傳的內(nèi)容類型 :
<form name="formName" method="POST" enctype="multipart/form-data">
<input type="file" name="myFile" />
</form>
thinkjs 提供了一個(gè) this.file([fileName]) 方法,這樣可以很方便的處理上傳文件了(開發(fā)人員并不需要自己拼接二進(jìn)制塊,上傳文件已經(jīng)被框架接收,并保存在系統(tǒng)臨時(shí)目錄中,方法返回的只是一個(gè)包含相關(guān)信息的 Object)。
這是一個(gè)簡潔明了的 thinkjs 文件上傳demo,可以看到其中的工作方法。
不過官網(wǎng)沒有說明的是同時(shí)上傳多個(gè)文件的返回?cái)?shù)據(jù)的結(jié)構(gòu),試一下就知道!來看代碼:
<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": '查詢身份證綁定的公眾號(hào).jpg',
"path": '/data/www/thinkjs_module/runtime/upload/twLYslNHfLzWxFaGR2Rqg_qb.jpg',
"headers": {
"content-disposition": 'form-data;name="myFile1";filename="查詢身份證綁定的公眾號(hào).jpg"',
"content-type": 'image/jpeg'
},
"size": 86411
},
"myFile2": {
"fieldName": 'myFile2',
"originalFilename": '查詢微信號(hào)綁定的公眾號(hào).jpg',
"path": '/data/www/thinkjs_module/runtime/upload/EP6KoSAMxlL9vU4uTFviNs7d.jpg',
"headers": {
"content-disposition": 'form-data;name="myFile2";filename="查詢微信號(hào)綁定的公眾號(hào).jpg"',
"content-type": 'image/jpeg'
},
"size": 95865
}
}
實(shí)踐出真知~ 如果是多個(gè)文件上傳,服務(wù)端 this.file() 返回的數(shù)據(jù)是以 input.name 為屬性的映射關(guān)系,處理起來非常方便。
其中 path 屬性是到臨時(shí)文件的位置。如果整個(gè)上傳業(yè)務(wù)邏輯正確,應(yīng)當(dāng)主動(dòng)將文件從臨時(shí)位置中移走,例如移動(dòng)到 www/static/upload/ 中;如果服務(wù)端代碼沒有將文件移動(dòng)到其他位置,那么最終框架會(huì)自動(dòng)刪除臨時(shí)文件。
輸出到響應(yīng)
處理完了用戶數(shù)據(jù),最終需要向客戶端瀏覽器返回內(nèi)容。返回內(nèi)容的處理不屬于 Controller 的工作范疇(當(dāng)然你可以用 Controller 也是可以做到的)。這個(gè)過程就是 Controller 挑選模版,給定數(shù)據(jù)(變量),然后統(tǒng)統(tǒng)交給模版引擎來處理。
ThinkJS 默認(rèn)支持的模版引擎有:
ejs,jade,swig和nunjucks,默認(rèn)模版引擎為ejs,可以根據(jù)需要修改為其他的模版引擎。(來自官網(wǎng)文檔)
這個(gè)過程簡化到兩個(gè)方法即可完成這一連串工作任務(wù)委托:
-
this.assign(dataName, data)將變量指派給模版引擎,并命名方便模版引擎調(diào)用 -
this.display([viewFileName])顯示模版引擎渲染后的結(jié)果
看一下 this.display() 的工作細(xì)節(jié):
// src/home/controller/index.js
indexAction() {
return this.display(); // 系統(tǒng)會(huì)去找 view/home/index_index.html 來渲染并輸出到響應(yīng)
}
listAction(){
return this.display(); // 系統(tǒng)會(huì)去找 view/home/index_list.html 來渲染并輸出到響應(yīng)
}
實(shí)質(zhì)上 this.display() 所做的遠(yuǎn)不止我們看到的這么少,除了輸出響應(yīng)正文(ResponseBody,一堆的 HTML 代碼讓瀏覽器去解析),還負(fù)責(zé)輸出合法正確的響應(yīng)頭(ResponseHeader,返回網(wǎng)絡(luò)響應(yīng)狀態(tài)、響應(yīng)內(nèi)容類型、響應(yīng)正文編碼等)??磮D:

箭頭所指就是 ResponseHeader 內(nèi)容(還包括一個(gè) X-Powered-By 字段,嘿嘿~)。
注:更詳細(xì)的模版引擎工作方式會(huì)另文描述。
輸出 JSON 到響應(yīng)
Controller 默認(rèn)輸出的響應(yīng) content-type 是 text/html 主要用于頁面顯示。但是以下兩種情況下需要 Controller 輸出 JSON 格式的響應(yīng):
- 作為 REST API 接口給請(qǐng)求方返回?cái)?shù)據(jù)
- 給 AJAX 請(qǐng)求返回?cái)?shù)據(jù)
thinkjs 提供了 this.success 和 this.fail 來負(fù)責(zé)輸出標(biāo)準(zhǔn)的 JSON 響應(yīng),如前所述,這兩個(gè)方法同樣能夠返回正確的響應(yīng)頭(content-type 是 application/json)。
this.success 方法接受一個(gè)入?yún)ⅲ梢允?Array 也可以是 Object,可根據(jù)業(yè)務(wù)需要自行組織結(jié)構(gòu)和內(nèi)容,調(diào)用之后返回的響應(yīng)正文是一個(gè)統(tǒng)一格式的 JSON,如:
{
"errno": 0,
"errmsg": "",
"data": {
"id": 234,
"user": "test"
}
}
可以看到入?yún)?shù)據(jù)被裝載在屬性 data 中(這個(gè)屬性名恒定為 data 不可更改)。
this.fail 方法接受兩個(gè)入?yún)ⅲ哄e(cuò)誤編號(hào)和錯(cuò)誤文本,兩個(gè)參數(shù)均可根據(jù)業(yè)務(wù)需要自行組織,調(diào)用之后返回的響應(yīng)正文是一個(gè)統(tǒng)一格式的 JSON,如:
{
"errno": 90001,
"errmsg": "缺少必要參數(shù)"
}
這兩個(gè)方法返回標(biāo)準(zhǔn)的 JSON 響應(yīng)正文格式,有兩個(gè) JSON 屬性用于自我描述結(jié)果:
- errno 錯(cuò)誤編號(hào),等于 0 表示沒有錯(cuò)誤,可以讀取 data 屬性;大于 0 表示出現(xiàn)錯(cuò)誤
- errmsg 錯(cuò)誤描述,errno 大于 0 的時(shí)候有值,可以用來提示用戶
假如覺得
errno和errmsg這兩個(gè)字段名不合適需要修改為其他名字的(比如我習(xí)慣使用err和msg),可以修改src/common/config/error.js文件達(dá)到目的。
假如不想使用 thinkjs 標(biāo)準(zhǔn)的 JSON 響應(yīng)正文格式,需要自行定義正文格式,thinkjs 也提供了
this.json方法,傳入一個(gè)Array或者Object參數(shù),方法會(huì)自動(dòng)對(duì)參數(shù)執(zhí)行JSON.stringify方法轉(zhuǎn)化為格式良好的 JSON 響應(yīng)正文。
輸出 JSONP
Controller 既然可以輸出 JSON,那么輸出 JSONP 也是沒跑了~
thinkjs 提供的方法是 this.jsonp 。callback 的請(qǐng)求參數(shù)名默認(rèn)為 callback。如果需要修改請(qǐng)求參數(shù)名,可以通過修改配置 callback_name 來完成。
下面我們簡單實(shí)現(xiàn)一個(gè) JSONP 業(yè)務(wù)看看:
- 配置 JSONP 的 callback 參數(shù)名,此參數(shù)為全局有效,定義一次即可
// src/common/config/config.js
export default {
// jsonp 請(qǐng)求的 callback 參數(shù)名,此參數(shù)名要告知前端開發(fā)人員
callback_name: 'callbacks'
}
- 客戶端 js 發(fā)起 JSONP 請(qǐng)求(注意
jsonp和jsonpCallback這兩個(gè)參數(shù)的值)
$.ajax({
url : '/home/user/ajax_get_user_info',
dataType : 'jsonp',
jsonp : 'callbacks',
jsonpCallback: 'myfunc',
success : function(res, textStatus) {
console.log(res);
}
});
- 服務(wù)端 獲取用戶詳情數(shù)據(jù)
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);
}
- 服務(wù)端輸出的 JSONP 正文
myfunc({"name": "xxuyou", "nickname": "荊秀網(wǎng)", "email": "cap@xxuyou.com"})
未完待續(xù)~
上一篇:Node.js 國產(chǎn) MVC 框架 ThinkJS 開發(fā) config 篇(荊秀網(wǎng))
下一篇:Node.js 國產(chǎn) MVC 框架 ThinkJS 開發(fā) controller 篇(續(xù))(荊秀網(wǎng))
原創(chuàng):荊秀網(wǎng) 網(wǎng)頁即時(shí)推送 https://xxuyou.com | 轉(zhuǎn)載請(qǐng)注明出處
鏈接:https://blog.xxuyou.com/nodejs-thinkjs-study-controller/