wok-server 0.1.2 → 0.1.5

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # node 后端框架
1
+ # Wok Server
2
2
 
3
3
  一个简洁易用的 Nodejs 后端框架,使用 Typescript 开发,有完整的类型约束和定义,注释详细,文档齐全,支持国际化。
4
4
 
@@ -20,7 +20,12 @@ function createJsonHandler(opts) {
20
20
  if (opts.validation) {
21
21
  // 切换语言
22
22
  (0, i18n_1.getI18n)().switchByRequest(exchange.request.headers);
23
- (0, validation_1.validate)(body, opts.validation);
23
+ if (typeof opts.validation === 'function') {
24
+ (0, validation_1.validate)(body, opts.validation());
25
+ }
26
+ else {
27
+ (0, validation_1.validate)(body, opts.validation);
28
+ }
24
29
  }
25
30
  const res = await opts.handle(body, exchange);
26
31
  if (!res) {
package/dist/mvc/index.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.stopWebServer = exports.startWebServer = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs_1 = require("fs");
6
+ const promises_1 = require("fs/promises");
6
7
  const http_1 = require("http");
7
8
  const os_1 = require("os");
8
9
  const path_1 = require("path");
@@ -73,7 +74,7 @@ async function handleRouter(exchange, routers, staticSettings) {
73
74
  const router = routers[path];
74
75
  if (!router) {
75
76
  // 路由找不不到,尝试静态文件
76
- if (staticSettings.length) {
77
+ if ((exchange.request.method || '').toLowerCase() === 'get' && staticSettings.length) {
77
78
  await handleStatic(exchange, routers, path, staticSettings);
78
79
  }
79
80
  else {
@@ -123,15 +124,15 @@ async function handleStatic(exchange, routers, path, staticSettings) {
123
124
  respond404(exchange, routers, path);
124
125
  return;
125
126
  }
126
- const stat = (0, fs_1.statSync)(fullPath);
127
+ const statRes = await (0, promises_1.stat)(fullPath);
127
128
  // 目录,寻找 index.html
128
- if (stat.isDirectory()) {
129
+ if (statRes.isDirectory()) {
129
130
  const indexPath = (0, path_1.resolve)(fullPath, 'index.html');
130
131
  if (!(0, fs_1.existsSync)(indexPath)) {
131
132
  respond404(exchange, routers, path);
132
133
  return;
133
134
  }
134
- const indexStat = (0, fs_1.statSync)(indexPath);
135
+ const indexStat = await (0, promises_1.stat)(indexPath);
135
136
  if (!indexStat.isFile()) {
136
137
  respond404(exchange, routers, path);
137
138
  return;
@@ -144,7 +145,7 @@ async function handleStatic(exchange, routers, path, staticSettings) {
144
145
  return;
145
146
  }
146
147
  // 文件直接渲染
147
- if (stat.isFile()) {
148
+ if (statRes.isFile()) {
148
149
  // Cache-Control
149
150
  if (matchedSetting.cacheAge >= 0) {
150
151
  exchange.response.setHeader('Cache-Control', matchedSetting.cacheAge === 0 ? 'no-store' : `max-age=${matchedSetting.cacheAge}`);
@@ -210,8 +211,8 @@ async function startWebServer(opts) {
210
211
  if (!(0, fs_1.existsSync)(dir)) {
211
212
  throw new Error(`Static file configuration error,path ${dir} does not exist,config dir:${setting.dir}`);
212
213
  }
213
- const stat = (0, fs_1.statSync)(dir);
214
- if (!stat.isDirectory()) {
214
+ const statRes = await (0, promises_1.stat)(dir);
215
+ if (!statRes.isDirectory()) {
215
216
  throw new Error(`Static file configuration error,path ${dir} is not a directory,config dir:${setting.dir}`);
216
217
  }
217
218
  let finalPath = path.startsWith('/') ? path : '/' + path;
@@ -243,6 +244,7 @@ async function startWebServer(opts) {
243
244
  interceptors.push(...opts.interceptors);
244
245
  }
245
246
  SERVER = (0, http_1.createServer)((req, res) => {
247
+ res.setHeader('Server', 'Wok Server');
246
248
  res.on('error', error => {
247
249
  // 如果响应流发生错误,只能把信息记录下来
248
250
  (0, log_1.getLogger)().error(`Response Error:${req.url}`, error);
@@ -266,7 +268,7 @@ async function startWebServer(opts) {
266
268
  await new Promise((resolve, reject) => {
267
269
  server.on('error', e => {
268
270
  if (e.code === 'EADDRINUSE') {
269
- reject(`端口号 ${config_1.config.port} 已经被占用`);
271
+ reject(`Port ${config_1.config.port} is already in use.`);
270
272
  }
271
273
  else {
272
274
  reject(e);
@@ -2,8 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderFile = void 0;
4
4
  const fs_1 = require("fs");
5
+ const promises_1 = require("fs/promises");
5
6
  const json_1 = require("./json");
6
7
  const path_1 = require("path");
8
+ const zlib_1 = require("zlib");
7
9
  /**
8
10
  * 常用的 content-type 对照表
9
11
  */
@@ -96,7 +98,7 @@ function decideContentType(fileName) {
96
98
  */
97
99
  async function renderFile(request, response, filePath, download = false) {
98
100
  if (!(0, fs_1.existsSync)(filePath)) {
99
- (0, json_1.renderError)(response, '文件不存在', 404);
101
+ (0, json_1.renderError)(response, 'Cannot find file.', 404);
100
102
  return;
101
103
  }
102
104
  const fileName = (0, path_1.basename)(filePath);
@@ -118,11 +120,31 @@ async function renderFile(request, response, filePath, download = false) {
118
120
  else if (contentType) {
119
121
  response.setHeader('Content-Type', contentType);
120
122
  }
123
+ const statRes = await (0, promises_1.stat)(filePath);
124
+ // 支持 If-Modified-Since
125
+ // 由于只是简单的文件映射,没有 etag,不能支持 If-None-Match
126
+ // 缓存校验只能支持时间的比对,修改时间是文件系统本来就有的
127
+ if (request.headers['if-modified-since']) {
128
+ const modifiedSince = new Date(request.headers['if-modified-since']);
129
+ // 判定日期是否有效
130
+ if (modifiedSince instanceof Date && !isNaN(modifiedSince.getTime())) {
131
+ // 比较更改日期,只精确到秒, UTC 格式只精确到秒,但是 mtime 是包含毫秒的
132
+ const { mtime } = statRes;
133
+ mtime.setMilliseconds(0);
134
+ if (modifiedSince >= mtime) {
135
+ response.statusCode = 304;
136
+ response.setHeader('Last-Modified', statRes.mtime.toUTCString());
137
+ response.end();
138
+ return;
139
+ }
140
+ }
141
+ }
121
142
  // 支持 Range
122
143
  // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Range
123
144
  const rangeHeader = request.headers['range'];
124
145
  if (!rangeHeader) {
125
- return streamFile(filePath, response);
146
+ response.setHeader('Last-Modified', statRes.mtime.toUTCString());
147
+ return streamFile(filePath, request, response);
126
148
  }
127
149
  // 解析,range 示例:bytes=200-1000, 2000-6576, 19000-
128
150
  // 多段的情况,暂时不做支持,非常麻烦,段数多还可能会有效率问题
@@ -130,38 +152,37 @@ async function renderFile(request, response, filePath, download = false) {
130
152
  const ranges = rangeHeader.split(',');
131
153
  let range = ranges.length ? ranges[0] : undefined;
132
154
  if (!range) {
133
- return streamFile(filePath, response);
155
+ return streamFile(filePath, request, response);
134
156
  }
135
157
  range = range.trim();
136
158
  if (!range.startsWith('bytes=')) {
137
- return streamFile(filePath, response);
159
+ return streamFile(filePath, request, response);
138
160
  }
139
161
  range = range.substring(6);
140
162
  const strs = range.split('-');
141
163
  let start = strs[0] ? parseInt(strs[0], 10) : NaN;
142
164
  let end = strs[1] ? parseInt(strs[1], 10) : NaN;
143
165
  // 解析文件
144
- const stat = (0, fs_1.statSync)(filePath);
145
166
  if (isNaN(start) || start < 0) {
146
167
  // 范围不合法,返回 416
147
168
  (0, json_1.renderError)(response, `Range not satisfiable,start is ${start}`, 416);
148
169
  return;
149
170
  }
150
171
  if (isNaN(end)) {
151
- end = stat.size - 1;
172
+ end = statRes.size - 1;
152
173
  }
153
- else if (end > stat.size - 1) {
174
+ else if (end > statRes.size - 1) {
154
175
  // 范围不合法,返回 416
155
- (0, json_1.renderError)(response, `Range not satisfiable,end must not be greater than ${stat.size - 1}`, 416);
176
+ (0, json_1.renderError)(response, `Range not satisfiable,end must not be greater than ${statRes.size - 1}`, 416);
156
177
  return;
157
178
  }
158
179
  // 注:Range 和 Content-Range 还有 createReadStream 中的字节范围,都是前后全包含的
159
180
  // Content-Range: bytes 42-1233/1234
160
- response.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
161
- return streamFile(filePath, response, { start, end });
181
+ response.setHeader('Content-Range', `bytes ${start}-${end}/${statRes.size}`);
182
+ return streamFile(filePath, request, response, { start, end });
162
183
  }
163
184
  exports.renderFile = renderFile;
164
- function streamFile(filePath, response, opts) {
185
+ function streamFile(filePath, request, response, opts) {
165
186
  return new Promise((res, rej) => {
166
187
  if (opts && typeof opts.start === 'number') {
167
188
  // 部分返回 206
@@ -171,6 +192,22 @@ function streamFile(filePath, response, opts) {
171
192
  // 全部返回 200
172
193
  response.statusCode = 200;
173
194
  }
195
+ // 支持 gzip
196
+ const acceptEncoding = request.headers['accept-encoding'];
197
+ if (acceptEncoding) {
198
+ // Accept-Encoding: br;q=1.0, gzip;q=0.8, *;q=0.1
199
+ const acceptEncodings = acceptEncoding
200
+ .trim()
201
+ .split(',')
202
+ .map(item => item.trim())
203
+ .map(item => item.split(';')[0]);
204
+ if (acceptEncodings.includes('gzip') || acceptEncodings.includes('*')) {
205
+ response.setHeader('Content-Encoding', 'gzip');
206
+ (0, fs_1.createReadStream)(filePath, opts).pipe((0, zlib_1.createGzip)()).pipe(response);
207
+ response.once('finish', res).once('error', rej);
208
+ return;
209
+ }
210
+ }
174
211
  (0, fs_1.createReadStream)(filePath, opts).pipe(response);
175
212
  response.once('finish', res).once('error', rej);
176
213
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wok-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "packageManager": "pnpm@8.9.0",
5
5
  "description": "一个基于 NodeJs 和 TypeScript 的后端框架,轻量级、克制、简洁。A lightweight, restrained, and concise backend framework based on Node.js and TypeScript.",
6
6
  "scripts": {
@@ -12,7 +12,7 @@ export declare function createJsonHandler<REQ, RES = void>(opts: {
12
12
  /**
13
13
  * 校验信息,可选,用于检查请求信息.对于一些特殊情况,无法使用校验器的,可以在 handle 中继续处理.
14
14
  */
15
- validation?: ValidationOpts<REQ>;
15
+ validation?: ValidationOpts<REQ> | (() => ValidationOpts<REQ>);
16
16
  /**
17
17
  * 处理请求.
18
18
  * @param body 正文内容