[TOC]
Command
安装:
npm install -g think-cli
生成:
thinkjs new helloword
cd helloword
npm install
npm start
目录结构
|--- development.js //开发环境下的入口文件
|--- nginx.conf //nginx 配置文件
|--- package.json
|--- pm2.json //pm2 配置文件
|--- production.js //生产环境下的入口文件
|--- README.md
|--- src
| |--- bootstrap //启动自动执行目录
| | |--- master.js //Master 进程下自动执行
| | |--- worker.js //Worker 进程下自动执行
| |--- config //配置文件目录
| | |--- adapter.js // adapter 配置文件
| | |--- config.js // 默认配置文件
| | |--- config.production.js //生产环境下的默认配置文件,和 config.js 合并
| | |--- extend.js //extend 配置文件
| | |--- middleware.js //middleware 配置文件
| | |--- router.js //自定义路由配置文件
| |--- controller //控制器目录
| | |--- base.js
| | |--- index.js
| |--- logic //logic 目录
| | |--- index.js
| |--- model //模型目录
| | |--- index.js
|--- view //模板目录
| |--- index_index.html
|--- www
| |--- static //静态资源目录
| | |--- css
| | |--- img
| | |--- js
项目启动
系统服务启动
- 执行
npm start
或者node development.js
; - 实例化 ThinkJS 里的 Application 类,执行
run
方法; - 根据不同的环境(Master 进程、Worker 进程、命令行调用)处理不同的逻辑;
如果是 Master 进程:
- 加载配置文件,生成
think.config
和think.logger
对象 - 加载
src/bootstrap/master.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
对象
用户请求处理
- 请求到达
WebServer
,通过反向代理将请求转发给Node
服务 -
Master
服务接收用户请求,转发给对应 Worker 进程 -
Work
进程通过注册的Middleware
处理用户请求 - 当
Worker
报错,触发onUncaughtException
或者onUnhandledRejection
事件,或者Worker
异常退出,Master
捕获到错误,重新fork
一个新的Worker
进程,并杀掉当前进程
所有的请求都是通过 middleware
来完成的,具体的项目中,根据需求可以组装更多 middleware
Middleware
在 src/config/middleware.js
管理中间件
[配置](##配置 Middleware)
框架扩展 App
参数:
module.exports = (options, app) => {
// app 为 think.app 对象
return (ctx, next) => {
...
}
}
项目中自定义中间件
有时候项目中根据一些特定需要添加中间件,那么可以放在 src/middleware
目录下,然后就可以直接通过字符串的方式引用
module.exports = [
{
handle: 'csrf',
options: {}
}
]
引入外部中间件
require
即可
const csrf = require('csrf');
module.exports = [
...,
{
handle: csrf,
options: {}
},
...
]
设置数据到 GET/POST 数据中
在中间件里可以通过 ctx.param、ctx.post
等方法来获取 query
参数或者表单提交上来的数据,但有些中间件里希望设置一些参数值、表单值以便在后续的 Logic、Controller
中获取,这时候可以通过 ctx.param、ctx.post
设置:
// 设置参数 name=value,后续在 Logic、Controller 中可以通过 this.get('name') 获取该值
// 如果原本已经有该参数,那么会覆盖
ctx.param('name', 'value');
// 设置 post 值,后续 Logic、Controller 中可以通过 this.post('name2') 获取该值
ctx.post('name2', 'value');
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
完全一致 - 如果不存在,那么当前请求返回 404
-
action
执行完成时,可以将结果放在this.body
属性上然后返回给用户
配置
配置 Middleware
框架统一在 src/config/middleware.js
中配置中间件:
const path = require('path')
const isDev = think.env === 'development'
module.exports = [
{
handle: 'meta', //中间件处理函数 内置中间件不用手工 require 进来,直接通过字符串方式引用
options: {
logRequest: isDev,
sendResponseTime: isDev,
},
},
{
handle: 'resource',
enable: isDev, //是否开启中间件
options: {
root: path.join(think.ROOT_PATH, 'www'),
publicPath: /^\/static|favicon\.ico)/,
},
}
]
handle:中间件函数名
enable:是否开启
options:传递的参数对象
match:匹配特定规则后才执行该中间件
- 路径匹配
- 函数匹配
module.exports = [
{
handle: 'xxx-middleware',
match: '/resource' //请求的 URL 是 /resource 打头时才生效这个 middleware
}
]
module.exports = [
{
handle: 'xxx-middleware',
match: ctx => { // match 为一个函数,将 ctx 传递给这个函数,如果返回结果为 true,则启用该 middleware
return true;
}
}
]
Extend
扩展配置文件路径为 src/config/extend.js
const view = require('think-view')
module.exports = [
view //make application support view
]
通过 view
扩展框架就支持渲染模板的功能,Controller
类上就有 assign、display
等方法
Context
Context
是 Koa
中处理用户请求中的一个对象,贯彻整个生命周期,一般在 middleware、controller、logic
中使用,简称 ctx
module.exports = options => {
// 调用时 ctx 作为第一个参数传递进来
return (ctx, next) => {
...
}
}
module.exports = class extends think.Controller {
indexAction() {
// controller 中 ctx 作为类的属性存在,属性名为 ctx
// controller 实例化时会自动把 ctx 传递进来
const ip = this.ctx.ip;
}
}
ThinkJS 框架继承该对象,并通过 Extend 机制可以扩展 ctx
对象
Logic
ThinkJS
在控制器前面增加了一层 Logic
,其实就是在前面增加一个 Logic
中间件提前处理请求,把一些重复的操作放到 Logic
中(参数校验、权限判断等)。
Controller
基类 think.Controller
控制器继承该基类
Action 执行
Action
执行通过中间件 think-controller
完成,通过 ctx.action
值在 controller
中寻找 xxxAction
方法名并调用,并且调用相关魔术方法,具体顺序如下:
- 实例化
Controller
类,传入ctx
对象 - 如果
__before
存在则调用,如果返回值为false
,则停止继续执行 - 如果方法
xxxAction
存在则执行,如果返回值为false
, 则停止继续执行 - 如果方法
xxxAction
不存在但__call
方法存在,则调用,如果返回值为false
,则停止继续执行 - 如果方法
__after
存在则执行
如果类想调用父级的 __before
方法,可以通过 super.__before
完成:
module.exports = class extends Base {
async __before(){
// 通过 Promise.resolve 将返回值包装为 Promise
// 如果返回值确定为 Promise,那么就不需要再包装了
return Promise.resolve(super.__before()).then(flag => {
// 如果父级想阻止后续继承执行会返回 false,这里判断为 false 的话不再继续执行了。
if(flag === false) return false;
// 其他逻辑代码
})
}
}
CTX 对象
Controller
实例化时会传入 ctx
对象,通过 this.ctx
获取该对象,子类重写 constructor
方法,需要调用父类 construction
方法,传入 ctx
参数
const Base = require('./base.js');
module.exports = class extends Base {
constructor(ctx){
super(ctx); // 调用父级的 constructor 方法,并把 ctx 传递进去
// 其他额外的操作
}
}
多级控制器
有时候项目比较复杂,文件较多,所以希望根据功能进行一些划分。如:用户端的功能放在一块、管理端的功能放在一块。
这时可以借助多级控制器来完成这个功能,在 src/controller/
目录下创建 user/
和 admin/
目录,然后用户端的功能文件都放在 user/
目录下,管理端的功能文件都放在 admin/
目录下。访问时带上对应的目录名,路由解析时会优先匹配目录下的控制器。
假如控制器下有 console
子目录,下有 user.js
文件,即:src/controller/console/user.js
,当访问请求为 /console/user/login
时,会优先解析出 Controller
为 console/user
,Action
为 login
。
透传数据
由于用户的请求处理经过了中间件、Logic、Controller 等多层的处理,有时候希望在这些环节中透传一些数据,这时候可以通过 ctx.state.xxx
来完成。
// 中间件中设置 state
(ctx, next) => {
ctx.state.userInfo = {};
}
// Logic、Controller 中获取 state
indexAction() {
const userInfo = this.ctx.state.userInfo;
}
透传数据时避免直接在 ctx
对象上添加属性,这样可能会覆盖已有的属性,引起一些奇怪的问题。
View
模板的配置由原来的 src/common/config/view.js
迁移至 src/config/config.js
中,配置方法和之前基本一致。
其中老版本的 preRender()
方法已经废弃,新方法名为 beforeRender()
。nunjucks
模板引擎的参数顺序由原来的 preRender(nunjucks, env, config)
修改为 beforeRender(env, nunjucks, config)
。 // 模板渲染预处理
assign
给模板赋值
//单条赋值
this.assign('title', 'thinkjs');
//多条赋值
this.assign({
title: 'thinkjs',
name: 'test'
});
//获取之前赋过的值,如果不存在则为 undefined
const title = this.assign('title');
//获取所有赋的值
const assignData = this.assign();
render
获取渲染后的内容,该方法为异步方法,需要通过 async/await
处理
//根据当前请求解析的 controller 和 action 自动匹配模板文件
const content1 = await this.render();
//指定文件名
const content2 = await this.render('doc');
const content3 = await this.render('doc/detail');
const content4 = await this.render('doc_detail');
//不指定文件名但切换模板类型
const content5 = await this.render(undefined, 'ejs');
//指定文件名且切换模板类型
const content6 = await this.render('doc', 'ejs');
//切换模板类型,并配置额外的参数
//切换模板类型时,需要在 adapter 配置里配置对应的类型
const content7 = await this.render('doc', {
type: 'ejs',
xxx: 'yyy'
});
display
渲染并输出内容,该方法实际上调用了 render
方法,然后将渲染后的内容赋值到 ctx.body
属性上,该方法为异步方法,需要 async/await
处理
//根据当前请求解析的 controller 和 action 自动匹配模板文件
await this.display();
//指定文件名
await this.display('doc');
await this.display('doc/detail');
await this.display('doc_detail');
//不指定文件名切换模板类型
await this.display(undefined, 'ejs');
//指定文件名且切换模板类型
await this.display('doc', 'ejs');
//切换模板类型,并配置额外的参数
await this.display('doc', {
type: 'ejs',
xxx: 'yyy'
});
模板预处理
beforeRender(env, nunjuncks, config)
方法进行预处理,常见的需求是增加 Filter
:
env.addFilter('utc', time => (new Date(time)).toUTCString());
默认注入的参数 Controller Config Ctx
Controller:当前控制器实例,在模板里可以直接调用控制器上的属性和方法。
这里以 nunjucks 模板引擎举例,如果是调用控制器里的方法,那么方法必须为一个同步方法。
Config:所有的配置,在模板里可以直接通过 config.xxx 来获取配置,如果属性不存在,那么值为 undefined。
Ctx:当前请求的 Context 对象,在模板里可以通过直接通过 ctx.xxx 调用其属性或者 ctx.yyy() 调用其方法。
如果是调用其方法,那么方法必须为一个同步方法。
Router
解析
/console/user/login controller=console/user action=login
解析后的 module、controller、action
分别放在 ctx.module、ctx.controller、ctx.action
上
自定义路由规则
配置文件 src/config/router.js
路由规则为二维数组:
module.exports = [
[/libs\/(.*)/i, '/libs/:1', 'get'],
[/fonts\/(.*)/i, '/fonts/:1', 'get,post'],
];
每一条路由规则也为一个数组,数组里面的项分别对应为:
match
:{String | RegExp}
pathname 匹配规则,可以是字符串或者正则,如果是字符串,会通过path-to-regexp模块转为正则
pathname
:匹配后调用的 pathname,后续根据这个路径解析 controller、action
method
:支持的请求类型,默认为所有
options
:额外的选项
获取 mactch 匹配的值
字符串路由:['/user/:name', 'user/info/:name']
,在controller
里可以通过 this.get("name")
获取
正则路由:[\/user\/(\w+)/, 'user?name=:1']
:1获取这个值
Redirect
有时候项目经过多次重构后,URL 地址可能会发生一些变化,为了兼容之前的 URL,一般需要把之前的 URL 跳转到新的 URL 上。这里可以通过将 method
设置为 redirect
来完成:
module.exporst = [
['/usersettings', '/user/setting', 'redirect', {statusCode: 301}]
]
Debug
DEBUG=think-router npm start
在路由解析时打印相关调试信息
当访问地址为 /usersettings
时会自动跳转到 /user/setting
,同时指定此次请求的 statusCode
为 301
。
Adapter
https://github.com/thinkjs/think-awesome#adapters
Adapter
是解决一类功能的多种实现问题,这些实现提供一套统一的接口,类似设计模式里的工厂模式,传入不同的参数返回不同的实现(支持多种数据库,多种模板引擎),方便在不同实现中切换,Adapter
一般配合 Extend
一起使用。
const nunjucks = require('think-view-nunjucks');
const ejs = require('think-view-ejs');
const path = require('path');
exports.view = {
type: 'nunjucks', // 默认的模板引擎为 nunjucks
common: { //通用配置
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
参数,最终获取到的参数是common
参数与该参数进行合并 -
handle
对应类型的处理函数,一般为一个类
Adapter 配置解析
Adapter 配置存储所以类型下的详细配置,具体使用时需要对其解析,选择对应的一种进行使用,通过 think-helper
模块中的 parseAdapterConfig
方法完成解析:
const helper = require('think-helper');
const viewConfig = think.config('view'); // 获取 view adapter 的详细配置
const nunjucks = helper.parseAdatperConfig(viewConfig); // 获取 nunjucks 的配置,默认 type 为 nunjucks
/**
{
type: 'nunjucks',
handle: nunjucks,
viewPath: path.join(think.ROOT_PATH, 'view'),
sep: '_',
extname: '.html'
}
*/
const ejs = helper.parseAdatperConfig(viewConfig, 'ejs') // 获取 ejs 的配置
/**
{
handle: ejs,
type: 'ejs',
viewPath: path.join(think.ROOT_PATH, 'view/ejs/'),
viewPath: path.join(think.ROOT_PATH, 'view'),
sep: '_',
extname: '.html'
}
*/
拿到配置后,调用对应的 handle
,传入配置然后执行。
配置解析并不需要使用者在项目中具体调用,一般都是在插件对应的方法里已经处理。
Adapter 使用
Adapter 都是一类功能的不同实现,一般是不能独立使用的,而是配合对应的扩展一起使用。如:view Adapter(think-view-nunjucks、think-view-ejs)
配合 think-view
扩展进行使用。
项目安装 think-view
扩展后,提供了对应的方法来渲染模板,但渲染不同的模板需要的模板引擎有对应的 Adapter
来实现,也就是配置中的 handle
字段。
Extend
扩展框架的功能
Model
基类 think.Model
阻止后续执行
移除了 think.prevent
等阻止后续执行的方法,替换为在 __before、xxxAction、__after
中返回 false
来阻止后续代码继续执行。
错误处理
2.x 创建项目时,会创建对应的 error.js
文件用来处理错误。3.0 里改为使用中间件 think-trace
处理。
加入 token 服务
npm install jsonwebtoken --save
thinkjs service token
生成 token 和 验证 token
'use strict';
//引入jwt
let jwt = require('jsonwebtoken');
//读取secret标记码
let secret = think.config("gotolion.secret");
//读取token有效期
let expiresIn = think.config("gotolion.expiresIn");
export default class extends think.service.base {
/**
* @description 创建token
* @param {Object} userinfo 用户信息
* @return 返回token
*/
createToken(userinfo) {
let result = jwt.sign(userinfo, secret);
return result;
}
/**
* @description 验证票据
* @param {Object} token 用户请求token
* @return 返回 错误或者解密过的token
*/
verifyToken(token) {
if (token) {
try {
let result = jwt.verify(token, secret);
return result;
} catch (err) {
//票据验证失败,需要提示需要重新登录
return "fail";
}
}
return "fail";
}
}
token中包含用户的姓名、部门等等用户基本信息或者程序所需要的信息,原则是前端每次登录获取token,每次请求后端都在header中带上token,服务端验证token的合法性。
Logic
逻辑处理。每个操作执行前可以先进行逻辑校验:参数是否合法、提交的数据是否正常、当前用户是否已经登录、当前用户是否有权限等等,可以降低 controller 里的复杂性。
'use strict';
/**
* logic
* @param {} []
* @return {} []
*/
export default class extends think.logic.base {
/**
* index action logic
* @return {} []
*/
indexAction(){
}
}
WWW
项目的可访问根目录,nginx 里的根目录会配置到此目录下。
www/development.js
开发模式下项目的入口文件,可以根据项目需要进行修改。www/production.js 为线上的入口文件。
www/static
存放一些静态资源文件。