概述
ThinkJS 是一款面向未来开发的 Node.js 框架,底层基于 Koa 2.x 实现,内核小巧、性能优异,内置自动编译、自动更新机制,使用更优雅的 async/await 处理异步问题,兼容中间件并提供了扩展和适配器等插件方式。针对企业级应用开发,使开发变得更加简单、高效。
个人见解
这一节内容是我在学习完 ThinkJS 基本用法后加上的,主要谈谈关于 ThinkJS 的一些个人见解。
这里不得不引用官方文档中的架构图。
从架构图我们可以清晰的看到 ThinkJS 底层是基于 Koa 2.x,主要分为中间件、扩展和适配器三个核心模块。其中中间件主要用来处理用户的请求,当用户向服务器发出请求时,顺序执行中间件,最终返回用户期望的结果。扩展和适配器往往搭配使用,提供一些诸如:Session、模板引擎、数据库模型等扩展功能。
在我看来,ThinkJS 其实就是对一个应用如何构建进行了划分,将一个应用分成了各个模块(中间件、扩展),各个模块各司其职,处理自己的逻辑。我们只需要将相应的逻辑写在相应的地方,然后进行相应的一些配置。在应用启动时,ThinkJS 根据配置文件装载相应的模块,当用户请求到达时,按照配置将处理权交给相应的模块,完成请求的响应。整个应用都是可配置的,大大增强了灵活性。
ThinkJS 的工作模式按照官方文档上的说法主要分为两个部分,一个是系统服务启动,另一个是用户请求处理。这一点我感觉没有什么特殊的地方,我觉得所有的后台应用都是这两个部分,都是需要启动服务监听端口,然后等待客户端的请求到来,处理请求并响应。只不过在这两个阶段进行的具体工作有些许差异罢了。ThinkJS 在系统服务启动阶段,除了启动服务监听端口外,最主要的工作就是装配。按照 ThinkJS 的架构,我们需要什么功能或者需要执行什么操作,只需要将相应的代码写在相应的地方,然后添加响应的配置。为什么我们这样做了就可以实现我们需要的功能?必然是 ThinkJS 在启动时进行了装配。
我们以在 ThinkJS 中添加模板页面功能为例,来梳理一下 ThinkJS 扩展和适配器机制以及系统服务启动。扩展功能在我理解就是在相应的环境(全局、上下文或者控制器)下添加了一个对象,我们可以直接使用它提供的一些方法,来实现相应的功能。按照 ThinkJS 的架构,我们需要先实现这个对象(扩展)或者直接使用别人的,这个对象至少有一个 render
方法,能将模板文件渲染成 html 文件。然后,在配置文件 src/config/extend.js
中我们需要引入这个对象并添加这个对象的配置。接着,我们还需要在适配器中添加一个具体的模板引擎配置。完成这几步后,我们就可以直接在控制器中通过 this.render()
渲染模板页面了。那么为什么经过这样的几个步骤后,就可以使用模板页面了呢?我们可以在控制器中直接使用 this.render()
,肯定是 ThinkJS 在控制器对象上添加了这个方法,添加这个方法的依据应该就是我们在扩展配置文件中添加了 view
配置,而我们执行这个方法具体的实现则是根据适配器中的配置来的。扩展相当于定义了一个功能接口,而适配器中进行具体的实现。服务启动后,读取扩展配置,装配相应的对象(扩展功能)。在具体使用该功能时,根据适配器中的配置,不同的类型有不同的实现。
后台应用的核心主要还是处理用户请求,处理用户请求主要通过各种中间件来完成。通过中间件,将处理请求的流程截取成了多个部分,各个中间件完成各自的功能,整个处理过程类似于一个流水线生产。扩展也是为这一过程服务的,定义的扩展功能可以在请求处理的过程中调用,去完成更加具体的工作。我觉得扩展是作为中间件的一部分存在的,去实现中间件中的某一个具体功能。请求处理流程大致如下图所示。
各种中间件分别对 request
进行加工,然后由路由分配给指定的控制器去处理,在控制器处理之前 ThinkJS 中会先经过一个叫做 Logic 的中间件,对数据进行验证、过滤。控制器在处理请求时,可以随时调用扩展功能去实现某个具体的操作,接口都是统一的,由适配器去完成各种不同的实现。最后,将响应结果返回给客户端。
那么,使用 ThinkJS 能带来什么好处呢?首先,应用如何启动我们不需要花费太多的精力,通过配置文件以及 worker 即可完成。其次,对于请求处理,通过中间件、扩展和适配器以及相应的配置,我们可以很快速的添加相应的处理逻辑。同时,还可以保证一个良好的代码结构。最后,ThinkJS 提供了很多通用的机制,去解决一些通用问题,例如:日志、数据模型、Cookie、错误处理等。
总之,就目前的了解来看,ThinkJS 最优秀的地方在于它很简单,可以快速的上手。只要按照它的架构,就能很轻松的构建出整个应用。框架本身并不算复杂,但通过一些内部机制又可以增添很多丰富的功能。内置的一些功能模块也解决的了很多通用的问题。更具体的优缺点还得待具体使用后才知道。
开始使用
ThinkJS提供了脚手架工具,可以帮助我们快速创建一个项目,要求 Node.js 的版本大于 6.x
。
1 | $ npm install -g think-cli |
执行完上述命令后,就可以创建出一个项目骨架。进入该项目,安装依赖后可直接启动。
1 | $ cd <project-name> |
启动后,访问 http://127.0.0.1:8360
可以看到初始化页面。
通过脚手架构建的项目目录结构如下:
1 | . |
基本用法
关于ThinkJS的基本用法主要是参考了官方文档的内容,根据文档说明依次尝试了相应的模块并记录下核心的内容。
配置
所有的配置文件都放在 src/config/
目录下,根据不同的功能划分为不同的配置文件。其中,通用配置和适配器配置支持在不同的环境下配置不同的值,只需要将配置文件命令为 [name].[env].js
这种形式即可。例如:config.production.js
表明这是生产环境中的配置,该文件中的 key 会覆盖 config.js
中同名的 key。运行环境在实例化 Application 对象时通过 env
参数指定,如下所示。
1 | const instance = new Application({ |
在程序运行时可以获取和动态设置配置,获取配置可以通过上下文、controller 和全局的 think 对象,动态设置只能通过 think。下面是获取和设置配置的例子。
1 | // HTTP 服务启动前执行 |
通过 DEBUG=think-loader-config-* npm start
命令来启动项目可以在控制台查看配置文件的详细加载情况。
上下文
Context 是 Koa 中处理用户请求中的一个对象,贯穿整个请求生命周期。一般在 middleware、controller、logic 中使用,简称为 ctx。
Context 中包含了许多信息以及一些便捷操作,下面列举部分,详情见 ThinkJS 文档
- 可以通过上下文获取 request 和 response 对象。
- 可以过ctx.state在中间件中传递信息,而不是直接添加到 ctx 上。
- ctx.throw 抛出包含
.status
属性的异常,默认状态码为 500。 - ctx.json 输出 JSON 格式数据。
- ctx.success 和 ctx.fail 输出成功和失败数据。
- ctx.confg 读取配置。
中间件
ThinkJS 中通过统一的配置文件来管理中间件,配置文件为 src/config/middleware.js
。中间件分为框架内置中间件和项目自定义中间件两种。内置的中间件可以通过字符串的方式直接引用,内置中间件有下面这些:
- meta 显示一些 meta 信息,如:发送 ThinkJS 的版本号,接口的处理时间等等。
- resource 处理静态资源,生产环境建议关闭,直接用 webserver 处理即可。
- trace 处理报错,开发环境将详细的报错信息显示处理,也可以自定义显示错误页面。
- payload 处理表单提交和文件上传,类似于 koa-bodyparser 等 middleware。
- router 路由解析,包含自定义路由解析。
- logic logic 调用,数据校验。
- controller controller 和 action 调用。
项目自定义的中间件定义在 src/middleware
目录下,然后也可以通过字符串的方式引用。下面是一个计算当前请求执行时间的中间件的例子。
首先在 src/middleware
目录下新建一个 compute-request-time.js
的文件,文件中写下如下代码:
1 | const defaultOptions = { |
接着在配置文件 src/config/middleware.js
中增加如下配置:
1 | module.exports = [{ |
参数及含义如下:
- handle 中间件的处理函数。
- enable 是否启用中间件。
- options 传递给中间件的参数,是一个对象,可以通过函数返回。
- match 匹配特定的规则后才执行该中间件,支持二种方式,一种是路径匹配,一种是自定义函数匹配。
注意:
- 如果中间件没有定义在
src/middleware/
目录下,在配置文件中配置时需要引入。 - 中间件的执行顺序是配置文件中的顺序。
Logic
Logic 层在 Controller 之前执行,主要是完成一些过滤、数据校验、权限判断等一些逻辑性不是特别强但又和业务逻辑相关的工作,降低 Action 中代码的复杂程度。Logic 里的 Action 和控制器里的 Action 一一对应,系统在调用控制器里的 Action 之前会自动调用 Logic 里的 Action。
Logic 代码类似如下:
1 | module.exports = class extends think.Logic { |
要点:
- Logic 和 Controller 的文件名应该相同。
- Logic 中进行数据校验时,会根据请求类型自动获取,也可以手动获取设置到数据的
value
属性上。
控制器
控制器负责处理用户请求的逻辑,每一个操作对应一个 Action。Controller 代码类似如下:
1 | const Base = require('./base.js'); |
访问 /user
或 /user/index
会执行 indexAction
;访问 /user/profile
会执行 profileAction
。
在 controller 目录下创建子目录并添加 controller 可以进行多级匹配。例如:在 controller 目录下创建一个 user 目录并添加一个名为 login.js 的控制器,当访问 /user/login
时,会执行 login.js 中的 indexAction
。
Controller 里的处理顺序依次为 __before
、xxxAction
、__after
,return false
可以提前结束请求。
View
ThinkJS 3.0 没有内置 View 功能,需要通过 Extend 和 Adapter 中的配置来实现,具体如何配置参考官网文档。
配置了 Extend 和 Adapter 后,就可以在 Controller 里使用了:
1 | module.exports = class extends think.Controller { |
在模板中可以使用 assign
方法添加的变量。同时,系统在渲染模板的时候,自动注入 controller、config、ctx 变量,以便于在模板里直接使用。
路由
上面说到当访问 /user/profile
时,会执行 user 控制器中的 profileAction
,这一过程由路由来完成。ThinkJS 使用 think-router
中间件。
当 Controller 有子目录时,会优先匹配子 Controller,然后才匹配 action。
支持在 src/config/router.js
中配置自定义路由规则,路由规则为一个二维数组,如下所示:
1 | module.exports = [ |
每一条路由规则也为一个数组,数组里面的项分别对应为:match、pathname、method、options:
- match {String | RegExp} pathname 匹配规则,可以是字符串或者正则。如果是字符串,那么会通过 path-to-regexp 模块转为正则。
- pathname {String} 匹配后映射后的 pathname,后续会根据这个映射的 pathname 解析出对应的 controller、action。
- method {String} 该条路由规则支持的请求类型,默认为所有。多个请求类型中间用逗号隔开,如:get,post。
- options {Object} 额外的选项,如:跳转时指定 statusCode。
适配器
Adapter 是一类功能的不同实现,这些实现提供一套相同的接口,例如:多种数据库、模板引擎。Adapter 一般是不能独立使用的,而是配合对应的扩展一起使用。
Adapter 可以根据不同的运行环境设置不同的配置,例如:创建 adapter.production.js
文件指定生产环境下的适配器配置。
除了引入外部的 Adapter 外,项目内也可以创建 Adapter 来使用。Adapter 文件放在 src/adapter/
目录下(多模块项目放在 src/common/adapter/),如:src/adapter/cache/xcache.js
,表示加了一个名为 xcache 的 cache Adapter 类型,然后该文件实现 cache 类型一样的接口即可。
扩展
扩展在 src/config/extend.js
文件中配置,格式为数组,如下所示:
1 | const view = require('think-view'); |
支持自定义扩展,文件放在 src/extend/
目录下,下面是给 ctx 添加判断当前请求是不是手机访问的扩展的例子。
首先在 src/extend
目录下新建一个 context.js
的文件,文件中写下如下代码:
1 | module.exports = { |
这样就可以通过 ctx.isMobile
来判断是否是手机访问了。
异步/错误处理
这一部分主要利用了 ES2017 标准中引入的 async 函数。基本的用法与 async 函数的用法相同,没有太多的封装。
由于 Node.js 原生的异步方法都是 callback 方式,为了方便的将 callback 接口封装为 Promise 接口,ThinkJS 提供了 think.promisify
用来快速的封装,如:
1 | const fs = require('fs'); |
但是,这个方法只能封装 callback(err, data)
形式的回调函数,否则只能手动封装。
错误处理除了 try/catch
和 then/catch
之外,还提供了一个 trace 中间件来统一处理运行时的异常。
模型/数据库
ThinkJS 默认没有提供模型功能,而是通过扩展的方式实现,对应的模块为 think-model。在 src/config/extend.js
中添加如下配置即可:
1 | const model = require('think-model'); |
添加了模型扩展后,就可以通过 think.model
、ctx.model
、controller.model
等进行使用。
模型可以支持多种数据库,在 scr/config/adapter.js
中配置即可。如下所示:
1 | const mysql = require('think-model-mysql'); |
在适配器中添加相关配置后,在使用模型时指定相应的类型即可。例如使用 sqlite 的配置:const user = think.model('user', 'sqlite');
模型文件创建在 src/model
目录下,继承模型基类 think.Model
,model 代码类似如下:
1 | // src/model/user.js |
定义了模型后,通过 const usreModel = think.model('user', 'sqlite')
获取模型实例,然后就可以调用模型提供的方法 usreModel.getList()
。
think.Model 基类提供了丰富的方法进行 CRUD 操作,我们可以对这些方法进行封装,例如上面的例子。具体有哪些方法详见官网文档。
think-model 扩展主要用来支持关系型数据库,还提供了 think-mongo 扩展来支持 MongoDB,添加如下扩展即可。
1 | const mongo = require('think-mongo'); |
添加扩展后,可以通过 think.mongo
、ctx.mongo
等方法使用。
MondoDB 数据库适配器的配置复用了关系型数据的配置。模型的定义和关系型数据库类似,只是继承的是 think.mongo
。官方API