@x-9lab/xlab 1.6.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/@types/cluster.d.ts +15 -3
  2. package/@types/components/common/index.d.ts +1 -2
  3. package/@types/components/cron/index.d.ts +4 -4
  4. package/@types/components/log/index.d.ts +37 -2
  5. package/@types/config/index.d.ts +9 -5
  6. package/@types/custom.d.ts +25 -3
  7. package/@types/dot-file.d.ts +6 -0
  8. package/@types/global.d.ts +44 -56
  9. package/@types/index.d.ts +18 -0
  10. package/@types/init-env.d.ts +3 -1
  11. package/@types/middleware/handle-pre-dir.d.ts +1 -5
  12. package/@types/middleware/request-filter.d.ts +0 -1
  13. package/@types/middleware/service-mark.d.ts +1 -6
  14. package/@types/router.d.ts +25 -0
  15. package/@types/server.d.ts +28 -6
  16. package/MIGRATION.md +182 -0
  17. package/README.md +105 -86
  18. package/dist/@config/config.dev.js +7 -3
  19. package/dist/@config/config.js +7 -4
  20. package/dist/@config/config.sand.js +7 -3
  21. package/dist/bin/watch.js +36 -16
  22. package/dist/cluster.js +121 -44
  23. package/dist/components/assets/index.js +9 -5
  24. package/dist/components/cluster/index.js +17 -4
  25. package/dist/components/common/index.js +65 -45
  26. package/dist/components/cron/index.js +50 -27
  27. package/dist/components/header/index.js +32 -17
  28. package/dist/components/header/time.js +6 -2
  29. package/dist/components/index.js +1 -1
  30. package/dist/components/log/index.js +91 -87
  31. package/dist/components/mime/index.js +6 -2
  32. package/dist/components/return-code/index.js +33 -18
  33. package/dist/components/uuid/index.js +22 -13
  34. package/dist/config/getEnv.js +9 -5
  35. package/dist/config/index.js +98 -66
  36. package/dist/config/process-custom-config-files.js +26 -22
  37. package/dist/config/process-def-config-file.js +15 -11
  38. package/dist/custom.js +77 -28
  39. package/dist/default-x-config.js +9 -5
  40. package/dist/dot-file.js +42 -17
  41. package/dist/global.js +37 -31
  42. package/dist/index.js +100 -0
  43. package/dist/init-env.js +17 -9
  44. package/dist/middleware/@bin/html-filter.js +19 -7
  45. package/dist/middleware/bad-request.js +12 -8
  46. package/dist/middleware/compress.js +11 -7
  47. package/dist/middleware/cors.js +20 -7
  48. package/dist/middleware/fresh-filter.js +13 -7
  49. package/dist/middleware/handle-pre-dir.js +22 -17
  50. package/dist/middleware/request-filter.js +39 -25
  51. package/dist/middleware/service-mark.js +22 -25
  52. package/dist/middlewares.js +22 -20
  53. package/dist/router.js +174 -141
  54. package/dist/server.js +202 -93
  55. package/package.json +25 -29
  56. package/@types/business/utils/cookie/1.1.0/clean/index.d.ts +0 -3
  57. package/@types/business/utils/cookie/1.1.0/clean/js/index.d.ts +0 -2
  58. package/@types/components/cache/index.d.ts +0 -2
  59. package/@types/components/cache/lru.d.ts +0 -21
  60. package/@types/components/html-processor/index.d.ts +0 -9
  61. package/@types/components/injection/index.d.ts +0 -25
  62. package/@types/components/js-processor/index.d.ts +0 -5
  63. package/@types/components/md5/index.d.ts +0 -2
  64. package/@types/components/platform/index.d.ts +0 -21
  65. package/@types/components/proxy/index.d.ts +0 -26
  66. package/@types/components/querystring/index.d.ts +0 -25
  67. package/@types/components/redirect/index.d.ts +0 -11
  68. package/@types/components/request/helper.d.ts +0 -3
  69. package/@types/components/request/index.d.ts +0 -51
  70. package/@types/components/request/resolve-uri.d.ts +0 -6
  71. package/@types/components/version/index.d.ts +0 -26
  72. package/@types/env.d.ts +0 -1
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  X-9lab 通用服务端
2
2
  =======
3
3
 
4
+ > 当前为 v2 版本(breaking change),从 v1 升级请先阅读 [MIGRATION.md](./MIGRATION.md)。
5
+
4
6
  ## 开发环境配置
5
- 1. 安装 [node](https://nodejs.org/) ,需要 10.0.0 以上版本
7
+ 1. 安装 [node](https://nodejs.org/) ,需要 20.0.0 以上版本
6
8
 
7
9
  ## 使用
8
10
  - 将 `@x-9lab/xlab` 加入到依赖中
@@ -18,14 +20,21 @@ X-9lab 通用服务端
18
20
  ```bash
19
21
  xlab --APP_NAME=nice
20
22
  ```
23
+ - 其他命令行参数
24
+ - `-w` / `--watch` 开启 watch 模式
25
+ - `-r` / `--root` 指定执行根目录
26
+ - `-f` / `--file` 指定启动配置文件地址
27
+ - `PORT=3001` 覆盖监听端口,`IP=127.0.0.1` 指定绑定地址,`ENV=<环境>` 指定运行环境
28
+ - 运行环境
29
+ 由 `ENV=` 参数或 `NODE_ENV` 决定,支持 `development`(开发) / `sandbox`(沙箱) / `production`(生产),也可使用数字别名 `0` / `2` / `1`;未指定时默认 development。环境决定加载哪份 `@config` 环境配置与 `.xlab` 环境文件
21
30
  - 配置文件 `xlab.config.js`
22
31
  模块会自动检测执行根目录中是否存在 `xlab.config.js` 文件。如存在该文件则会使用该文件为模块的启动配置文件。支持的配置项:
23
32
  1. `watch` 是否开启 watch 模式
24
- 1. `business` 业务目录名称。注意当前只支持根目录下的文件夹
33
+ 1. `businessDir` 服务端业务目录名称,默认 `@server`。注意当前只支持根目录下的文件夹
25
34
  - 配置文件 `.xlab`
26
- 该文件用于定义系统中的全局变量,模块会自动检测执行根目录中是否存在 `.xlab` 文件。该类型文件一般用于定义一些不方便在代码中声明的敏感数据,因此 `.xlab` 及附属的环境文件 **不应该** 被提交到仓库中。
27
- - 使用 `<key>=<value>` 的方式定义数据
28
- - 文件中的数据会被加载到 `global.XLAB` 对象中
35
+ 该文件用于定义系统级的环境数据,模块会自动检测执行根目录中是否存在 `.xlab` 文件。该类型文件一般用于定义一些不方便在代码中声明的敏感数据,因此 `.xlab` 及附属的环境文件 **不应该** 被提交到仓库中。
36
+ - 使用 `<key>=<value>` 的方式定义数据,value 按 JSON 解析
37
+ - 文件中的数据通过 `getXlabEnv()` 获取
29
38
  - `.xlab` 为生产环境定义,`.xlab.development` 为开发环境定义,`.xlab.sandbox` 为沙箱环境定义
30
39
  - `watch` 模式
31
40
  `xlab` 支持自动重启业务,开发中使用该功能可以减少大量手动重启业务的操作。
@@ -42,6 +51,7 @@ X-9lab 通用服务端
42
51
  ```
43
52
  - 除了静态资源,服务端业务需要放在项目根目录下的 `@server` 目录中
44
53
  - 支持业务自定义以下内容 (无特殊说明的都是存放在 @server 目录中)
54
+ - 环境预处理,存放于 `@env` 目录(可选),`@env/index.js` 会在启动最早阶段(配置装载前)被执行,可用于设置 `process.env` 等
45
55
  - 配置项,存放于 `@config` 目录
46
56
  - 支持不同环境配置文件
47
57
  - 无中间词缀的 config 文件为生产环境配置文件
@@ -56,30 +66,56 @@ X-9lab 通用服务端
56
66
  - 按照目录结构生成 api
57
67
  - 文件名是 `index` 的会被从 api 上先强行去掉
58
68
  - 默认都是 `get` 请求
69
+ - 文件名即方法:文件名是 `get/post/put/delete/patch/head/options/all` 之一时,从 api 路径上去掉并作为该接口的请求方法
59
70
  - 支持以下形式定义接口
60
- - 模块返回的是数组,则使用文件名做为方法名,并在 api 上去掉文件名
61
- - 非数组则文件当作正常的 api 地址
62
71
  - 模块返回为函数的则直接绑定为接口处理函数
63
- - 返回是对象则取对应的字段
64
- - `method` 接口类型,如 `get` 或 `post`
72
+ - 返回是对象则取对应的字段(推荐配合 `defineApi` 获得类型推导)
73
+ - `method` 接口类型,如 `get` 或 `post`,不指定时由文件名约定决定
74
+ - `api` 自定义接口地址
65
75
  - `middleware` 接口中间件
66
76
  - `handler` 接口处理函数
77
+ - `ignoreApiNameCheck` 是否忽略 `/api` 前缀检测
67
78
  - 特殊文件夹
68
79
  - 以 `@` 开头的文件夹, 该文件夹不会被当成接口文件夹,但可在正常的接口中作为普通模块使用
69
80
  - 以 `$` 开头的文件夹, 该文件夹中的文件会成为后续接口的通用中间件,可以用于某类型业务的统一处理,如后台某些接口的额外身份处理。为了防止滥用,该文件夹不会递归查找,也就是说不支持子文件夹的组织形式。
70
81
  - 整个 api 地址的每个父层都可以有独立的中间件文件,执行顺序会按照目录层级执行
71
82
  - 中间件文件夹内的文件只会按照文件名做简单排序,因此请特别注意执行顺序是否符合自己的预期,或采用带序号的文件名
72
- - 自定义逻辑,存放于 `custom` 目录,模块需要导出一个函数作为执行入口
73
- - 中间件,存放于 `middleware` 目录
74
- - 定时任务,存放于 `cron` 目录
75
- - 静态资源,默认存放在项目根目录下的 `public` 目录中
76
- ### 全局对象与函数
83
+ - 自定义逻辑,存放于 `custom` 目录,需在配置项 `custom` 数组中声明才会执行(`"模块名"` 或 `["模块名", 参数]`)。模块导出一个函数作为执行入口(视为 `setup`),或导出 `{ setup, ready, shutdown }` 生命周期钩子对象
84
+ - `setup` 服务装配前执行,接收 `custom` 配置中声明的参数
85
+ - `ready` 服务启动完成(listen 成功)后执行
86
+ - `shutdown` 服务退出前执行,可用于释放连接等资源
87
+ - 中间件,存放于 `middleware` 目录,通过配置项 `middlewares` 启用与排序
88
+ - 名称命中内置中间件时加载内置实现,否则加载业务 `middleware` 目录下的同名模块;模块需导出工厂函数 `(config) => koaMiddleware`
89
+ - 内置中间件:`bad-request`(异常返回兜底,总是启用)、`fresh-filter`(304 协商缓存)、`handle-pre-dir`(代理路径层级处理)、`request-filter`(请求过滤)、`service-mark`(X-Mark 服务标识)、`compress`(压缩)、`cors`(跨域)
90
+ ```js
91
+ "middlewares": {
92
+ "service-mark": true
93
+ , "cors": { "config": { "type": "sameroot" } }
94
+ }
95
+ ```
96
+ - 定时任务,存放于 `cron` 目录,由配置项 `enableCron` 开启,默认只在 master 进程执行(`enableWorkerCron` 可放开到 worker)
97
+ - 任务模块导出一个函数作为任务体,可另外导出 `getDelay()`(间隔,毫秒) 与 `enable()`(开关)
98
+ - 配置项 `crons` 可按文件名覆盖任务的间隔与开关,优先级高于模块自身导出
99
+ - 静态资源,默认存放在项目根目录下的 `public` 目录中(配置项 `root` 可改)
100
+ ### 模块 API
77
101
 
78
- #### 全局对象
79
- - `log` 全局日志对象
80
- - `masterLog` 只在 master 上输出的日志对象
102
+ v2 起框架 API 不再挂载到 global,统一从包导入(v1 升级请看 [MIGRATION.md](./MIGRATION.md)):
81
103
 
82
- #### 全局函数
104
+ ```js
105
+ const {
106
+ boot, shutdown // 生命周期
107
+ , app, getApp // Koa 应用实例
108
+ , getSysConfig, setSysConfig
109
+ , requireMod, requireModel, requireService
110
+ , getLogger, masterLog
111
+ , getXlabEnv // .xlab 环境数据
112
+ , defineApi // 路由定义帮助函数
113
+ } = require("@x-9lab/xlab");
114
+ ```
115
+
116
+ - `getLogger(cat)` 获取分类日志实例
117
+ - `masterLog(cat, type?, ...msg)` 只在 master 进程上输出的日志方法
118
+ - `requireMod` / `getLogger` 等不依赖服务启动状态,共享库可独立引用;`getSysConfig` / `getXlabEnv` 的数据在启动后才填充,应在调用时读取
83
119
  - `getApp` 获取应用实例对象
84
120
  ```ts
85
121
  /**
@@ -113,7 +149,10 @@ X-9lab 通用服务端
113
149
  */
114
150
  function setSysConfig(conf: XLab.IConfig): void;
115
151
  ```
116
- - `requireModel` 全局获取 model 的方法
152
+ - `getXlabEnv` 获取 `.xlab` 系列文件定义的环境数据
153
+ - `boot` 启动服务,按固定阶段装配并监听;正常业务由 `xlab` 命令调用,无需手动执行
154
+ - `shutdown` 优雅退出:停止定时任务 → 等待在途请求 → 执行 `shutdown` 钩子 → 退出进程;收到 `SIGTERM` / `SIGINT` 时自动触发
155
+ - `requireModel` 获取 model 的方法
117
156
  该方法只是为获取 model 提供一个快捷方式,也可以通过正常的方式去 require
118
157
  - 存放路径 `business/@models`
119
158
  - 使用方式
@@ -121,7 +160,7 @@ X-9lab 通用服务端
121
160
  const { testModel } = requireModel("test");
122
161
  ```
123
162
  - 类型支持
124
- 由于 `requireModel` 是一个 `xlab` 的内置方法没有业务本身的 model 定义,因此需要业务方自行追加。出于管理方面的考虑,建议将所有的 model 定义放在一个文件中
163
+ 由于 `requireModel` 是一个 `xlab` 的内置方法没有业务本身的 model 定义,因此需要业务方自行追加(`XLab` 类型命名空间仍为全局,declaration merging 方式与 v1 一致)。出于管理方面的考虑,建议将所有的 model 定义放在一个文件中
125
164
  ```ts
126
165
  declare global {
127
166
  namespace XLab {
@@ -133,7 +172,7 @@ X-9lab 通用服务端
133
172
  }
134
173
  export { }
135
174
  ```
136
- - `requireService` 全局获取 service 的方法
175
+ - `requireService` 获取 service 的方法
137
176
  该方法只是为获取 service 提供一个快捷方式,也可以通过正常的方式去 require
138
177
  - 存放路径 `business/@services`
139
178
  - 使用方式
@@ -169,84 +208,64 @@ X-9lab 通用服务端
169
208
  |-|-|-|-|
170
209
  |name|`string`| package.json 中的 name 字段 |服务(应用)名称|
171
210
  |version|`string`|package.json 中的 version 字段|版本|
172
- |env|`string`|development|环境标识|
173
- |host|`string`| |业务绑定的域名|
211
+ |env|`string`|DEVELOPMENT|环境标识(大写),由启动环境决定,业务不应配置|
212
+ |host|`string`| |业务绑定的域名,cors 中间件 `sameroot` 模式使用|
174
213
  |304|`boolean`| true |是否开启 304 协商缓存|
175
- |workers|`number`|0|Worker 数量|
176
- |biServer|`string`||业务服务器地址|
177
- |protocol|`string`|http|业务服务器协议|
178
- |debug|`boolean`|false|是否开启 debug 模式|
179
- |staticMaxage|`number`|1800000|静态文件缓存时间|
214
+ |workers|`number`|0|Worker 数量,大于 0 时以 cluster 模式启动|
215
+ |ip|`string`||服务绑定地址,也可通过 `IP=` 命令行参数指定|
216
+ |timezone|`string`|Asia/Shanghai|时区|
217
+ |debug|`boolean`|非生产环境为 true|是否开启 debug 模式|
218
+ |staticMaxage|`number`|1800000|静态文件缓存时间(仅生产环境启用)|
180
219
  |staticHtmlFileMaxage|`number`|0|静态 html 文件缓存时间|
181
220
  |staticCros|`boolean`|false|是否允许静态资源跨域访问|
182
- |enableComboCache|`boolean`|true|是否开启 Combo 缓存|
183
221
  |enableCron|`boolean`|false|是否开启定时任务|
184
- |middleware|`Array<string | (string | Record<string, any>)[]>`|[]|开启的中间件列表。因不方便合并替换,v1.1.0 开始建议使用 `middlewares` 来配置 |
222
+ |enableWorkerCron|`boolean`|false|是否允许 worker 上也执行定时任务|
185
223
  |middlewares|`Record<string, MiddlewareConfig>`|{}|开启的中间件列表 |
186
224
  ||MiddlewareConfig||`index` 用于定义中间件位置,不提供将使用配置对象中的默认顺序<br/> `name` 中间件名称,不提供将使用配置对象的键名<br/>`config` 中间件配置|
187
- |custom|`string[]`||自定模块配置|
188
- |strictSSL|`boolean`||是否启用严格 ssl|
189
- |apis|`Record<string, string>`|{}|页端注入的 api 设置|
190
- |hasLo|`boolean`||本地是否存在本地开发配置文件|
191
- |passExtApis|`boolean`||不处理业务 api|
192
- |allowCache|`boolean`||是否允许页端缓存|
225
+ |custom|`(string | [string, any])[]`||自定模块配置,元组形式可传入参数|
226
+ |mark|`string`|name 配置|服务标识,用于 service-mark 中间件|
227
+ |apis|`Record<string, string>`|{}|页端注入的 api 设置,自动合并 `@config/@apis` 目录内容|
228
+ |hasLo|`boolean`||本地是否存在本地开发配置文件,由框架自动设置|
229
+ |passExtApis|`boolean`||不合并 `@config/@apis` 目录的 api 配置|
230
+ |allowCache|`boolean`|true|是否允许页端缓存|
193
231
  |root|`string`|public|静态文件根目录|
194
- |port|`number`|5000|监听端口|
195
- |isMaster|`boolean`||是否是主进程|
196
- |clearLocalStorage|`boolean`||是否每次都强制清除 LocalStorage|
197
- |pathReplaceRegExp|`string`||处理代理过来多余的地址层级路径替换判断正则|
232
+ |port|`number`|5000|监听端口,也可通过 `PORT=` 命令行参数指定|
233
+ |isMaster|`boolean`||是否是主进程,由框架自动设置|
234
+ |pathReplaceRegExp|`string`||handle-pre-dir 中间件处理代理多余地址层级的判断正则|
198
235
  |routeMobile|`string`||移动端入口文件地址(旧版逻辑)|
199
- |launchRouter|`Record<string, string>`||不同端入口地址设置, 由 `@x-drive/launch-detect` 提供支持|
200
- |cron|`{def?: number;}`|{"def": 60}|定时任务设置|
201
- |injection|`string[]`|[]|注入参数列表|
202
- |indexPageCacheTime|`number`||首页缓存时间|
203
- |staticResourceCacheTime|`number`||静态资源缓存时间|
236
+ |cron|`{def?: number;}`|{"def": 60}|定时任务设置,`def` 为默认间隔(秒)|
237
+ |crons|`Record<string, {delay?: number; enable?: boolean;}>`||按任务(文件名)的定时任务配置,`delay` 单位秒,优先级高于任务模块自身的 `getDelay()` / `enable()`|
238
+ |shutdownTimeout|`number`|10000|优雅退出时等待在途请求/worker 结束的超时时间(毫秒)|
239
+ |indexPageCacheTime|`number`||fresh-filter 中间件的首页缓存时间(毫秒)|
240
+ |staticResourceCacheTime|`number`||fresh-filter 中间件的静态资源缓存时间(毫秒)|
241
+ |biServer / protocol / strictSSL / internalServers / internalApis|||框架不读取的业务约定字段,供业务通过 `getSysConfig` 自取|
242
+
243
+ > v2 起移除:`middleware`(数组形式,v1.1.0 已弃用)、`enableComboCache` 与 `launchRouter`(对应实现已不存在)。
204
244
 
205
- ## 文件结构
245
+ ## 业务项目文件结构
206
246
 
207
247
  ```
208
- ├── README.md
209
- ├── business
210
- │ └── @services
211
- │ └── @models
212
- │ │ └── forumUser.js
213
- └── ...
214
- ├── components
215
- ├── cache
216
- ├── common.js
217
- │ └── ...
218
- ├── @config
219
- │ ├── config.dev.ts
220
- └── config.ts
221
- ├── middleware
222
- ├── cron
223
- ├── public
224
- ├── test
225
- ├── route
226
- ├── private
227
- │ └── log
228
- ├── package.json
229
- ├── config.js
230
- ├── router.js
231
- └── server.js
248
+ 业务项目根目录
249
+ ├── package.json # scripts 中调用 xlab 启动
250
+ ├── xlab.config.js # 启动配置(watch / businessDir),可选
251
+ ├── .xlab # 环境数据文件(.xlab.development / .xlab.sandbox),不入库
252
+ ├── public # 静态资源(root 配置可改)
253
+ └── @server # 服务端业务目录(businessDir 配置可改)
254
+ ├── @env # 环境预处理,启动最早执行,可选
255
+ └── index.js
256
+ ├── @config # 配置: config.js / config.dev.js / config.sand.js / config.lo.js
257
+ │ └── @apis # 页端注入的 api 配置,可选
258
+ ├── business # 约定路由目录,按目录结构生成 /api/...
259
+ │ ├── @models # model,requireModel 读取
260
+ ├── @services # service,requireService 读取
261
+ ├── $mws # $ 前缀目录级中间件
262
+ │ └── ...
263
+ ├── middleware # 业务中间件,middlewares 配置启用
264
+ ├── custom # 自定义逻辑与生命周期钩子,custom 配置声明
265
+ └── cron # 定时任务,enableCron 开启
232
266
  ```
233
267
 
234
- ### 说明
235
- - ``business目录``:前端业务
236
- - ``@services目录``:业务services
237
- - ``@models目录``:存放model
238
- - ``components目录``: 存放当前项目组件
239
- - ``@config``:存放环境配置,不同环境在名称与文件后缀中间使用不同代号区分
240
- - ``middleware目录``:存放请求特殊处理模块,一般作为中间件
241
- - ``public目录``:存放前端资源文件
242
- - ``cron目录``:存放定时任务文件
243
- - ``test目录``:存放单元测试
244
- - ``route目录``:存放不同平台的路由配置
245
- - ``private目录``:存放业务 log ,或其他服务端相关的内容
246
- - ``package.json``:nodejs后端所需要的依赖描述文件,即npm的 [package.json](https://www.npmjs.org/doc/files/package.json.html) 文件
247
- - ``router.js``:路由模块
248
- - ``server.js``:服务器主模块
249
- - ``config.js``:配置处理模块
268
+ 完整可运行的示例见仓库中的 [example](./example) 目录。
250
269
 
251
270
  ## TODO
252
271
 
@@ -2,10 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- exports.default = void 0;
5
+ Object.defineProperty(exports, "default", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return _default;
9
+ }
10
+ });
6
11
  const CONFIG = {
7
12
  "304": false,
8
13
  "workers": 0
9
14
  };
10
- var _default = CONFIG;
11
- exports.default = _default;
15
+ const _default = CONFIG;
@@ -2,7 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- exports.default = void 0;
5
+ Object.defineProperty(exports, "default", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return _default;
9
+ }
10
+ });
6
11
  const CONFIG = {
7
12
  "host": "",
8
13
  "304": true,
@@ -12,12 +17,10 @@ const CONFIG = {
12
17
  "debug": false,
13
18
  "staticMaxage": 1800000,
14
19
  "staticHtmlFileMaxage": 0,
15
- "enableComboCache": true,
16
20
  "enableCron": false,
17
21
  "enableWorkerCron": false,
18
22
  "strictSSL": false,
19
23
  "root": "public",
20
24
  "timezone": "Asia/Shanghai"
21
25
  };
22
- var _default = CONFIG;
23
- exports.default = _default;
26
+ const _default = CONFIG;
@@ -2,7 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- exports.default = void 0;
5
+ Object.defineProperty(exports, "default", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return _default;
9
+ }
10
+ });
6
11
  const CONFIG = {};
7
- var _default = CONFIG;
8
- exports.default = _default;
12
+ const _default = CONFIG;
package/dist/bin/watch.js CHANGED
@@ -1,11 +1,14 @@
1
1
  "use strict";
2
2
  const XConfig = require("../default-x-config").default;
3
- const { date } = require("@x-drive/utils");
4
- const crossSpawn = require("cross-spawn");
5
- const colors = require("colors/safe");
3
+ const { spawn } = require("child_process");
4
+ const { date } = require("@x-drive/utils");
6
5
  const path = require("path");
7
6
  const fs = require("fs");
8
- /**输出内容名称 */ const X_LAB_STR = colors.bold(colors.cyan("\uD83D\uDEF8 XLab"));
7
+ /**终端着色,替代对 colors 包的依赖 */ const colors = {
8
+ "cyanBold": (str)=>`\x1b[1m\x1b[36m${str}\x1b[0m`,
9
+ "green": (str)=>`\x1b[32m${str}\x1b[0m`
10
+ };
11
+ /**输出内容名称 */ const X_LAB_STR = colors.cyanBold("🛸 XLab");
9
12
  /**
10
13
  * 子进程对象
11
14
  * @type child_process.ChildProcess
@@ -33,7 +36,7 @@ const fs = require("fs");
33
36
  }
34
37
  }
35
38
  /**启动 */ function start() {
36
- childProcess = crossSpawn("node", buildSpawnOptions(), {
39
+ childProcess = spawn(process.execPath, buildSpawnOptions(), {
37
40
  "stdio": "inherit"
38
41
  }).on("error", (err)=>{
39
42
  console.error(err);
@@ -41,8 +44,8 @@ const fs = require("fs");
41
44
  if (Number(code) !== 0) {
42
45
  const err = new Error(`子进程退出, Code: ${code}`);
43
46
  err.code = code;
44
- // TODO: 自动拉起来?
45
47
  console.error(err);
48
+ log("业务进程异常退出, watch 保持运行, 修复代码保存后将自动重启");
46
49
  }
47
50
  }).once("exit", ()=>{
48
51
  childProcess.removeAllListeners();
@@ -90,7 +93,7 @@ const fs = require("fs");
90
93
  } else {
91
94
  if (initing) {
92
95
  initing = false;
93
- log("Watch \u6A21\u5F0F\u542F\u52A8\n");
96
+ log("Watch 模式启动\n");
94
97
  setTimeout(()=>{
95
98
  inited = true;
96
99
  }, wait);
@@ -118,29 +121,46 @@ const fs = require("fs");
118
121
  for(let i = 0; i < config.paths.length; i++){
119
122
  fs.statSync(config.paths[i]);
120
123
  stats += 1;
124
+ ;
121
125
  }
122
126
  } catch (e) {
123
127
  log(e);
124
128
  }
125
129
  if (stats > 0) {
126
- // 传递主进程 SIGTERM
127
- process.on("SIGTERM", ()=>{
128
- if (childProcess && childProcess.connected) {
129
- childProcess.kill("SIGTERM");
130
- }
131
- process.exit(0);
130
+ // 传递退出信号: 通知业务进程优雅退出并等待其结束后自身再退出
131
+ // 注意不能用 childProcess.connected 判断( IPC 通道时恒为 false)
132
+ [
133
+ "SIGINT",
134
+ "SIGTERM"
135
+ ].forEach((signal)=>{
136
+ process.on(signal, ()=>{
137
+ if (!childProcess) {
138
+ process.exit(0);
139
+ }
140
+ const child = childProcess;
141
+ // 兜底强杀, 时长需覆盖业务的优雅退出(shutdownTimeout 默认 10s)
142
+ const timer = setTimeout(()=>{
143
+ child.kill("SIGKILL");
144
+ process.exit(0);
145
+ }, 30000);
146
+ child.once("exit", ()=>{
147
+ clearTimeout(timer);
148
+ process.exit(0);
149
+ });
150
+ child.kill("SIGTERM");
151
+ });
132
152
  });
133
153
  watch(chokidar, config, wait);
134
154
  start();
135
155
  } else {
136
- log("\u4E1A\u52A1\u81EA\u5B9A\u4E49\u76EE\u5F55\u4E0D\u5B58\u5728, watch \u6A21\u5F0F\u542F\u52A8\u5931\u8D25");
156
+ log("业务自定义目录不存在, watch 模式启动失败");
137
157
  }
138
158
  stats = null;
139
159
  } else {
140
- log("\u8BF7\u5C06 chokidar \u5B89\u88C5\u5230\u5F00\u53D1\u4F9D\u8D56\u4E2D\u4EE5\u542F\u7528 watch \u6A21\u5F0F");
160
+ log("请将 chokidar 安装到开发依赖中以启用 watch 模式");
141
161
  }
142
162
  } else {
143
- log("\u914D\u7F6E\u4E0D\u5B58\u5728\u6216 watch.enable \u4E3A false, watch \u6A21\u5F0F\u542F\u52A8\u5931\u8D25\n", config);
163
+ log("配置不存在或 watch.enable false, watch 模式启动失败\n", config);
144
164
  }
145
165
  }
146
166
  module.exports = setup;
package/dist/cluster.js CHANGED
@@ -2,53 +2,79 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- exports.default = void 0;
6
- var clusterCom = _interopRequireWildcard(require("./components/cluster"));
7
- var _config = require("./config");
8
- var _cluster = _interopRequireDefault(require("cluster"));
9
- var _server = _interopRequireDefault(require("./server"));
10
- var _os = _interopRequireDefault(require("os"));
11
- function _interopRequireDefault(obj) {
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get default () {
13
+ return _default;
14
+ },
15
+ get shutdownWorkers () {
16
+ return shutdownWorkers;
17
+ }
18
+ });
19
+ const _cluster = /*#__PURE__*/ _interop_require_wildcard(require("./components/cluster"));
20
+ const _log = require("./components/log");
21
+ const _config = require("./config");
22
+ const _cluster1 = /*#__PURE__*/ _interop_require_default(require("cluster"));
23
+ const _os = /*#__PURE__*/ _interop_require_default(require("os"));
24
+ function _interop_require_default(obj) {
12
25
  return obj && obj.__esModule ? obj : {
13
26
  default: obj
14
27
  };
15
28
  }
16
- function _interopRequireWildcard(obj) {
17
- if (obj && obj.__esModule) {
18
- return obj;
19
- } else {
20
- var newObj = {};
21
- if (obj != null) {
22
- for(var key in obj){
23
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
24
- var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {};
25
- if (desc.get || desc.set) {
26
- Object.defineProperty(newObj, key, desc);
27
- } else {
28
- newObj[key] = obj[key];
29
- }
30
- }
31
- }
29
+ function _getRequireWildcardCache(nodeInterop) {
30
+ if (typeof WeakMap !== "function") return null;
31
+ var cacheBabelInterop = new WeakMap();
32
+ var cacheNodeInterop = new WeakMap();
33
+ return (_getRequireWildcardCache = function(nodeInterop) {
34
+ return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
35
+ })(nodeInterop);
36
+ }
37
+ function _interop_require_wildcard(obj, nodeInterop) {
38
+ if (!nodeInterop && obj && obj.__esModule) return obj;
39
+ if (obj === null || typeof obj !== "object" && typeof obj !== "function") return {
40
+ default: obj
41
+ };
42
+ var cache = _getRequireWildcardCache(nodeInterop);
43
+ if (cache && cache.has(obj)) return cache.get(obj);
44
+ var newObj = {
45
+ __proto__: null
46
+ };
47
+ var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
48
+ for(var key in obj){
49
+ if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
50
+ var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
51
+ if (desc && (desc.get || desc.set)) Object.defineProperty(newObj, key, desc);
52
+ else newObj[key] = obj[key];
32
53
  }
33
- newObj.default = obj;
34
- return newObj;
35
54
  }
55
+ newObj.default = obj;
56
+ if (cache) cache.set(obj, newObj);
57
+ return newObj;
36
58
  }
37
- const logger = log.getLogger("cluster");
59
+ const logger = _log.globalLog.getLogger("cluster");
38
60
  const CPUnum = _os.default.cpus().length;
39
- const config = (0, _config).get();
61
+ /**判定为快速失败的存活时间窗口(毫秒) */ const QUICK_DEATH_MS = 10 * 1000;
62
+ /**连续快速失败达到该次数后停止自动拉起 */ const MAX_QUICK_DEATHS = 5;
63
+ /**worker 启动时间记录 */ const FORK_AT = new Map();
64
+ /**连续快速失败计数 */ var quickDeaths = 0;
65
+ /**是否正在退出流程中 */ var stopping = false;
40
66
  /**
41
67
  * 消息处理函数
42
68
  * @param msg 消息配置对象
43
69
  */ function masterMessageHandler(msg) {
44
70
  if (msg && msg.cmd) {
45
- let handler = clusterCom.get(msg.cmd);
71
+ let handler = _cluster.get(msg.cmd);
46
72
  if (handler) {
47
73
  let args = msg.args || [];
48
74
  args.push(msg.returnData);
49
75
  let re = handler.apply(handler, args);
50
76
  if (msg.wid) {
51
- let worker = _cluster.default.workers[msg.wid];
77
+ let worker = _cluster1.default.workers[msg.wid];
52
78
  if (worker) {
53
79
  worker.send({
54
80
  "cmd": msg.cmd,
@@ -62,7 +88,18 @@ const config = (0, _config).get();
62
88
  }
63
89
  }
64
90
  }
65
- /**尝试以 cluster 模式启动服务 */ function start() {
91
+ /**fork 一个 worker 并记录启动时间 */ function forkWorker() {
92
+ const worker = _cluster1.default.fork();
93
+ FORK_AT.set(worker.id, Date.now());
94
+ }
95
+ /**
96
+ * 启动服务监听
97
+
98
+ * master 且配置了 workers 时 fork 集群, 否则当前进程直接 listen
99
+ * @param app Koa 实例
100
+ * @return 直接监听时返回 http.Server, master fork 模式下无返回
101
+ */ function start(app) {
102
+ const config = (0, _config.get)();
66
103
  var server;
67
104
  var serverListenConf = {
68
105
  "port": config.port
@@ -75,26 +112,66 @@ const config = (0, _config).get();
75
112
  // 小于使用数则取一半
76
113
  let num = CPUnum < config.workers ? Math.ceil(CPUnum / 2) : config.workers;
77
114
  for(var i = 0; i < num; i++){
78
- _cluster.default.fork();
115
+ forkWorker();
79
116
  }
80
- // if(!config.isProd){
81
- _cluster.default.on("listening", function(worker, address) {
82
- masterLog("cluster", "info", " Worker ", worker.process.pid, " online, Address: ", address.address, ":", address.port);
117
+ _cluster1.default.on("listening", function(worker, address) {
118
+ logger.info("\tWorker ", worker.process.pid, " online, Address: ", address.address, ":", address.port);
83
119
  });
84
- _cluster.default.on("message", masterMessageHandler);
85
- // }
86
- _cluster.default.on("exit", function(worker) {
87
- logger.warn("Worker " + worker.process.pid + " is dead!");
88
- _cluster.default.fork();
120
+ _cluster1.default.on("message", masterMessageHandler);
121
+ _cluster1.default.on("exit", function(worker) {
122
+ const bornAt = FORK_AT.get(worker.id);
123
+ FORK_AT.delete(worker.id);
124
+ if (stopping) {
125
+ return;
126
+ }
127
+ const uptime = Date.now() - (bornAt || 0);
128
+ if (bornAt && uptime < QUICK_DEATH_MS) {
129
+ quickDeaths += 1;
130
+ } else {
131
+ quickDeaths = 0;
132
+ }
133
+ if (quickDeaths >= MAX_QUICK_DEATHS) {
134
+ logger.error(`Worker ${worker.process.pid} is dead! 连续 ${quickDeaths} 次快速退出, 停止自动拉起, 请排查业务异常`);
135
+ return;
136
+ }
137
+ logger.warn(`Worker ${worker.process.pid} is dead! Reforking.`);
138
+ forkWorker();
89
139
  });
90
140
  } else {
91
- server = _server.default.listen(serverListenConf);
141
+ server = app.listen(serverListenConf);
92
142
  }
93
- masterLog("cluster", "info", "[%s] online,listening on %s port %d", config.env, config.ip || "localhost", config.port);
143
+ logger.info("[%s] online,listening on %s port %d", config.env, config.ip || "localhost", config.port);
94
144
  } else {
95
- server = _server.default.listen(serverListenConf);
145
+ server = app.listen(serverListenConf);
96
146
  }
97
147
  return server;
98
148
  }
99
- var _default = start;
100
- exports.default = _default;
149
+ const _default = start;
150
+ /**
151
+ * 通知全部 worker 退出并等待其结束
152
+ * @param timeout 等待超时(毫秒), 超时后不再等待
153
+ */ function shutdownWorkers(timeout = 10 * 1000) {
154
+ stopping = true;
155
+ return new Promise(function(resolve) {
156
+ const workers = Object.values(_cluster1.default.workers || {}).filter(Boolean);
157
+ if (!workers.length) {
158
+ resolve();
159
+ return;
160
+ }
161
+ var alive = workers.length;
162
+ const timer = setTimeout(function() {
163
+ logger.warn(`等待 worker 退出超时(${timeout}ms)`);
164
+ resolve();
165
+ }, timeout);
166
+ workers.forEach(function(worker) {
167
+ worker.once("exit", function() {
168
+ alive -= 1;
169
+ if (alive === 0) {
170
+ clearTimeout(timer);
171
+ resolve();
172
+ }
173
+ });
174
+ worker.process.kill("SIGTERM");
175
+ });
176
+ });
177
+ }