简介
最近几年,前端技术呈现出突飞猛进的发展,涌现出了一大批优秀的前端框架,今天给大家带来的就是基于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 命令
代码语言:javascript复制npm install -g think-cli
安装完成后,系统中会有 thinkjs 命令(可以通过 thinkjs -v 查看 think-cli 的版本号,此版本号非 thinkjs 的版本号)。 注:如果是从 2.x 升级,需要将之前的命令删除,然后重新安装。
卸载旧版本命令
代码语言:javascript复制npm uninstall -g thinkjs
创建项目
执行 命令“thinkjs new [project_name]” 来创建项目,如:
代码语言:javascript复制$ thinkjs new demo;
$ cd demo;
$ npm install;
$ npm start;
运行后可以看到相关的命令。
项目结构
默认创建的项目结构如下:
代码语言:javascript复制|--- 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 对象。
代码语言:javascript复制module.exports = options => {
// 调用时 ctx 会作为第一个参数传递进来
return (ctx, next) => {
...
}
}
在 controller 中使用 ctx 对象。
代码语言:javascript复制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 上加属性,这样可能会覆盖掉已有的属性,导致出现奇怪的问题。例如:
代码语言:javascript复制ctx.state.user = await User.find(id);
这样后续在 controller 里可以通过 this.ctx.state.user 来获取对应的值。
代码语言:javascript复制module.exports = class extends think.Controller {
indexAction() {
const user = this.ctx.state.user;
}
}
关于更多的介绍,读者可以访问ThinkJS 上下文介绍
Middleware
Middleware,又称为中间件,是 Koa 中一个非常重要的概念,利用中间件,可以很方便的处理用户的请求。由于 ThinkJS 3.0 是基于 Koa@2 版本之上构建的,所以完全兼容 Koa 里的中间件。
使用中间件的格式如下:
代码语言:javascript复制module.exports = options => {
return (ctx, next) => {
// do something
}
}
中间件格式为一个高阶函数,外部的函数接收一个 options 参数,这样方便中间件提供一些配置信息,用来开启/关闭一些功能。执行后返回另一个函数,这个函数接收 ctx, next 参数,其中 ctx 为 context 的简写,是当前请求生命周期的一个对象,存储了当前请求的一些相关信息,next 为调用后续的中间件,返回值是 Promise,这样可以很方便的处理后置逻辑。相信做过异步程序开发的同学对这个不会陌生。这种中间件格式是常见的洋葱头模型。
例如,官方提供的一个例子:
代码语言:javascript复制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 的方式来使用中间件。
代码语言:javascript复制const app = new Koa();
const execTime = require('koa-execTime'); // 引入统计执行时间的模块
app.use(execTime({})); // 需要将这个中间件第一个注册,如果还有其他中间件放在后面注册
扩展 app 参数
默认的中间件外层一般只是传递了 options 参数,有的中间件需要读取 app 相关的信息,框架在这块做了扩展,自动将 app 对象传递到中间件中。
代码语言:javascript复制module.exports = (options, app) => {
// 这里的 app 为 think.app 对象
return (ctx, next) => {
}
}
如果在中间件中需要用到 think 对象上的一些属性或者方法,那么可以通过 app.think.xxx 来获取。
配置格式
为了方便管理和使用中间件,框架使用统一的配置文件来管理中间件,配置文件为 src/config/middleware.js(多模块项目配置文件为 sr/common/config/middleware.js)。
代码语言:javascript复制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 的函数格式为:
代码语言:javascript复制module.exports = (options, app) => {
return (ctx, next) => {
}
}
这里中间件接收的参数除了 options 外,还多了个 app 对象,该对象为 Koa Application 的实例。
enable
是否开启当前的中间件,比如:某个中间件只在开发环境下才生效。
代码语言:javascript复制{
handle: 'resouce',
enable: think.env === 'development' //这个中间件只在开发环境下生效
}
options
传递给中间件的配置项,格式为一个对象,中间件里获取到这个配置。
代码语言:javascript复制module.exports = [
{
options: {
key: value
}
}
]
有时候需要的配置项需要从远程获取,如:配置值保存在数据库中,这时候就要异步从数据库中获取,这时候可以将 options 定义为一个函数来完成:
代码语言:javascript复制module.exports = [
{
// 将 options 定义为一个异步函数,将获取到的配置返回
options: async () => {
const config = await getConfigFromDb();
return {
key: config.key,
value: config.value
}
}
}
]
match
匹配特定的规则后才执行该中间件,支持二种方式,一种是路径匹配,一种是自定义函数匹配。如:
代码语言:javascript复制module.exports = [
{
handle: 'xxx-middleware',
match: '/resource' //请求的 URL 是 /resource 打头时才生效这个 middleware
}
]
或者:
代码语言:javascript复制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目录下。例如:
代码语言:javascript复制module.exports = [
{
handle: 'csrf',
options: {}
}
]
引入外部的中间件非常简单,只需要 require 进来即可。这和JSX的语法是一样的。
代码语言:javascript复制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} 多模块项目下,禁止访问的模块列表,默认为 []
例如,下面是项目中的配置:
代码语言:javascript复制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的格式如下:
代码语言:javascript复制{
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),路由规则为二维数组:
代码语言:javascript复制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),格式如下:
代码语言:javascript复制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 方法来完成的,如:
代码语言:javascript复制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 类型一样的接口即可。
代码语言:javascript复制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),格式为数组:
代码语言:javascript复制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 方法来判断当前请求是不是手机访问:
代码语言:javascript复制module.exports = {
isMobile(){
const userAgent = this.userAgent.toLowerCase();
const mList = ['iphone', 'android'];
return mList.some(item => userAgent.indexOf(item) > -1);
}
}
然后使用ctx.isMobile() 来判断是否是手机访问。当然这个方法没有任何的参数,我们也可以变成一个 getter。
代码语言:javascript复制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 属性来完成。
代码语言:javascript复制module.exports = {
get isMobile(){
return this.ctx.isMobile;
}
}
不过这种方式只能在本模块使用,如果要在其他项目中使用,可以将这些扩展发布为一个 npm 模块。这和React的写法是一样的。
代码语言:javascript复制const controllerExtend = require('./controller.js');
const contextExtend = require('./context.js');
// 模块入口文件
module.exports = {
controller: controllerExtend,
context: contextExtend
}
扩展里使用 app 对象
有些 Extend 需要使用一些 app 对象上的数据,那么可以导出为一个函数,配置时把 app 对象传递进去即可。
代码语言:javascript复制const model = require('think-model');
module.exports = [
model(think.app) //将 think.app 传递给 model 扩展
];
运行流程
前面说了这么多基本的概念,现在再来看一下ThinkJS的运行流程。 Node.js 提供了 http 模块直接创建 HTTP 服务,用来响应用户的请求,比如 Node.js 官网提供的创建 HTTP 服务的例子:
代码语言:javascript复制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 Worldn');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
ThinkJS 也是调用 http.createServer 的方式来创建服务的,所以整个运行流程包含了启动服务和响应用户请求两个部分。
系统服务
- 执行 npm start 或者 node development.js;
- 实例化 ThinkJS 里的 Application 类,执行 run 方法。
- 根据不同的环境(Master 进程、Worker 进程、命令行调用)处理不同的逻辑。
- 如果是 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。
- 如果是 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 对象。