logo头像
Snippet 博客主题

前端ThinkJS框架解析

Thinkjs 是一个快速、简单的基于MVC和面向对象的轻量级Node.js开发框架,遵循MIT协议发布。秉承简洁易用的设计原则,在保持出色的性能和至简的代码同时,注重开发体验和易用性,为WEB应用开发提供强有力的支持。

Thinkjs里面很多特性来源于ThinkPHP,同时根据Node.js的特点,使用了Promise, WebSocket等特性,让代码更简洁、优雅。

Thinkjs最大的特色是对目前比较潮的语法特性支持的特别好,比如es6、es7、typescript等,有了这些,对aysnc/await等特性支持,让代码更加fashion。

安装

安装命令行工具:

1
$ npm install -g thinkjs

然后使用thinkjs new demo创建一个新项目。为了确保用户错误操作导致现有文件被覆盖,thinkjs new 命令仅适用于文件夹不存在的,或者空文件夹。否则会报如下错误:

1
path `/data/www/demo` is already a thinkjs project.

实现这一特性其实是依赖一个项目根目录下的隐藏文件 .thinkjsrc ,使用 ls -a 可以查看隐藏文件,打开这个文件可以看到如下内容:

1
2
3
4
5
{
"createAt": "2017-02-12 19:08:38",
"mode": "module",
"es": true
}

使用命令后,系统就开始构建项目了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
$ thinkjs new demo
create : demo
create : demo/package.json
create : demo/.babelrc
create : demo/.thinkjsrc
create : demo/nginx.conf
create : demo/pm2.json
create : demo/.gitignore
create : demo/README.md
create : demo/www
create : demo/www/development.js
create : demo/www/production.js
create : demo/www/testing.js
create : demo/www/README.md
create : demo/www/static
create : demo/www/static/js
create : demo/www/static/css
create : demo/www/static/img
create : demo/src
create : demo/src/common/bootstrap
create : demo/src/common/bootstrap/middleware.js
create : demo/src/common/bootstrap/global.js
create : demo/src/common/config
create : demo/src/common/config/config.js
create : demo/src/common/config/view.js
create : demo/src/common/config/db.js
create : demo/src/common/config/hook.js
create : demo/src/common/config/session.js
create : demo/src/common/config/error.js
create : demo/src/common/config/env
create : demo/src/common/config/env/development.js
create : demo/src/common/config/env/testing.js
create : demo/src/common/config/env/production.js
create : demo/src/common/config/locale
create : demo/src/common/config/locale/en.js
create : demo/src/common/controller
create : demo/src/common/controller/error.js
create : demo/view/common
create : demo/view/common/error_400.html
create : demo/view/common/error_403.html
create : demo/view/common/error_404.html
create : demo/view/common/error_500.html
create : demo/view/common/error_503.html
create : demo/src/home/config
create : demo/src/home/config/config.js
create : demo/src/home/controller
create : demo/src/home/controller/base.js
create : demo/src/home/controller/index.js
create : demo/src/home/logic
create : demo/src/home/logic/index.js
create : demo/src/home/model
create : demo/src/home/model/index.js
create : demo/view/home
create : demo/view/home/index_index.html
enter path:
$ cd demo
install dependencies:
$ npm install
run the app:
$ npm start

需要注意的是,新建项目的时候需要好多babel,所以项目的构建会比较慢,依赖的包主要有:

1
2
3
4
5
"dependencies": {
"thinkjs": "2.2.x",
"babel-runtime": "6.x.x",
"source-map-support": "0.4.0"
},

目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
.
├── README.md
├── app
│ ├── common
│ │ ├── bootstrap
│ │ ├── config
│ │ └── controller
│ └── home
│ ├── config
│ ├── controller
│ ├── logic
│ └── model
├── nginx.conf
├── package.json
├── pm2.json
├── src
│ ├── common
│ │ ├── bootstrap
│ │ ├── config
│ │ └── controller
│ └── home
│ ├── config
│ ├── controller
│ ├── logic
│ └── model
├── tree.txt
├── view
│ ├── common
│ │ ├── error_400.html
│ │ ├── error_403.html
│ │ ├── error_404.html
│ │ ├── error_500.html
│ │ └── error_503.html
│ └── home
│ └── index_index.html
└── www
├── README.md
├── development.js
├── production.js
├── static
│ ├── css
│ ├── img
│ └── js
└── testing.js
388 directories, 1381 files

启动流程分析

1)启动命令

1
npm start

那使用start后系统做了什么呢?

1
2
3
4
5
6
"scripts": {
"start": "node www/development.js",
"compile": "babel src/ --out-dir app/",
"watch-compile": "node -e \"console.log('<npm run watch-compile> no longer need, use <npm start> command direct.');console.log();\"",
"watch": "npm run watch-compile"
},

即使用Node执行www/development.js,这是env环境处理,thinkjs采用了3中env,比较常见的有:

  • development 开发模式
  • production 线上模式
  • testing 测试模式

thinkjs是把www当成node项目目录,而www下的static才是静态资源文件目录。
www/development.js目录如下:

1
2
3
4
5
6
7
var instance = new thinkjs({
APP_PATH: rootPath + path.sep + 'app',
RUNTIME_PATH: rootPath + path.sep + 'runtime',
ROOT_PATH: rootPath,
RESOURCE_PATH: __dirname,
env: 'development'
});

当然,可以使用“tree src -L 3” 命令来查看项目的目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ tree src -L 3
src
├── common
│ ├── bootstrap
│ │ ├── global.js
│ │ └── middleware.js
│ ├── config
│ │ ├── config.js
│ │ ├── db.js
│ │ ├── env
│ │ ├── error.js
│ │ ├── hook.js
│ │ ├── locale
│ │ ├── session.js
│ │ └── view.js
│ └── controller
│ └── error.js
├── home
│ ├── config
│ │ └── config.js
│ ├── controller
│ │ ├── base.js
│ │ └── index.js
│ ├── logic
│ │ └── index.js
│ └── model
│ └── index.js
16 directories, 19 files

常见模块配置(后文会具体涉及):

1
2
3
4
5
6
7
8
9
10
11
12
$ thinkjs module topic(能创建不能删除,略遗憾)
create : src/topic/config
create : src/topic/config/config.js
create : src/topic/controller
create : src/topic/controller/base.js
create : src/topic/controller/index.js
create : src/topic/logic
create : src/topic/logic/index.js
create : src/topic/model
create : src/topic/model/index.js
exist : /Users/sang/workspace/github/nodewebframework/demo/view/topic/index_index.html

此时目录结构如下:

1
2
3
4
src
├── common
├── home
└── topic

3)业务模块目录

1
2
3
4
5
6
7
8
9
10
├── home
│ ├── config
│ │ └── config.js
│ ├── controller
│ │ ├── base.js
│ │ └── index.js
│ ├── logic
│ │ └── index.js
│ └── model
│ └── index.js

4)路由和view识别
路由识别,默认根据 模块/控制器/操作/参数1/参数1值/参数2/参数2值 其实就是一个约定。

比如/解析为:

  • 默认模块是home
  • 控制是index
  • 操作是indexAction

那如果再来一个呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'use strict';
import Base from './base.js';
export default class extends Base {
/**
* index action
* @return {Promise} []
*/
indexAction(){
//auto render template file index_index.html
return this.display();
}
myAction(){
//auto render template file index_index.html
return this.display();
}
}

增加myAction,报错[Error] Error: can’t find template file /Users/sang/workspace/github/nodewebframework/demo/view/home/index_my.html

将view/home/index_index.html复制为view/home/index_my.html。原理是my要对应index_my.html模块。即index是controller,而my是action。

理解了这个,你就会觉得index_index这样的命名也不是很怪异了。剩下的就是view编写之类的,此处就不在熬述。

性能

前面提到了,开发阶段采用babel写的,所以效率不会很高。

1
2
3
4
5
6
7
8
9
10
$ autocannon -c 100 -d 5 -p 10 localhost:8360
Running 5s test @ http://localhost:8360
100 connections with 10 pipelining factor
Stat Avg Stdev Max
Latency (ms) 108.9 201.32 866
Req/Sec 891.8 148.37 1000
Bytes/Sec 417.79 kB 50.76 kB 458.75 kB
4k requests in 5s, 2.09 MB read

点惨,是吧?但是这是开发模式啊,我们肯定要拿线上的production模式来测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ npm run compile
$ node www/production.js
$ autocannon -c 100 -d 5 -p 10 localhost:8360
Running 5s test @ http://localhost:8360
100 connections with 10 pipelining factor
Stat Avg Stdev Max
Latency (ms) 61.76 124.71 763
Req/Sec 1567.2 734.94 1993
Bytes/Sec 679.12 kB 242.25 kB 884.74 kB
8k requests in 5s, 3.4 MB read
$ autocannon -c 100 -d 5 -p 10 localhost:8360
Running 5s test @ http://localhost:8360
100 connections with 10 pipelining factor
Stat Avg Stdev Max
Latency (ms) 54.65 105.47 707
Req/Sec 1813.4 368.21 1999
Bytes/Sec 807.73 kB 156.09 kB 917.5 kB
9k requests in 5s, 4.09 MB read
$ autocannon -c 100 -d 5 -p 10 localhost:8360
Running 5s test @ http://localhost:8360
100 connections with 10 pipelining factor
Stat Avg Stdev Max
Latency (ms) 54.14 89.81 465
Req/Sec 1816.4 319.14 2000
Bytes/Sec 914.23 kB 145.96 kB 1.05 MB
9k requests in 5s, 4.55 MB read

下面以同样的功能express + ejs模板的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ autocannon -c 100 -d 5 -p 10 localhost:3000
Running 5s test @ http://localhost:3000
100 connections with 10 pipelining factor
Stat Avg Stdev Max
Latency (ms) 53.85 177.72 1309
Req/Sec 1728 385.85 2075
Bytes/Sec 702.87 kB 159.56 kB 851.97 kB
9k requests in 5s, 3.53 MB read
$ autocannon -c 100 -d 5 -p 10 localhost:3000
Running 5s test @ http://localhost:3000
100 connections with 10 pipelining factor
Stat Avg Stdev Max
Latency (ms) 46.06 141.52 739
Req/Sec 2061.2 320.53 2275
Bytes/Sec 842.14 kB 134.95 kB 950.27 kB
10k requests in 5s, 4.2 MB read
$ autocannon -c 100 -d 5 -p 10 localhost:3000
Running 5s test @ http://localhost:3000
100 connections with 10 pipelining factor
Stat Avg Stdev Max
Latency (ms) 45.97 139.58 620
Req/Sec 2059.4 122.93 2167
Bytes/Sec 829.03 kB 52.43 kB 884.74 kB
10k requests in 5s, 4.2 MB read

模块分解

创建项目之后,基本的代码框架已经建立起来了,其中默认的 home 和 common 肯定是无法满足要求的。我们需要给自己的项目建立起相关的层次结构。这里给大家列举一些常见的模块分类方式。仅供参考。

简单网站

官方网站、博客、社区等,这类系统结构较为简单,通常一个前端一个后端管理即可满足要求。通常需要包含以下模块:

1
2
3
4
5
src/
src/common/ # 通用模块,放置主配置参数、boostrap adapter middleware service 等相关组件
src/home/ # 前端默认模块
src/backend/ # 后端管理模块
src/util/ # 系统工具类

电商平台

电商平台系统主要考虑到入驻的商户、注册的客户、管理人员、运营人员等使用人群,还需要考虑到较大的功能模块切分(如果足够大到类似京东、天猫那种体量的系统,则需要进行数据、功能、服务、位置等角度的分割)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
src/
src/common/
src/home/
src/sso/ # 单点登录、令牌管理等
src/rest/ # 针对Wap、App等多客户端的 rest api
src/goods/ # 商品管理及服务
src/storage/ # 库存管理及服务
src/cart/ # 购物车
src/order/ # 订单
src/delivery/ # 快递
src/pay/ # 在线支付、空中支付
src/member/ #
src/coupon/ # 电子券
src/promotion/ # 促销
src/points/ # 积分
src/merchant/ # 入驻商户
src/shop/ # 商户门店
src/finance/ # 财务核算及款项清算
src/stat/
src/log/
src/monitor/
src/util/
src/task/
src/message/ # 消息队列

即时消息平台

实时推送平台不仅仅要处理 WebSocket 连接和消息囤积发送,还要处理多用户购买相应服务套餐、统计连接数、统计下行流量、进行连接鉴权等。通常包含的模块如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
src/
src/common/
src/home/
src/rest/
src/storage/
src/websocket/ # ws 或者 wss 服务
src/webhook/ # 钩子服务
src/middleware/ # 搭载中间件运行
src/pay/
src/member/
src/stat/
src/log/
src/monitor/
src/util/
src/message/ # 消息队列

在线教育、直播平台

在线教育或直播平台通常具备实时音视频上传、转码、存储、广播等硬性要求,因此系统除了管理相关课件、学生、教师、选课等,还要负责处理相关媒体文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
src/
src/common/
src/home/
src/rest/
src/sso/ # 单点登录、令牌管理等
src/media/ # 课件、音视频等媒体文件
src/bulk/ # 流媒体
src/process/ # 编解码处理
src/storage/
src/live/ # 直播
src/pay/
src/student/
src/teacher/
src/schedule/
src/stat/
src/log/
src/monitor/
src/util/
src/task/
src/message/ # 消息队列

参数配置

官网是这么描述配置文件加载顺序的:框架默认的配置 -> 项目模式下框架配置 -> 项目公共配置 -> 项目模式下的公共配置 -> 模块下的配置。

第三个和第四个则是在不同的项目创建模式下的默认 config 配置文件夹,位置在:

1
2
3
4
# normal mode
thinkjs_normal/src/config/*
# module mode
thinkjs_module/src/common/config/*

最后一个是指的在 module mode 下的项目,每个 module 自己的 config,位置在:

1
thinkjs_module/src/home/config/*

明白了多个地方多个配置文件的玩法之后,你可以创建多个 module,并给每个 module 配置自身独特的配置参数。

需要注意的是:thinkjs 加载配置文件是有顺序的!!!多个配置文件最终会在 thinkjs 运行时被全部加载,并合并在一起。所以当存在多个配置文件时,需要注意配置参数的 key(即属性名)尽量不要重复,因为按照加载顺序,后加载的 key 的值会覆盖先加载的 key 的值,导致出现不希望的结果。

举例来说,有两个配置文件 src/common/config/assets.js 和 src/home/config/assets.js,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/common/config/assets.js
export default {
"site_title": "my site"
};
// src/home/config/assets.js
export default {
"site_title": "my test"
};
// src/home/controller/index.js
let assets = this.config('assets');
let siteTitle = assets['site_title'];
console.log('siteTitle is: ', siteTitle); // my test

Babel 编译时删除注释

开发时的工作代码都在 src 下面,运行时才会编译到 app 下面成为运行脚本(经过 Babel 编译),如果不想自己写的各种注释也出现在 app 下面的代码中,可以修改项目目录下的一个隐藏文件 .babelrc 增加相应 comments 参数。

1
2
3
4
5
6
7
8
9
{
"presets": [
["es2015", {"loose": true}],
"stage-1"
],
"plugins": ["transform-runtime"],
"sourceMaps": true,
"comments": false # <-- 就是这个参数
}

controller

目前,thinkJs支持两种控制器:普通的控制器和多级控制器。
支持before和after这样的回调钩子,对于app和controller控制来说是非常实用的。使用co来实现也是可圈可点,此处如果使用koa可以更加优雅。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class PathController extends BaseController {
constructor(app, ctx, next) {
super(app, ctx, next)
this.path = '/c'
// this.global_filter.push('custom_filter')
this.post_filter = [this.log]
}
before() {
}
log(ctx, next) {
ctx.someText = 'some'
// console.log('before')
return next().then(function(){
// console.log('after')
})
}
post(req, res) {
console.log(this.ctx.someText)
var a = this.reqbody.a
return res.body = this.ctx.someText
}
after() {
}
}

修改 pm2 日志位置

pm2 (官网 http://pm2.keymetrics.io)是一个优秀的 Node.js 进程管理器。thinkjs 推荐使用 pm2 来管理项目运行,并自动生成了 pm2 的配置文件 pm2.json 。

它的强大之处在于不仅可以作为 Node.js 项目的守护进程,还具备可配置化启动、分布式支持、内存监控、热重载(优雅重载)、支持数据统计、运行日志记录、实时运行监控、API 和脚本支持等强大的特性。

默认生成的 pm2 配置文件不含日志记录部分,如果不单独配置,pm2 的日志将会保存在安装目录中,查找起来很不方便。普遍的做法是:在项目目录下建立 logs 文件夹,用来放置 pm2 以及其他(诸如 log4js 等等)日志,打开 pm2.json ,给 apps[0] 增加如下几行配置参数:

1
2
3
4
5
6
7
8
{
"apps": [{
"error_file" : "/data/www/thinkjs_module/logs/pm2-err.log",
"out_file" : "/data/www/thinkjs_module/logs/pm2-out.log",
"log_date_format" : "YYYY-MM-DD HH:mm:ss Z",
"merge_logs" : false
}]
}
  • error_file pm2 捕捉到的致命错误记录在这里
  • out_file pm2 接收到的 console 输出记录在这里
  • log_date_format 日期和时间格式
  • merge_logs 是否给日志文件增加进程id的后缀

总结

主要优势:

  • 完全自己实现,对已有框架很少借鉴
  • 内置各种adapter,db,中间件,hook,插件,非常丰富,all in one 比组装更适合新手
  • 遵循mvc和coc
  • 使用最潮的es6/es7/ts特性,对aysnc函数,exports等都非常好的支持
  • 支持i18n等实用功能
  • 内置pm2和nginx集成,部署方便
  • 有自己的脚手架,稍弱
  • 性能不错,虽然比express稍弱,但功能强大许多
  • 测试丰富,代码质量有保障
  • 文档健全,是经过设计的,支持多语言
  • 背后有75团和李成银支持,最近一周内有更新,代码提交2600+,35人贡献,整体来说算健康

附:ThinkJS官网文档

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者

上一篇