ThinksJS3.0

[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

项目启动

系统服务启动

  1. 执行 npm start 或者 node development.js
  2. 实例化 ThinkJS 里的 Application 类,执行 run 方法;
  3. 根据不同的环境(Master 进程、Worker 进程、命令行调用)处理不同的逻辑;
    如果是 Master 进程:
  • 加载配置文件,生成 think.configthink.logger 对象
  • 加载 src/bootstrap/master.js 文件
  • 如果配置文件监听服务,那么开始监听文件的变化,目录为 src/
  • 文件修改后,如果配置文件编译服务,那么会对文件进行编译,编译到 app/ 目录下
  • 根据配置 workers 来 fork 对应数量的 Worker。Worker 进程启动完成后,触发 appReady 事件(根据 think.app.on("appReady") 来捕获)
  • 如果文件发生了新的修改,会触发编译,杀掉所有 Worker 进程并重新 fork

如果是 Worker 进程:

  • 加载配置文件,生成think.configthink.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 里的 onUncaughtExceptiononUnhandledRejection 错误事件,并进行处理。可以在配置 src/config.js 自定义这二个错误的处理函数
  • 等待 think.beforeStartServer 注册的启动前处理函数执行,这里可以注册一些服务启动前的事务处理
  • 如果自定义了创建服务配置 createServer,那么执行这个函数 createServer(port, host, callback) 来创建服务
  • 如果没有自定义,则通过 think.app.listen 来启动服务
  • 服务启动完成时,触发 appReady 事件,其他地方可以通过 think.app.on("appReady") 监听
  • 创建的服务赋值给 think.app.server 对象

用户请求处理

  1. 请求到达 WebServer,通过反向代理将请求转发给 Node 服务
  2. Master 服务接收用户请求,转发给对应 Worker 进程
  3. Work 进程通过注册的 Middleware 处理用户请求
  4. 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

解析路由,解析出请求处理对应的 ControllerAction,放在 ctx.controllerctx.action 上,方便后续处理。如果项目是多模块结构,那么还有 ctx.module

Logic

根据解析出来的 controlleraction,调用 logic 里对应的方法。

  • 实例化 logic 类,并将 ctx 传递进去。如果不存在则直接跳过
  • 执行 __before 方法,如果返回 false 则不再执行后续所有的逻辑(提前结束处理)
  • 如果 xxxAction 方法存在则执行,结果返回 false 则不再执行后续所有的逻辑
  • 如果 xxxAction 方法不存在,则试图执行 __call 方法
  • 执行 __after 方法,如果返回 false 则不再执行后续所有的逻辑
  • 通过方法返回 false 来阻断后续逻辑的执行

Controller

根据解析出来的 controlleraction,调用 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:匹配特定规则后才执行该中间件

  1. 路径匹配
  2. 函数匹配
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

ContextKoa 中处理用户请求中的一个对象,贯彻整个生命周期,一般在 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 时,会优先解析出 Controllerconsole/userActionlogin

透传数据

由于用户的请求处理经过了中间件、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,同时指定此次请求的 statusCode301

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容