mm_statics 1.9.0 → 1.9.2

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.
@@ -0,0 +1,823 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ // 加载 String 原型扩展(fullname/loadText/extname/basename)
4
+ require('mm_helper');
5
+ const { cacheAdmin } = require('mm_cache');
6
+ const { EsToAmd } = require('mm_es6_to_amd');
7
+ const VueSFCCompiler = require('./vue_compiler.js');
8
+ const prettier = require('prettier');
9
+ const { marked } = require('marked');
10
+
11
+ var vueCompiler = new VueSFCCompiler();
12
+
13
+ /**
14
+ * 静态文件处理类
15
+ */
16
+ class StaticHandle {
17
+ static config = {
18
+ // 默认索引文件
19
+ index: 'index.html',
20
+ // 默认缓存过期时间
21
+ max_age: 7200,
22
+ // 静态文件缓存键前缀
23
+ prefix: 'static:',
24
+ // 缓存过期时间
25
+ cache_age: 7200,
26
+ // 是否开启缓存
27
+ cache: true,
28
+ // 是否开启不可变缓存
29
+ immutable: true,
30
+ // 是否隐藏静态文件
31
+ hidden: false,
32
+ // 是否格式化静态文件内容
33
+ format: true,
34
+ // 是否开启文件扩展名
35
+ extensions: false,
36
+ // 是否开启Brotli压缩
37
+ brotli: false,
38
+ // 是否压缩静态文件
39
+ gzip: false,
40
+ // 静态文件根目录
41
+ root: './static',
42
+ // 路由路径
43
+ path: '/',
44
+ // 是否编译Vue文件为JS
45
+ compile_vue: true,
46
+ // 是否编译Markdown文件为HTML
47
+ compile_md: true,
48
+ // 编译Markdown文件时,是否添加引入css和js文件
49
+ markdown_link: ['/css/common.css', '/css/markdown.css', '/js/markdown.js'],
50
+ // 需要转换的静态文件路径前缀
51
+ src_path: '/src',
52
+ // 需要转换的静态文件扩展名
53
+ files: ['.js', '.vue', '.html'],
54
+ // 是否转换ES6模块为AMD模块
55
+ convert_amd: false, // 修改为 false,避免 Vue 文件被错误转换为 AMD 格式
56
+ // 是否开启文件监听
57
+ watch: false,
58
+ // 监听的文件扩展名
59
+ watch_files: ['.js', '.css', '.html', '.vue', '.json', '.md']
60
+ };
61
+
62
+ /**
63
+ * 初始化静态文件处理类
64
+ * @param {object} config 配置
65
+ */
66
+ constructor(config) {
67
+ this.config = { ...StaticHandle.config };
68
+ this.convert = new EsToAmd();
69
+ this._cache = null;
70
+ this._watcher = null;
71
+ this.setConfig(config);
72
+ this._init();
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 设置配置
78
+ * @param {object} config 配置
79
+ */
80
+ StaticHandle.prototype.setConfig = function (config) {
81
+ Object.assign(this.config, config || {});
82
+ // 判断root是否绝对路径,不是则改成绝对路径
83
+ this.config.root = this.config.root.fullname();
84
+ };
85
+
86
+ /**
87
+ * 初始化缓存
88
+ */
89
+ StaticHandle.prototype._init = function () {
90
+ this._cache = cacheAdmin('os', this.config);
91
+
92
+ // 如果配置了文件监听,初始化文件监听器
93
+ if (this.config.watch && this.config.watch_files.length > 0) {
94
+ this._initWatcher();
95
+ }
96
+ };
97
+
98
+ /**
99
+ * 检测代码是否包含ES6语法
100
+ * @param {string} code 代码内容
101
+ * @returns {boolean} 是否包含ES6语法
102
+ */
103
+ StaticHandle.prototype._isEs6Code = function (code) {
104
+ if (!code || typeof code !== 'string') {
105
+ return false;
106
+ }
107
+
108
+ // ES6语法特征检测
109
+ const es6_patterns = [
110
+ /\bimport\s+/g, // import语句(支持多行和无分号)
111
+ /\bexport\s+/g, // export语句(支持多行和无分号)
112
+ /\bclass\s+\w+/g, // class声明
113
+ /\bconst\s+\w+/g, // const声明
114
+ /\blet\s+\w+/g, // let声明
115
+ /\([^)]*\)\s*=>/g, // 箭头函数
116
+ /`[^`]*`/g, // 模板字符串
117
+ /\{[^}]*\}\s*=\s*\w+/g, // 解构赋值
118
+ /\bPromise\b/g, // Promise对象
119
+ /\basync\s+function\b/g, // async函数
120
+ /\bawait\b/g // await关键字
121
+ ];
122
+
123
+ for (let i = 0; i < es6_patterns.length; i++) {
124
+ if (es6_patterns[i].test(code)) {
125
+ return true;
126
+ }
127
+ }
128
+
129
+ return false;
130
+ };
131
+
132
+ /**
133
+ * 格式化代码
134
+ * @param {string} code 代码内容
135
+ * @param {string} file_type 文件类型
136
+ * @returns {Promise<string>} 格式化后的代码
137
+ */
138
+ StaticHandle.prototype._formatCode = async function (code, file_type) {
139
+ if (!this.config.format || !code || typeof code !== 'string') {
140
+ return code;
141
+ }
142
+
143
+ try {
144
+ // 根据文件类型设置prettier配置
145
+ let parser = 'babel';
146
+ let plugins = [];
147
+
148
+ switch (file_type) {
149
+ case 'js':
150
+ parser = 'babel';
151
+ break;
152
+ case 'html':
153
+ parser = 'html';
154
+ break;
155
+ case 'vue':
156
+ parser = 'vue';
157
+ plugins = [require('prettier/parser-html'), require('prettier/parser-babel')];
158
+ break;
159
+ default:
160
+ parser = 'babel';
161
+ }
162
+
163
+ // prettier.format是异步函数
164
+ const formatted_code = await prettier.format(code, {
165
+ parser: parser,
166
+ plugins: plugins,
167
+ semi: true,
168
+ singleQuote: true,
169
+ trailingComma: 'es5',
170
+ printWidth: 100,
171
+ tabWidth: 2,
172
+ useTabs: false,
173
+ bracketSpacing: true,
174
+ arrowParens: 'avoid'
175
+ });
176
+
177
+ return formatted_code;
178
+ } catch (error) {
179
+ // 格式化失败时返回原代码
180
+ console.warn(`格式化代码失败: ${error.message}`);
181
+ return code;
182
+ }
183
+ };
184
+
185
+ // /**
186
+ // * 处理require模块语句
187
+ // * @param {string} script 包含require语句的代码
188
+ // * @returns {string|null} 处理后的代码或null
189
+ // */
190
+ // StaticHandle.prototype._requireMod = function (script) {
191
+ // if (script) {
192
+ // var src = script.between('[', ']');
193
+ // var imports = src;
194
+ // var arr = imports.split(',');
195
+ // var len = arr.length;
196
+ // for (var i = 0; i < len; i++) {
197
+ // var s = arr[i];
198
+ // if (s.endsWith('.vue"')) {
199
+ // imports = imports.replace(s, '"vue!' + s.trim().trim('"') + '"');
200
+ // } else if (s.endsWith('.css"')) {
201
+ // imports = imports.replace(s, '"css!' + s.trim().trim('"') + '"');
202
+ // } else if (s.endsWith('.html"')) {
203
+ // imports = imports.replace(s, '"text!' + s.trim().trim('"') + '"');
204
+ // }
205
+ // }
206
+ // return script.replace(src, imports);
207
+ // }
208
+ // return null;
209
+ // };
210
+
211
+ /**
212
+ * 转换为AMD模块
213
+ * @param {string} file_type 文件类型
214
+ * @param {string} text 代码
215
+ * @returns {Promise<string>} AMD模块代码
216
+ */
217
+ StaticHandle.prototype._toAmd = async function (file_type, text) {
218
+ let txt = text;
219
+ if (file_type === 'js') {
220
+ // 对JS文件,先检测是否为ES6代码,只有ES6代码才进行转换
221
+ if (this._isEs6Code(txt)) {
222
+ let code = this.convert.toAmd(txt);
223
+ // 处理require模块语句
224
+ // code = this._requireMod(code);
225
+ // 添加return语句确保AMD模块正确导出
226
+ // code = this._addAmdReturn(code);
227
+ // 格式化代码
228
+ return await this._formatCode(code, file_type);
229
+ } else {
230
+ // 格式化非ES6代码
231
+ return await this._formatCode(txt, file_type);
232
+ }
233
+ } else if (file_type === 'html' || file_type === 'vue') {
234
+ // 获取html或vue中script标签的代码,将ES6语法的代码转换为AMD模块
235
+ let match = txt.match(/<script[^>]*>([\s\S]*?)<\/script>/g);
236
+ if (match) {
237
+ for (let i = 0; i < match.length; i++) {
238
+ let script = match[i];
239
+ // 直接提取script标签内容,避免重复匹配
240
+ let inner_code = script.replace(/^<script[^>]*>([\s\S]*?)<\/script>$/, '$1').trim();
241
+ if (inner_code) {
242
+ // 先检测script标签内的代码是否为ES6语法
243
+ if (this._isEs6Code(inner_code)) {
244
+ let amd_code = this.convert.toAmd(inner_code);
245
+ // 处理require模块语句
246
+ // amd_code = this._requireMod(amd_code);
247
+ // // 添加return语句确保AMD模块正确导出
248
+ // amd_code = this._addAmdReturn(amd_code);
249
+ // 移除type="module"属性,因为AMD模块不需要这个属性
250
+ let new_script = script.replace(/type\s*=\s*["']module["']/g, '').replace(inner_code, amd_code);
251
+ // 清理多余的空格
252
+ new_script = new_script.replace(/<script\s+>/, '<script>').replace(/<script\s+([^>]+)>/, '<script $1>');
253
+ txt = txt.replace(script, new_script);
254
+ }
255
+ // 如果不是ES6代码,保持原样不进行转换
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ // 格式化代码后返回,确保返回字符串而不是Promise
262
+ const result = await this._formatCode(txt, file_type);
263
+ return result;
264
+ };
265
+
266
+ /**
267
+ * 编译Vue组件
268
+ * @param {string} code Vue组件代码
269
+ * @param {string} filePath 文件路径
270
+ * @returns {Promise<string>} 可直接调用的JavaScript组件代码
271
+ */
272
+ StaticHandle.prototype._compileVue = async function (code, filePath = '') {
273
+ try {
274
+ // 使用修复后的 Vue 编译器
275
+ const { code: compiledCode } = await vueCompiler.toJS(code, filePath);
276
+
277
+ // Vue 编译后的代码是 JavaScript,应该使用 babel 解析器格式化
278
+ const result = await this._formatCode(compiledCode, 'babel');
279
+ return result;
280
+ } catch (error) {
281
+ console.error('Vue编译失败:', error);
282
+ return `console.error('Vue编译失败: ${error.message}');\nexport default { name: 'ErrorComponent' };`;
283
+ }
284
+ };
285
+
286
+ /**
287
+ * 发送文件
288
+ * 兼容两种上下文:
289
+ * - Koa 风格 ctx(具备 ctx.set / ctx.body),以流方式写入 ctx.body
290
+ * - mm_web_server 原生 ctx(仅含 ctx.res),直接写 Node 响应并标记 ctx.handled
291
+ * @param {object} ctx 请求上下文
292
+ * @param {string} path 文件路径
293
+ * @returns {Promise<string>} 发送成功返回物理文件路径
294
+ */
295
+ StaticHandle.prototype._sendFile = async function (ctx, path) {
296
+ const file_path = this._pathToRoot(path);
297
+ // 路径越界校验,防止目录穿越
298
+ if (!this._checkFilePath(file_path)) {
299
+ throw new Error(`非法路径: ${file_path}`);
300
+ }
301
+ const stat = await fs.promises.stat(file_path);
302
+ if (!stat.isFile()) {
303
+ throw new Error(`文件不存在: ${file_path}`);
304
+ }
305
+ const headers = this._getHeaders(this._getType(path));
306
+ if (path.endsWith('.md')) {
307
+ headers['content-type'] = 'text/plain; charset=utf-8';
308
+ }
309
+ if (typeof ctx.set === 'function') {
310
+ // Koa 风格上下文
311
+ ctx.status = 200;
312
+ headers['content-length'] = stat.size;
313
+ ctx.set(headers);
314
+ ctx.body = fs.createReadStream(file_path);
315
+ return file_path;
316
+ }
317
+ // mm_web_server 原生上下文:直接写响应
318
+ const res = ctx.res;
319
+ if (res && !res.headersSent) {
320
+ const range = this._parseRange(ctx, stat.size);
321
+ if (range) {
322
+ headers['content-range'] = `bytes ${range.start}-${range.end}/${stat.size}`;
323
+ headers['content-length'] = range.end - range.start + 1;
324
+ res.writeHead(206, headers);
325
+ fs.createReadStream(file_path, { start: range.start, end: range.end })
326
+ .on('error', () => res.destroy())
327
+ .pipe(res);
328
+ } else {
329
+ headers['content-length'] = stat.size;
330
+ headers['accept-ranges'] = 'bytes';
331
+ res.writeHead(200, headers);
332
+ fs.createReadStream(file_path)
333
+ .on('error', () => res.destroy())
334
+ .pipe(res);
335
+ }
336
+ ctx.handled = true;
337
+ }
338
+ return file_path;
339
+ };
340
+
341
+ /**
342
+ * 校验物理文件路径不越出配置的静态根目录
343
+ * @param {string} file_path 物理文件路径
344
+ * @returns {boolean} 合法返回 true
345
+ */
346
+ StaticHandle.prototype._checkFilePath = function (file_path) {
347
+ const root_path = path.resolve(this.config.root);
348
+ const resolved = path.resolve(file_path);
349
+ return resolved === root_path || resolved.startsWith(root_path + path.sep);
350
+ };
351
+
352
+ /**
353
+ * 解析 HTTP Range 请求头
354
+ * @param {object} ctx 请求上下文
355
+ * @param {number} total 文件总字节数
356
+ * @returns {object|null} {start, end} 范围对象,无法解析时返回 null
357
+ */
358
+ StaticHandle.prototype._parseRange = function (ctx, total) {
359
+ const headers = (ctx.params && ctx.params.headers) || (ctx.request && ctx.request.headers) || {};
360
+ const range_header = headers['range'];
361
+ if (!range_header) return null;
362
+ const match = /^bytes=(\d*)-(\d*)$/.exec(range_header);
363
+ if (!match) return null;
364
+ let start = match[1] ? parseInt(match[1], 10) : null;
365
+ let end = match[2] ? parseInt(match[2], 10) : null;
366
+ // 后缀范围,如 bytes=-500 表示取最后 500 字节
367
+ if (start === null && end !== null) {
368
+ start = Math.max(0, total - end);
369
+ end = total - 1;
370
+ }
371
+ if (end === null) {
372
+ end = total - 1;
373
+ }
374
+ if (start === null || start >= total || start > end) {
375
+ return null;
376
+ }
377
+ end = Math.min(end, total - 1);
378
+ return { start, end };
379
+ };
380
+
381
+ /**
382
+ * 运行AMD模块
383
+ * @param {string} path 文件路径
384
+ * @param {string} file_type 文件类型
385
+ * @returns {Promise<string>} AMD模块代码
386
+ */
387
+ StaticHandle.prototype._runAmd = async function (path, file_type) {
388
+ let type = file_type || this._getType(path);
389
+ let code = null;
390
+ let text = `.${path}`.fullname(this.config.root).loadText();
391
+ if (text) {
392
+ code = await this._toAmd(type, text);
393
+ }
394
+ return code;
395
+ };
396
+
397
+ /**
398
+ * 运行Vue组件
399
+ * @param {string} path 文件路径
400
+ * @returns {Promise<string>} 编译后的代码
401
+ */
402
+ StaticHandle.prototype._runVue = async function (path) {
403
+ let code = null;
404
+ let file = `.${path}`.fullname(this.config.root);
405
+ let text = file.loadText();
406
+ if (text) {
407
+ code = await this._compileVue(text, file);
408
+ }
409
+ return code;
410
+ };
411
+
412
+ /**
413
+ * 从Markdown内容中提取第一个一级标题
414
+ * @param {string} text Markdown文本内容
415
+ * @returns {string|null} 提取的标题,未找到时返回null
416
+ */
417
+ StaticHandle.prototype._extractMarkdownTitle = function (text) {
418
+ if (!text || typeof text !== 'string') {
419
+ return null;
420
+ }
421
+
422
+ // 匹配一级标题格式:# 标题
423
+ var title_regex = /^#\s+(.+)$/m;
424
+ var match = text.match(title_regex);
425
+
426
+ if (match && match[1]) {
427
+ return match[1].trim();
428
+ }
429
+
430
+ return null;
431
+ };
432
+
433
+ /**
434
+ * 处理Markdown文件转换
435
+ * @param {string} path 文件路径
436
+ * @returns {string|null} 转换后的HTML内容
437
+ */
438
+ StaticHandle.prototype._runMarkdown = async function (path) {
439
+ let html = null;
440
+ let file = `.${path}`.fullname(this.config.root);
441
+ let text = file.loadText();
442
+
443
+ if (text) {
444
+ try {
445
+ // 使用marked将markdown转换为HTML
446
+ let markdown_html = marked.parse(text);
447
+
448
+ // 优先从Markdown内容中提取一级标题,后备使用文件名
449
+ var title_from_content = this._extractMarkdownTitle(text);
450
+ var title_from_file = path.split('/').pop().replace('.md', '');
451
+ var title = title_from_content || title_from_file;
452
+ // 构建外部资源链接
453
+ let links = '';
454
+ if (this.config.markdown_link && Array.isArray(this.config.markdown_link)) {
455
+ this.config.markdown_link.forEach(path => {
456
+ if (path.endsWith('.css')) {
457
+ links += ` <link rel="stylesheet" href="${path}">\n`;
458
+ } else if (path.endsWith('.js')) {
459
+ links += ` <script src="${path}"></script>\n`;
460
+ }
461
+ });
462
+ }
463
+
464
+ // 构建完整的HTML结构
465
+ html = `<!DOCTYPE html>
466
+ <html>
467
+ <head>
468
+ <meta charset="UTF-8">
469
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
470
+ <title>${title}</title>
471
+ ${links}
472
+ </head>
473
+ <body>
474
+ <div id="app" class="markdown-body">
475
+ ${markdown_html}
476
+ </div>
477
+ </body>
478
+ </html>`;
479
+
480
+ // 如果启用了格式化,使用prettier格式化完整的HTML
481
+ if (this.config.format) {
482
+ try {
483
+ html = prettier.format(html, {
484
+ parser: 'html',
485
+ printWidth: 100,
486
+ tabWidth: 2
487
+ });
488
+ } catch (error) {
489
+ console.warn('HTML格式化失败,使用原始HTML:', error.message);
490
+ }
491
+ }
492
+ } catch (error) {
493
+ console.error('Markdown转换错误:', error);
494
+ return null;
495
+ }
496
+ }
497
+
498
+ return html;
499
+ };
500
+
501
+ /**
502
+ * 返回响应
503
+ * 兼容两种上下文:
504
+ * - Koa 风格 ctx:写入 ctx.body 并通过 ctx.set 设置响应头
505
+ * - mm_web_server 原生 ctx:直接写 Node 响应并标记 ctx.handled
506
+ * @param {object} ctx 请求上下文
507
+ * @param {string} body 响应体
508
+ * @param {object} headers 响应头
509
+ * @returns {object} 响应对象
510
+ */
511
+ StaticHandle.prototype._return = function (ctx, body, headers) {
512
+ if (typeof ctx.set === 'function') {
513
+ ctx.body = body;
514
+ if (headers) {
515
+ ctx.set(headers);
516
+ }
517
+ } else {
518
+ const res = ctx.res;
519
+ if (res && !res.headersSent) {
520
+ res.writeHead(200, headers || {});
521
+ res.end(body);
522
+ }
523
+ ctx.handled = true;
524
+ }
525
+ return { body, headers };
526
+ };
527
+
528
+ /**
529
+ * 初始化文件监听器
530
+ */
531
+ StaticHandle.prototype._initWatcher = function () {
532
+ try {
533
+ const chokidar = require('chokidar');
534
+
535
+ // 构建监听模式,只监听指定类型的文件
536
+ const watch_patterns = this.config.watch_files.map(ext =>
537
+ `${this.config.root}/**/*${ext}`
538
+ );
539
+
540
+ // 监听指定类型的文件
541
+ this._watcher = chokidar.watch(watch_patterns, {
542
+ ignored: /(^|[\/\\])\\./, // 忽略隐藏文件
543
+ persistent: true,
544
+ ignoreInitial: true // 忽略初始扫描事件
545
+ });
546
+
547
+ // 文件变化时的处理逻辑
548
+ this._watcher.on('all', (event, file_path) => {
549
+ this._handleFileChange(event, file_path);
550
+ });
551
+ } catch (error) {
552
+ console.error('文件监听初始化失败:', error.message);
553
+ }
554
+ };
555
+
556
+ /**
557
+ * 处理文件变化事件
558
+ * @param {string} event 事件类型
559
+ * @param {string} file_path 文件路径
560
+ */
561
+ StaticHandle.prototype._handleFileChange = function (event, file_path) {
562
+ // 构建缓存键(相对于静态目录的路径)
563
+ const relative_path = file_path.replace(this.config.root, '').replace(/^[\\\/]/, '');
564
+ const cache_key = this.config.prefix + relative_path;
565
+
566
+ // 根据事件类型处理缓存
567
+ switch (event) {
568
+ case 'change':
569
+ case 'add':
570
+ // 文件修改或新增,删除缓存,下次请求时重新生成
571
+ this._cache.del(cache_key);
572
+ break;
573
+ case 'unlink':
574
+ // 文件删除,删除缓存
575
+ this._cache.del(cache_key);
576
+ break;
577
+ }
578
+ };
579
+
580
+ /**
581
+ * 获取响应头
582
+ * @param {string} file_type 文件类型
583
+ * @returns {object} 响应头
584
+ */
585
+ StaticHandle.prototype._getHeaders = function (file_type) {
586
+ let headers = {};
587
+
588
+ // 缓存控制逻辑
589
+ if (this.config.max_age > 0) {
590
+ let cache_control = `max-age=${this.config.max_age}`;
591
+ if (this.config.immutable) {
592
+ cache_control += ', immutable';
593
+ }
594
+ headers['cache-control'] = cache_control;
595
+ }
596
+ switch (file_type) {
597
+ case 'js':
598
+ headers['content-type'] = 'application/javascript; charset=utf-8';
599
+ break;
600
+ case 'vue':
601
+ if (this.config.convert_amd) {
602
+ headers['content-type'] = 'text/html; charset=utf-8';
603
+ }
604
+ else if (this.config.compile_vue) {
605
+ headers['content-type'] = 'application/javascript; charset=utf-8';
606
+ }
607
+ else {
608
+ // 当不编译 Vue 文件时,设置为 JavaScript MIME 类型
609
+ headers['content-type'] = 'application/javascript; charset=utf-8';
610
+ }
611
+ break;
612
+ case 'html':
613
+ headers['content-type'] = 'text/html; charset=utf-8';
614
+ break;
615
+ case 'css':
616
+ headers['content-type'] = 'text/css; charset=utf-8';
617
+ break;
618
+ case 'md':
619
+ if (this.config.compile_md) {
620
+ headers['content-type'] = 'text/html; charset=utf-8';
621
+ }
622
+ else {
623
+ headers['content-type'] = 'text/plain; charset=utf-8';
624
+ }
625
+ break;
626
+ case 'mp3':
627
+ case 'wav':
628
+ case 'ogg':
629
+ case 'm4a':
630
+ headers['content-type'] = 'audio/mpeg';
631
+ break;
632
+ case 'swf':
633
+ headers['content-type'] = 'application/x-shockwave-flash';
634
+ break;
635
+ default:
636
+ headers['content-type'] = 'text/plain; charset=utf-8';
637
+ break;
638
+ }
639
+ return headers;
640
+ };
641
+
642
+ /**
643
+ * 设置缓存
644
+ * @param {string} path 文件路径(包含查询参数)
645
+ * @param {object} ret 响应体和响应头
646
+ */
647
+ StaticHandle.prototype._setCache = async function (path, ret) {
648
+ // 使用完整路径(包含查询参数)作为缓存键
649
+ await this._cache.set(this.config.prefix + path, ret, this.config.cache_age);
650
+ };
651
+
652
+ /**
653
+ * 获取文件类型
654
+ * @param {string} path 文件路径
655
+ * @returns {string} 文件类型(不含点号)
656
+ */
657
+ StaticHandle.prototype._getType = function (path) {
658
+
659
+ // 使用mm_expand提供的extname方法获取扩展名
660
+ const ext = path.extname().toLowerCase();
661
+ if (ext) {
662
+ return ext.slice(1); // 去掉点号
663
+ }
664
+
665
+ // 备用方法:处理多扩展名情况
666
+ const basename = path.basename();
667
+ const parts = basename.split('.');
668
+
669
+ // 如果文件名包含多个点号,取最后一个作为扩展名
670
+ if (parts.length > 1) {
671
+ return parts[parts.length - 1];
672
+ }
673
+
674
+ return ''; // 无扩展名
675
+ };
676
+
677
+ /**
678
+ * 获取缓存
679
+ * @param {string} path 文件路径(包含查询参数)
680
+ * @returns {object} 响应体和响应头
681
+ */
682
+ StaticHandle.prototype._getCache = async function (path) {
683
+ // 使用完整路径(包含查询参数)作为缓存键
684
+ return await this._cache.get(this.config.prefix + path);
685
+ };
686
+
687
+ /**
688
+ * 删除缓存
689
+ * @param {string} path 文件路径(包含查询参数)
690
+ */
691
+ StaticHandle.prototype._delCache = async function (path) {
692
+ await this._cache.del(this.config.prefix + path);
693
+ };
694
+
695
+ /**
696
+ * 处理缓存逻辑
697
+ * @param {string} url 完整URL
698
+ * @param {object} ctx Koa上下文对象
699
+ * @returns {boolean} 是否命中缓存
700
+ */
701
+ StaticHandle.prototype._handleCache = async function (url, ctx) {
702
+ if (!this.config.cache) return false;
703
+
704
+ let ret = await this._getCache(url);
705
+ if (ret) {
706
+ this._return(ctx, ret.body, ret.headers);
707
+ return true;
708
+ }
709
+ return false;
710
+ };
711
+
712
+ /**
713
+ * 处理文件编译逻辑
714
+ * @param {string} path 文件路径
715
+ * @returns {string|null} 编译后的内容
716
+ */
717
+ StaticHandle.prototype._handleFileCompile = async function (path) {
718
+ let { src_path, compile_md, files, compile_vue, convert_amd } = this.config;
719
+ if (compile_md && path.extname() === '.md') {
720
+ return await this._runMarkdown(path);
721
+ } else if (!path.startsWith(src_path)) return null;
722
+
723
+ let file_type = this._getType(path);
724
+
725
+ if (convert_amd && file_type !== 'md' && files.includes('.' + file_type)) {
726
+ return await this._runAmd(path, file_type);
727
+ }
728
+ else if (compile_vue && file_type === 'vue') {
729
+ return await this._runVue(path);
730
+ }
731
+ return null;
732
+ };
733
+
734
+ /**
735
+ * 处理编译后的文件响应
736
+ * @param {string} url 完整URL
737
+ * @param {string} path 文件路径
738
+ * @param {string} body 响应体
739
+ * @param {object} ctx Koa上下文对象
740
+ */
741
+ StaticHandle.prototype._handleCompiledResp = async function (url, path, body, ctx) {
742
+ let file_type = this._getType(path);
743
+ // 只有当启用了Markdown转换时,才将markdown文件类型设置为HTML
744
+ if (file_type === 'md' && this.config.compile_md) {
745
+ file_type = 'html';
746
+ }
747
+ let headers = this._getHeaders(file_type);
748
+ let ret = this._return(ctx, body, headers);
749
+
750
+ // 只有当缓存启用时才设置缓存
751
+ if (this.config.cache) {
752
+ await this._setCache(url, ret);
753
+ }
754
+ };
755
+
756
+ /**
757
+ * 获取真实路径
758
+ * @param {string} path 文件路径
759
+ * @returns {string} 真实路径
760
+ */
761
+ StaticHandle.prototype._getRealPath = function (path) {
762
+ return path;
763
+ }
764
+
765
+ /**
766
+ * 将路由路径转换为物理路径
767
+ * @param {string} path 文件路径
768
+ * @returns {string} 根路径
769
+ */
770
+ StaticHandle.prototype._pathToRoot = function (path) {
771
+ return ("." + path).fullname(this.config.root);
772
+ }
773
+
774
+ /**
775
+ * 主处理函数
776
+ * 兼容两种上下文:
777
+ * - Koa 风格 ctx:使用 ctx.path / ctx.url
778
+ * - mm_web_server 原生 ctx:从 ctx.params 读取 path,发送文件后标记 ctx.handled
779
+ * @param {object} ctx 请求上下文
780
+ * @param {Function} next 跳过当前处理函数
781
+ */
782
+ StaticHandle.prototype.main = async function (ctx, next) {
783
+ let p = ctx.path || (ctx.params && ctx.params.path) || '';
784
+ let url = ctx.url || p; // 使用完整URL(包含查询参数)
785
+ if (p.indexOf('.') === -1) {
786
+ await next();
787
+ return '';
788
+ }
789
+ let path = this._getRealPath(p);
790
+
791
+ // 检查缓存
792
+ if (await this._handleCache(url, ctx)) {
793
+ return this._pathToRoot(path);
794
+ }
795
+
796
+ // 处理文件编译
797
+ let body = await this._handleFileCompile(path);
798
+ if (body) {
799
+ await this._handleCompiledResp(url, path, body, ctx);
800
+ return this._pathToRoot(path);
801
+ }
802
+
803
+ // 直接发送文件
804
+ try {
805
+ return await this._sendFile(ctx, path);
806
+ }
807
+ catch {
808
+ await next();
809
+ }
810
+ return '';
811
+ };
812
+
813
+ /**
814
+ * 关闭文件监听器
815
+ */
816
+ StaticHandle.prototype.closeWatcher = function () {
817
+ if (this._watcher) {
818
+ this._watcher.close();
819
+ this._watcher = null;
820
+ }
821
+ };
822
+
823
+ module.exports = StaticHandle;