[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
存放一些静态资源文件。





网友评论