#2.控制器 ## 加载controller: 上一章讲到了整个项目的入口文件为:http.ts。但程序实际运行的并不是.ts文件而是http.js文件.我们可以看到项目里每一个模块都由.ts .js .js.map这三个文件组成.每当我们修改了.ts文件后,TypeScript编译器就会自动将其重新编译成对应的.js文件.至于.js.map文件则是用来描述.js文件与.ts文件的行号对应关系.将.js.map文件删除对程序运行并没有什么影响,只是没法针对.ts文件debug了. ```typescript /** * 加载controllers */ route.load(app, [ { menu: path.join(__dirname, 'controllers', 'web'), onRoute: RouteController.onRoute(void 0, true), }, { menu: path.join(__dirname, 'controllers', 'api'), onRoute: RouteController.onRoute(RouteController.loginCheckApi), }, ]); ``` onRoute接收一个类似koa中间件的回调函数,用来拦截Controller中的每一次URL请求,在这里处理对get,post参数验证,登陆验证与错误捕获也可以再这里统一处理。RouteController.onRoute函数里封装的就是这些工作,其产生了一个匿名函数返回给onRoute。 web目录存放的是给浏览器访问的页面controller,api目录存放的是给ajax或第三方客户端调用的controller.两者的登录验证方式与错误提示有所不同,以上是用了一个boolean参数做区分. ---------- ## 响应URL请求: 每一个Controller都有如下固定的格式: ```typescript import {dfvContext, route} from "dfv"; export class HomeController { /** * 不论是koa context还是Express的req,resp都被转换为了统一的ctx属性 */ ctx: dfvContext!; /** * url为 /home/index */ @route.get() async index() { //返回值作为response body return "index:" + this.ctx.request.ip; } } ``` 如上所示,Controller不需要继承任何类,express或koa的request与response参数都在ctx属性里。 可以像koa一样通过`this.ctx.status = 500`来设置响应状态。 每一个成员函数对应的url转换规则为:`“/” + class类名转小写去掉Controller后缀 + “/” + 成员函数名`。 #### 自定义URL: 可以使用@route.path("")装饰器来自定义URL: ```typescript import {dfvContext, route} from "dfv"; @route.path("") export class HomeController { ctx: dfvContext; /** * url为 / */ @route.get('/') async index() { //返回值作为response body return "index:" + this.ctx.request.ip; } /** * 等同express或koa的router规则 * url为 /test-123 */ @route.get('/test-:id') async test(id: number) { return "test: " + id; } } ``` 如上所示最终url为`route.path参数 + route.get参数`。 ## 入参验证: 在响应URL请求的回调函数之前,需要先过滤掉那些非法访问者,错误的请求参数,以及自动转换不正确的请求参数类型(string->number)等等,以此来保证后续业务逻辑的稳定性。 和express一样,访问**this.ctx.request.params**与**this.ctx.request.query**可以获取url参数 通过**this.ctx.request.body**可以获取post内容参数,这样得到的都是原始数据。 为了方便维护,我们一般将参数封装到一个model里: ```typescript export class TestReq { para1 = 0; para2 = ""; } ``` 然后这样使用: ```typescript export class HomeController { @route.all() async test(dat: TestReq) { return dat; } } ``` 如上所示test函数接收get,post,put在内的所有请求。 并且,post的body内容与url中的参数都被合并成了dat参数。 然后我们使用如下请求: ``` http://localhost/home/test?para1=123¶2=456 ``` 即可看到返回结果:`{para1:123,para2:"456"}` 如果get或post请求中没有找到对应的参数(null或undefined),dat就使用TestReq属性的初始化值。 有时我们不想从url中获取参数,只从body中获取也可以: ```typescript export class HomeController { /** * 对参数使用@route.fromBody装饰器则只从body中获取数据 * @param dat */ @route.all() async test(@route.fromBody dat: TestReq) { return dat; } } ``` 当然还有很多其他各种各样的装饰器,可以通过打route.智能感知查看代码注释。 我们也可以自己定义装饰器,然后在onRute回调里解析并使用。 ------ 接下来让我们更进一步的对入参格式进行限制: ```typescript import {valid} from "dfv/src/public/valid"; class TestReq { //限制para1>0,且不能为null或undefined @valid.intNotZero() para1 = 0; //自定义验证函数(当入参为null或undefined时,r.val改为使用para2初始值,这里是:1) @valid.int(r => r.val > 0 && r.val < 100) para2 = 1; //自定义验证函数与错误提示 @valid.float(r => r.val > 0 && r.val < 100, "para3必须在1-100之间") para3 = 1; //限制para4不能为空字符串或null或undefined,第一个参数传null表示没有自定义验证函数,第二个表示错误提示。 @valid.stringNotEmpty(null, "para4不能为空") para4 = ""; //在调用验证函数之前会将r.val强制转换为装饰器对应的类型,这里是valid.string,所以不用担心startsWith调用失败) @valid.string(r=>r.val.startsWith("123"), "para5必须123开头") para5 = ""; } ``` 如上所示,所有参数验证装饰器都在valid模块里。valid里封装了intNotZero,intNullAble,stringNotEmpty等几个默认验证函数,或者传递一个自定义验证函数来获取最大程度的灵活度。 自定义验证函数是一个返回值为boolean的函数,并且有一个入参r其类型为**IFieldRes**: ```typescript export class IFieldRes { /** * 接收到的值 */ val: T; /** * 错误提示 */ msg: string; //字段缺省值 defaul: T; /** * 验证成功与否 */ ok: boolean; } ``` 我们可以在自定义表达式中直接修改**r.msg**,来显示不同的错误提示: ```typescript import {valid} from "dfv/src/public/valid"; class TestReq { //自定义验证表达式 @valid.int(r => { if (r.val < 0) { r.msg = "不能小于0" return false; } if (r.val == 0) { r.msg = "不能等于0" return false; } return true }) para2 = 1; } ``` 入参验证结果触发在http.ts加载控制器的onRoute回调里。我们跟进代码可以在RouteController.onRoute里看到默认的验证失败行为:返回500错误,并对web controller返回一个html错误页面,而对api controller则只返回自定义的错误消息。 ### 数组与嵌套对象验证: 当把post内容格式设置为application/json时可以提交复杂的数组与嵌套格式,验证方式如下: ```typescript import {valid} from "dfv/src/public/valid"; import {array, arrayString} from "dfv/src/public/dfv"; class TestObj { a = 1; @valid.stringNotEmpty() b = ""; } class TestReq { //嵌套对象,并且验证TestObj所有子成员 obj = new TestObj(); //先验证所有子成员,最后执行自定义验证函数 @valid.object(r => r.val.a > 0, "obj2.a需大于0") obj2 = new TestObj(); //默认情况是不对数组的内容做遍历验证的,因此数组里可能会出现不同的类型:[1,"2",3] //这里我们使用了arrayString()来创建数组而不是new Array()或[],是应为typescript的泛型只在编译期有效。 //arrayString()通过给Array添加额外的属性来记录运行时泛型类型 arr1 = arrayString(); //在自定义array验证函数里调用了一个Array的扩展函数(见dfv/srv/public/FuncExt.ts),遍历并转换数组成员为string @valid.array(r => r.val.length > 0 && r.val.eachToString(), "数组不能为空") arr2 = arrayString(); //与上面类似,只不过array里储存的为TestObj类型 @valid.array(r => r.val.length > 0 && r.val.eachToObj(TestObj)) arr3 = array(TestObj); } ``` ### 上传文件验证: 上传文件部分基于[node-formidable](https://github.com/felixge/node-formidable)模块。 通过@route.multipart装饰器来声明解析multipart/form-data格式的文件: ```typescript export class HomeController { /** * 该装饰器通过回调函数来设置文件上传规则 */ @route.multipart(form => { //文件哈希算法 form.hash = 'md5'; //编码 form.encoding = 'utf-8'; //上传目录 //form.uploadDir = dfv.getTemp() //限制每个文件的最大长度(单位字节) form.maxFileSize = 1024 * 1024 * 10; }) @route.post() async test(dat:TestFile) { return dat; } } ``` 对应的TestFile参数类型: ```typescript import {FileMultiple, valid} from "dfv/src/public/valid"; import {array, arrayString, dfv} from "dfv/src/public/dfv"; export class TestFile { //一个普通字串属性 para1 = ""; //name为file1的文件类型(文件验证不是使用装饰器,而是直接赋值),当客户端不传此参数,得到的就是null值 file1?: FileMultiple = valid.file(); //指定该文件不能为null,且长度大于0 file2 = valid.file(r => r.val && r.val.size > 0, "file2不能为空"); /** * 文件数组,form表单里的name需要加上[]后缀:file3[] */ file3 = valid.fileArray(r => r.val.length > 0 && r.val.each(f => f.size > 0)); } ``` ## 下一章:[3. 数据库ORM](3.orm.md) ##