有组织在!

ThinkJS 简介




简介

最近几年,前端技术呈现出突飞猛进的发展,涌现出了一大批优秀的前端框架,今天给大家带来的就是基于node的一款优秀的优秀的前端框架。之前一直用Express来搭建比较简单的Node应用,但是对于相对复杂的应用来说,Express还是太轻量了。而作为一款优秀的国产前端框架,ThinkJS整合了大量的项目最佳实践,让企业级开发变得更加简单、高效。从 3.0 开始,框架底层基于 Koa 2.x 实现,兼容 Koa 的所有功能。在最新的版本中,ThinkJS全面支持ES6和ES7的语法规则。

特性

  • 基于 Koa 2.x,兼容 middleware
  • 内核小巧,支持 Extend、Adapter 等插件方式
  • 性能优异,单元测试覆盖程度高
  • 内置自动编译、自动更新机制,方便快速开发
  • 使用更优雅的 async/await 处理异步问题,不再支持 */yield
  • 从 3.2 开始支持 TypeScript

架构模型

ThinkJS的架构模型如下:

环境搭建

借助 ThinkJS 提供的脚手架,可以快速的创建一个项目。需要注意的是使用ThinkJS框架需要Node 6.x以上环境的支持。大家ThinkJS环境需要用到如下的步骤:

安装 ThinkJS 命令

npm install -g think-cli

安装完成后,系统中会有 thinkjs 命令(可以通过 thinkjs -v 查看 think-cli 的版本号,此版本号非 thinkjs 的版本号)。
注:如果是从 2.x 升级,需要将之前的命令删除,然后重新安装。

卸载旧版本命令

npm uninstall -g thinkjs

创建项目

执行 命令“thinkjs new [project_name]” 来创建项目,如:

$ thinkjs new demo;
$ cd demo;
$ 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

2.x 到3.x的变化

由于2.x到3.x对接口改动较大,所以建议直接删除掉之前的版本,然后重新安装信息的版本。2.x到3.x变化的内容有:

核心变化

3.0 抛弃了 2.x 的核心架构,基于 Koa 2.x 版本构建,兼容 Koa 里的所有功能。主要变化为:

  • 之前的 http 对象改为 ctx 对象
  • 执行完全改为调用 middleware 来完成
  • 框架内置的很多功能不再默认内置,可以通过扩展来支持

启动方式

2.x 中项目启动时,会自动加载 src/bootstrap/ 目录下的所有文件。3.0 中不再自动加载所有的文件,而是改为(分为两种情况):

  • 在 Master 进程中加载 src/boostrap/master.js 文件;
  • 在 Worker 进程中加载 src/boostrap/worker.js 文件;

配置

2.x 中会自动加载 src/config/ 目录下的所有文件,3.0 中改为根据功能加载对应的文件。

hook 和 middleware

移除 2.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。

View

模板的配置由原来的 src/common/config/view.js 迁移至 src/config/config.js 中,配置方法和之前基本一致。

其中老版本的 preRender() 方法已经废弃,新方法名为 beforeRender()。nunjucks 模板引擎的参数顺序由原来的 preRender(nunjucks, env, config) 修改为 beforeRender(env, nunjucks, config)。

阻止代码执行

在新的语法规则中,为了实现阻止某些代码的执行,对原来的语法进行了调整。移除了 think.prevent 等阻止后续执行的方法,替换为在 __before、xxxAction、__after 中返回 false 来阻止后续代码继续执行。

注:由于 3.0 改动了很多东西,所以不太容易基于原有项目代码简单修改来升级。建议使用新的脚手架工具创建项目,然后一一将之前的代码拷贝到新项目中进行修改。

基础概念

在介绍ThinkJS框架运行流程之前,我们先来看几个比较重要的概念:

Context

Context,在大多数的编程语言中,被称为上下文,也就是对象关联关系的。Context 是 Koa 中处理用户请求中的一个对象,贯穿整个请求生命周期。一般在 middleware、controller、logic 中使用,简称为 ctx。
例如,在 middleware 中使用 ctx 对象。

module.exports = options => {
  // 调用时 ctx 会作为第一个参数传递进来
  return (ctx, next) => {
    ...
  }
}

在 controller 中使用 ctx 对象。

module.exports = class extends think.Controller {
  indexAction() {
    // controller 中 ctx 作为类的属性存在,属性名为 ctx
    // controller 实例化时会自动把 ctx 传递进来
    const ip = this.ctx.ip;
  }
}

ThinkJS框架继承了Koa的一些属性和方法,并通过 Extend 机制扩展了很多非常有用的属性和方法。

Koa 内置 API

ctx.req:Node 的 request 对象。
ctx.res:Node 的 response 对象,不支持 绕开 Koa 对 response 的处理。
ctx.request:Koa 的 Request 对象。
ctx.response:Koa 的 Response 对象。
ctx.state:在中间件之间传递信息以及将信息发送给模板时,推荐的命名空间。避免直接在 ctx 上加属性,这样可能会覆盖掉已有的属性,导致出现奇怪的问题。例如:

ctx.state.user = await User.find(id);

这样后续在 controller 里可以通过 this.ctx.state.user 来获取对应的值。

module.exports = class extends think.Controller {
  indexAction() {
    const user = this.ctx.state.user;
  }
}

关于更多的介绍,读者可以访问ThinkJS 上下文介绍

Middleware

Middleware,又称为中间件,是 Koa 中一个非常重要的概念,利用中间件,可以很方便的处理用户的请求。由于 ThinkJS 3.0 是基于 Koa@2 版本之上构建的,所以完全兼容 Koa 里的中间件。

使用中间件的格式如下:

module.exports = options => {
  return (ctx, next) => {
    // do something
  }
}

中间件格式为一个高阶函数,外部的函数接收一个 options 参数,这样方便中间件提供一些配置信息,用来开启/关闭一些功能。执行后返回另一个函数,这个函数接收 ctx, next 参数,其中 ctx 为 context 的简写,是当前请求生命周期的一个对象,存储了当前请求的一些相关信息,next 为调用后续的中间件,返回值是 Promise,这样可以很方便的处理后置逻辑。相信做过异步程序开发的同学对这个不会陌生。这种中间件格式是常见的洋葱头模型。

例如,官方提供的一个例子:

const defaultOptions = {
  consoleExecTime: true // 是否打印执行时间的配置
}
module.exports = (options = {}) => {
  // 合并传递进来的配置
  options = Object.assign({}, defaultOptions, options);
  return (ctx, next) => {
    if(!options.consoleExecTime) {
      return next(); // 如果不需要打印执行时间,直接调用后续执行逻辑
    }
    const startTime = Date.now();
    let err = null;
    // 调用 next 统计后续执行逻辑的所有时间
    return next().catch(e => {
      err = e; // 这里先将错误保存在一个错误对象上,方便统计出错情况下的执行时间
    }).then(() => { const endTime = Date.now(); console.log(`request exec time: ${endTime - startTime}ms`); if(err) return Promise.reject(err); // 如果后续执行逻辑有错误,则将错误返回 }) } }

在 Koa 中,可以通过调用 app.use 的方式来使用中间件。

const app = new Koa();
const execTime = require('koa-execTime'); // 引入统计执行时间的模块
app.use(execTime({}));  // 需要将这个中间件第一个注册,如果还有其他中间件放在后面注册

扩展 app 参数

默认的中间件外层一般只是传递了 options 参数,有的中间件需要读取 app 相关的信息,框架在这块做了扩展,自动将 app 对象传递到中间件中。

module.exports = (options, app) => {
  // 这里的 app 为 think.app 对象
  return (ctx, next) => {

  }
}

如果在中间件中需要用到 think 对象上的一些属性或者方法,那么可以通过 app.think.xxx 来获取。

配置格式

为了方便管理和使用中间件,框架使用统一的配置文件来管理中间件,配置文件为 src/config/middleware.js(多模块项目配置文件为 sr/common/config/middleware.js)。

const path = require('path')
const isDev = 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)/,
    },
  }
]

配置项为项目中要使用的中间件列表,每一项支持 handle,enable,options,match 等属性。

handle

中间件的处理函数,可以用系统内置的,也可以是引入外部的,也可以是项目里的中间件。handle 的函数格式为:

module.exports = (options, app) => {
  return (ctx, next) => {

  }
}

这里中间件接收的参数除了 options 外,还多了个 app 对象,该对象为 Koa Application 的实例。

enable

是否开启当前的中间件,比如:某个中间件只在开发环境下才生效。

{ handle: 'resouce', enable: think.env === 'development' //这个中间件只在开发环境下生效 }

options

传递给中间件的配置项,格式为一个对象,中间件里获取到这个配置。

module.exports = [
  {
    options: {
      key: value
    } 
  }
]

有时候需要的配置项需要从远程获取,如:配置值保存在数据库中,这时候就要异步从数据库中获取,这时候可以将 options 定义为一个函数来完成:

module.exports = [
  {
    // 将 options 定义为一个异步函数,将获取到的配置返回
    options: async () => {
      const config = await getConfigFromDb();
      return {
        key: config.key,
        value: config.value
      }
    }
  }
]

match

匹配特定的规则后才执行该中间件,支持二种方式,一种是路径匹配,一种是自定义函数匹配。如:

module.exports = [
  {
    handle: 'xxx-middleware',
    match: '/resource' //请求的 URL 是 /resource 打头时才生效这个 middleware
  }
]

或者:

module.exports = [
  {
    handle: 'xxx-middleware',
    match: ctx => { // match 为一个函数,将 ctx 传递给这个函数,如果返回结果为 true,则启用该 middleware
      return true;
    }
  }
]

框架内置的中间件

框架内置了几个中间件,可以通过字符串的方式直接引用。常见的有:

  • meta 显示一些 meta 信息,如:发送 ThinkJS 的版本号,接口的处理时间等等
  • resource 处理静态资源,生产环境建议关闭,直接用 webserver 处理即可。
  • trace 处理报错,开发环境将详细的报错信息显示处理,也可以自定义显示错误页面。
  • payload 处理表单提交和文件上传,类似于 koa-bodyparser 等 middleware
  • router 路由解析,包含自定义路由解析
  • logic logic 调用,数据校验
  • controller controller 和 action 调用

自定义的中间件

在项目开发中,有时候需要根据一些特定需要添加中间件,那么我们可以自定义一些中间件,放在src/middleware目录下。例如:

module.exports = [
  {
    handle: 'csrf',
    options: {}
  }
]

引入外部的中间件非常简单,只需要 require 进来即可。这和JSX的语法是一样的。

const csrf = require('csrf'); 
module.exports = [
  ...,
  {
    handle: csrf,
    options: {}
  },
  ...
]

Router

Router,及路由,用来进行页面跳转的,用户访问一个地址时,需要有一个对应的逻辑进行处理。传统的处理方式下,一个请求对应的一个文件,如访问时 /user/about.php,那么就会在项目对应的目录下有 /user/about.php 这个实体文件。这种方式虽然能解决问题,但会导致文件很多,同时可能很多文件里逻辑功能其实比较简单。

在 MVC 开发模型里,一般都是通过路由来解决此类问题。由于 Node.js 是自己启动 HTTP(S) 服务的,所以已经天然将用户的请求汇总到一个入口了,这样处理路由映射就更简单了。

在 ThinkJS 中,当用户访问一个 URL 时,最后是通过 controller 里具体的 action 来响应的。所以就需要解析出 URL 对应的 controller 和 action,这个解析工作是通过 think-router 模块实现的。

路由配置

think-router 是一个 middleware,项目创建时默认已经加到配置文件 src/config/middleware.js 里了,其中 options 支持如下的参数:

  • defaultModule {String} 多模块项目下,默认的模块名。默认值为 home
  • defaultController {String} 默认的控制器名,默认值为 index
  • defaultAction {String} 默认的操作名,默认值为 index
  • prefix {Array} 默认去除的 pathname 前缀,默认值为 []
  • suffix {Array} 默认去除的 pathname 后缀,默认值为 [‘.html’]
  • enableDefaultRouter {Boolean} 在不匹配情况下是否使用默认路由解析,默认值为 true
  • subdomainOffset {Number} 子域名映射下的偏移量,默认值为 2
  • subdomain {Object|Array} 子域名映射列表,默认为 {}
  • denyModules {Array} 多模块项目下,禁止访问的模块列表,默认为 []

例如,下面是项目中的配置:

module.exports = [
  {
    handle: 'router',
    options: {
      defaultModule: 'home',
      defaultController: 'index',
      defaultAction: 'index',
      prefix: [],
      suffix: ['.html'],
      enableDefaultRouter: true,
      subdomainOffset: 2,
      subdomain: {},
      denyModules: []
    }
  }
];

路径预处理

当用户访问服务时,通过 ctx.url 属性,可以得到初始的 pathname,如:访问本页面 https://www.thinkjs.org/zh-cn/doc/3.0/router.html,初始 pathname 为 /zh-cn/doc/3.0/router.html。为了方便后续通过 pathname 解析出对应的 controller 和 action,需要对 pathname 进行预处理。

prefix & suffix

有时候为了搜索引擎优化或者一些其他的原因,URL 上会多加一些东西。比如:当前页面是一个动态页面,为了 SEO,会在 URL 后面加上 .html 后缀假装页面是一个静态页面,但 .html 对于路由解析来说是无用的,是要去除的。
prefix 与 subffix 为数组,数组的每一项可以为字符串或者正则表达式, 在匹配到第一个之后停止后续匹配。在ThinkJS中prefix的格式如下:

{ prefix: [], suffix: ['.html'], }

路由解析

通过 prefix & suffix 和 subdomain 预处理后,得到真正后续要解析的 pathname。默认的路由解析规则为 /controller/action,如果是多模块项目,那么规则为 /module/controller/action,根据这个规则解析出对应的 module、controller、action 值。

如果 controller 有子级,那么会优先匹配子级 controller,然后再匹配 action。常见的匹配有:

pathname 项目类型 子级控制器 module controller action 备注
/ 单模块 index index controller、action 为配置的默认值
/user 单模块 user index action 为配置的默认值
/user/login 单模块 user login
/console/user/login 单模块 console/user login 有子级控制器 console/user
/console/user/login/aaa/bbb 单模块 console/user login 剩余的 aaa/bbb 不再解析
/admin/user 多模块 admin user index 多模块项目,有名为 admin 的模块
/admin/console/user/login 多模块 admin console/user login

解析后的 module、controller、action 分别放在 ctx.module、ctx.controller、ctx.action 上,方便后续调用处理。如果不想要默认的路由解析,那么可以通过配置 enableDefaultRouter: false 关闭。

自定义路由规则

虽然默认的路由解析方式能够满足需求,但有时候会导致 URL 看起来不够优雅,我们更希望 URL 比较简短,这样会更利于记忆和传播。框架提供了自定义路由来处理这种需求。
自定义路由规则配置文件为 src/config/router.js(多模块项目放在 src/common/config/router.js),路由规则为二维数组:

module.exports = [
  [/libs\/(.*)/i, '/libs/:1', 'get'],
  [/fonts\/(.*)/i, '/fonts/:1', 'get,post'],
];

每一条路由规则也为一个数组,数组里面的项分别对应为: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 一般配合 Extend 一起使用。

框架默认提供了很多种 Adapter,如: View、Model、Cache、Session、Websocket,项目中也可以根据需要进行扩展,也可以引入第三方的 Adapter。

Adapter 都是一类功能的不同实现,一般是不能独立使用的,而是配合对应的扩展一起使用。如:view Adapter(think-view-nunjucks、think-view-ejs)配合 think-view 扩展进行使用。

项目安装 think-view 扩展后,提供了对应的方法来渲染模板,但渲染不同的模板需要的模板引擎有对应的 Adapter 来实现,也就是配置中的 handle 字段。

Adapter 配置

Adapter 的配置文件为 src/config/adapter.js(多模块项目文件为 src/common/config/adapter.js),格式如下:

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.development.js 和 adapter.production.js 存放有差异的配置,系统启动后会读取对应的运行环境配置和默认配置进行合并。

Adapter 配置解析

Adapter 配置存储了所有类型下的详细配置,具体使用时需要对其解析,选择对应的一种进行使用。比如上面的配置文件中,配置了 nunjucks 和 ejs 二种模板引擎的详细配置,但具体使用时一种场景下肯定只会用其一种模板引擎。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' } */

通过 parseAdapterConfig 方法就可以拿到对应类型的配置,然后就可以调用对应的 handle,传入配置然后执行了。当然,配置解析并不需要使用者在项目中具体调用,一般都是在插件对应的方法里已经处理。

Adapter使用

除了引入外部的 Adapter 外,项目内也可以创建 Adapter 来使用。Adapter 文件放在 src/adapter/ 目录下(多模块项目放在 src/common/adapter/),如:src/adapter/cache/xcache.js,表示加了一个名为 xcache 的 cache Adapter 类型,然后该文件实现 cache 类型一样的接口即可。

exports.cache = {
  type: 'file',
  xcache: {
    handle: 'xcache', //这里配置字符串,项目启动时会自动查找 src/adapter/cache/xcache.js 文件
    ...
  }
}

Extend

虽然框架内置了很多功能,但在实际项目开发中,提供的功能还是远远不够的。3.0 里引入了扩展机制,方便对框架进行扩展。支持的扩展类型为:think、application、context、request、response、controller、logic 和 service。框架内置的很多功能也是扩展来实现的,如:Session、Cache。

扩展配置

扩展配置文件路径为 src/config/extend.js(多模块项目文件路径为 src/common/config/extend.js),格式为数组:

const view = require('think-view');

module.exports = [
  view //make application support view
];

除了引入外部的 Extend 来丰富框架的功能,也可以在项目中对对象进行扩展,扩展文件放在 src/extend/ 目录下(多模块项目放在 src/common/extend/ 下)。

  • src/extend/think.js 扩展 think 对象,think.xxx
  • src/extend/application.js 扩展 Koa 里的 app 对象(think.app)
  • src/extend/request.js 扩展 Koa 里的 request 对象(think.app.request)
  • src/extend/response.js 扩展 Koa 里的 response 对象(think.app.response)
  • src/extend/context.js 扩展 ctx 对象(think.app.context)
  • src/extend/controller.js 扩展 controller 类(think.Controller)
  • src/extend/logic.js 扩展 logic 类(think.Logic)- logic 继承
  • controller 类,所以 logic 包含 controller 类所有方法
  • src/extend/service.js 扩展 service 类(think.Service)

比如:我们想给 ctx 添加个 isMobile 方法来判断当前请求是不是手机访问:

module.exports = {
  isMobile(){
    const userAgent = this.userAgent.toLowerCase();
    const mList = ['iphone', 'android'];
    return mList.some(item => userAgent.indexOf(item) > -1);
  }
}

然后使用ctx.isMobile() 来判断是否是手机访问。当然这个方法没有任何的参数,我们也可以变成一个 getter。

module.exports = {
  get isMobile(){
    const userAgent = this.userAgent.toLowerCase();
    const mList = ['iphone', 'android'];
    return mList.some(item => userAgent.indexOf(item) > -1);
  }
}

如果在 controller 中也想通过 this.isMobile 使用,怎么办呢? 可以给 controller 也扩展一个 isMobile 属性来完成。

module.exports = {
  get isMobile(){
    return this.ctx.isMobile;
  }
}

不过这种方式只能在本模块使用,如果要在其他项目中使用,可以将这些扩展发布为一个 npm 模块。这和React的写法是一样的。

const controllerExtend = require('./controller.js');
const contextExtend = require('./context.js');

// 模块入口文件
module.exports = {
  controller: controllerExtend,
  context: contextExtend
}

扩展里使用 app 对象

有些 Extend 需要使用一些 app 对象上的数据,那么可以导出为一个函数,配置时把 app 对象传递进去即可。

const model = require('think-model');
module.exports = [
  model(think.app) //将 think.app 传递给 model 扩展
];

运行流程

前面说了这么多基本的概念,现在再来看一下ThinkJS的运行流程。
Node.js 提供了 http 模块直接创建 HTTP 服务,用来响应用户的请求,比如 Node.js 官网提供的创建 HTTP 服务的例子:

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World\n'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });

ThinkJS 也是调用 http.createServer 的方式来创建服务的,所以整个运行流程包含了启动服务和响应用户请求两个部分。

系统服务

  1. 执行 npm start 或者 node development.js;
  2. 实例化 ThinkJS 里的 Application 类,执行 run 方法。
  3. 根据不同的环境(Master 进程、Worker 进程、命令行调用)处理不同的逻辑。
  4. 如果是 Master 进程。加载配置文件,生成 think.config 和 think.logger 对象。
    1)加载文件 src/bootstrap/master.js 文件
    2)如果配置文件监听服务,那么开始监听文件的变化,目录为 src/。
    3)文件修改后,如果配置文件编译服务,那么会对文件进行编译,编译到 app/ 目录下。
    4)根据配置 workers 来 fork 对应数目的 Worker。Worker 进程启动完成后,触发 appReady 事件。(可以通过 think.app.on(“appReady”) 来捕获)
    5)如果文件发生了新的修改,那么会触发编译,然后杀掉所有的 Worker 进程并重新 fork。
  5. 如果是 Worker 进程
    1)加载配置文件,生成 think.config 和 think.logger 对象。
    2)加载 Extend,为框架提供更多的功能,配置文件为 src/config/extend.js。
    3)获取当前项目的模块列表,放在 think.app.modules 上,如果为单模块,那么值为空数组。
    4)加载项目里的 controller 文件(src/controller/*.js),放在 think.app.controllers 对象上。
    5)加载项目里的 logic 文件(src/logic/*.js),放在 think.app.logics 对象上。
    6)加载项目里的 model 文件(src/model/*.js),放在 think.app.models 对象上。
    7)加载项目里的 service 文件(src/service/*.js),放在 think.app.services 对象上。
    8)加载路由配置文件 src/config/router.js,放在 think.app.routers 对象上。
    9)加载校验配置文件 src/config/validator.js,放在 think.app.validators 对象上。
    10)加载 middleware 配置文件 src/config/middleware.js,并通过 think.app.use 方法注册。
    11)加载定时任务配置文件 src/config/crontab.js,并注册定时任务服务。
    12)加载 src/bootstrap/worker.js 启动文件。
    13)监听 process 里的 onUncaughtException 和 onUnhandledRejection 错误事件,并进行处理。可以在配置 src/config.js 自定义这二个错误的处理函数。
    14)等待 think.beforeStartServer 注册的启动前处理函数执行,这里可以注册一些服务启动前的事务处理。
    15)如果自定义了创建服务配置 createServer,那么执行这个函数 createServer(port, host, callback) 来创建服务;如果没有自定义,则通过 think.app.listen 来启动服务。
    16)服务启动完成时,触发 appReady 事件,其他地方可以通过 think.app.on(“appReady”) 监听;创建的服务赋值给 think.app.server 对象。

服务启动后,会打印下面的日志:

发布了1011 篇原创文章 ·
获赞 2054 ·
访问量 429万+