ThinkJS3.0Documentation快速入门,ThinkJS

文件 6
3.0Documentation 快速入门 介绍 ThinkJS是一款面向未来的Node.js开发框架,整合了很多最佳实践,让企业级开发变得更加简单、高效。
从3.0开始,ThinkJS基于koa2.x,完全兼容koa里的middleware等插件。
同时,ThinkJS支持Extend和Adapter等方式,方便扩展框架里的各种功能。
特性 支持Middleware、Extend、Adapter等扩展方式基于Koa2.x开发性能优异,单元测试覆盖程度高内置自动编译、自动更新机制,方便开发环境快速开发直接使用async/await解决异步问题,不支持*/yield 架构 快速入门 借助ThinkJS提供的脚手架,可以快速的创建一个项目。
为了可以使用更多的ES6特性,框架要求Node.js的版本至少是6.x,建议使用LTS版本。
安装ThinkJS命令 $npminstall-gthink-cli 安装完成后,系统中会有thinkjs命令(可以通过thinkjs-v查看版本号)。
如果找不到这个命令,请确认环境变量是否正确。
如果是从2.x升级,需要将之前的命令删除,然后重新安装。
创建项目 执行thinkjsnew[project_name]来创建项目,如: $thinkjsnewdemo;$cddemo;$npminstall;$npmstart; 执行完成后,控制台下会看到类似下面的日志: [2017-06-2515:21:35.408][INFO]-Serverrunningathttp://127.0.0.1:8360[2017-06-2515:21:35.412][INFO]-ThinkJSversion:3.0.0-beta1[2017-06-2515:21:35.413][INFO]-Enviroment:development[2017-06-2515:21:35.413][INFO]-Workers:
8 打开浏览器访问http://127.0.0.1:8360/,如果是在远程机器上创建的项目,需要把IP换成对应的地址。
项目结构 默认创建的项目结构如下: |---development.js//开发环境下的⼊入⼝口⽂文件|---nginx.conf//nginx配置⽂文件|---package.json|---pm2.json//pm2配置⽂文件|---production.js//⽣生成环境⼊入⼝口⽂文件|---README.md|---src||---bootstrap//启动字执⾏行行⽬目录|||---master.js//Master进程下⾃自动执⾏行行|||---worker.js//Wokre进程下⾃自动执⾏行行||---config//配置⽂文件⽬目录|||---adapter.js//adapter配置|||---config.js//默认配置⽂文件|||---config.production.js//⽣生产环境下的默认配置⽂文件,和config.js合并|||---extend.js//项⽬目扩展配置⽂文件|||---middleware.js//中间件配置⽂文件|||---router.js//⾃自定义路路由配置⽂文件||---controller//控制器器⽬目录|||---base.js|||---index.js||---logic//logic⽬目录 |||---index.js||---model//模型⽬目录|||---index.js|---view//模板⽬目录||---index_index.html|---www||---static//存放静态资源⽬目录|||---css|||---img|||---js 升级指南 本文档为2.x升级到3.x的文档,由于本次升级接口改动较大,所以无法平滑升级。
本文档更多的是介绍接口变化指南。
核心变化 3.0抛弃了已有的核心架构,基于Koa2.x版本构建,兼容Koa里的所有功能。
主要变化为:之前的http对象改为ctx对象执行完全改为调用middleware来完成框架内置的很多功能不再默认内置,可以通过扩展来支持 项目启动 2.x中项目启动时,会自动加载src/bootstrap/目录下的所有文件。
3.0中不再自动加载所有的文件,而是改为: 在Master进程中加载src/boostrap/master.js文件在Woker进程中加载src/boostrap/worker.js文件如果还要加载其他的文件,那么可以在对应的文件中require进去。
配置 2.x中会自动加载src/config/目录下的所有文件,3.0中改为根据功能加载对应的文件。
hook和middleware 移除3.x里的hook和middleware,改为Koa里的middleware,middleware的管理放在src/config/middleware.js配置文件中。
2.x下的middleware类无法在3.0下使用,3.0下可以直接使用Koa的middleware。
Controller 将基类think.controller.base改为think.Controller,并移除think.controller.rest类。
Model 将基类think.model.base改为think.Model。
基础功能 启动流程 本文档带领大家一起看看ThinkJS是如何启动服务和处理用户请求的。
系统服务启动 执行npmstart或者nodedevelopment.js实例化ThinkJS里的Application类,执行run方法。
根据不同的环境(Master进程、Worker进程、命令行调用)处理不同的逻辑如果是Master进程 加载配置文件,生成think.config和think.logger对象。
加载文件src/bootstrap/matser.js文件如果配置文件监听服务,那么开始监听文件的变化,目录为src/。
文件修改后,如果配置文件编译服务,那么会对文件进行编译,编译到app/目录下。
根据配置workers来fork对应数目的Worker。
Worker进程启动完成后,触发appReady事件。
(可以通过think.app.on("appReady")来捕获)如果文件发生了新的修改,那么会触发编译,然后杀掉所有的Worker进程并重新fork。
如果是Worker进程加载配置文件,生成think.config和think.logger对象。
加载Extend,为框架提供更多的功能,配置文件为src/config/extend.js。
获取当前项目的模块列表,放在think.app.modules上,如果为单模块,那么值为空数组。
加载项目里的controller文件(src/controller/*.js),放在think.app.controllers对 象上。
加载项目里的logic文件(src/logic/*.js),放在think.app.logics对象上。
加载项目里的model文件(src/model/*.js),放在think.app.models对象上。
加载项目里的service文件(src/service/*.js),放在think.app.services对象上。
加载路由配置文件src/config/router.js,放在think.app.routers对象上。
加载校验配置文件src/config/validator.js,放在think.app.validators对象上。
加载middleware配置文件src/config/middleware.js,并通过think.app.use方法注册。
加载定时任务配置文件src/config/crontab.js,并注册定时任务服务。
加载src/bootstrap/worker.js启动文件。
监听process里的onUncaughtException和onUnhandledRejection错误事件,并进行处理。
可以在配置src/config.js自定义这二个错误的处理函数。
等待think.beforeStartServer注册的启动前处理函数执行,这里可以注册一些服务启动前的事务处理。
如果自定义了创建服务配置createServer,那么执行这个函数createServer(port,host,callback)来创建服务。
如果没有自定义,则通过think.app.listen来启动服务。
服务启动完成时,触发appReady事件,其他地方可以通过think.app.on("appReady")监听。
创建的服务赋值给think.app.server对象。
服务启动后,会打印下面的日志: [2017-07-0213:36:40.646][INFO]-Serverrunningathttp://127.0.0.1:8360[2017-07-0213:36:40.649][INFO]-ThinkJSversion:3.0.0-beta1[2017-07-0213:36:40.649][INFO]-Enviroment:development[2017-07-0213:36:40.649][INFO]-Workers:
8 用户请求处理 当用户请求服务时,会经过下面的步骤进行处理。
请求到达webserver(如:nginx),通过反向代理将请求转发给node服务。
如果直接通过端口访问node服务,那么就没有这一步了。
node服务接收用户请求,Master进程将请求转发给对应的Worker进程。
Worker进程通过注册的middleware来处理用户的请求: meta来处理一些通用的信息,如:设置请求的超时时间、是否发送ThinkJS版本号、是否发送处理的时间等。
resource处理静态资源请求,静态资源都放在www/static/下,如果命中当前请求是个静态资源,那么这个middleware处理完后提前结束,不再执行后面的middleware。
trace处理一些错误信息,开发环境下打印详细的错误信息,生产环境只是报一个通用的错误。
payload处理用户上传的数据,包含:表单数据、文件等。
解析完成后将数据放在request.body对象上,方便后续读取。
router解析路由,解析出请求处理对应的Controller和Action,放在ctx.controller和ctx.action上,方便后续处理。
如果项目是多模块结构,那么还有ctx.module。
logic根据解析出来的controller和action,调用logic里对应的方法。
实例化logic类,并将ctx传递进去。
如果不存在则直接跳过执行__before方法,如果返回false则不再执行后续所有的逻辑(提前结束处理)如果xxxAction方法存在则执行,结果返回false则不再执行后续所有的逻辑如果xxxAction方法不存在,则试图执行__call方法执行__after方法,如果返回false则不再执行后续所有的逻辑通过方法返回false来阻断后续逻辑的执行controller根据解析出来的controller和action,调用controller里的对应的方法。
具体的调用策略和logic完全一致如果不存在,那么当前请求返回404action执行完成时,可以将结果放在this.body属性上用户返回给用户。
当Worker报错,触发onUncaughtException或者onUnhandledRejection事件,或者Worker异常退出时,Master会捕获到错误,重新fork一个新的Worker进程,并杀掉当前的进程。
可以看到,所有的用户请求处理都是通过middleware来完成的。
具体的项目中,可以根据需求,组装更多的middleware来处理用户的请求。
阻止后续行为 上面说到,处理用户请求的所有逻辑都是通过执行middleware来完成的。
middleware执行是一个洋葱模型,中可以通过next来控制是否执行后续的行为。
functionmiddlewareFunction(options){return(ctx,next)=>{if(userLogin){//这⾥里里判断如果⽤用户登录了了才执⾏行行后续的⾏行行为returnnext();}} } 在Logic和Controller中,提供了__before和__after这些魔术方法,如果想在这些魔术方法里阻止后续的行为执行怎么办呢? 可以通过返回false来处理,如: __before(){if(!
userLogin) 执⾏行行} return false; //这⾥里里⽤用户未登录时返回了了 false,那么后⾯面的 xxxAction 不不再 配置 实际项目中,肯定需要各种配置,包括:框架需要的配置以及项目自定义的配置。
ThinkJS中所有的配置文件都是放在src/config/目录下,并根据不同的功能划分为不同的配置文件。
config.js通用的一些配置adapter.jsadapter配置router.js自定义路由配置middleware.jsmiddlware配置validator.js数据校验配置extend.js项目扩展功能配置 配置格式 //src/config.js module.exports={port:1234,redis:{host:'192.168.1.2',port:2456,password:''} } 配置值即可以是一个简单的字符串,也可以是一个复杂的对象。
具体是什么类型根据具体的需求来决定。
多环境配置 有些配置需要在不同的环境下配置不同的值,如:数据库的配置在开发环境和生产环境是不一样的。
此时可以通过环境下对应不同的配置文件来完成。
多环境配置文件格式为:[name].[env].js,如:config.development.js, config.production.js 在以上的配置文件中,config.js和adapter.js是支持不同环境配置文件的。
配置合并方式 系统启动时,会对配置合并,最终提供给开发者使用。
具体流程为: 加载加载加载加载加载加载加载加载 [ThinkJS]/lib/config.js[ThinkJS]/lib/config.[env].js[ThinkJS]/lib/adapter.js[ThinkJS]/lib/adapter.[env].jssrc/config/config.jssrc/config/config.[env].jssrc/config/adapter.jssrc/config/adapter.[env].js [env]为当前环境名称。
最终会将这些配置按顺序合并在一起,同名的key后面会覆盖前面的。
配置加载是通过think-loader模块实现的,具体代码见:/thinkjs/think-loader/blob/master/loader/config.js。
使用配置 框架提供了在不同的环境下不同的方式快速获取配置: 在ctx中,可以通过ctx.config(key)来获取配置在controller中,可以通过controller.config(key)来获取配置其他情况下,可以通过think.config(key)来获取配置 实际上,ctx.config和controller.config是基于think.config包装的一种更方便的获取配置的方式。
constredis=ctx.config('redis');//获取redis配置 动态设置配置 除了获取配置,有时候需要动态设置配置,如:将有些配置保存在数据库中,项目启动时将配置从数据库中读取出来,然后设置上去。
框架也提供了动态设置配置的方式,如:think.config(key,value) //src/bootstrap/worker.js //HTTP服务启动前执⾏行行think.beforeStartServer(async()=>{ constconfig=awaitthink.model('config').select();think.config('userConfig',config);//从数据库中将配置读取出来,然后设置}) 常见问题 能否将请求中跟用户相关的值设置到配置中?不不能。
配置设置是全局的,会在所有请求中生效。
如果将请求中跟用户相关的值设置到配置中,那么多个用户同时请求时会相互干扰。
config.js和adapter.js中的key能否重名?不不能。
由于config.js和adapter.js是合并在一起的,所以要注意这二个配置不能有相同的key,否则会被覆盖。
Context Context是Koa中处理用户请求中的一个对象,包含了request和response,在middleware和controller中使用,一般简称为ctx。
ThinkJS里继承了该对象,但扩展了更多的方法以便使用。
这些方法是通过Extend来实现的,具体代码见/thinkjs/thinkjs/blob/3.0/lib/extend/context.js。
API userAgent可以通过ctx.userAgent属性获取用户的userAgent。
constuserAgent=ctx.userAgent;if(userAgent.indexOf('spider')){ ...} isGet可以通过ctx.isGet判断当前请求类型是否是GET。
constisGet=ctx.isGet;if(isGet){ ...} isPost可以通过ctx.isPost判断当前请求类型是否是POST。
constisPost=ctx.isPost;if(isPost){ ...} isCli可以通过ctx.isCli判断当前请求类型是否是CLI(命令行调用)。
constisCli=ctx.isCli;if(isCli){ ...} referer(onlyHost)onlyHost{Boolean}是否只返回hostreturn{String} 获取请求的referer。
constreferer1=ctx.referer();//constreferer2=ctx.referer(true);// referrer(onlyHost)等同于referer方法。
isMethod(method)method{String}请求类型return{Boolean} 判断当前请求类型与method是否相同。
constisPut=ctx.isMethod('PUT'); isAjax(method)method{String}请求类型return{Boolean} 判断是否是ajax请求(通过header中x-requested-with值是否为XMLHttpRequest判断),如果执行了method,那么也会判断请求类型是否一致。
constisAjax=ctx.isAjax();constisPostAjax=ctx.isAjax('POST'); isJsonp(callbackField)callbackField{String}callback字段名,默认值为this.config('jsonpCallbackField')return{Boolean} 判断是否是jsonp请求。
constisJsonp=ctx.isJson('callback');if(isJsonp){ ctx.jsonp(data);} jsonp(data,callbackField) data{Mixed}要输出的数据callbackField{String}callback字段名,默认值为this.config('jsonpCallbackField')return{Boolean}false输出jsonp格式的数据,返回值为false。
可以通过配置jsonContentType指定返回的Content-Type。
ctx.jsonp({name:'test'}); //outputjsonp111({ name:'test'}) json(data) data{Mixed}要输出的数据return{Boolean}false输出json格式的数据,返回值为false。
可以通过配置jsonContentType指定返回的Content-Type。
ctx.json({name:'test'}); //output{ name:'test'} ess(data,message) data{Mixed}要输出的数据message{String}errmsg字段的数据return{Boolean}false输出带有errno和errmsg格式的数据。
其中errno值为0,errmsg值为message。
{errno:0,errmsg:'',data:... } 字段名errno和errmsg可以通过配置errnoField和errmsgField来修改。
fail(errno,errmsg,data)errno{Number}错误号errmsg{String}错误信息data{Mixed}额外的错误数据return{Boolean}false {errno:1000,errmsg:'nopermission',data:'' } 字段名errno和errmsg可以通过配置errnoField和errmsgField来修改。
expires(time)time{Number}缓存的时间,单位是毫秒。
可以1s,1m这样的时间return{undefined} 设置Cache-Control和Expires缓存头。
ctx.expires('1h');//缓存⼀一⼩小时 config(name,value,m)name{Mixed}配置名value{Mixed}配置值m{String}模块名,多模块项目下生效return{Mixed} 获取、设置配置项。
内部调用think.config方法。
ctx.config('name');//获取配置ctx.config('name',value);//设置配置值ctx.config('name',undefined,'admin');//获取admin模块下配置值,多模块项⽬目下⽣生效 param(name,value) name{String}参数名value{Mixed}参数值return{Mixed} 获取、设置URL上的参数值。
由于get、query等名称已经被Koa使用,所以这里只能使用param。
ctx.param('name');//获取参数值,如果不不存在则返回undefinedctx.param();//获取所有的参数值,包含动态添加的参数ctx.param('name1,name2');//获取指定的多个参数值,中间⽤用逗号隔开ctx.param('name',value);//重新设置参数值ctx.param({name:'value',name2:'value2'});//重新设置多个参数值 post(name,value) name{String}参数名value{Mixed}参数值return{Mixed} 获取、设置POST数据。
ctx.post('name');//获取POST值,如果不不存在则返回undefinedctx.post();//获取所有的POST值,包含动态添加的数据ctx.post('name1,name2');//获取指定的多个POST值,中间⽤用逗号隔开ctx.post('name',value);//重新设置POST值ctx.post({name:'value',name2:'value2'});//重新设置多个POST值 file(name,value) name{String}参数名value{Mixed}参数值return{Mixed}获取、设置文件数据。
ctx.file('name');//获取FILE值,如果不不存在则返回undefinedctx.file();//获取所有的FILE值,包含动态添加的数据ctx.file('name',value);//重新设置FILE值ctx.file({name:'value',name2:'value2'});//重新设置多个FILE值 文件的数据格式为: {"size":287313,//⽂文件⼤大⼩小"path":"/var/folders/4j/g57qvmmd1lb_9h605w_d38_r0000gn/T/upload_fa6bf8c44179851 f1cfec99544b4ef22",//临时存放的位置"name":"AnIntroductiontolibuv.pdf",//⽂文件名"type":"application/pdf",//类型"mtime":"2017-07-02T07:55:23.763Z"//最后修改时间 } cookie(name,value,options) name{String}Cookie名value{mixed}Cookie值options{Object}Cookie配置项return{Mixed}获取、设置Cookie值。
ctx.cookie('name');//获取Cookiectx.cookie('name',value);//设置Cookiectx.cookie(name,null);//删除Cookie 设置Cookie时,如果value的长度大于4094,则触发cookieLimit事件,该事件可以通过think.app.on("cookieLimit")来捕获。
controller(name,m) name{String}要调用的controller名称m{String}模块名,多模块项目下生效return{Object}ClassInstance获取另一个Controller的实例,底层调用think.controller方法。
//获取src/controller/user.js的实例例constcontroller=ctx.controller('user'); service(name,m) name{String}要调用的service名称m{String}模块名,多模块项目下生效return{Mixed}获取service,这里获取到service后并不会实例化。
//获取src/service/github.js模块constgithub=ctx.service('github'); Middleware 由于3.0是在Koa@2版本之上构建的,所以完全兼容Koa@2里的middleware。
在Koa中,一般是通过app.use的方式来使用中间件的,如: constapp=newKoa();constbodyparser=require('koa-bodyparser');app.use(bodyparser({})); 为了方便管理和使用middleware,ThinkJS提供了一个统一的配置来管理middleware,配置文件为src/config/middleware.js。
配置格式 constpath=require('path')constisDev=think.env==='development' module.exports=[{handle:'meta',options:{logRequest:isDev,sendResponseTime:isDev,},},{handle:'resource',enable:isDev,options:{root:path.join(think.ROOT_PATH, 'www'), publicPath:/^\/(static|favicon\.ico)/,},}] 配置项为项目中要使用的middleware列表,每一项支持handle,enable,options,match等属性。
handlemiddleware的处理函数,可以用系统内置的,也可以是引入外部的,也可以是项目里的middleware。
middleware的函数格式为: module.exports=(options,app)=>{return(ctx,next)=>{} } 这里middleware接收的参数除了options外,还多了个app对象,该对象为KoaApplication的实例。
enable是否开启当前的middleware,比如:某个middleware只在开发环境下才生效。
{handle:'resouce',enable:think.env==='development'//这个middleware只在开发环境下⽣生效 } options传递给middleware的配置项,格式为一个对象。
module.exports=[{handle:} ] match 匹配特定的规则后才执行该middleware,支持二种方式,一种是路径匹配,一种是自定义函数匹配。
如: module.exports=[{handle:'xxx-middleware',match:'/resource'//请求的URL是/resource打头时才⽣生效这个middleware} ] module.exports=[{handle:'xxx-middleware',match:ctx=>{//match为⼀一个函数,将ctx传递给这个函数,如果返回结果为true,则启⽤用 该middlewarereturntrue; }}] 框架内置的middleware 框架内置了几个middleware,可以通过字符串的方式直接引用。
module.exports=[{handle:'meta',options:{}} ] meta显示一些meta信息,如:发送ThinkJS的版本号,接口的处理时间等等resource处理静态资源,生产环境建议关闭,直接用webserver处理即可。
trace处理报错,开发环境将详细的报错信息显示处理,也可以自定义显示错误页面。
payload处理表单提交和文件上传,类似于koa-bodyparser等middlewarerouter路由解析,包含自定义路由解析 logiclogic调用,数据校验controllercontroller和action调用 项目中自定义的middleware 有时候项目中根据一些特定需要添加middleware,那么可以放在src/middleware目录下,然后就可以直接通过字符串的方式引用了。
如:添加了src/middleware/csrf.js,那么就可以直接通过csrf字符串引用这个middleware。
module.exports=[{handle:'csrf',options:{}} ] 引入外部的middleware 引入外部的middleware非常简单,只需要require进来即可。
注意:引入的middlware必须是Koa@2的。
constcsrf=require('csrf');module.exports=[ ...,{ handle:csrf,options:{}},...] 常见问题 middleware配置是否需要考虑顺序?middleaware执行是安排配置的顺序执行的,所以需要开发者考虑配置单顺序。
怎么看当前环境下哪些middleware生效? 可以通过DEBUG=koa:applicationnodedevelopment.js来启动项目,这样控制台下会看到koa:applicationuse...相关的信息。
注意:如果启动了多个worker,那么会打印多遍。
Logic 当在Action里处理用户的请求时,经常要先获取用户提交过来的数据,然后对其校验,如果校验没问题后才能进行后续的操作;当参数校验完成后,有时候还要进行权限判断等,这些都判断无误后才能进行真正的逻辑处理。
如果将这些代码都放在一个Action里,势必让Action的代码非常复杂且冗长。
为了解决这个问题,ThinkJS在控制器前面增加了一层Logic,Logic里的Action和控制器里的Action一一对应,系统在调用控制器里的Action之前会自动调用Logic里的Action。
Logic层 Logic目录在src/[module]/logic,在项目根目录通过命令thinkjscontrollertest会创建名为test的Controller同时会自动创建对应的Logic。
Logic代码类似如下: module.exports=classextendsthink.Logic{__before(){//todo}indexAction(){//todo}__after(){//todo} } 注:若自己手工创建时,Logic文件名和Controller文件名要相同其中,Logic里的Action和Controller里的Action一一对应。
Logic里也支持__before和__after魔术方法。
请求类型校验 对应一个特定的Action,有时候需要限定为某些请求类型,其他类型的请求给拒绝掉。
可以通过配置特定的请求类型来完成对请求的过滤。
module.exports=classextendsthink.Logic{indexAction(){this.allowMethods='post';//只允许POST请求类型}detailAction(){this.allowMethods='get,post';//允许GET、POST请求类型} } 校验规则格式数据校验的配置格式为字段名:JSON配置对象,如下: module.exports=classextendsthink.Logic{ indexAction(){ letrules={ username:{ string:true, //字段类型为String类型 required:true, //字段必填 default:'thinkjs',//字段默认值为'thinkjs' trim:true, //字段需要trim处理理 method:'GET' //指定获取数据的⽅方式 }, age:{ int:{min:20,max:60}//20到60之间的整数 } } letflag=this.validate(rules); } } 基本数据类型支持的数据类型有:boolean、string、int、float、array、object,对于一个字段只允许指定为一种基本数据类型,默认为string类型。
手动设置数据值如果有时候不能自动获取值的话(如:从header里取值),那么可以手动获取值后配置进去。
如: module.exports=classextendsthink.Logic{saveAction(){ letrules={username:{value:this.header('x-name')//从header中获取值} }}} 指定获取数据来源 如果校验version参数,默认情况下会根据当前请求的类型来获取字段对应的值,如果当前请求类型是GET,那么会通过this.param('version')来获取version字段的值;如果请求类型是POST,那么会通过this.post('version')来获取字段的值,如果当前请求类型是FILE,那么会通过this.file('version')来获取verison字段的值。
有时候在POST类型下,可能会获取上传的文件或者获取URL上的参数,这时候就需要指定获取数据的方式了。
支持的获取数据方式为GET,POST和FILE。
字段默认值 使用default:value来指定字段的默认值,如果当前字段值为空,会把默认值赋值给该字段,然后执行后续的规则校验。
消除前后空格 使用trim:true如果当前字段支持trim操作,会对该字段首先执行trim操作,然后再执行后续的规则校验。
数据校验方法 配置好校验规则后,可以通过this.validate方法进行校验。
如: module.exports=classextendsthink.Logic{indexAction(){letrules={username:{required:true}}letflag=this.validate(rules);if(!
flag){returnthis.fail('validateerror',this.validateErrors);//如果出错,返回//{"errno":1000,"errmsg":"validateerror","data":{"username":"usernamecan notbeblank"}}} }} 如果你在controller的action中使用了this.isGet或者this.isPost来判断请求的话,在上面的代码中也需要加入对应的this.isGet或者this.isPost,如: module.exports=classextendsthink.LogicindexAction(){if(this.isPost){letrules={username:{required:true}}letflag=this.validate(rules);if(!
flag){returnthis.fail('validateerror',}} {this.errors()); }} 如果返回值为false,那么可以通过访问this.validateErrors属性获取详细的错误信息。
拿到错误信息后,可以通过this.fail方法把错误信息以JSON格式输出,也可以通过this.display方法输出一个页面,Logic继承了Controller可以调用Controller的方法。
自动调用校验方法 多数情况下都是校验失败后,输出一个JSON错误信息。
如果不想每次都手动调用this.validate进行校验,可以通过将校验规则赋值给this.rules属性进行自动校验,如: module.exports=classextendsthink.Logic{indexAction(){this.rules={username:{required:true}}} } 相当于 module.exports=classextendsthink.Logic{indexAction(){letrules={username:{required:true}}letflag=this.validate(rules);if(!
flag){returnthis.fail(this.config('validateDefaultErrno'),this.validateErrors);}} } 将校验规则赋值给this.rules属性后,会在这个Action执行完成后自动校验,如果有错误则直接输出JSON格式的错误信息。
数组校验 数据校验支持数组校验,但是数组校验只支持一级数组,不支持多层级嵌套的数组。
children为所有数组元素指定一个相同的校验规则。
module.exports=classextendsthink.Logic{letrules={username:{array:true,children:{string:true,trim:true,default:'thinkjs'},method:'GET'}}this.validate(rules); } 对象校验 数据校验支持对象校验,但是对象校验只支持一级对象,不支持多层级嵌套的对象。
children为所有对象属性指定一个相同的校验规则。
module.exports=classextendsthink.Logic{ letrules={username:{object:true,children:{string:true,trim:true,default:'thinkjs'},method:'GET'} }this.validate(rules);} 校验前数据的自动转换 对于指定为boolean类型的字段,'yes','on','1','true',true会被转换成true,其他情况转换成false,然后再执行后续的规则校验; 对于指定为array类型的字段,如果字段本身是数组,不做处理;如果该字段为字符串会进行split(',')处理,其他情况会直接转化为[字段值],然后再执行后续的规则校验。
校验后数据的自动转换 对于指定为int、float、numeric数据类型的字段在校验之后,会自动对数据进行parseFloat转换。
module.exports=classextendsthink.Logic{indexAction(){letrules={age:{int:true,method:'GET'}}letflag=this.validate(rules);} } 如果url中存在参数age=26,在经过Logic层校验之后,typeofthis.param('age')为number类型。
全局定义校验规则 在单模块下项目下的config目录下建立valadator.js文件;在多模块项目下的mon/config目录下建立valadator.js。
在valadator.js中添加自定义的校验方法: 例如,我们想要验证GET请求中的name1参数是否等于字符串lucy可以如下添加校验规则;访问你的服务器地址/index/?
name1=jack //logicindex.jsmodule.exports=classextendsthink.Logic{ indexAction(){letrules={name1:{eqValid:'lucy',method:'GET'}}letflag=this.validate(rules);if(!
flag){console.log(this.validateErrors);//name1shoudeqlucy} }}} //src/config/validator.js module.exports={ rules:{ /** *@param{Mixed}value*@param{Mixed}parsedValue*@param{String}validName*@return{Boolean} [相应字段的请求值][校验规则的参数值][校验规则的参数名称][校验结果] */ eqValid(value,parsedValue,validName){ console.log(value,parsedValue,validName);//'jack', returnvalue===parsedValue; } }, messages:{ eqValid:'{name}shouldeq{args}' } } 'lucy', 'eqValid' 解析校验规则参数 有时我们想对校验规则的参数进行解析,只需要建立一个下划线开头的同名方法在其中执行相应的解析,并将解析后的结果返回即可。
例如我们要验证GET请求中的name1参数是否等于name2参数,可以如下添加校验方法:访问你的服务器地址/index/?
name1=tom&name2=lily //logicindex.jsmodule.exports=classextendsthink.Logic{ indexAction(){letrules={name1:{eqValid:'name2',method:'GET'}}letflag=this.validate(rules);if(!
flag){console.log(validateErrors);//name1shoudeqname2} }} //src/config/validator.js module.exports={ rules:{ /** *需要下划线开头的同名⽅方法 *@param{Mixed}validValue[校验规则的参数值] *@param{Mixed}query [校验规则method指定的参数来源下的参数] *@param{String}validName[校验规则的参数名称] *@return{Mixed} [解析之后的校验规则的参数值] */ _eqValid(validValue,query,validName){ console.log(validValue,query,validName);//'name2',{name1: 'lily'},'eqValid' letparsedValue=query[validValue]; returnparsedValue; }, 'tom', name2: /** *@param{Mixed}value*@param{Mixed}parsedValue*@param{String}validName*@return{Boolean} [相应字段的请求值][_eqValid⽅方法返回的解析之后的校验规则的参数值][校验规则的参数名称][校验结果] */ eqValid(value,parsedValue,validName){ console.log(value,parsedValue,validName);//'tom','lily','eqValid' returnvalue===parsedValue; } }, messages:{ eqValid:'{name}shouldeq{args}' } } 自定义错误信息 错误信息中可以存在三个插值变量{name}、{args}、{pargs}。
{name}会被替换为校验的字段名称,{args}会被替换为校验规则的值,{pargs}会被替换为解析方法返回的值。
如果{args}、{pargs}不是字符串,将做JSON.stringify处理。
对于非Object:true类型的字段,支持三种自定义错误的格式:规则1:规则:错误信息;规则2:字段名:错误信息;规则3:字段名:{规则:错误信息}。
对于同时指定了多个错误信息的情况,优先级规则3>规则2>规则
1。
module.exports=classextendsthink.Logic{letrules={username:{required:true,method:'GET'}}letmsgs={required:'{name}cannotblank',username:'{name}cannotblank',username:{required:'{name}cannotblank'}}this.validate(rules,msgs); } //规则1//规则2 //规则
3 对于Object:true类型的字段,支持以下方式的自定义错误。
优先级为规则5>(4=3)>2>
1。
module.exports=classextendsthink.Logic{ letrules={ address:{ object:true, children:{ int:true } } } letmsgs={ int:'thisisinterrormessageforallfield', //规则
1 address:{ int:'thisisinterrormessageforaddress', //规则
2 a:'thisisinterrormessageforaofaddress', //规则
3 'b,c':'thisisinterrormessageforbandcofaddress'//规则
4 d:{ int:'thisisinterrormessagefordofaddress'}}}letflag=this.validate(rules,msgs);} //规则
5 支持的校验类型 required required:true字段必填,默认required:false。
undefined、空字符串串、null、NaN在required:true时校验不通过。
module.exports=classextendsthink.Logic{indexAction(){letrules={name:{required:true}}this.validate(rules);//todo} } name为必填项。
requiredIf 当另一个项的值为某些值其中一项时,该项必填。
如: module.exports=classextendsthink.Logic{indexAction(){letrules={name:{requiredIf:['username','lucy','tom'],method:'GET'}}this.validate(rules);//todo} } 对于上述例子,当GET请求中的username的值为lucy、tom任何一项时,name的值必填。
requiredNotIf 当另一个项的值不在某些值中时,该项必填。
如: module.exports=classextendsthink.Logic{indexAction(){letrules={name:{requiredNotIf:['username','lucy','tom'],method:'POST'}}this.validate(rules);//todo} } 对于上述例子,当POST请求中的username的值不为lucy或者tom任何一项时,name的值必填。
requiredWith 当其他几项有一项值存在时,该项必填。
module.exports=classextendsthink.Logic{indexAction(){letrules={name:{requiredWith:['id','email'],method:'GET'}}this.validate(rules);//todo} } 对于上述例子,当GET请求中id,email有一项值存在时,name的值必填。
requiredWithAll 当其他几项值都存在时,该项必填。
module.exports=classextendsthink.Logic{indexAction(){letrules={name:{requiredWithAll:['id','email'],method:'GET'}}this.validate(rules);//todo} } 对于上述例子,当GET请求中id,email所有项值存在时,name的值必填。
requiredWithOut 当其他几项有一项值不存在时,该项必填。
module.exports=classextendsthink.Logic{indexAction(){letrules={name:{requiredWithOut:['id','email'],method:'GET'}}this.validate(rules);//todo} } 对于上述例子,当GET请求中id,email有任何一项值不存在时,name的值必填。
requiredWithOutAll 当其他几项值都不存在时,该项必填。
module.exports=classextendsthink.Logic{ indexAction(){letrules={name:{requiredWithOutAll:['id','email'],method:'GET'}}this.validate(rules);//todo }} 对于上述例子,当GET请求中id,email所有项值不存在时,name的值必填。
contains 值需要包含某个特定的值。
module.exports=classextendsthink.Logic{indexAction(){letrules={name:{contains:'ID-',method:'GET'}}this.validate(rules);//todo} } 对于上述例子,当GET请求中name得值需要包含字符串ID-。
equals 和另一项的值相等。
module.exports=classextendsthink.Logic{indexAction(){letrules={name:{equals:'username',method:'GET'}}this.validate(rules); //todo}} 对于上述例子,当GET请求中的name与username的字段要相等。
different 和另一项的值不等。
module.exports=classextendsthink.Logic{indexAction(){letrules={name:{equals:'username',method:'GET'}}this.validate(rules);//todo} } 对于上述例子,当GET请求中的name与username的字段要不相等。
before值需要在一个日期之前,默认为需要在当前日期之前。
module.exports=classextendsthink.Logic{indexAction(){letrules={time:{before:'2099-12-1212:00:00',//before:method:'GET'}}this.validate(rules);//todo} } true 早于当前时间 对于上述例子,当GET请求中的time字段对应的时间值要早于2099-12-1212:00:00。
after值需要在一个日期之后,默认为需要在当前日期之后,after:true|timestring。
alpha值只能是[a-zA-Z]组成,alpha:true。
alphaDash值只能是[a-zA-Z_]组成,alphaDash:true。
alphaNumeric值只能是[a-zA-Z0-9]组成,alphaNumeric:true。
alphaNumericDash值只能是[a-zA-Z0-9_]组成,alphaNumericDash:true。
ascii值只能是ascii字符组成,ascii:true。
base64值必须是base64编码,base64:true。
byteLength字节长度需要在一个区间内,byteLength:options。
module.exports=classextendsthink.Logic{indexAction(){letrules={field_name:{byteLength:{min:2,max:4}//字节⻓长度需要在2-4之间//byteLength:{min:2}//字节最⼩小⻓长度需要为2//byteLength:{max:4}//字节最⼤大⻓长度需要为4}}} } creditCard需要为信用卡数字,creditCard:true。
currency需要为货币,currency:true|options,options参见/chriso/validator.js。
date需要为日期,date:true。
decimal需要为小数,例如:0.1,.3,1.1,1.00003,4.0,decimal:true。
divisibleBy需要被一个数整除,divisibleBy:number。
module.exports=classextendsthink.Logic{indexAction(){letrules={field_name:{divisibleBy:2//可以被2整除}}} } email需要为email格式,email:true|options,options参见/chriso/validator.js。
fqdn 需要为合格的域名,fqdn:true|options,options参见/chriso/validator.js。
float 需要为浮点数,float:true|options,options参见/chriso/validator.js。
module.exports=classextendsthink.LogicindexAction(){letrules={money:{float:true,//需要是个浮点数//float:{min:1.0,max:9.55}//}}this.validate();//todo} } {需要是个浮点数,且最⼩小值为 1.0,最⼤大值为 9.55 fullWidth需要包含宽字节字符,fullWidth:true。
halfWidth需要包含半字节字符,halfWidth:true。
hexColor需要为个十六进制颜色值,hexColor:true。
hex需要为十六进制,hex:true。
ip需要为ip格式,ip:true。
ip4需要为ip4格式,ip4:true。
ip6需要为ip6格式,ip6:true。
isbn需要为国际标准书号,isbn:true。
isin需要为证券识别编码,isin:true。
iso8601需要为iso8601日期格式,iso8601:true。
issn国际标准连续出版物编号,issn:true。
uuid需要为UUID(3,4,5版本),uuid:true。
dataURI需要为dataURI格式,dataURI:true。
md5需要为md5,md5:true。
macAddress需要为mac地址,macAddress:true。
variableWidth 需要同时包含半字节和全字节字符,variableWidth:true。
in 在某些值中,in:[...]。
module.exports=classextendsthink.Logic{indexAction(){letrules={version:{in:['2.0','3.0']//需要是2.0,3.0其中⼀一个}}this.validate();//todo} } notIn 不能在某些值中,notIn:[...]。
int 需要为int型,int:true|options,options参见/chriso/validator.js。
module.exports=classextendsthink.Logic{indexAction(){letrules={field_name:{int:true,//需要是int型//int:{min:10,max:100}//需要在10-100之间}}this.validate();//todo} } length 长度需要在某个范围,length:options。
module.exports=classextendsthink.Logic{indexAction(){letrules={field_name:{length:{min:10},//⻓长度不不能⼩小于10//length:{max:20},//⻓长度不不能⼤大于10//length:{min:10,max:20}//⻓长度需要在10-20之间}}this.validate();//todo} } lowercase 需要都是小写字母,lowercase:true。
uppercase 需要都是大写字母,uppercase:true。
mobile 需要为手机号,mobile:true|options,options参见/chriso/validator.js。
module.exports=classextendsthink.Logic{indexAction(){letrules={mobile:{mobile:'zh-CN'//必须为中国的⼿手机号}}this.validate();//todo} } mongoId需要为MongoDB的ObjectID,mongoId:true。
multibyte需要包含多字节字符,multibyte:true。
url需要为url,url:true|options,options参见/chriso/validator.js。
order需要为数据库查询order,如:nameDESC,order:true。
field需要为数据库查询的字段,如:name,title,field:true。
image上传的文件需要为图片,image:true。
startWith需要以某些字符打头,startWith:string。
endWith需要以某些字符结束,endWith:string。
string需要为字符串,string:true。
array需要为数组,array:true,对于指定为array类型的字段,如果字段对应的值是字符串不做处理;如果字段对应的值是字符串,进行split(,)处理;其他情况转化为[字段值]。
boolean需要为布尔类型。
'yes','on','1','true',true会自动转为布尔true。
object需要为对象,object:true。
regexp字段值要匹配给出的正则。
module.exports=classextendsthink.Logic{indexAction(){this.rules={name:{regexp:/thinkjs/g}}this.validate();//todo} } Controller 控制器是一类操作的集合,用来响应用户同一类的请求。
比如:将用户相关的操作都放在user.js里,每一个操作就是里面一个Action。
创建Controller 创建的controller都需要继承自think.Controller类,这样就能使用一些内置的方法。
当然项目一般会创建一些通用的基类,然后实际的controller都继承自这个基类。
项目创建时会自动创建了一个名为base.js的基类,其他controller继承该类即可。
//src/controller/user.jsconstBase=require('./base.js');module.exports=classextendsBase{ indexAction(){this.body='helloword!
'; }} 创建完成后,框架会监听变化然后重启服务。
这时访问http://127.0.0.1:8360/user/index就可以看到输出的helloword!
前置操作__before 项目中,有时候需要在一个统一的地方做一些操作,如:判断是否已经登录,如果没有登录就不能继续后面行为。
此种情况下,可以通过内置的__before来实现。
__before是在调用具体的Action之前调用的,这样就可以在其中做一些处理。
module.exports=classextendsthink.Controller{async__before(){constuserInfo=awaitthis.session('userInfo');//获取⽤用户的session信息,如果为空,返回false阻⽌止后续的⾏行行为继续执⾏行行if(think.isEmpty(userInfo)){returnfalse;}}indexAction(){//__before调⽤用完成后才会调⽤用indexAction} } 后置操作__after 后置操作__after与__before对应,只是在具体的Action执行之后执行,如果具体的Action执行返回了false,那么__after不再执行。
module.exports=classextendsthink.Controller{indexAction(){ }__after(){ //indexAction执⾏行行完成后执⾏行行,如果indexAction返回了了false则不不再执⾏行行}} ctx对象 controller实例化时会传入ctx对象,在controller里可以通过this.ctx来获取ctx对象。
并且controller上很多方法也是通过调用ctx里的方法来实现的。
多级控制器 有时候项目比较复杂,文件较多,所以希望根据功能进行一些划分。
如:用户端的功能放在一块、管理端的功能放在一块。
这时可以借助多级控制器来完成这个功能,在src/controller/目录下创建user/和admin/目录,然后用户端的功能文件都放在user/目录下,管理端的功能文件都放在admin/目录下。
访问时带上对应的目录名,路由解析时会优先匹配目录下的控制器。
API 属性 controller.ctx传递进来的ctx对象。
controller.bodyreturn{String} 设置或者获取返回内容 module.exports=classextendsthink.Controller{indexAction(){this.body='helloworld';} } controller.ipreturn{String} 获取当前请求用户的ip,等同于ctx.ip方法。
controller.ips return{Array}获取当前请求链路的所有ip,等同于ctx.ips方法。
controller.methodreturn{String} 获取当前请求的类型,转化为小写。
controller.isGetreturn{Boolean} 判断是否是GET请求。
controller.isPostreturn{Boolean} 判断是否是POST请求。
controller.isClireturn{Boolean} 是否是命令行下调用。
controller.userAgent获取userAgent。
方法 controller.isMethod(method)method{String}类型return{Boolean} 判断当前的请求类型是否是指定的类型。
controller.isAjax(method) method{String}return{Boolean}判断是否是Ajax请求。
如果指定了method,那么请求类型也要相同。
module.exports=classextendsthink.Controller{indexAction(){//是ajax且请求类型是POSTletisAjax=this.isAjax('post');} } controller.isJsonp(callback) callback{String}callback名称return{Boolean}是否是jsonp请求。
controller.get(name) name{String}参数名获取GET参数值。
module.exports=classextendsthink.Controller{indexAction(){//获取⼀一个参数值letvalue=this.get('xxx');//获取所有的参数值letvalues=this.get();} } controller.post(name) name{String}参数名获取POST提交的参数。
module.exports=classextendsthink.Controller{indexAction(){//获取⼀一个参数值 letvalue=this.post('xxx');//获取所有的POST参数值letvalues=this.post();}} controller.param(name) name{String}参数名获取参数值,优先从POST里获取,如果取不到再从GET里获取。
controller.file(name) name{String}上传文件对应的字段名获取上传的文件,返回值是个对象,包含下面的属性: {fieldName:'file',//表单字段名称originalFilename:filename,//原始的⽂文件名path:filepath,//⽂文件保存的临时路路径,使⽤用时需要将其移动到项⽬目⾥里里的⽬目录,否则请求结束时会被删 除size:1000//⽂文件⼤大⼩小 } 如果文件不存在,那么值为一个空对象{}。
该方法等同于ctx.file方法。
controller.header(name,value) name{String}header名value{String}header值获取或者设置header。
module.exports=classextendsthink.Controller{indexAction(){letept=this.header('ept');//获取headerthis.header('X-NAME','thinks');//设置header} } controller.expires(time)time{Number}过期时间,单位为秒 强缓存,设置Cache-Control和Expires头信息。
module.exports=classextendsthink.Controller{indexAction(){this.expires(86400);//设置过期时间为1天。
} } 该方法等同于ctx.expires方法。
controller.referer(onlyHost)referrer{Boolean}是否只需要host 获取referrer。
controller.referrer(onlyHost)该方法等同于controller.referer方法。
controller.cookie(name,value,options)name{String}cookie名value{String}cookie值options{Object} 获取、设置或者删除cookie。
module.exports=classextendsthink.Controller{indexAction(){//获取cookie值letvalue=this.cookie('think_name');} } module.exports=classextendsthink.Controller{indexAction(){//设置cookie值 this.cookie('think_name',value,{timeout:3600*24*7//有效期为⼀一周 });}} module.exports=classextendsthink.Controller{indexAction(){//删除cookiethis.cookie('think_name',null);} } controller.redirect(url)url{String}要跳转的url 页面跳转。
controller.jsonp(data,callback)data{Mixed}要输出的内容callback{String}callback方法名 jsonp的方法输出内容,获取callback名称安全过滤后输出。
module.exports=classextendsthink.Controller{indexAction(){this.jsonp({name:'thinkjs'},'callback_fn_name');//writes'callback_fn_name({name:"thinkjs"})'} } controller.json(data)data{Mixed}要输出的内容 json的方式输出内容。
controller.status(status) status{Number}状态码,默认为404设置状态码。
ess(data,message)格式化输出一个正常的数据,一般是操作成功后输出,等同于ess。
controller.fail(errno,errmsg,data)格式化输出一个异常的数据,一般是操作失败后输出,等同于ctx.fail。
View 由于某些项目下并不需要View的功能,所以3.0里并没有直接内置View的功能,而是通过Extend和Adapter来实现的。
Extend来支持View 配置src/config/extend.js,添加如下的配置,如果已经存在则不需要再添加: constview=require('think-view');module.exports=[ view] 通过添加view的扩展,让框架有渲染模板文件的能力。
配置ViewAdapter 在src/config/adapter.js中添加如下的配置,如果已经存在则不需要再添加: constnunjucks=require('think-view-nunjucks');constpath=require('path'); exports.view={type:'nunjucks',mon:{viewPath:path.join(think.ROOT_PATH,'view'),sep:'_',//Controller与Action之间的连接符extname:'.html'//⽂文件扩展名}, //模板⽂文件的根⽬目录 nunjucks:{handle:nunjucks }} 这里用的模板引擎是nunjucks,项目中可以根据需要修改。
具体使用 配置了Extend和Adapter后,就可以在Controller里使用了。
如: module.exports=classextendsthink.Controller{indexAction(){this.assign('title','thinkjs');//给模板赋值returnthis.display();//渲染模板} } assign给模板赋值。
this.assign('title','thinkjs');//单条赋值this.assign({title:'thinkjs',name:'test'});//单条赋值this.assign('title');//获取之前赋过的值,如果不不存在则为undefinedthis.assign();//获取所有赋的值 render获取渲染后的内容。
constcontent1=awaitthis.render();//根据当前请求解析的controller和action⾃自动匹配模板⽂文件constcontent2=awaitthis.render('doc');//指定⽂文件名constcontent3=awaitthis.render('doc','ejs');//切换模板类型constcontent4=awaitthis.render('doc',{type:'ejs',xxx:'yyy'});//切换模板类型,并配置额外的参数 display渲染并输出内容。
returnthis.display();//根据当前请求解析的controller和action⾃自动匹配模板⽂文件returnthis.display('doc');//指定⽂文件名returnthis.display('doc','ejs');//切换模板类型returnthis.display('doc',{type:'ejs',xxx:'yyy'});//切换模板类型,并配置额外的参数 支持的Adapter View支持的Adapter见/thinkjs/think-awesome#view。
路由 当用户访问一个URL时,最终执行哪个模块(module)下哪个控制器(controller)的哪个操作(action),这是路由模块解析后决定的。
除了默认的解析外,ThinkJS提供了一套灵活的路由机制,让URL更加简单友好。
路由在ThinkJS中是以中间件(middleware)的形式存在的。
路由参数配置 在路由中间件配置文件中可以针对路由进行基础的参数配置。
单模块下,配置文件src/config/middleware.js: constrouter=require('think-router'); module.exports=[ { handle:router, options:{ prefix:'home',defaultController:'index',defaultAction:'index',prefix:[],suffix:['.html'],enableDefaultRouter:true,subdomainOffset:2,subdomain:{},denyModules:[] //多模块下,默认的模块名//默认的控制器器名//默认的操作名//默认去除的url前缀//默认去除的url后缀//在不不匹配情况下是否使⽤用默认路路由//⼦子域名偏移//⼦子域名映射//多模块下,禁⽌止访问的模块 } } ]; pathname预处理 当用户访问服务时,服务端首先拿到的是一个完整的URL,如:访问本页面,得到的URL为,我们得到的初始pathname为//doc/3.0/router.html。
有时候为了搜索引擎优化或者一些其他的原因,URL上会多加一些东西。
比如:当前页面是一个动态页面,我们在URL最后加了.html,这样对搜索引擎更加友好,但这在后续的路由解析中是无用的。
ThinkJS里提供了下面的配置可以去除pathname的某些前缀和后缀。
在路由中间件配置文件中: {prefix:[],suffix:['.html'],//其他配置... } prefix与subffix为数组,数组的每一项可以为字符串或者正则表达式,在匹配到第一个之后停止后续匹配。
对于上述pathname在默认配置下进行过滤后,拿到纯净的pathname为//doc/3.0/router。
如果访问的URL是/,那么最后拿到纯净的pathname则为字符串/。
路由规则 单模块路由规则配置文件src/config/router.js,路由规则为二维数组: module.exports=[[/libs\/(.*)/i,'/libs/:1','get'],[/fonts\/(.*)/i,'/fonts/:1','get,post'], ]; 路由的匹配规则为:从前向后逐一匹配,如果命中到了该项规则,则不再向后匹配。
对于每一条匹配规则,参数为: [match,path, //url匹配规则,预先使⽤用path-to-regexp转换//对应的操作(action)的路路径 method,[options]] //允许匹配的请求类型,多个请求类型之间逗号分隔,get|post|redirect|rest|cli//额外参数,如method=redirect时,指定跳转码{statusCode:301} 路由解析 对于匹配规则中的match会使用path-to-regexp预先转换: module.exports=[['/api_libs/(.*)/:id','/libs/:1/','get'], ] 对于match中的:c(c为字符串),在match匹配pathname之后获取到c的值,c的值会被附加到this.ctx;path可以引用match匹配pathname之后的结果,对于path中的:n(n为数字),会被match匹配pathname的第n个引用结果替换。
路由识别默认根据[模块]/控制器器/操作/...来识别过滤后的pathname,从而对应到最终要执行的操作(action)。
例如,对于上述规则,在访问URLhttp://api_libs/inbox/123时,规则的path将变为/libs/inbox/,同时{id:'123'}会附加到this.ctx上。
之后对pathname进行解析,就对应到了libs控制器(controller)下的inbox操作(action)。
在inboxAction下可以通过this.ctx.param('id')获取到id的值123。
子域名部署 当项目比较复杂时,可能希望将不同的功能部署在不同的域名下,但代码还是在一个项目下。
ThinkJS提供子域名来处理这个需求,在路由中间件配置文件中: {subdomainOffset:2,prefix:[],suffix:['.html'],subdomain:{'news,zm':'news'},//其他配置... } 域名偏移(subdomainOffset)默认为
2,例如对于域名,this.ctx.subdomains为['news','zm'],当域名偏移为3时,this.ctx.subdomains为['zm']。
如果路由中间件配置的subdomain项中存在this.ctx.subdomains.join(,)对应的key,此key对应的值将会被附加到pathname上,然后再进行路由的解析。
对于上述配置,当我们访问:8360/api_lib/inbox/123,我们得到的pathname将为/news/api_lib/inbox/123。
另外subdomain的配置也可以为一个数组,我们会将数组转化为对象。
例如subdomain:['admin','user']将会被转化为subdomain:{admin:'admin',user:'user'}。
Adapter Adapter是用来解决一类功能的多种实现,这些实现提供一套相同的接口。
如:支持多种数据库,支持多种模版引擎等。
通过这种方式,可以很方便的再不同的类型中进行切换。
框架提供了多种Adapter,如:View,Model,Cache,Session,Websocket等。
Adapter配置 Adapter的配置文件为src/config/adapter.js,格式如下: constnunjucks=require('think-view-nunjucks');constejs=require('think-view-ejs');constpath=require('path'); exports.view={type:'nunjucks',//默认的模板引擎为mon:{//通⽤用配置viewPath:path.join(think.ROOT_PATH,'view'),sep:'_',extname:'.html'},nunjucks:{//nunjucks的具体配置handle:nunjucks},ejs:{//ejs的具体配置handle:ejs,viewPath:path.join(think.ROOT_PATH,'view/ejs/'),} } exports.cache={... } type默认使用Adapter的类型,具体调用时可以传递参数改写 common配置通用的一些参数,会跟具体的adapter参数作合并nunjucks,ejs配置特定类型的Adapter参数,最终获取到的参数是mon参数与该参数进行合并handle对应类型的处理函数,一般为一个类 项目中创建Adapter 除了引入外部的Adapter外,项目内也可以创建Adapter来使用。
Adapter文件放在src/adapter/目录下,如:src/adapter/cache/xcache.js,表示加了一个名为xcache的cacheAdapter类型,然后该文件实现cache类型一样的接口即可。
实现完成后,就可以直接通过字符串引用这个Adapter了,如: exports.cache={type:'file',xcache:{handle:'xcache', ⽂文件... }} //这⾥里里配置字符串串,项⽬目启动时会⾃自动查找 src/adapter/cache/xcache.js 支持的Adapter 框架支持的Adapter为/thinkjs/think-awesome#adapters。
Extend 虽然框架内置了很多功能,但在实际项目开发中,提供的功能还是远远不够的。
3.0里引入了Extend机制,方便对框架进行扩展。
支持的扩展类型为:think,application,context,request,response,controller和logic。
框架内置的很多功能也是Extend来实现的,如:Session,Cache。
Extend配置 Extend配置文件为src/config/extend.js,格式为数组: constview=require('think-view');constfetch=require('think-fetch'); constmodel=require('think-model'); module.exports=[view,//makeapplicationsupportfetch,//HTTPrequestclientmodel(think.app) ]; view 如上,通过viewExtend框架就支持渲染模板的功能,Controller类上就有了assign、display等方法。
项目里的Extend 除了引入外部的Extend来丰富框架的功能,也可以在项目中对对象进行扩展。
扩展文件放在src/extend/目录下。
src/extend/think.js扩展think对象,如:think.xxxsrc/extend/application.js扩展think.app对象,Koa里的application实例src/extend/request.js扩展Koa里的request对象src/extend/response.js扩展Koa里的response对象src/extend/context.js扩展ctx对象src/extend/controller.js扩展controller类src/extend/logic.js扩展logic类,logic继承controller类,所以logic包含controller类所有方法 比如:我们想给ctx添加个isMobile方法来判断当前请求是不是手机访问,可以通过下面的方式: //src/extend/context.jsmodule.exports={ isMobile:function(){constuserAgent=this.userAgent.toLowerCase();constmList=['iphone','android'];returnmList.some(item=>userAgent.indexOf(item)>-1); }} 这样后续就可以通过ctx.isMobile()来判断是否是手机访问了。
当然这个方法没有任何的参数,我们也可以变成一个getter。
//src/extend/context.jsmodule.exports={ getisMobile:function(){ constuserAgent=this.userAgent.toLowerCase();constmList=['iphone','android'];returnmList.some(item=>userAgent.indexOf(item)>-1);}} 这样在ctx中就可以直接用this.isMobile来使用,其他地方通过ctx.isMobile使用,如:在controller中用this.ctx.isMobile。
如果在controller中也想通过this.isMobile使用,怎么办呢?可以给controller也扩展一个isMobile属性来完成。
//src/extend/controller.jsmodule.exports={ getisMobile:function(){returnthis.ctx.isMobile; }} 通过也给controller扩展isMobile属性后,后续在controller里可以直接使用this.isMobile了。
当然这样扩展后,只能在当前项目里使用这些功能,如果要在其他项目中使用,可以将这些扩展发布为一个npm模块。
发布的模块中在入口文件里需要定义对应的类型的扩展,如: constcontrollerExtend=require('./controller.js');constcontextExtend=require('./context.js'); //模块⼊入⼝口⽂文件module.exports={ controller:controllerExtend,context:contextExtend} Extend里使用app对象 有些Extend需要使用一些app对象上的数据,那么可以导出为一个函数,配置时把app对象传递进去即可。
//src/config/extend.jsconstmodel=require('think-model'); module.exports=[model(think.app)//将think.app传递给model扩展 ]; 当然传了app对象,也可以根据需要传递其他对象。
推荐Extend列表 推荐的Extend列表见/thinkjs/think-awesome#extends,如果你开发了比较好的Extend,也欢迎发PullRequest。
进阶应用 think对象 框架中内置think全局对象,方便在项目中随时随地使用。
think.app think.app为KoaApplication对象的实例,系统启动时生成。
此外为app扩展了更多的属性。
think.app.think等同于think对象,方便有些地方传入了app对象,但又要使用think对象上的其他方法think.app.modules模块列表,单模块项目下为空数组think.app.controllers存放项目下的controller文件,便于后续快速调用think.app.logics存放项目下的logic文件think.app.models存放项目下的模型文件think.app.services存放service文件think.app.routers存放自定义路由配置think.app.validators存放校验配置如果想要查下这些属性具体的值,可以在appReady事件中进行。
think.app.on('appReady',()=>{console.log(think.app.controllers) }) API think对象上集成了think-helper上的所有方法,所以可以通过think.xxx来使用这些方法。
think.env当前运行环境,等同于think.app.env,值在development.js之类的入口文件中定义。
think.isCli是否为命令行调用。
think.version当前ThinkJS的版本号。
think.Controller控制器基类。
think.Logiclogic基类。
think.controller(name,ctx,m)name{String}控制器名称ctx{Object}Koactx对象m{String}模块名,多模块项目下使用 获取控制器的实例,不存在则报错。
think.beforeStartServer(fn)fn{Function}要注册的函数名 服务启动之前要注册执行的函数,如果有异步操作,fn需要返回Promise。
启动自定义 当通过npmstart或者nodeproduction.js来启动项目时,虽然可以在这些入口文件里添加其他的逻辑代码,但并不推荐这么做。
系统给出了其他启动自定义的入口。
bootstrap 系统启动时会加载src/boostrap/目录下的文件,具体为:Master进程下时加载src/bootstrap/master.jsWorker进程下时加载src/bootstrap/worker.js 所以可以将一些需要在系统启动时就需要执行的逻辑放在对应的文件里执行。
如果有一些代码需要在Master和Worker进程下都调用,那么可以放在一个单独的文件里,然后master.js和worker.js去required。
//src/mon.jsmonFn=function(){}//src/bootstrap/master.jsrequire('mon.js') //src/boostrap/worker.jsrequire('mon.js') 启动服务前执行 有时候需要在node启动http服务之前做一些特殊的逻辑处理,如:从数据库中读取配置并设置,从远程还在一些数据设置到缓存中。
这时候可以借助think.beforeStartServer方法来处理,如: think.beforeStartServer(async()=>{constdata=awaitgetDataFromApi();think.config(key,data); }) 可以通过think.beforeStartServer注册多个需要执行的逻辑,如果逻辑函数中有异步的操作,需要返回Promise。
系统会等待注册的多个逻辑执行完成后才启动服务,当然也不会无限制的等待,会有个超时时间。
超时时间可以通过配置startServerTimeout修改,默认为3秒。
//src/config/config.jsmodule.exports={ startServerTimeout:5000//将超时时间改为5s} 自定义创建http服务 系统默认是通过Koa里的listen方法来创建http服务的,如果想要创建https服务,此时需要自定义创建服务,可以通过createServer配置来完成。
//src/config/config.jsconsthttps=require('https');constfs=require('fs'); constoptions={key:fs.readFileSync('test/fixtures/keys/agent2-key.pem'),cert:fs.readFileSync('test/fixtures/keys/agent2-cert.pem') }; module.exports={//app为Koaapp对象createServer:function(app,...args){https.createServer(options,app.callback()).listen(...args);} } think.app.server对象 创建完http服务后,会将server对象赋值给think.app.server,以便于在其他地方使用。
appReady事件 http服务创建完成后,会触发appReady事件,其他地方可以通过think.app.on("appReady")来捕获该事件。
think.app.on("appReady",()=>{constserver=think.app.server; }) Service Cookie 配置 没有默认配置。
需要在src/config/config.js中添加cookie配置,比如: module.exports={cookie:{domain:'',path:'/',httponly:false,//是否httponlysecure:false,timeout:0//有效时间,0表示载浏览器器进程内有效,单位为秒。
} } 获取cookie 在controller或者logic中,可以通过this.cookie方法来获取cookie。
如: module.exports=classextendsthink.Controller{indexAction(){letcookie=this.cookie('theme');//获取名为theme的cookie} } this.ctx也提供了cookie方法来设置cookie。
如: this.ctx.cookie('theme'); 设置cookie 在controller或者logic中,可以通过this.cookie方法来设置cookie。
如: module.exports=classextendsthink.Controller{indexAction(){letcookie=this.cookie('theme','default');//将cookietheme值设置为defau lt} } this.ctx也提供了cookie方法来设置cookie。
如: this.ctx.cookie('theme','default'); 如果希望在设置cookie时改变配置参数,可以通过第三个参数来控制。
比如: module.exports=classextendsthink.Controller{indexAction(){letcookie=this.cookie('theme','default',{timeout:7*24*3600//设置cookie有效期为7天});//将cookietheme值设置为default} } 删除cookie 在controller或者logic中,可以通过this.cookie方法来删除。
比如: module.exports=classextendsthink.Controller{indexAction(){letcookie=this.cookie('theme',null);//删除名为theme的cookie} } this.ctx也提供了cookie方法来删除cookie。
如: this.ctx.cookie('theme',null); Session Thinkjs内置了Session功能。
框架已经为controller和context添加了session方法。
我们支持用多种方式存储session,包括:cookie,mysql,file,redis等。
支持的Session存储方式 cookieCookie方式mysqlMysql数据库方式file文件方式redisRedis方式 Mysqlsession使用mysql类型的Session需要创建对应的数据表,可以用下面的SQL语句创建 DROPTABLEIFEXISTS`think_session`;CREATETABLE`think_session`( `id`int(11)unsignedNOTNULLAUTO_INCREMENT,`cookie`varchar(255)NOTNULLDEFAULT'',`data`text,`expire`bigint(11)NOTNULL,PRIMARYKEY(`id`),UNIQUEKEY`cookie`(`cookie`),KEY`expire`(`expire`))ENGINE=InnoDBDEFAULTCHARSET=utf8; redisSession使用redis类型的Session需要依赖think-redis模块。
如何配置Session 配置文件src/config/adapter.js,添加如下选项(假设你默认使用Cookie方式的Session): constcookieSession=require('think-session-cookie'); constfileSession=require('think-session-file'); exports.session={type:'cookie',mon:{cookie:{name:'test',keys:['werwer','werwer'],signed:true}},cookie:{handle:cookieSession,cookie:{maxAge:1009990*1000,keys:['welefen','suredy'],encrypt:true}},file:{handle:fileSession,sessionPath:path.join(think.ROOT_PATH,} } 'runtime/session') 接下来解释一下这个配置文件的各种参数:*type:默认使用的Session类型,具体调用时可以传递参数改写(见“使用session方法”一节)*mon:配置通用的一些参数,会跟具体的Adapter参数合并*cookie,file,mysql:配置特定类型的Adapter参数,最终获取到的参数是mon参数与该参数进行合并后的结果。
我们注意到,在这几个配置里面都有一个handle参数。
*handle:对应类型的处理函数,一般为一个类。
具体看一下Cookie方式的参数。
handle上面已经介绍过,略过不表。
*maxAge:该session要在cookie中保留多长时间*keys:当encrypt参数为true时,需要提供keys数组。
该数组充当了加解密的密钥。
*encrypt:为true表示需要加密存储session。
接着看一下file方式的参数:*sessionPath:存储session的文件的路径。
在本例中,如果使用文件方式,会将session存储'/path_of_your_project/runtime/session'下。
使用session方法: 读取Session this.session()获取所有的session数据this.session(name)获取name对应的session数据 设置Sessionthis.session(name,value)设置session数据。
清除Sessionthis.session(null)删除所有的session数据。
在读取/设置/清除时,改写默认配置例如: this.session(name,undefined,options)以options配置项来获取session数据。
options配置项会与adapter中的默认配置合并。
如果有相同的配置属性,对每个ctx,首次调用this.session时,将以options的配置属性为准。
注意:对于每个ctx,session只会初始化一次。
Logger ThinkJS通过think-logger3模块实现了强大的日志功能,并提供适配器扩展,可以很方便的扩展内置的日志模块。
系统默认使用log4js模块作为底层日志记录模块,具有日志分级、日志分割、多进程等丰富的特性。
基本使用 系统已经全局注入了logger对象think.logger,其提供了debug,info,warn,error四种方法来输出日志,日志默认是输出至控制台中。
think.logger.debug('debugmessage');think.logger.info('infomessage');think.logger.warn('warning');think.logger.error(newError('error')); 基本配置 系统默认自带了Console,File,DateFile三种适配器。
默认是使用Console将日志输出到控制台中。
文件 如果想要将日志输出到文件,你可以这么设置: 将以下内容追加到src/config/adapter/logger.js文件中:```javascriptconstpath=require('path');const{File}=require('think-logger3'); module.exports={type:'file',file:{handle:File,backups:10,absolute:true,maxLogSize:50*1024,//50Mfilename:path.join(think.ROOT_PATH,'logs/xx.log')}} 编辑src/config/adater.js文件,增加exports.logger=require('./adapter/logger.js')。
该配置表示系统会将日志写入到logs/xx.log文件中。
当该文件超过maxLogSize值时,会创建一个新的文件logs/xx.log.1。
当日志文件数超过backups值时,旧的日志分块文件会被删除。
文件类型目前支持如下参数: filename:日志文件地址maxLogSize:单日志文件最大大小,单位为KB,默认日志没有大小限制。
backups:最大分块地址文件数,默认为
5。
absolute:filename是否为绝对路径地址,如果filename是绝对路径,absolute要设置为true。
layouts:定义日志输出的格式。
的值需 日期文件如果想要将日志按照日期文件划分的话,可以如下配置: 将以下内容追加到src/config/adapter/logger.js文件中:```javascriptconstpath=require('path');const{DateFile}=require('think-logger3'); module.exports={type:'dateFile',dateFile:{handle:DateFile,level:'ALL',absolute:true,pattern:'-yyyy-MM-dd',alwaysIncludePattern:false,filename:path.join(think.ROOT_PATH,'logs/xx.log')}} 编辑src/config/adater.js文件,增加exports.logger=require('./adapter/logger.js')。
该配置会将日志写入到logs/xx.log文件中。
隔天,该文件会被重命名为xx.log-2017-07-01(时间以当前时间为准),然后会创建新的logs/xx.log文件。
时间文件类型支持如下参数: level:日志等级filename:日志文件地址absolute:filename是否为绝对路径地址,如果filename是绝对路径,absolute的值需要设置为true。
pattern:该参数定义时间格式字串,新的时间文件会按照该格式格式化后追加到原有的文件名后面。
目前支持如下格式化参数: yyyy-四位数年份,也可以使用yy获取末位两位数年份MM-数字表示的月份,有前导零dd-月份中的第几天,有前导零的2位数字hh-小时,24小时格式,有前导零mm-分钟数,有前导零ss-秒数,有前导零SSS-毫秒数(不建议配置该格式以毫秒级来归类日志)O-当前时区alwaysIncludePattern:如果alwaysIncludePattern设置为true,则初始文件直接会被命名为xx.log-2017-07-01,然后隔天会生成xx.log-2017-07-02的新文件。
layouts:定义日志输出的格式。
Level 日志等级用来表示该日志的级别,目前支持如下级别: ALLERRORWARNINFODEBUG Layout Console,File和DateFile类型都支持layout参数,表示输出日志的格式,值为对象,下面是一个简单的示例: constpath=require('path');const{File}=require('think-logger3'); module.exports={type:'file',file:{handle:File,backups:10,absolute:true,maxLogSize:50*1024,//50Mfilename:path.join(think.ROOT_PATH,layouts:{type:'coloured',pattern:'%[[%d][%p]%]-%m',}} } 'logs/xx.log'), layouts支持如下参数: type:目前支持如下类型 basiccolouredmessagePassThroughdummypattern 自定义输出类型可参考Addingyourownlayoutspattern:输出格式字串,目前支持如下格式化参数 %r-.toLocaleTimeString()输出的时间格式,例如下午5:13:04。
%p-日志等级%h-机器名称%m-日志内容%d-时间,默认以ISO8601规范格式化,可选规范有ISO8601,ISO8601_WITH_TZ_OFFSET,ABSOUTE,DATE或者任何可支持格式化的字串,例如%d{DATE}或者%d{yyyy/MM/dd-hh.mm.ss}。
%%-输出%字符串%n-换行%z-从process.pid中获取的进程ID%[-颜色块开始区域%]-颜色块结束区域 自定义handle 如果觉得提供的日志输出类型不满足大家的需求,可以自定义日志处理的handle。
自定义handle需要实现一下几个方法: module.exports=class{/***@param{Object}config{}配置传⼊入的参数*@param{Boolean}clusterModetrue是否是多进程模式*/constructor(config,clusterMode){ } debug(){ } info(){ } warn(){ } error(){ }} 多进程 node中提供了cluster模块来创建多进程应用,这样可以避免单一进程挂了导致服务异常的情况。
框架是通过think-cluster模块来运行多进程模型的。
多进程配置 可以配置workers指定子进程的数量,默认为0(当前cpu的个数) //src/config/config.jsmodule.exports={ workers:0//可以根据实际情况修改,0为cpu的个数} 多进程模型 多进程模型下,Master进程会根据workers的大小fork对应数量的Worker进程,由Woker进程来处理用户的请求。
当Worker进程异常时会通知Master进程Fork一个新的Worker进程,并让当前Worker不再接收用户的请求。
进程间通信 多个Worker进程之间有时候需要进行通信,交换一些数据。
但Worker进程之间并不能直接通信,而是需要借助Master进程来中转。
框架提供think.messenger来处理进程之间的通信,目前有下面几种方法: think.messenger.broadcast将消息广播到所有Worker进程 //监听事件think.messenger.on('test',data=>{//所有进程都会捕获到该事件,包含当前的进程}); if(xxx){//发送⼴广播事件think.messenger.broadcast('test',} data) 自定义进程通信 有时候内置的一些通信方式还不能满足所有的需求,这时候可以自定义进程通信。
由于Master进程执行时调用src/bootstrap/master.js,Worker进程执行时调用src/bootstrap/worker.js,那么处理进程通信就比较简单。
//src/bootstrap/master.jsprocess.on('message',(worker,message)=>{ //接收到特定的消息进程处理理if(message&&message.act==='xxx'){ }}) //src/bootstrap/worker.jsprocess.send({act:'xxxx',...args});//发送数据到Master进程 Babel转译 由于框架依赖的Node最低版本为6.0.0,但这个版本还不支持async/await,所以在项目里使用async/await时,需要借助babel转译。
Babel会将src/目录转译到app/目录下,并添加sourceMap文件。
关闭Babel转译 如果想关闭Babel转译,那么需要Node的版本大于7.6.0(推荐使用8.x.xLTS版本),创建项目时可以指定-w参数来关闭Babel转译。
thinkjsnewdemo-w; 其实使不使用Babel转译,其实只是入口文件里引用有一些区别。
有Babel转译的入口文件(development.js) constconstconstconst Application=require('thinkjs');babel=require('think-babel');watcher=require('think-watcher');notifier=require('node-notifier'); constinstance=newApplication({ROOT_PATH:__dirname,watcher:watcher,transpiler:[babel,{//转译器器,这⾥里里使⽤用的是babel,并指定转译参数presets:['think-node']}],notifier:notifier.notify.bind(notifier),//通知器器,当转译报错时如何通知env:'development' }); instance.run(); 去除Babel转译的入口文件(development.js) constApplication=require('thinkjs');constwatcher=require('think-watcher');constinstance=newApplication({ ROOT_PATH:__dirname,watcher:watcher,env:'development'}); instance.run(); 对比可以看到,去除Babel转译,只是移除了transpiler和notifier2个配置,一个是指定转译器,一个是当转译报错时的通知处理方式。
当然除了指定-w参数关闭Babel转译,手工删除development.js里的相关代码也是可以的。
RESTfulAPI 项目中,经常要提供一个API供第三方使用,一个通用的API设计规范就是使用RESTAPI。
RESTAPI是使用HTTP中的请求类型来标识对资源的操作。
如: GET/ticket获取ticket列表GET/ticket/:id查看某个具体的ticketPOST/ticket新建一个ticketPUT/ticket/:id更新ticket12DELETE/ticket/:id删除ticekt12 创建RESTController 可以通过-r参数来创建RESTController。
如: thinkjscontrolleruser-r 会创建下面几个文件: create:src/controller/rest.jscreate:src/controller/user.jscreate:src/logic/user.js 其中src/controller/user.js会继承src/controller/rest.js类,rest.js会RESTController的基类,具体的逻辑可以根据项目如果进行修改。
添加自定义路由 添加RESTController后并不能立即对其访问,需要添加对应的自定义路由。
修改路由配置文件src/config/router.js,添加如下的配置: module.exports=[[/\/user(?
:\/(\d+))?
/,'user?
id=:1','rest'] ] 上面自定义路由的含义为:/\/user(?
:\/(\d+))?
/URL的正则user?
id=:1映射后要解析的路由,:1表示取正则里的(\d+)的值rest表示为RESTAPI 通过自定义路由,将/user/:id相关的请求指定为RESTController,然后就可以对其访问了。
GET/user获取用户列表,执行getActionGET/user/:id获取某个用户的详细信息,执行getActionPOST/user添加一个用户,执行postActionPUT/user/:id更新一个用户,执行putActionDELETE/user/:id删除一个用户,执行deleteAction 数据校验 Controller里的方法执行时并不会对传递过来的数据进行校验,数据校验可以放在Logic里处理,文件为src/logic/user.js,具体的Action与Controller里一一对应。
具体的使用方式请见Logic。
子级RESTAPI 有时候有子级RESTAPI,如:某篇文章的评论接口,这时候可以通过下面的自定义路由完成: module.exports=[[/\/post\/(\d+)\ments(?
:\/(\d+))?
/,ment?
postId=:1&id=:2','rest'] ] 这样在对应的Action里,可以通过this.get("postId")来获取文章的id,然后放在过滤条件里处理即可。
多版本RESTAPI 有些RESTAPI有时候前后不能完全兼容,需要有多个版本,这时候也可以通过自定义路由管理,如: module.exports=[[/\/v1\/user(?
:\/(\d+))?
/,[/\/v2\/user(?
:\/(\d+))?
/, ] 'v1/user?
id=:1','v2/user?
id=:1', 'rest'],'rest'] //v1//v2 版本版本 这时候只要在src/controller/下建立子目录v1/和v2/即可,执行时会自动查找,具体见多级控制器。
Mongo的RESTAPI 由于Mongo的id并不是纯数字的,所以处理Mongo的RESTAPI时只需要修改下对应的正则即可(将\d改为\w): module.exports=[[/\/user(?
:\/(\w+))?
/,'user?
id=:1','rest'] ] 模型 模型介绍 项目开发中,经常要操作数据库,如:增删改查等操作。
模型就是为了方便操作数据库进行的封装,一个模型对应数据库中的一个数据表。
目前支持的数据库有:MySQL。
创建模型 可以在项目目录下通过命令thinkjsmodel[name]来创建模型: thinkjsmodeluser; 执行完成后,会创建文件src/model/user.js。
模型属性 model.pk主键key,默认为id。
model.schema数据表字段定义,默认会从数据库中读取,读到的信息类似如下: {id:{name:'id',type:'int',//类型required:true,//是否必填primary:true,//是否是主键unique:true,//是否唯⼀一auto_increment:true//是否⾃自增} } 可以在模型添加额外的属性,如:默认值和是否只读,如: constmoment=require('moment'); module.exports=classextendsthink.Model{/***数据表字段定义*@type{Object} */ schema={view_nums:{//阅读数default:0//默认为0},fullname:{//全名default(){//first_name和last_name的组合,这⾥里里不不能⽤用Arrowsreturnthis.first_name+this.last_name; } },create_time:{//创建时间 default:()=>{//获取当前时间returnmoment().format('YYYY-MM-DDHH:mm:ss') },readonly:true//只读,添加后不不可修改} } } Function default默认只在添加数据时有效。
如果希望在更新数据时也有效,需要添加属性update:true。
readonly只在更新时有效。
注:如果设置了readonly,那么会忽略update属性。
更多属性请见Model->API。
模型实例化 模型实例化在不同的地方使用的方式有所差别,如果当前类含有model方法,那可以直接通过该方法实例化,如: module.exports=classextendsthink.Controller{indexAction(){letmodel=this.model('user');} } 否则可以通过调用think.model方法获取实例化,如: letgetModelInstance=function(){letmodel=think.model('user',think.config('model')); } 使用think.model获取模型的实例化时,需要带上模型的配置。
链式调用 模型中提供了很多链式调用的方法(类似jQuery里的链式调用),通过链式调用方法可以方便的进行数据操作。
链式调用是通过返回this来实现的。
module.exports=classextendsthink.Model{/***获取列列表数据*/asyncgetList(){letdata=awaitthis.field('title,content').where({id:['>',100]}).order('idDESC').select();...} } 模型中支持链式调用的方法有: where,用于查询或者更新条件的定义table,用于定义要操作的数据表名称alias,用于给当前数据表定义别名data,用于新增或者更新数据之前的数据对象赋值field,用于定义要查询的字段,也支持字段排除order,用于对结果进行排序limit,用于限制查询结果数据page,用于查询分页,生成sql语句时会自动转换为limitgroup,用于对查询的group支持having,用于对查询的having支持join,用于对查询的join支持union,用于对查询的union支持distinct,用于对查询的distinct支持cache用于查询缓存 链式调用方法具体使用方式请见Model->API。
数据库配置 数据库配置 修改以下内容,并将其写入到src/config/adapter/model.js文件中: jsmodule.exports={type:'mysql',mon:{log_sql:true,log_connect:true,},adapter:{mysql:{host:'127.0.0.1',port:'',database:'',//数据库名称user:'',//数据库帐号password:'',//数据库密码prefix:'think_',encoding:'utf8'}}}; 编辑src/config/adater.js文件,增加exports.model=require('./adapter/model.js')。
也可以在其他模块下配置,这样请求该模块时就会使用对应的配置。
数据表定义 默认情况下,模型名和数据表名都是一一对应的。
假设数据表前缀是think_,那么user模型对应的数据表为think_user,user_group模型对应的数据表为think_user_group。
如果需要修改,可以通过下面2个属性进行: tablePrefix表前缀tableName表名,不包含前缀 module.exports=classextendsthink.Model{constructor(...args){super(...args);this.tablePrefix='';//将数据表前缀设置为空this.tableName='user2';//将对应的数据表名设置为} } user2 修改主键 模型默认的主键为id,如果数据表里的PrimaryKey设置不是id,那么需要在模型中设置主键。
module.exports=classextendsthink.Mode{constructor(...args){super(...args);this.pk='user_id';//将主键字段设置为user_id} } count,sum,min,max等很多查询操作都会用到主键,用到这些操作时需要修改主键。
配置多个数据库 如果项目中有连接多个数据库的需求,可以通过下面的方式连接多个数据库。
//src/config/adapter/model.jsmodule.exports={ type:'mysql',mysql:{ host:'127.0.0.1',port:'',database:'test1',user:'root1',password:'root1',prefix:'',encoding:'utf8'},mysql2:{type:'mysql',//这⾥里里需要将host:'127.0.0.1',port:'',database:'test2',user:'root2',password:'root2',prefix:'',encoding:'utf8'}} type 重新设置为 mysql 注意:mysql2的配置中需要额外增加type字段将类型设置为mysql。
配置完成后,调用的地方可以通过下面的方式调用。
module.exports=indexAction(){letmodel1=letmodel2=} } classextendsthink.Controller{ this.model('test');//this.model('test','mysql2');//指定使⽤用 mysql2 的配置连接数据库 分布式数据库 大的系统中,经常有多个数据库用来做读写分离,从而提高数据库的操作性能。
ThinkJS里可以通过parser来自定义解析,可以在文件src/config/adapter/model.js中修改: //读配置constMYSQL_READ={ host:'10.0.10.1',}//写配置constMYSQL_WRITE={ host:'10.0.10.2'}module.exports={ host:'127.0.0.1',adapter:{ mysql:{parser(options){//mysql的配置解析⽅方法letsql=options.sql;//接下来要执⾏行行的SQL语句句if(sql.indexOf('SELECT')===0){//SELECT查询returnMYSQL_READ;}returnMYSQL_WRITE;} }}} parser解析的参数options里会包含接下来要执行的SQL语句,这样就很方便的在parser里返回对应的数据库配置。
CRUD操作 添加数据 添加一条数据使用add方法可以添加一条数据,返回值为插入数据的id。
如: module.exports=classextendsthink.Controller{asyncaddAction(){letmodel=this.model('user');letinsertId=awaitmodel.add({name:'xxx',pwd:} } 'yyy'}); 有时候需要借助数据库的一些函数来添加数据,如:时间戳使用mysql的CURRENT_TIMESTAMP函数,这时可以借助exp表达式来完成。
exportdefaultclassextendsthink.controller.base{asyncaddAction(){letmodel=this.model('user');letinsertId=awaitmodel.add({name:'test',time:['exp','CURRENT_TIMESTAMP()']});} } 添加多条数据 使用addMany方法可以添加多条数据,如: module.exports=classextendsthink.Controller{asyncaddAction(){letmodel=this.model('user');letinsertId=awaitmodel.addMany([{name:'xxx',pwd:'yyy'},{name:'xxx1',pwd:'yyy1'}]);} } thenAdd 数据库设计时,我们经常需要把某个字段设为唯
一,表示这个字段值不能重复。
这样添加数据的时候只能先去查询下这个数据值是否存在,如果不存在才进行插入操作。
模型中提供了thenAdd方法简化这一操作。
module.exports=classextendsthink.Controller{asyncaddAction(){letmodel=this.model('user');//第⼀一个参数为要添加的数据,第⼆二个参数为添加的条件,根据第⼆二个参数的条件查询⽆无相关记录时才会 添加letresult=awaitmodel.thenAdd({name:'xxx',pwd:'yyy'},{name:'xxx'});//resultreturns{id:1000,type:'add'}or{id:1000,type:'exist'} }} 更新数据 update更新数据使用update方法,返回值为影响的行数。
如: module.exports=classextendsthink.ControllerasyncupdateAction(){letmodel=this.model('user');letaffectedRows=awaitmodel.where({name: '});} } {'thinkjs'}).update({email: 'admin@ 默认情况下更新数据必须添加where条件,以防止误操作导致所有数据被错误的更新。
如果确认是更新所有数据的需求,可以添加1=1的where条件进行,如: module.exports=classextendsthink.Controller{asyncupdateAction(){letmodel=this.model('user');letaffectedRows=awaitmodel.where('1=1').update({email: });} } 'admin@' 有时候更新值需要借助数据库的函数或者其他字段,这时候可以借助exp来完成。
exportdefaultclassextendsthink.controlle.base{asyncupdateAction(){letmodel=this.model('user');letaffectedRows=awaitmodel.where('1=1').update({email:'admin@',view_nums:['exp','view_nums+1'],update_time:['exp','CURRENT_TIMESTAMP()']});} } increment可以通过increment方法给符合条件的字段增加特定的值,如: module.exports=classextendsthink.Model{updateViewNums(id){ returnthis.where({id:id}).increment('view_nums',1);//将阅读数加1}} decrement 可以通过decrement方法给符合条件的字段减少特定的值,如: module.exports=classextendsthink.Model{updateViewNums(id){returnthis.where({id:id}).decrement('coins',10);//将⾦金金币减10} } 查询数据 模型中提供了多种方式来查询数据,如:查询单条数据,查询多条数据,读取字段值,读取最大值,读取总条数等。
查询单条数据可以使用find方法查询单条数据,返回值为对象。
如: module.exports=classextendsthink.Controller{asynclistAction(){letmodel=this.model('user');letdata=awaitmodel.where({name:'thinkjs'}).find();//datareturns{name:'thinkjs',email:'admin@',} } ...} 如果数据表没有对应的数据,那么返回值为空对象{},可以通过think.isEmpty方法来判断返回值是否为空。
查询多条数据可以使用select方法查询多条数据,返回值为数据。
如: module.exports=classextendsthink.Controller{asynclistAction(){ letmodel=this.model('user');letdata=awaitmodel.limit
(2).select();//datareturns[{name:'thinkjs',email:'admin@'},...]}} 如果数据表中没有对应的数据,那么返回值为空数组[],可以通过think.isEmpty方法来判断返回值是否为空。
分页查询数据 页面中经常遇到按分页来展现某些数据,这种情况下就需要先查询总的条数,然后在查询当前分页下的数据。
查询完数据后还要计算有多少页。
模型中提供了countSelect方法来方便这一操作,会自动进行总条数的查询。
module.exports=classextendsthink.Controller{asynclistAction(){letmodel=this.model('user');letdata=awaitmodel.page(this.get('page'),} } 10).countSelect(); 返回值格式如下: {numsPerPage:10,//每⻚页显示的条数currentPage:1,//当前⻚页count:100,//总条数totalPages:10,//总⻚页数data:[{//当前⻚页下的数据列列表name:'thinkjs',email:'admin@'},...]} 如果传递的当前页数超过了页数范围,可以通过传递参数进行修正。
true为修正到第一页,false为修正到最后一页,即:countSelect(true)或countSelect(false)。
如果总条数无法直接查询,可以将总条数作为参数传递进去,如:countSelect(1000),表示总条数有1000条。
count 可以通过count方法查询符合条件的总条数,如: module.exports=classextendsthink.Model{getMin(){//查询status为publish的总条数returnthis.where({status:'publish'}).count();} } sum 可以通过sum方法查询符合条件的字段总和,如: module.exports=classextendsthink.Model{getMin(){//查询status为publish字段view_nums的总和returnthis.where({status:'publish'}).sum('view_nums');} } max 可以通过max方法查询符合条件的最大值,如: module.exports=classextendsthink.Model{getMin(){//查询status为publish,字段ments的最⼤大值returnthis.where({status:'publish'}).max(ments');} } min 可以通过min方法查询符合条件的最小值,如: module.exports=classextendsthink.Model{getMin(){//查询status为publish,字段ments的最⼩小值returnthis.where({status:'publish'}).min(ments');} } 查询缓存 为了性能优化,项目中经常要对一些从数据库中查询的数据进行缓存。
如果手工将查询的数据进行缓存,势必比较麻烦,模型中直接提供了cache方法来设置查询缓存。
如: module.exports=classextendsthink.Model{getList(){//设定缓存key和缓存时间returnthis.cache('get_list',3600).where({id:} } {'>': 100}}).select(); 上面的代码为对查询结果进行缓存,如果已经有了缓存,直接从缓存里读取,没有的话才从数据库里查询。
缓存保存的key为get_list,缓存时间为一个小时。
也可以不指定缓存key,这样会自动根据SQL语句生成一个缓存key。
如: module.exports=classextendsthink.Model{getList(){//只设定缓存时间returnthis.cache(3600).where({id:{'>':} } 100}}).select(); 缓存配置 缓存配置为模型配置中的cache字段(配置文件在mon/config/db.js),如: module.exports={cache:{on:true,type:'',timeout:3600} } on数据库缓存配置的总开关,关闭后即使程序中调用cache方法也无效。
type缓存配置类型,默认为内存,支持的缓存类型请见Adapter->Cache。
timeout默认缓存时间。
删除数据 可以使用delete方法来删除数据,返回值为影响的行数。
如: module.exports=classextendsthink.Controller{asyncdeleteAction(){letmodel=this.model('user');letaffectedRows=awaitmodel.where({id:['>',} } 100]}).delete(); 模型中更多的操作方式请见相关的API->model。
事务 模型中提供了对事务操作的支持,但前提需要数据库支持事务。
Mysql中的InnoDB和BDB存储引擎支持事务,如果在Mysql下使用事务的话,需要将数据库的存储引擎设置为InnoDB或BDB。
SQLite直接支持事务。
使用事务 模型中提供了startTrans,mit和rollback3种方法来操作事务。
startTrans开启事务mit正常操作后,提交事务rollback操作异常后进行回滚 module.exports=classextendsthink.Controller{asyncindexAction(){letmodel=this.model('user');try{awaitmodel.startTrans();letuserId=awaitmodel.add({name:'xxx'});letinsertId=awaitthis.model('user_group').add({user_id: :1000});awaitmit(); }catch(e){awaitmodel.rollback(); }} userId, group_id } transaction方法 使用事务时,要一直使用startTrans,mit和rollback这3个方法进行操作,使用起来有一些不便。
为了简化这一操作,模型中提供了transaction方法来更加方便的处理事务。
module.exports=classextendsthink.Controller{asyncindexAction(self){letmodel=this.model('user');letinsertId=awaitmodel.transaction(async()=>{letuserId=awaitmodel.add({name:'xxx'});returnawaitself.model('user_group').add({user_id: ;}); }} userId, group_id: 1000}) transaction接收一个回调函数,这个回调函数中处理真正的逻辑,并需要返回。
操作多个模型 如果同一个事务下要操作多个数据表数据,那么要复用同一个数据库连接(开启事务后,每次事务操作会开启一个独立的数据库连接)。
可以通过db方法进行数据库连接复用。
indexAction(){letmodel=this.model('user');awaitmodel.transaction(async()=>{//通过db⽅方法将user模型的数据库连接传递给article模型letmodel2=this.model('article').db(model.db());//dosomething}) } 关联模型 数据库中表经常会跟其他数据表有关联,数据操作时需要连同关联的表一起操作。
如:一个博客文章会有分类、标签、评论,以及属于哪个用户。
ThinkJS中支持关联模型,让处理这类操作非常简单。
支持的类型 关联模型中支持常见的4类关联关系。
如:think.Model.Relation.HAS_ONE一对一模型think.Model.Relation.BELONG_TO一对一属于think.Model.Relation.HAS_MANY一对多think.Model.Relation.MANY_TO_MANY多对多 创建关联模型 可以通过命令thinkjsmodel[name]--relation来创建关联模型。
如: thinkjsmodelpost--relation 会创建模型文件src/model/post.js。
指定关联关系 可以通过relation属性来指定关联关系。
如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);//通过relation属性指定关联关系,可以指定多个关联关系this.relation={cate:{},ment:{}};} } 也可以直接使用ES7里的语法直接定义relation属性。
如: module.exports=classextendsthink.Model.Relation{//直接定义relation属性relation={cate:{},ment:{}}; } 单个关系模型的数据格式 module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={cate:{type:think.Model.Relation.MANY_TO_MANY,//relationmodel:'',//modelnamename:'profile',//datanamekey:'id',fKey:'user_id',//forignkeyfield:'id,name',where:'name=xx',order:'',limit:'',rModel:'',rfKey:''},};} } type 各个字段含义如下: type关联关系类型model关联表的模型名,默认为配置的key,这里为catename对应的数据字段名,默认为配置的key,这里为catekey当前模型的关联keyfKey关联表与只对应的keyfield关联表查询时设置的field,如果需要设置,必须包含where关联表查询时设置的where条件order关联表查询时设置的orderlimit关联表查询时设置的limitpage关联表查询时设置的pagerModel多对多下,对应的关联关系模型名rfKey多对多下,对应里的关系关系表对应的key fKey 对应的值 如果只用设置关联类型,不用设置其他字段信息,可以通过下面简单的方式: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args); this.relation={cate:think.Model.Relation.MANY_TO_MANY };}} HAS_ONE 一对一关联,表示当前表含有一个附属表。
假设当前表的模型名为user,关联表的模型名为info,那么配置中字段key的默认值为id,字段fKey的默认值为user_id。
module.exports=classextendsthink.Model.Relation{init(..args){super(...args);this.relation={info:think.Model.Relation.HAS_ONE};} } 执行查询操作时,可以得到类似如下的数据: [{id:1,name:'111',info:{//关联表⾥里里的数据信息user_id:1,desc:'info'}},...] BELONG_TO 一对一关联,属于某个关联表,和HAS_ONE是相反的关系。
假设当前模型名为info,关联表的模型名为user,那么配置字段key的默认值为user_id,配置字段fKey的默认值为id。
module.exports=classextendsthink.Model.Relation{init(..args){ super(...args);this.relation={ user:think.Model.Relation.BELONG_TO};}} 执行查询操作时,可以得到类似下面的数据: [{id:1,user_id:1,desc:'info',user:{name:'thinkjs'}},... ] HAS_MANY 一对多的关系。
加入当前模型名为post,关联表的模型名为ment,那么配置字段key默认值为id,配置字段fKey默认值为post_id。
module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args); this.relation={ment:{type:think.Model.Relation.HAS_MANY} };}} 执行查询数据时,可以得到类似下面的数据: [{id:1,title:'firstpost', content:'content',ment:[{ id:1,post_id:1,name:'welefen',content:'ment'},...]},...] 如果关联表的数据需要分页查询,可以通过page参数进行,如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args); this.relation={ment:{type:think.Model.Relation.HAS_MANY} };}getList(page){ returnthis.setRelation(ment',{page:page}).select();}} 除了用setRelation来合并参数外,可以将参数设置为函数,合并参数时会自动执行该函数。
MANY_TO_MANY 多对多关系。
假设当前模型名为post,关联模型名为cate,那么需要一个对应的关联关系表。
配置字段rModel默认值为post_cate,配置字段rfKey默认值为cate_id。
module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args); this.relation={cate:{type:think.Model.Relation.MANY_TO_MANY,rModel:'post_cate',rfKey:'cate_id'} };} } 查询出来的数据结构为: [{id:1,title:'firstpost',cate:[{id:1,name:'cate1',post_id:1},...] },...] 关联死循环 如果2个关联表,一个设置对方为HAS_ONE,另一个设置对方为BELONG_TO,这样在查询关联表的数据时会将当前表又查询了一遍,并且会再次查询关联表,最终导致死循环。
可以在配置里设置relation字段关闭关联表的关联查询功能,从而避免死循环。
如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,relation:false//关联表user查询时关闭关联查询}};} } 也可以设置只关闭当前模型的关联关系,如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,relation:'info'//关联表user查询时关闭对info}}; 模型的关联关系 }} 临时关闭关联关系 设置关联关系后,查询等操作都会自动查询关联表的数据。
如果某些情况下不需要查询关联表的数据,可以通过setRelation方法临时关闭关联关系查询。
全部关闭 通过setRelation(false)关闭所有的关联关系查询。
module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={ment:think.Model.Relation.HAS_MANY,cate:think.Model.Relation.MANY_TO_MANY};}getList(){returnthis.setRelation(false).select();} } 部分启用 通过setRelation(ment')只查询ment的关联数据,不查询其他的关联关系数据。
module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={ment:think.Model.Relation.HAS_MANY,cate:think.Model.Relation.MANY_TO_MANY};}getList2(){returnthis.setRelation(ment').select();} } 部分关闭 通过setRelation(ment',false)关闭ment的关联关系数据查询。
module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={ment:think.Model.Relation.HAS_MANY,cate:think.Model.Relation.MANY_TO_MANY};}getList2(){returnthis.setRelation(ment',false).select();} } 重新全部启用 通过setRelation(true)重新启用所有的关联关系数据查询。
module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={ment:think.Model.Relation.HAS_MANY,cate:think.Model.Relation.MANY_TO_MANY};}getList2(){returnthis.setRelation(true).select();} } 设置查询条件 field 设置field可以控制查询关联表时数据字段,这样可以减少查询的数据量,提高查询查询效率。
默认情况会查询所有数据。
如果设置了查询的字段,那么必须包含关联字段,否则查询出来的数据无法和之前的数据关联。
module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,field:'id,name,email'//必须要包含关联字段id}};} } 如果某些情况下必须动态的设置的话,可以将field设置为一个函数,执行函数时返回对应的字段。
如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,field:model=>model._relationField}};}selectData(relationfield){//将要查询的关联字段设置到⼀一个私有属性中,便便于动态设置fieldthis._relationField=relationfield;returnthis.select();} } ⾥里里获取 形参model指向当前模型类。
where 设置where可以控制查询关联表时的查询条件,如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,where:{grade:1//只查询关联表⾥里里grade=1的数据 }}};}} 也可以动态的设置where条件,如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,where:model=>model._relationWhere}};}selectData(relationWhere){this._relationWhere=relationWhere;returnthis.select();} } 形参model指向当前模型类。
page 可以通过设置page进行分页查询,page参数会被解析为limit数据。
module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,page:[1,15]//第⼀一⻚页,每⻚页15条}};} } 也可以动态设置分页,如: module.exports=classextendsthink.Model.Relation{ constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,page:model=>model._relationPage}}; }selectData(page){ this._relationPage=[page,15];returnthis.select();}} 形参model指向当前模型类。
limit 可以通过limit设置查询的条数,如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,limit:[10]//限制10条}};} } 也可以动态设置limit,如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,limit:model=>model._relationLimit}};}selectData(){this._relationLimit=[1,15]; returnthis.select();}} 形参model指向当前模型类。
注:如果设置page,那么limit会被忽略,因为page会转为limit。
order 通过order可以设置关联表的查询排序方式,如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,order:'levelDESC'}};} } 也可以动态的设置order,如: module.exports=classextendsthink.Model.Relation{constructor(...args){super(...args);this.relation={user:{type:think.Model.Relation.BELONG_TO,order:model=>model._relationOrder}};}selectData(){this._relationOrder='levelDESC';returnthis.select();} } 形参model指向当前模型类。
注意事项 关联字段的数据类型要一致比如:数据表里的字段id的类型为int,那么关联表里的关联字段user_id也必须为int相关的类型,否则无法匹配数据。
这是因为匹配的时候使用绝对等于进行判断的。
Mysql ThinkJS对Mysql操作有很好的支持,底层使用的库为/package/mysql。
连接池 默认连接Mysql始终只有一个连接,如果想要多个连接,可以使用连接池的功能。
修改配置src/config/adapter/model.js,如: module.exports={mon:{connectionLimit:10//建⽴立10个连接} } socketPath 默认情况下是通过host和port来连接Mysql的,如果想通过unixdomainsocket来连接Mysql,可以设置下面的配置: module.exports={mon:{socketPath:'/tmp/mysql.socket'} } SSLoptions 可以通过下面的配置来指定通过SSL来连接: constfs=require('fs');module.exports={ common:{ssl:{ca:fs.readFileSync(__dirname+'/mysql-ca.crt')} }} 数据库支持emoji表情 数据库的编码一般会设置为utf8,但utf8并不支持emoji表情,如果需要数据库支持emoji表情,需要将数据库编码设置为utf8mb4。
同时需要将src/config/adapter/model.js里的encoding配置值修改为utf8mb4。
如: module.exports={mon:{encoding:'utf8mb4'} } model 自定义模型需要先集成think.Model类 module.exports=classextendsthink.Model{getList(){} } 属性 model.pk数据表主键,默认为id。
model.name模型名,默认从当前文件名中解析。
当前文件路径为for/bar/app/home/model/user.js,那么解析的模型名为user。
model.tablePrefix数据表名称前缀,默认为think_。
model.tableName数据表名称,不包含前缀。
默认等于模型名。
model.schema数据表字段,关系型数据库默认自动从数据表分析。
model.indexes数据表索引,关系数据库会自动从数据表分析。
model.config配置,实例化的时候指定。
model._db连接数据库句柄。
model._data操作的数据。
model._options操作选项。
方法 model.model(name,options,module) name{String}模型名称options{Object}配置项module{String}模块名return{Object}获取模型实例,可以跨模块获取。
module.exports=classextendsthink.Model{asyncgetList(){//获取user模型实例例letinstance=this.model('user');letlist=awaitinstance.select();letids=list.map(item=>{returnitem.id;});letdata=awaitthis.where({id:['IN',ids]}).select();returndata;} } model.getTablePrefix() return{string}获取表名前缀。
model.getConfigKey() return{String}获取配置对应的key,缓存db句柄时使用。
model.db() return{Object}根据当前的配置获取db实例,如果已经存在则直接返回。
model.getModelName() return{String}模型名称 如果已经配置则直接返回,否则解析当前的文件名。
model.getTableName()return{String}获取表名,包含表前缀 获取表名,包含表前缀。
model.cache(key,timeout) key{String}缓存keytimeout{Number}缓存有效时间,单位为秒return{this}设置缓存选项。
设置缓存key和时间 module.exports=classextendsthink.Model{getList(){returnthis.cache('getList',1000).where({id:} } {'>': 100}}).select(); 只设置缓存时间,缓存key自动生成 module.exports=classextendsthink.Model{getList(){returnthis.cache(1000).where({id:{'>':} } 100}}).select(); 设置更多的缓存信息 module.exports=classextendsthink.Model{getList(){returnthis.cache({key:'getList',timeout:1000,type:'file'//使⽤用⽂文件⽅方式缓存}).where({id:{'>':100}}).select();} } model.limit(offset,length) offset{Number}设置查询的起始位置length{Number}设置查询的数据长度return{this}设置查询结果的限制条件。
限制数据长度 module.exports=classextendsthink.ModelgetList(){//查询20条数据returnthis.limit(20).where({id:{'>':} } {100}}).select(); 限制数据起始位置和长度 module.exports=classextendsthink.Model{getList(){//从起始位置100开始查询20调数据returnthis.limit(100,20).where({id:{'>':} } 100}}).select(); 也可以直接传入一个数组,如: module.exports=classextendsthink.Model{getList(){//从起始位置100开始查询20调数据returnthis.limit([100,20]).where({id:{'>':} } 100}}).select(); model.page(page,listRows) page{Number}当前页,从1开始listRows{Number}每页的条数return{this} 设置查询分页数据,自动转化为limit数据。
module.exports=classextendsthink.Model{getList(){//查询第2⻚页数据,每⻚页10条数据returnthis.page(2,10).where({id:{'>':} } 100}}).select(); 也可以直接设置一个参数为数组,关联模型等情况下可能会有用。
如: module.exports=classextendsthink.Model{getList(){//查询第2⻚页数据,每⻚页10条数据returnthis.page([2,10]).where({id:{'>':} } 100}}).select(); model.where(where) where{String|Object}where条件return{this} 设置where查询条件。
可以通过属性_logic设置逻辑,默认为AND。
可以通过属性plex设置复合查询。
注:
1.以下示例不适合mongomodel,mongo中设置where条件请见model.mongo里的where条件设定。
2.where条件中的值需要在Logic里做数据校验,否则可能会有漏洞。
普通条件 module.exports=classextendsthink.Model{where1(){//SELECT*FROM`think_user`returnthis.where().select();}where2(){//SELECT*FROM`think_user`WHERE(`id`=10)returnthis.where({id:10}).select();}where3(){//SELECT*FROM`think_user`WHERE(id=10ORid<2) returnthis.where('id=10ORid<2').select();}where4(){ //SELECT*FROM`think_user`WHERE(`id`!
=10)returnthis.where({id:['!
=',10]}).select();}} null条件 module.exports=classextendsthink.Model{where1(){//SELECT*FROM`think_user`where(titleISNULL);returnthis.where({title:null}).select();}where2(){//SELECT*FROM`think_user`where(titleISNOTNULL);returnthis.where({title:['!
=',null]}).select();} } EXP条件 ThinkJS默认会对字段和值进行转义,防止安全漏洞。
有时候一些特殊的情况不希望被转义,可以使用EXP的方式,如: module.exports=classextendsthink.Model{where1(){//SELECT*FROM`think_user`WHERE((`name`='name'))returnthis.where({name:['EXP',"=\"name\""]}).select();} } LIKE条件 module.exports=classextendsthink.Model{where1(){//SELECT*FROM`think_user`WHERE(`title`NOTLIKE'welefen')returnthis.where({title:['NOTLIKE','welefen']}).select();}where2(){//SELECT*FROM`think_user`WHERE(`title`LIKE'%welefen%')returnthis.where({title:['like','%welefen%']}).select();}//like多个值 where3(){//SELECT*FROM`think_user`WHERE((`title`LIKE'welefen'OR`title`LIKE' suredy'))returnthis.where({title:['like',['welefen','suredy']]}).select(); }//多个字段或的关系like⼀一个值where4(){ //SELECT*FROM`think_user`WHERE((`title`LIKE'%welefen%')OR(`content`LIKE'%welefen%')) returnthis.where({'title|content':['like','%welefen%']}).select();}//多个字段与的关系Like⼀一个值where5(){ //SELECT*FROM`think_user`WHERE((`title`LIKE'%welefen%')AND(`content`LIKE'%welefen%')) returnthis.where({'title&content':['like','%welefen%']}).select();}} IN条件 module.exports=classextensthink.Model{where1(){//SELECT*FROM`think_user`WHERE(`id`IN('10','20'))returnthis.where({id:['IN','10,20']}).select();}where2(){//SELECT*FROM`think_user`WHERE(`id`IN(10,20))returnthis.where({id:['IN',[10,20]]}).select();}where3(){//SELECT*FROM`think_user`WHERE(`id`NOTIN(10,20))returnthis.where({id:['NOTIN',[10,20]]}).select();} } BETWEEN查询 module.exports=classextensthink.Model{where1(){//SELECT*FROM`think_user`WHERE((`id`BETWEEN1AND2))returnthis.where({id:['BETWEEN',1,2]}).select();}where2(){//SELECT*FROM`think_user`WHERE((`id`BETWEEN'1'AND'2'))returnthis.where({id:['between','1,2']}).select();} } 多字段查询 module.exports=classextendsthink.Model{where1(){//SELECT*FROM`think_user`WHERE(`id`=10)AND(`title`='www')returnthis.where({id:10,title:"www"}).select();}//修改逻辑为ORwhere2(){//SELECT*FROM`think_user`WHERE(`id`=10)OR(`title`='www')returnthis.where({id:10,title:"www",_logic:'OR'}).select();}//修改逻辑为XORwhere2(){//SELECT*FROM`think_user`WHERE(`id`=10)XOR(`title`='www')returnthis.where({id:10,title:"www",_logic:'XOR'}).select();} } 多条件查询 module.exports=classextendsthink.Model{where1(){//SELECT*FROM`think_user`WHERE(`id`>10AND`id`<20)returnthis.where({id:{'>':10,'<':20}}).select();}//修改逻辑为ORwhere2(){//SELECT*FROM`think_user`WHERE(`id`<10OR`id`>20)returnthis.where({id:{'<':10,'>':20,_logic:'OR'}}).select()} } 复合查询 module.exports=classextendsthink.Model{where1(){//SELECT*FROM`think_user`WHERE(`title`='test')AND( 3))OR(`content`='www'))returnthis.where({title:'test',plex:{id:['IN',[1,2,3]],content:'www',_logic:'or'}}).select() (`id`IN(1,
2, }} model.field(field) field{String|Array}设置要查询的字段,可以是字符串,也可以是数组return{this} 设置要查询的字段。
字符串方式 module.exports=classextendsthink.controller.base{asyncindexAction(){letmodel=this.model('user');//设置要查询的字符串串,字符串串⽅方式,多个⽤用逗号隔开letdata=awaitmodel.field('name,title').select();} } 调用SQL函数 module.exports=classextendsthink.controller.base{//字段⾥里里调⽤用SQL函数asynclistAction(){letmodel=this.model('user');letdata=awaitmodel.field('id,INSTR(\'30,35,31,\',id+\',\')asd').selec t();} } 数组方式 module.exports=classextendsthink.controller.base{asyncindexAction(){letmodel=this.model('user');//设置要查询的字符串串,数组⽅方式letdata=awaitmodel.field(['name','title']).select();} } model.fieldReverse(field) field{String|Array}反选字段,即查询的时候不包含这些字段return{this}设置反选字段,查询的时候会过滤这些字段,支持字符串和数组2种方式。
model.table(table,hasPrefix) table{String}表名hasPrefix{Boolean}是否已经有了表前缀,如果table值含有空格,则不在添加表前缀return{this}设置表名,可以将一个SQL语句设置为表名。
设置当前表名 module.exports=classextendsthink.Model{getList(){returnthis.table('test',true).select();} } SQL语句作为表名 module.exports=classextendsthink.Model{asyncgetList(){letsql=awaitthis.model('group').group('name').buildSql();letdata=awaitthis.table(sql).select();returndata;} } model.union(union,all) union{String|Object}联合查询SQL或者表名all{Boolean}是否是UNIONALL方式return{this}联合查询。
SQL联合查询 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`UNION(SELECT*FROMthink_pic2)returnthis.union('SELECT*FROMthink_pic2').select();} } 表名联合查询 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`UNIONALL(SELECT*FROM`think_pic2`)returnthis.union({table:'think_pic2'},true).select();} } model.join(join) join{String|Object|Array}要组合的查询语句,默认为LEFTJOINreturn{this} 组合查询,支持字符串、数组和对象等多种方式。
字符串 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`LEFTJOINthink_cateONthink_group.cate_id=think _cate.idreturnthis.join('think_cateONthink_group.cate_id=think_cate.id').select(); }} 数组 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`LEFTJOINthink_cateONthink_group.cate_id=think _cate.idRIGHTJOINthink_tagONthink_group.tag_id=think_tag.idreturnthis.join(['think_cateONthink_group.cate_id=think_cate.id','RIGHTJOINthink_tagONthink_group.tag_id=think_tag.id']).select(); }} 对象:单个表 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`INNERJOIN`think_cate`AScON id`=c.`id`returnthis.join({table:'cate',join:'inner',//join⽅方式,有left,right,inner3种⽅方式as:'c',//表别名on:['cate_id','id']//ON条件}).select(); }} think_user.`cate_ 对象:多次JOIN module.exports=classextendsthink.Model{getList(){//SELECT*FROMthink_userASaLEFTJOIN`think_cate`AScONa.`cate_id`=c.` id`LEFTJOIN`think_group_tag`ASdONa.`id`=d.`group_id`returnthis.alias('a').join({table:'cate',join:'left',as:'c',on:['cate_id','id']}).join({table:'group_tag',join:'left',as:'d',on:['id','group_id']}).select() }} 对象:多个表 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`LEFTJOIN`think_cate`ONthink_user.`id`=think_c ate.`id`LEFTJOIN`think_group_tag`ONthink_user.`id`=think_group_tag.`group_id`returnthis.join({cate:{ on:['id','id']},group_tag:{ on:['id','group_id']}}).select();}} module.exports=classextendsthink.Model{getList(){//SELECT*FROMthink_userASaLEFTJOIN`think_cate`AScONa.`id`=c.`id`
L EFTJOIN`think_group_tag`ASdONa.`id`=d.`group_id`returnthis.alias('a').join({cate:{join:'left',//有left,right,inner3个值as:'c',on:['id','id']},group_tag:{join:'left',as:'d',on:['id','group_id']}}).select() }} 对象:ON条件含有多个字段 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`LEFTJOIN`think_cate`ONthink_user.`id`=think_c ate.`id`LEFTJOIN`think_group_tag`ONthink_user.`id`=think_group_tag.`group_id`LEFTJOIN`think_tag`ON(think_user.`id`=think_tag.`id`ANDthink_user.`title`=t hink_tag.`name`)returnthis.join({cate:{on:'id,id'},group_tag:{on:['id','group_id']},tag:{on:{//多个字段的ONid:'id',title:'name'}}}).select() }} 对象:table值为SQL语句 module.exports=classextendsthink.Model{asyncgetList(){letsql=awaitthis.model('group').buildSql();//SELECT*FROM`think_user`LEFTJOIN(SELECT*FROM`think_group`)ONthin k_user.`gid`=(SELECT*FROM`think_group`).`id`returnthis.join({table:sql,on:['gid','id']}).select(); }} model.order(order) order{String|Array|Object}排序方式return{this} 设置排序方式。
字符串 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`ORDERBYidDESC,nameASCreturnthis.order('idDESC,nameASC').select();}getList1(){//SELECT*FROM`think_user`ORDERBYcount(num)DESCreturnthis.order('count(num)DESC').select();} } 数组 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`ORDERBYidDESC,nameASCreturnthis.order(['idDESC','nameASC']).select();} } 对象 module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`ORDERBY`id`DESC,`name`ASCreturnthis.order({id:'DESC',name:'ASC'}).select();} } model.alias(tableAlias) tableAlias{String}表别名return{this} 设置表别名。
module.exports=classextendsthink.Model{getList(){//SELECT*FROMthink_userASa;returnthis.alias('a').select();} } model.having(having) having{String}having查询的字符串return{this}设置having查询。
module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`HAVINGview_nums>1000returnthis.having('view_nums>1000ANDview_nums<} } ANDview_nums<20002000').select(); model.group(group)group{String}分组查询的字段 return{this}设定分组查询。
module.exports=classextendsthink.Model{getList(){//SELECT*FROM`think_user`GROUPBY`name`returnthis.group('name').select();} } model.distinct(distinct)distinct{String}去重的字段return{this} 去重查询。
module.exports=classextendsthink.Model{getList(){//SELECTDISTINCT`name`FROM`think_user`returnthis.distinct('name').select();} } model.explain(explain)explain{Boolean}是否添加explain执行return{this} 是否在SQL之前添加explain执行,用来查看SQL的性能。
model.optionsFilter(options)操作选项过滤。
model.dataFilter(data)data{Object|Array}要操作的数据 数据过滤。
model.beforeAdd(data)data{Object}要添加的数据 添加前置操作。
model.afterAdd(data)data{Object}要添加的数据 添加后置操作。
model.afterDelete(data)删除后置操作。
model.beforeUpdate(data)data{Object}要更新的数据 更新前置操作。
model.afterUpdate(data)data{Object}要更新的数据 更新后置操作。
model.afterFind(data)data{Object}查询的单条数据return{Object|Promise} find查询后置操作。
model.afterSelect(data)data[Array]查询的数据数据return{Array|Promise} select查询后置操作。
model.data(data) data{Object}添加和更新操作时设置操作的数据。
model.options(options)options{Object} 设置操作选项。
如: module.exports=classextendsthink.Model{getList(){returnthis.options({where:'id=1',limit:[10,1]}).select();} } model.close()关闭数据库连接,一般情况下不要直接调用。
model.getSchema(table)table{String}表名return{Promise} 获取表的字段信息,自动从数据库中读取。
model.getTableFields(table)已废弃,使用model.getSchema方法替代。
model.getLastSql()return{String} 获取最后执行的SQL语句。
model.buildSql() return{Promise}将当前的查询条件生成一个SQL语句。
model.parseOptions(oriOpts,extraOptions)oriOpts{Object}extraOptions{Object}return{Promise} 根据已经设定的一些条件解析当前的操作选项。
model.getPk()return{Promise} 返回pk的值,返回一个Promise。
model.parseType(field,value)field{String}数据表中的字段名称value{Mixed}return{Mixed} 根据数据表中的字段类型解析value。
model.parseData(data)data{Object}要解析的数据return{Object} 调用parseType方法解析数据。
model.add(data,options,replace)data{Object}要添加的数据options{Object}操作选项replace{Boolean}是否是替换操作return{Promise}返回插入的ID 添加一条数据。
model.thenAdd(data,where)data{Object}要添加的数据where{Object}where条件return{Promise} 当where条件未命中到任何数据时才添加数据。
model.addMany(dataList,options,replace)dataList{Array}要添加的数据列表options{Object}操作选项replace{Boolean}是否是替换操作return{Promise}返回插入的ID 一次添加多条数据。
model.delete(options)options{Object}操作选项return{Promise}返回影响的行数 删除数据。
删除id为7的数据。
model.delete({where:{id:7} }); model.update(data,options)data{Object}要更新的数据options{Object}操作选项return{Promise}返回影响的行数 更新数据。
model.thenUpdate(data,where)data{Object}要更新的数据 where{Object}where条件return{Promise}当where条件未命中到任何数据时添加数据,命中数据则更新该数据。
updateMany(dataList,options)dataList{Array}要更新的数据列表options{Object}操作选项return{Promise} 更新多条数据,dataList里必须包含主键的值,会自动设置为更新条件。
model.increment(field,step)field{String}字段名step{Number}增加的值,默认为1return{Promise} 字段值增加。
model.decrement(field,step)field{String}字段名step{Number}增加的值,默认为1return{Promise} 字段值减少。
model.find(options)options{Object}操作选项return{Promise}返回单条数据 查询单条数据,返回的数据类型为对象。
如果未查询到相关数据,返回值为{}。
model.select(options)options{Object}操作选项return{Promise}返回多条数据 查询多条数据,返回的数据类型为数组。
如果未查询到相关数据,返回值为[]。
model.countSelect(options,pageFlag) options{Object}操作选项pageFlag{Boolean}当页数不合法时处理,true为修正到第一页,false为修正到最后一页,默认不修正return{Promise} 分页查询,一般需要结合page方法一起使用。
如: module.exports=classextendsthink.controller.base{asynclistAction(){letmodel=this.model('user');letdata=awaitmodel.page(this.get('page')).countSelect();} } 返回值数据结构如下: {numsPerPage:10,//每⻚页显示的条数currentPage:1,//当前⻚页count:100,//总条数totalPages:10,//总⻚页数data:[{//当前⻚页下的数据列列表name:"thinkjs",email:"admin@"},...] } model.getField(field,one) field{String}字段名,多个字段用逗号隔开one{Boolean|Number}获取的条数return{Promise} 获取特定字段的值。
model.count(field) field{String}字段名return{Promise}返回总条数 获取总条数。
model.sum(field)field{String}字段名return{Promise} 对字段值进行求和。
model.min(field)field{String}字段名return{Promise} 求字段的最小值。
model.max(field)field{String}字段名return{Promise} 求字段的最大值。
model.avg(field)field{String}字段名return{Promise} 求字段的平均值。
model.query(...args)return{Promise} 指定SQL语句执行查询。
model.execute(...args)return{Promise} 执行SQL语句。
model.parseSql(sql,...args) sql{String}要解析的SQL语句return{String} 解析SQL语句,调用util.format方法解析SQL语句,并将SQL语句中的__TABLENAME__解析为对应的表名。
module.exports=classextendsthink.Model{getSql(){letsql='SELECT*FROM__GROUP__WHEREsql=this.parseSql(sql,10);//sqlisSELECT*FROMthink_groupWHERE} } id=%d';id=10 model.startTrans()return{Promise} 开启事务。
mit()return{Promise} 提交事务。
model.rollback()return{Promise} 回滚事务。
model.transaction(fn)fn{Function}要执行的函数return{Promise} 使用事务来执行传递的函数,函数要返回Promise。
module.exports=classextendsthink.Model{updateData(data){returnthis.transaction(async()=>{letinsertId=awaitthis.add(data); letresult=await100}); returnresult;})}} this.model('user_cate').add({user_id: insertId, cate_id: 扩展功能 多模块项目 一般的项目我们推荐使用单模块项目,如果项目较为复杂的话,可以使用多级控制器来按功能划分。
如果这些还不能满足项目复杂度的需求,那么可以创建多模块项目。
创建项目时可以指定--mode=multi参数创建多模块项目。
thinkjsnewdemo--mode=multi 项目结构 项目结构跟单模块项目结构上有一些差别:mon存放一些公共的代码src/home默认的模块src/xxx按照功能添加模块 线上部署 定时任务

标签: #位置 #乱码 #迅雷 #文件 #文件夹 #文件 #映像 #文件