mm_statics 1.5.4 → 1.5.6

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 (2) hide show
  1. package/index.js +642 -1229
  2. package/package.json +4 -3
package/index.js CHANGED
@@ -1,1229 +1,642 @@
1
- let send = require('koa-send');
2
- const { EsToAmdConvert } = require('mm_es6_to_amd');
3
- const { parse } = require('@vue/compiler-sfc');
4
-
5
- // 初始化mm_expand模块
6
- require('mm_expand');
7
-
8
- // 确保$对象已初始化
9
- if (!global.$) {
10
- throw new Error('mm_expand模块初始化失败,$对象不存在');
11
- }
12
-
13
- /**
14
- * 静态文件处理类
15
- */
16
- class Static {
17
- static config = {
18
- index: 'index.html',
19
- // 前端缓存时间,单位秒
20
- max_age: 7200,
21
- // 缓存键前缀
22
- key_prefix: 'static:',
23
- // 是否开启缓存
24
- cache: true,
25
- // 后端缓存时间,单位秒
26
- cache_age: 7200,
27
- // 是否开启immutable缓存头
28
- immutable: true,
29
- //
30
- hidden: false,
31
- format: true,
32
- extensions: false,
33
- brotli: false,
34
- gzip: false,
35
- root: './static',
36
- // 编译vue文件,启动后会将vue转为js,可让前端通过 import xxx from './xxx.vue' 引入
37
- compile_vue: true,
38
- // 指定路径文件需要转换
39
- path: '/src',
40
- //
41
- files: ['.js', '.vue'],
42
- // 是否将files中的文件的js转换为amd格式
43
- convert_amd: true,
44
- // 是否启用文件修改监听(实时检测文件修改)
45
- watch_files: false,
46
- // 是否检查文件修改时间(当watch_files为false时生效)
47
- check_file_mtime: false
48
- };
49
-
50
- /**
51
- * 初始化静态文件处理类
52
- * @param {object} config 配置
53
- */
54
- constructor(config) {
55
- this.config = { ...Static.config };
56
- this.convert = new EsToAmdConvert();
57
- this._cache = null;
58
- this._cache_config_prefix = this._generateConfigPrefix(); // 初始化配置前缀
59
- this._cache_headers = this._generateCacheHeaders(); // 初始化缓存头
60
- this.setConfig(config);
61
- this._init();
62
- }
63
- }
64
-
65
- /**
66
- * 初始化缓存
67
- */
68
- Static.prototype._init = function () {
69
- if (this.config.cache) {
70
- try {
71
- if (!$.cache) {
72
- var { Cache } = require('mm_cache');
73
- $.cache = new Cache();
74
- }
75
- this._cache = $.cache;
76
- } catch (_error) {
77
- // 如果mm_cache模块不存在,使用简单的内存缓存
78
- let map = new Map();
79
-
80
- // 为Map实例添加兼容性包装器,支持统一的异步接口
81
- this._cache = {
82
- get: async function (key) { return map.get(key); },
83
- set: async function (key, value, ttl) {
84
- map.set(key, value);
85
- // 简单的TTL实现,使用setTimeout清除过期缓存
86
- if (ttl) {
87
- setTimeout(() => {
88
- map.delete(key);
89
- }, ttl * 1000);
90
- }
91
- },
92
- del: async function (key) { return map.delete(key); }
93
- };
94
- }
95
- }
96
-
97
- // 初始化文件监听器
98
- this._initFileWatcher();
99
- };
100
-
101
- /**
102
- * 初始化文件监听器
103
- */
104
- Static.prototype._initFileWatcher = function () {
105
- // 存储文件监听器和对应的缓存键
106
- this._fileWatchers = new Map();
107
-
108
- // 如果启用文件监听,设置监听器
109
- if (this.config.watch_files) {
110
- try {
111
- this._chokidar = require('chokidar');
112
- } catch (error) {
113
- this.config.watch_files = false;
114
- }
115
- }
116
- };
117
-
118
- /**
119
- * 添加文件监听
120
- * @param {string} file_path 文件路径
121
- * @param {string} cache_key 缓存键
122
- */
123
- Static.prototype._addFileWatcher = function (file_path, cache_key) {
124
- if (!this.config.watch_files || !this._chokidar) return;
125
-
126
- try {
127
- // 如果已经存在监听器,先移除
128
- if (this._fileWatchers.has(file_path)) {
129
- let watcher = this._fileWatchers.get(file_path);
130
- watcher.close();
131
- this._fileWatchers.delete(file_path);
132
- }
133
-
134
- // 使用chokidar创建更稳定的文件监听器
135
- let watcher = this._chokidar.watch(file_path, {
136
- persistent: true,
137
- ignoreInitial: true,
138
- awaitWriteFinish: {
139
- stabilityThreshold: 100,
140
- pollInterval: 50
141
- }
142
- });
143
-
144
- // 监听文件修改事件
145
- watcher.on('change', (path) => {
146
- // 文件修改时清除缓存
147
- this._cache.del(cache_key).catch(error => {
148
- console.error(`[Static] 清除缓存失败: ${error.message}`);
149
- });
150
- });
151
-
152
- // 监听错误事件
153
- watcher.on('error', (error) => {
154
- console.error(`[Static] 文件监听错误: ${error.message}`);
155
- });
156
-
157
- // 存储监听器
158
- this._fileWatchers.set(file_path, watcher);
159
-
160
- } catch (error) {
161
- console.error(`[Static] 创建文件监听器失败: ${error.message}`);
162
- }
163
- };
164
-
165
- /**
166
- * 移除文件监听
167
- * @param {string} file_path 文件路径
168
- */
169
- Static.prototype._removeFileWatcher = function (file_path) {
170
- if (!this.config.watch_files) return;
171
-
172
- if (this._fileWatchers.has(file_path)) {
173
- let watcher = this._fileWatchers.get(file_path);
174
- watcher.close();
175
- this._fileWatchers.delete(file_path);
176
- }
177
- };
178
-
179
- /**
180
- * 获取缓存内容
181
- * @param {string} path 文件路径
182
- * @param {string} full_path 完整文件路径(用于检查文件修改时间)
183
- * @returns {Promise<object|null>} 缓存内容
184
- */
185
- Static.prototype._getCache = async function (path, full_path) {
186
- if (!this._cache) return null;
187
-
188
- // 生成缓存键
189
- let cache_key = this._generateCacheKey(path);
190
-
191
- // 统一获取缓存数据 - 缓存模块会自动处理TTL过期
192
- let data = await this._cache.get(cache_key);
193
-
194
- if (!data) return null;
195
-
196
- // 仅在未启用文件监听且需要检查文件修改时进行检查
197
- // 减少文件系统操作,提高性能
198
- if (!this.config.watch_files && this.config.check_file_mtime && full_path && data.file_mtime) {
199
- let current_mtime = this._getFileMtime(full_path);
200
- if (current_mtime && current_mtime !== data.file_mtime) {
201
- // 文件已修改,清除缓存
202
- await this._cache.del(cache_key);
203
- return null;
204
- }
205
- }
206
-
207
- return data;
208
- };
209
-
210
- /**
211
- * 获取文件修改时间戳
212
- * @param {string} file_path 文件路径
213
- * @returns {number|null} 文件修改时间戳,获取失败返回null
214
- */
215
- Static.prototype._getFileMtime = function (file_path) {
216
- try {
217
- let stats = file_path.stat();
218
- return stats ? stats.mtime.getTime() : null;
219
- } catch (_error) {
220
- return null;
221
- }
222
- };
223
-
224
- /**
225
- * 设置缓存内容
226
- * @param {string} path 文件路径
227
- * @param {string} content 文件内容
228
- * @param {string} mime_type MIME类型
229
- * @param {number} file_mtime 文件修改时间戳
230
- * @param {string} full_path 完整文件路径(用于文件监听)
231
- */
232
- Static.prototype._setCache = async function (path, content, mime_type, file_mtime, full_path) {
233
- if (!this._cache) return;
234
-
235
- // 生成包含配置信息和内容哈希的缓存键
236
- let cache_key = this._generateCacheKey(path);
237
-
238
- let data = {
239
- content: content,
240
- mime_type: mime_type,
241
- timestamp: Date.now(),
242
- file_mtime: file_mtime || Date.now() // 如果没有提供文件修改时间,使用当前时间
243
- };
244
-
245
- // 统一设置缓存数据 - 充分利用缓存模块的TTL机制
246
- // 缓存模块会自动处理过期,无需手动检查时间戳
247
- await this._cache.set(cache_key, data, this.config.cache_age);
248
-
249
- // 如果启用文件监听,添加文件监听器
250
- if (full_path) {
251
- this._addFileWatcher(full_path, cache_key);
252
- }
253
- };
254
-
255
- /**
256
- * 生成配置前缀
257
- * @returns {string} 配置前缀
258
- */
259
- Static.prototype._generateConfigPrefix = function () {
260
- return `${this.config.convert_amd ? 'amd' : 'noamd'}_${this.config.files.join('-')}_${this.config.path.replace(/[^a-zA-Z0-9]/g, '_')}`;
261
- };
262
-
263
- /**
264
- * 生成缓存头信息
265
- * @returns {object} 缓存头对象
266
- */
267
- Static.prototype._generateCacheHeaders = function () {
268
- let headers = {};
269
-
270
- if (this.config.max_age) {
271
- let cache_control = 'max-age=' + this.config.max_age;
272
- if (this.config.immutable) {
273
- cache_control += ',immutable';
274
- }
275
- headers['Cache-Control'] = cache_control;
276
- }
277
-
278
- return headers;
279
- };
280
-
281
- /**
282
- * 生成包含配置信息和文件内容哈希的缓存键
283
- * @param {string} path 文件路径
284
- * @returns {string} 缓存键
285
- */
286
- Static.prototype._generateCacheKey = function (path) {
287
- // 直接使用缓存的配置前缀
288
- return this.config.key_prefix + this._cache_config_prefix + ':' + path.replace(/[^a-zA-Z0-9]/g, '_');
289
- };
290
-
291
- /**
292
- * 初始化基础配置
293
- * @param {object} config 配置
294
- */
295
- Static.prototype.setConfig = function (config) {
296
- if (config) {
297
- Object.assign(this.config, config);
298
- // 配置改变时,立即重新计算缓存配置前缀和缓存头
299
- this._cache_config_prefix = this._generateConfigPrefix();
300
- this._cache_headers = this._generateCacheHeaders();
301
- }
302
- if (this.config.extensions && this.config.extensions.length === 0) {
303
- this.config.extensions = false;
304
- }
305
- };
306
-
307
- /**
308
- * 执行静态文件处理 (统一处理流程)
309
- * @param {object} ctx http请求上下文
310
- * @param {Function} next 跳过当前执行,先执行后面函数
311
- * @returns {boolean} 是否执行成功
312
- */
313
- Static.prototype.main = async function (ctx, next) {
314
- await next();
315
- if (!this._shouldProcess(ctx)) return;
316
-
317
- // 统一使用_send方法处理所有文件,避免重复转换逻辑
318
- await this._send(ctx, ctx.path);
319
- };
320
-
321
- /**
322
- * 判断是否需要处理请求
323
- * @param {object} ctx 上下文
324
- * @returns {boolean} 是否需要处理
325
- */
326
- Static.prototype._shouldProcess = function (ctx) {
327
- if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return false;
328
- if (ctx.body != null || ctx.status !== 404) return false;
329
- return true;
330
- };
331
-
332
- /**
333
- * 判断是否需要转换ES6到AMD
334
- * @param {string} path 文件路径
335
- * @returns {boolean} 是否需要转换
336
- */
337
- Static.prototype._convertible = function (path) {
338
- let files = this.config.files;
339
- let len = files.length;
340
- for (let i = 0; i < len; i++) {
341
- if (path.endsWith(files[i])) {
342
- return true;
343
- }
344
- }
345
- return false;
346
- };
347
-
348
- /**
349
- * 判断是否在指定路径下
350
- * @param {string} path 文件路径
351
- * @returns {boolean} 是否在指定路径下
352
- */
353
- Static.prototype._inTargetPath = function (path) {
354
- return path.startsWith(this.config.path);
355
- };
356
-
357
- /**
358
- * 从Vue文件中提取JavaScript代码
359
- * @param {string} vue_content Vue文件内容
360
- * @returns {string|null} 提取的JavaScript代码
361
- */
362
- Static.prototype._extractJsFromVue = function (vue_content) {
363
- // 匹配<script>标签内的内容
364
- let script_match = vue_content.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
365
- if (!script_match || !script_match[1]) return null;
366
-
367
- let script_content = script_match[1].trim();
368
-
369
- // 如果包含export default,提取组件定义
370
- if (script_content.includes('export default')) {
371
- // 提取export default后面的内容
372
- let export_match = script_content.match(/export default\s*({[\s\S]*})/);
373
- if (export_match && export_match[1]) {
374
- // 将Vue组件转换为AMD模块
375
- return `define([], function() {\n return ${export_match[1]};\n});`;
376
- }
377
- }
378
-
379
- return script_content;
380
- };
381
-
382
- /**
383
- * 检查文件是否需要转换
384
- * @param {string} file_path 文件路径
385
- * @returns {boolean} 是否需要转换
386
- */
387
- Static.prototype._shouldConvert = function (file_path) {
388
- // 检查是否启用AMD转换或Vue编译
389
- if (!this.config.convert_amd && !this.config.compile_vue) {
390
- return false;
391
- }
392
-
393
- // 然后检查文件扩展名是否在配置列表中
394
- let files = this.config.files;
395
- let len = files.length;
396
- for (let i = 0; i < len; i++) {
397
- if (file_path.endsWith(files[i])) {
398
- return true;
399
- }
400
- }
401
- return false;
402
- };
403
-
404
- /**
405
- * 处理Vue文件转换
406
- * @param {string} vue_content Vue文件内容
407
- * @returns {string} 转换后的代码
408
- */
409
- Static.prototype._handleVueFile = function (vue_content) {
410
- // 快速检查是否需要处理
411
- if (!this.config.convert_amd && !this.config.compile_vue) {
412
- return vue_content;
413
- }
414
-
415
- // 使用更高效的正则表达式匹配<script>标签
416
- let script_start = vue_content.indexOf('<script');
417
- if (script_start === -1) return vue_content;
418
-
419
- let script_end = vue_content.indexOf('</script>', script_start);
420
- if (script_end === -1) return vue_content;
421
-
422
- // 提取script标签开始和结束位置
423
- let tag_start = vue_content.indexOf('>', script_start) + 1;
424
- let script_content = vue_content.substring(tag_start, script_end).trim();
425
-
426
- if (!script_content) return vue_content;
427
-
428
- let conv_script;
429
-
430
- // 根据配置选择不同的转换方式
431
- if (this.config.convert_amd) {
432
- // 使用完整的AMD转换器处理script内容
433
- conv_script = this.convert.toAmd(script_content);
434
- } else if (this.config.compile_vue) {
435
- // 转换为可直接使用的Vue组件
436
- conv_script = this._convertToDirectVueComponent(script_content, vue_content);
437
- } else {
438
- // 保持原样
439
- conv_script = script_content;
440
- }
441
-
442
- // 当compile_vue为true时,直接返回转换后的JavaScript代码
443
- if (this.config.compile_vue && !this.config.convert_amd) {
444
- return conv_script;
445
- }
446
-
447
- // 直接替换script内容,避免复杂的正则表达式
448
- return vue_content.substring(0, tag_start) +
449
- '\n' + conv_script + '\n' +
450
- vue_content.substring(script_end);
451
- };
452
-
453
- /**
454
- * 将Vue文件转换为可直接使用的组件
455
- * @param {string} script_content script标签内容
456
- * @param {string} vue_content 完整的Vue文件内容
457
- * @returns {string} 可直接使用的Vue组件代码
458
- */
459
- Static.prototype._convertToDirectVueComponent = function (script_content, vue_content) {
460
- if (!this._shouldParseVue(vue_content)) {
461
- return script_content;
462
- }
463
-
464
- try {
465
- const desc = this._parseVueDescriptor(vue_content);
466
- const script_parsed = desc.script ? desc.script.content.trim() : script_content;
467
- const name = this._extractComponentName(script_parsed);
468
- const opts = this._cleanScriptContent(script_parsed);
469
-
470
- return this._buildVueComponentCode(name, opts, desc);
471
- } catch (_error) {
472
- // Vue文件解析失败,使用回退方案
473
- return this._fallbackVueConversion(script_content, vue_content);
474
- }
475
- };
476
-
477
- /**
478
- * 检查是否需要解析Vue文件
479
- * @param {string} vue_content Vue文件内容
480
- * @returns {boolean} 是否需要解析
481
- */
482
- Static.prototype._shouldParseVue = function (vue_content) {
483
- if (!this.config.compile_vue) {
484
- return false;
485
- }
486
- return vue_content.includes('<template') || vue_content.includes('<script');
487
- };
488
-
489
- /**
490
- * 解析Vue文件描述符
491
- * @param {string} vue_content Vue文件内容
492
- * @returns {object} Vue描述符
493
- */
494
- Static.prototype._parseVueDescriptor = function (vue_content) {
495
- let { descriptor, errors } = parse(vue_content);
496
-
497
- if (errors && errors.length > 0) {
498
- // Vue文件解析错误
499
- throw new Error('Vue解析错误');
500
- }
501
-
502
- return descriptor;
503
- };
504
-
505
- /**
506
- * 提取组件名称
507
- * @param {string} script_content 脚本内容
508
- * @returns {string} 组件名称
509
- */
510
- Static.prototype._extractComponentName = function (script_content) {
511
- const name_match = script_content.match(/name:\s*['"]([^'"]+)['"]/);
512
- return name_match && name_match[1] ? name_match[1] : 'VueComponent';
513
- };
514
-
515
- /**
516
- * 清理脚本内容
517
- * @param {string} script_content 脚本内容
518
- * @returns {string} 清理后的脚本
519
- */
520
- Static.prototype._cleanScriptContent = function (script_content) {
521
- let cleaned = script_content;
522
-
523
- if (cleaned.includes('import ') || cleaned.includes('export default')) {
524
- cleaned = cleaned
525
- .replace(/^\s*import\s+.*?\s+from\s+['"].*?['"];?\s*$/gm, '')
526
- .replace(/^\s*export\s+default\s*/, '')
527
- .trim();
528
- }
529
-
530
- if (cleaned.includes('ref(') || cleaned.includes('computed(')) {
531
- cleaned = cleaned
532
- .replace(/\bconst\s+(\w+)\s*=\s*ref\(/g, 'var $1 = Vue.ref(')
533
- .replace(/\bconst\s+(\w+)\s*=\s*computed\(/g, 'var $1 = Vue.computed(')
534
- .replace(/\bref\(/g, 'Vue.ref(')
535
- .replace(/\bcomputed\(/g, 'Vue.computed(')
536
- .replace(/Vue\.Vue\./g, 'Vue.');
537
- }
538
-
539
- return cleaned;
540
- };
541
-
542
- /**
543
- * 构建Vue组件代码
544
- * @param {string} name 组件名称
545
- * @param {string} opts 组件选项
546
- * @param {object} desc Vue描述符
547
- * @returns {string} 生成的组件代码
548
- */
549
- Static.prototype._buildVueComponentCode = function (name, opts, desc) {
550
- const template_content = desc.template ? desc.template.content.trim() : '';
551
- const has_styles = desc.styles && desc.styles.length > 0;
552
- const styles_content = has_styles ? desc.styles.map(style => style.content.trim()).join('\n') : '';
553
-
554
- const parts = this._buildVueComponentHeader(name, opts, template_content, styles_content);
555
-
556
- this._addStyleCode(parts, has_styles);
557
- this._addVueVersionCode(parts);
558
- this._addComponentRegistration(parts, name);
559
- this._addComponentExport(parts, name);
560
-
561
- parts.push(' return options;\n', '})();\n');
562
-
563
- return parts.join('');
564
- };
565
-
566
- /**
567
- * 添加样式代码
568
- * @param {Array} parts 代码部分数组
569
- * @param {boolean} has_styles 是否有样式
570
- */
571
- Static.prototype._addStyleCode = function (parts, has_styles) {
572
- if (has_styles) {
573
- parts.push(
574
- ' if (style) {\n',
575
- ' // 创建样式元素并添加到文档\n',
576
- ' if (typeof document !== \'undefined\') {\n',
577
- ' var styleElement = document.createElement(\'style\');\n',
578
- ' styleElement.textContent = style;\n',
579
- ' document.head.appendChild(styleElement);\n',
580
- ' }\n',
581
- ' }\n',
582
- ' \n'
583
- );
584
- }
585
- };
586
-
587
- /**
588
- * 添加Vue版本适配代码
589
- * @param {Array} parts 代码部分数组
590
- */
591
- Static.prototype._addVueVersionCode = function (parts) {
592
- parts.push(
593
- ' // 如果是Vue 3,使用defineComponent\n',
594
- ' if (typeof Vue !== \'undefined\' && Vue.version && Vue.version.startsWith(\'3\')) {\n',
595
- ' if (typeof Vue.defineComponent === \'function\') {\n',
596
- ' options = Vue.defineComponent(options);\n',
597
- ' }\n',
598
- ' }\n',
599
- ' \n'
600
- );
601
- };
602
-
603
- /**
604
- * 添加组件注册代码
605
- * @param {Array} parts 代码部分数组
606
- * @param {string} name 组件名称
607
- */
608
- Static.prototype._addComponentRegistration = function (parts, name) {
609
- parts.push(
610
- ' // 注册组件\n',
611
- ' if (typeof Vue !== \'undefined\') {\n',
612
- ' if (typeof Vue.component === \'function\') {\n',
613
- ' Vue.component(\'', name, '\', options);\n',
614
- ' }\n',
615
- ' }\n',
616
- ' \n'
617
- );
618
- };
619
-
620
- /**
621
- * 添加组件导出代码
622
- * @param {Array} parts 代码部分数组
623
- * @param {string} name 组件名称
624
- */
625
- Static.prototype._addComponentExport = function (parts, name) {
626
- parts.push(
627
- ' // 导出组件(仅CommonJS和全局变量)\n',
628
- ' if (typeof module !== \'undefined\' && module.exports) {\n',
629
- ' module.exports = options;\n',
630
- ' }\n',
631
- ' \n',
632
- ' if (typeof window !== \'undefined\') {\n',
633
- ' window.', name, ' = options;\n',
634
- ' }\n',
635
- ' \n'
636
- );
637
- };
638
-
639
- /**
640
- * 构建Vue组件头部代码
641
- * @param {string} name 组件名称
642
- * @param {string} opts 组件选项
643
- * @param {string} template_content 模板内容
644
- * @param {string} styles_content 样式内容
645
- * @returns {Array} 代码部分数组
646
- */
647
- Static.prototype._buildVueComponentHeader = function (
648
- name, opts, template_content, styles_content
649
- ) {
650
- return [
651
- '// Vue组件: ', name, ' (使用@vue/compiler-sfc解析)\n',
652
- '(function() {\n',
653
- ' var template = \`', template_content, '\`;\n',
654
- ' var style = \`', styles_content, '\`;\n',
655
- ' \n',
656
- ' // 组件选项\n',
657
- ' var options = ', opts, ';\n',
658
- ' \n',
659
- ' // 添加template\n',
660
- ' if (template) {\n',
661
- ' options.template = template;\n',
662
- ' }\n',
663
- ' \n',
664
- ' // 添加样式(如果存在)\n'
665
- ];
666
- };
667
-
668
- /**
669
- * Vue文件解析失败时的回退转换方案
670
- * @param {string} script_content script标签内容
671
- * @param {string} vue_content 完整的Vue文件内容
672
- * @returns {string} 可直接使用的Vue组件代码
673
- */
674
- Static.prototype._fallbackVueConversion = function (script_content, vue_content) {
675
- const template_content = this._extractTemplateContent(vue_content);
676
- const style_content = this._extractStyleContent(vue_content);
677
- const name = this._extractComponentName(script_content);
678
- const clean_opts = this._simplifyComponentOptions(script_content);
679
-
680
- return this._buildFallbackVueCode(name, clean_opts, template_content, style_content);
681
- };
682
-
683
- /**
684
- * 提取模板内容
685
- * @param {string} vue_content Vue文件内容
686
- * @returns {string} 模板内容
687
- */
688
- Static.prototype._extractTemplateContent = function (vue_content) {
689
- if (!vue_content.includes('<template>')) {
690
- return '';
691
- }
692
-
693
- const template_start = vue_content.indexOf('<template>');
694
- const template_end = vue_content.indexOf('</template>', template_start);
695
-
696
- if (template_start !== -1 && template_end !== -1) {
697
- return vue_content.substring(template_start + 10, template_end).trim();
698
- }
699
-
700
- return '';
701
- };
702
-
703
- /**
704
- * 提取样式内容
705
- * @param {string} vue_content Vue文件内容
706
- * @returns {string} 样式内容
707
- */
708
- Static.prototype._extractStyleContent = function (vue_content) {
709
- if (!vue_content.includes('<style')) {
710
- return '';
711
- }
712
-
713
- const style_start = vue_content.indexOf('<style');
714
- const style_end = vue_content.indexOf('</style>', style_start);
715
-
716
- if (style_start !== -1 && style_end !== -1) {
717
- return vue_content.substring(vue_content.indexOf('>', style_start) + 1, style_end).trim();
718
- }
719
-
720
- return '';
721
- };
722
-
723
- /**
724
- * 简化组件选项
725
- * @param {string} script_content 脚本内容
726
- * @returns {string} 简化后的选项
727
- */
728
- Static.prototype._simplifyComponentOptions = function (script_content) {
729
- let clean_opts = script_content
730
- .replace(/^\s*import\s+.*?from\s+['"].*?['"];?\s*$/gm, '')
731
- .trim();
732
-
733
- if (clean_opts.includes('export default')) {
734
- clean_opts = clean_opts.replace(/^\s*export\s+default\s*/, '').trim();
735
- }
736
-
737
- return clean_opts
738
- .replace(/setup\(\)\s*\{[\s\S]*?\}/, 'setup() { return {}; }')
739
- .replace(/ref\([^)]*\)/g, '0')
740
- .replace(/computed\([^)]*\)/g, 'function() { return 0; }')
741
- .replace(/ChildComponent/g, 'null');
742
- };
743
-
744
- /**
745
- * 构建回退方案Vue代码
746
- * @param {string} name 组件名称
747
- * @param {string} clean_opts 清理后的选项
748
- * @param {string} template_content 模板内容
749
- * @param {string} style_content 样式内容
750
- * @returns {string} 生成的代码
751
- */
752
- Static.prototype._buildFallbackVueCode = function (
753
- name, clean_opts, template_content, style_content
754
- ) {
755
- const parts = this._buildFallbackHeader(name, clean_opts, template_content, style_content);
756
-
757
- this._addFallbackStyleCode(parts, style_content);
758
- this._addComponentRegistration(parts, name);
759
- this._addComponentExport(parts, name);
760
-
761
- parts.push(' return options;\n', '})();\n');
762
-
763
- return parts.join('');
764
- };
765
-
766
- /**
767
- * 构建回退方案头部代码
768
- * @param {string} name 组件名称
769
- * @param {string} clean_opts 清理后的选项
770
- * @param {string} template_content 模板内容
771
- * @param {string} style_content 样式内容
772
- * @returns {Array} 代码部分数组
773
- */
774
- Static.prototype._buildFallbackHeader = function (
775
- name, clean_opts, template_content, style_content
776
- ) {
777
- return [
778
- '// Vue组件: ', name, ' (回退方案)\n',
779
- '(function() {\n',
780
- ' var template = \`', template_content, '\`;\n',
781
- ' var style = \`', style_content, '\`;\n',
782
- ' \n',
783
- ' // 组件选项\n',
784
- ' var options = ', clean_opts, ';\n',
785
- ' \n',
786
- ' // 添加template\n',
787
- ' if (template) {\n',
788
- ' options.template = template;\n',
789
- ' }\n',
790
- ' \n'
791
- ];
792
- };
793
-
794
- /**
795
- * 添加回退方案样式代码
796
- * @param {Array} parts 代码部分数组
797
- * @param {string} style_content 样式内容
798
- */
799
- Static.prototype._addFallbackStyleCode = function (parts, style_content) {
800
- if (style_content) {
801
- parts.push(
802
- ' // 添加样式(如果存在)\n',
803
- ' if (style) {\n',
804
- ' // 创建样式元素并添加到文档\n',
805
- ' if (typeof document !== \'undefined\') {\n',
806
- ' var styleElement = document.createElement(\'style\');\n',
807
- ' styleElement.textContent = style;\n',
808
- ' document.head.appendChild(styleElement);\n',
809
- ' }\n',
810
- ' }\n',
811
- ' \n'
812
- );
813
- }
814
- };
815
-
816
- /**
817
- * 处理HTML文件,转换其中的script标签
818
- * @param {string} html_content HTML文件内容
819
- * @returns {string} 转换后的HTML文件
820
- */
821
- Static.prototype._handleHtmlFile = function (html_content) {
822
- // 快速检查是否需要处理
823
- if (!this.config.convert_amd && !this.config.compile_vue) {
824
- return html_content;
825
- }
826
-
827
- // 使用更高效的正则表达式匹配<script>标签
828
- let script_regex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
829
- let conv_html = html_content;
830
- let match;
831
-
832
- while ((match = script_regex.exec(html_content)) !== null) {
833
- let script_content = match[1].trim();
834
-
835
- // 跳过空内容和已转换的内容
836
- if (!script_content || script_content.includes('define(')) {
837
- continue;
838
- }
839
-
840
- // 转换script内容
841
- let conv_script = this.convert.toAmd(script_content);
842
-
843
- // 直接替换,避免多次字符串操作
844
- conv_html = conv_html.replace(match[0], `<script>\n${conv_script}\n</script>`);
845
- }
846
-
847
- return conv_html;
848
- };
849
-
850
-
851
-
852
- /**
853
- * 获取MIME类型
854
- * @param {string} path 文件路径
855
- * @returns {string} MIME类型
856
- */
857
- Static.prototype._getMimeType = function (path) {
858
- if (path.endsWith('.js')) {
859
- return 'application/javascript; charset=utf-8';
860
- } else if (path.endsWith('.css')) {
861
- return 'text/css; charset=utf-8';
862
- } else if (path.endsWith('.html')) {
863
- return 'text/html; charset=utf-8';
864
- } else if (path.endsWith('.vue')) {
865
- return 'application/javascript; charset=utf-8';
866
- } else {
867
- return 'text/plain; charset=utf-8';
868
- }
869
- };
870
-
871
- /**
872
- * 设置响应头
873
- * @param {object} ctx 上下文
874
- * @param {string} path 文件路径
875
- */
876
- Static.prototype._setHeaders = function (ctx, path) {
877
- if (!ctx.response.type) {
878
- ctx.response.type = this._getMimeType(path);
879
- }
880
-
881
- if (this.config.max_age) {
882
- let cache_control = 'max-age=' + this.config.max_age;
883
- if (this.config.immutable) {
884
- cache_control += ',immutable';
885
- }
886
- ctx.set('Cache-Control', cache_control);
887
- }
888
- };
889
-
890
- /**
891
- * 发送二进制文件
892
- * @param {object} ctx 请求上下文
893
- * @param {string} file_path 文件路径
894
- */
895
- Static.prototype._sendBinaryFile = async function (ctx, file_path) {
896
- // 处理二进制文件 - 使用koa-send优化大文件传输
897
- let relative_path = file_path.startsWith('/') ? file_path.substring(1) : file_path;
898
-
899
- // 使用koa-send发送文件,支持流式传输、断点续传等优化
900
- await send(ctx, relative_path, {
901
- root: this.config.root,
902
- maxage: this.config.max_age || 7200000, // 2小时缓存
903
- immutable: this.config.immutable || false
904
- });
905
- };
906
-
907
- /**
908
- * 从缓存发送文件内容
909
- * @param {object} ctx 请求上下文
910
- * @param {string} file_path 文件路径
911
- * @param {object} data 缓存数据
912
- */
913
- Static.prototype._sendFromCache = function (ctx, file_path, data) {
914
- // 使用缓存内容
915
- ctx.status = 200;
916
- ctx.type = data.mime_type;
917
- ctx.body = data.content;
918
-
919
- // 设置缓存头
920
- let headers = this._getCacheHeaders();
921
- for (let k in headers) {
922
- ctx.set(k, headers[k]);
923
- }
924
- };
925
-
926
- /**
927
- * 发送新文件内容并缓存
928
- * @param {object} ctx 请求上下文
929
- * @param {string} file_path 文件路径
930
- * @param {string} full_path 完整文件路径
931
- */
932
- Static.prototype._sendNewContent = async function (ctx, file_path, full_path) {
933
- let original_content = full_path.loadText();
934
-
935
- // 检查文件内容是否成功加载
936
- if (original_content === undefined) {
937
- ctx.status = 404;
938
- return;
939
- }
940
-
941
- let mime_type = this._getMimeType(file_path);
942
-
943
- // 检查是否需要转换
944
- let final_content = original_content;
945
- if (this._shouldConvert(file_path)) {
946
- if (file_path.endsWith('.vue')) {
947
- final_content = this._handleVueFile(original_content);
948
- } else if (file_path.endsWith('.html')) {
949
- final_content = this._handleHtmlFile(original_content);
950
- } else if (this.config.convert_amd) {
951
- // 只有启用AMD转换时才转换JS文件
952
- final_content = this.convert.toAmd(original_content);
953
- }
954
- // 如果convert_amd为false但compile_vue为true,JS文件保持原样
955
- }
956
-
957
- // 获取文件修改时间戳
958
- let file_mtime = this._getFileMtime(full_path);
959
-
960
- // 缓存转换后的内容(包含文件修改时间戳和文件监听)
961
- await this._setCache(file_path, final_content, mime_type, file_mtime, full_path);
962
-
963
- // 设置响应
964
- ctx.status = 200;
965
- ctx.type = mime_type;
966
- ctx.body = final_content;
967
-
968
- // 设置缓存头
969
- let headers = this._getCacheHeaders();
970
- for (let k in headers) {
971
- ctx.set(k, headers[k]);
972
- }
973
- };
974
-
975
- Static.prototype._send = async function (ctx, file_path) {
976
- try {
977
- let relative_path = file_path.startsWith('/') ? file_path.substring(1) : file_path;
978
- let full_path = relative_path.fullname(this.config.root);
979
-
980
- if (!full_path.isFile()) {
981
- ctx.status = 404;
982
- return;
983
- }
984
-
985
- let is_text = this.isTextFile(file_path);
986
-
987
- if (!is_text) {
988
- await this._sendBinaryFile(ctx, file_path);
989
- return;
990
- }
991
-
992
- // 先检查基于路径的缓存(不读取文件内容)
993
- let data = await this._getCache(file_path, full_path);
994
- if (data) {
995
- this._sendFromCache(ctx, file_path, data);
996
- return;
997
- }
998
-
999
- // 处理新内容并设置缓存
1000
- await this._sendNewContent(ctx, file_path, full_path);
1001
- } catch (error) {
1002
- ctx.status = 500;
1003
- ctx.body = 'Internal Server Error: ' + error.message;
1004
- }
1005
- };
1006
-
1007
- /**
1008
- * 获取文本文件扩展名列表
1009
- * @returns {Array<string>} 文本文件扩展名数组
1010
- */
1011
- Static.prototype._getTextExtensions = function () {
1012
- return [
1013
- // 代码文件
1014
- '.js', '.jsx', '.ts', '.tsx', '.vue', '.html', '.htm', '.css', '.scss', '.less',
1015
- '.json', '.xml', '.yaml', '.yml', '.md', '.markdown',
1016
-
1017
- // 配置文件
1018
- '.config', '.conf', '.ini', '.properties', '.env', '.gitignore',
1019
-
1020
- // 脚本文件
1021
- '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
1022
-
1023
- // 模板文件
1024
- '.ejs', '.pug', '.jade', '.handlebars', '.hbs',
1025
-
1026
- // 数据文件
1027
- '.csv', '.txt', '.log', '.sql', '.graphql',
1028
-
1029
- // 其他文本格式
1030
- '.rtf', '.tex', '.rst', '.adoc'
1031
- ];
1032
- };
1033
-
1034
- /**
1035
- * 通过扩展名判断是否为文本文件
1036
- * @param {string} file_path 文件路径
1037
- * @returns {boolean} 是否为文本文件
1038
- */
1039
- Static.prototype._isTextByExtension = function (file_path) {
1040
- let lower_path = file_path.toLowerCase();
1041
- let text_exts = this._getTextExtensions();
1042
- return text_exts.some(ext => lower_path.endsWith(ext));
1043
- };
1044
-
1045
- /**
1046
- * 通过文件内容判断是否为文本文件
1047
- * @param {string} file_path 文件路径
1048
- * @returns {boolean} 是否为文本文件
1049
- */
1050
- Static.prototype._isTextByContent = function (file_path) {
1051
- try {
1052
- if (!file_path.hasFile()) return false;
1053
-
1054
- let stats = file_path.stat();
1055
- if (!stats || !stats.isFile()) return false;
1056
-
1057
- let content = file_path.loadText('binary');
1058
- if (!content) return false;
1059
-
1060
- let buffer = Buffer.from(content, 'binary');
1061
- let bytes_read = Math.min(buffer.length, 512);
1062
-
1063
- let count = 0;
1064
- for (let i = 0; i < bytes_read; i++) {
1065
- if ((buffer[i] >= 32 && buffer[i] <= 126) ||
1066
- buffer[i] === 9 || buffer[i] === 10 || buffer[i] === 13) {
1067
- count++;
1068
- }
1069
- }
1070
-
1071
- return (count / bytes_read) > 0.9;
1072
- } catch (_error) {
1073
- return false;
1074
- }
1075
- };
1076
-
1077
- Static.prototype.isTextFile = function (file_path) {
1078
- let is_text_by_content = this._isTextByContent(file_path);
1079
- let is_text_by_ext = this._isTextByExtension(file_path);
1080
-
1081
- return is_text_by_content || is_text_by_ext;
1082
- };
1083
-
1084
- /**
1085
- * 获取二进制文件扩展名列表
1086
- * @returns {Array<string>} 二进制文件扩展名数组
1087
- */
1088
- Static.prototype._getBinaryExtensions = function () {
1089
- return [
1090
- // 图片文件
1091
- '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.ico', '.svg',
1092
- '.tiff', '.tif', '.psd', '.ai', '.eps', '.raw',
1093
-
1094
- // 视频文件
1095
- '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.m4v',
1096
- '.3gp', '.mpeg', '.mpg', '.ts', '.m2ts',
1097
-
1098
- // 音频文件
1099
- '.mp3', '.wav', '.ogg', '.aac', '.flac', '.wma', '.m4a', '.ape',
1100
-
1101
- // 字体文件
1102
- '.woff', '.woff2', '.ttf', '.eot', '.otf',
1103
-
1104
- // 文档和压缩文件
1105
- '.pdf', '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz',
1106
- '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
1107
-
1108
- // 可执行文件和安装包
1109
- '.exe', '.dll', '.so', '.dmg', '.msi', '.deb', '.rpm', '.apk',
1110
-
1111
- // 数据文件
1112
- '.db', '.sqlite', '.dat', '.bin', '.iso', '.img'
1113
- ];
1114
- };
1115
-
1116
- /**
1117
- * 通过扩展名判断是否为二进制文件
1118
- * @param {string} file_path 文件路径
1119
- * @returns {boolean} 是否为二进制文件
1120
- */
1121
- Static.prototype._isBinaryByExtension = function (file_path) {
1122
- let lower_path = file_path.toLowerCase();
1123
- let binary_exts = this._getBinaryExtensions();
1124
- return binary_exts.some(ext => lower_path.endsWith(ext));
1125
- };
1126
-
1127
- /**
1128
- * 通过文件内容判断是否为二进制文件
1129
- * @param {string} file_path 文件路径
1130
- * @returns {boolean} 是否为二进制文件
1131
- */
1132
- Static.prototype._isBinaryByContent = function (file_path) {
1133
- try {
1134
- if (!file_path.hasFile()) return false;
1135
-
1136
- let stats = file_path.stat();
1137
- if (!stats || !stats.isFile()) return false;
1138
-
1139
- let content = file_path.loadText('binary');
1140
- if (!content) return false;
1141
-
1142
- let buffer = Buffer.from(content, 'binary');
1143
- let bytes_read = Math.min(buffer.length, 512);
1144
-
1145
- let has_binary_chars = false;
1146
- for (let i = 0; i < bytes_read; i++) {
1147
- if (buffer[i] === 0 ||
1148
- (buffer[i] < 32 && buffer[i] !== 9 && buffer[i] !== 10 && buffer[i] !== 13)) {
1149
- has_binary_chars = true;
1150
- break;
1151
- }
1152
- }
1153
-
1154
- return has_binary_chars;
1155
- } catch (_error) {
1156
- return false;
1157
- }
1158
- };
1159
-
1160
- Static.prototype.isBinaryFile = function (file_path) {
1161
- if (this.isTextFile(file_path)) {
1162
- return false;
1163
- }
1164
-
1165
- let is_binary_by_content = this._isBinaryByContent(file_path);
1166
- let is_binary_by_ext = this._isBinaryByExtension(file_path);
1167
-
1168
- return is_binary_by_content || is_binary_by_ext;
1169
- };
1170
-
1171
- /**
1172
- * 获取缓存头信息
1173
- * @returns {object} 缓存头对象
1174
- */
1175
- Static.prototype._getCacheHeaders = function () {
1176
- // 直接使用缓存的头对象
1177
- return this._cache_headers;
1178
- };
1179
-
1180
- /**
1181
- * 读取二进制文件
1182
- * @param {string} file_path 文件路径
1183
- * @returns {Buffer} 文件内容
1184
- */
1185
- Static.prototype._readBinaryFile = async function (file_path) {
1186
- let relative_path = file_path.startsWith('/') ? file_path.substring(1) : file_path;
1187
- let full_path = relative_path.fullname(this.config.root);
1188
-
1189
- if (full_path.isFile() && full_path.hasFile()) {
1190
- return full_path.loadText('binary');
1191
- }
1192
-
1193
- return null;
1194
- };
1195
-
1196
- /**
1197
- * 执行静态文件处理
1198
- * @param {object} ctx http请求上下文
1199
- * @param {Function} next 跳过当前执行,先执行后面函数
1200
- * @returns {boolean} 是否执行成功
1201
- */
1202
- Static.prototype.run = async function (ctx, next) {
1203
- await next();
1204
- if (!this._shouldProcess(ctx)) return;
1205
-
1206
- // 处理静态文件请求
1207
- let result = await this._send(ctx.path);
1208
-
1209
- // 设置响应状态和内容
1210
- ctx.status = result.status;
1211
- if (result.type) ctx.type = result.type;
1212
- if (result.body) ctx.body = result.body;
1213
- if (result.headers) {
1214
- for (let key in result.headers) {
1215
- ctx.set(key, result.headers[key]);
1216
- }
1217
- }
1218
- };
1219
-
1220
- // 导出Static类用于测试
1221
- module.exports = {
1222
- Static,
1223
- static(config) {
1224
- let instance = new Static(config);
1225
- return async function (ctx, next) {
1226
- await instance.main(ctx, next);
1227
- };
1228
- }
1229
- };
1
+ let send = require('koa-send');
2
+ const { EsToAmdConvert } = require('mm_es6_to_amd');
3
+ const { parse } = require('@vue/compiler-sfc');
4
+ const { compile } = require('@vue/compiler-dom');
5
+ const prettier = require('prettier');
6
+
7
+ /**
8
+ * 静态文件处理类
9
+ */
10
+ class Static {
11
+ static config = {
12
+ index: 'index.html',
13
+ max_age: 7200,
14
+ key_prefix: 'static:',
15
+ cache: true,
16
+ cache_age: 7200,
17
+ immutable: true,
18
+ hidden: false,
19
+ format: true,
20
+ extensions: false,
21
+ brotli: false,
22
+ gzip: false,
23
+ root: './static',
24
+ compile_vue: true,
25
+ path: '/src',
26
+ files: ['.js', '.vue', '.html'],
27
+ convert_amd: true,
28
+ watch: false,
29
+ watch_files: ['.js', '.css', '.html', '.vue', '.json', '.md', '.txt', '.xml']
30
+ };
31
+
32
+ /**
33
+ * 初始化静态文件处理类
34
+ * @param {object} config 配置
35
+ */
36
+ constructor(config) {
37
+ this.config = { ...Static.config };
38
+ this.convert = new EsToAmdConvert();
39
+ this._cache = null;
40
+ this._watcher = null;
41
+ this.setConfig(config);
42
+ this._init();
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 设置配置
48
+ * @param {object} config 配置
49
+ */
50
+ Static.prototype.setConfig = function (config) {
51
+ Object.assign(this.config, config || {});
52
+ };
53
+
54
+ /**
55
+ * 初始化缓存
56
+ */
57
+ Static.prototype._init = function () {
58
+ if (!$.cache) {
59
+ const { cacheAdmin } = require('mm_cache');
60
+ $.cache = cacheAdmin('sys', this.config);
61
+ }
62
+ this._cache = $.cache;
63
+
64
+ // 如果配置了文件监听,初始化文件监听器
65
+ if (this.config.watch && this.config.watch_files.length > 0) {
66
+ this._initWatcher();
67
+ }
68
+ };
69
+
70
+ /**
71
+ * 检测代码是否包含ES6语法
72
+ * @param {string} code 代码内容
73
+ * @returns {boolean} 是否包含ES6语法
74
+ */
75
+ Static.prototype._isEs6Code = function (code) {
76
+ if (!code || typeof code !== 'string') {
77
+ return false;
78
+ }
79
+
80
+ // ES6语法特征检测
81
+ const es6_patterns = [
82
+ /\bimport\s+[^;]+;/g, // import语句
83
+ /\bexport\s+[^;]+;/g, // export语句
84
+ /\bclass\s+\w+/g, // class声明
85
+ /\bconst\s+\w+/g, // const声明
86
+ /\blet\s+\w+/g, // let声明
87
+ /\([^)]*\)\s*=>/g, // 箭头函数
88
+ /`[^`]*`/g, // 模板字符串
89
+ /\{[^}]*\}\s*=\s*\w+/g, // 解构赋值
90
+ /\bPromise\b/g, // Promise对象
91
+ /\basync\s+function\b/g, // async函数
92
+ /\bawait\b/g // await关键字
93
+ ];
94
+
95
+ for (let i = 0; i < es6_patterns.length; i++) {
96
+ if (es6_patterns[i].test(code)) {
97
+ return true;
98
+ }
99
+ }
100
+
101
+ return false;
102
+ };
103
+
104
+ /**
105
+ * 格式化代码
106
+ * @param {string} code 代码内容
107
+ * @param {string} file_type 文件类型
108
+ * @returns {Promise<string>} 格式化后的代码
109
+ */
110
+ Static.prototype._formatCode = async function (code, file_type) {
111
+ if (!this.config.format || !code || typeof code !== 'string') {
112
+ return code;
113
+ }
114
+
115
+ try {
116
+ // 根据文件类型设置prettier配置
117
+ let parser = 'babel';
118
+ let plugins = [];
119
+
120
+ switch (file_type) {
121
+ case 'js':
122
+ parser = 'babel';
123
+ break;
124
+ case 'html':
125
+ parser = 'html';
126
+ break;
127
+ case 'vue':
128
+ parser = 'vue';
129
+ plugins = [require('prettier/parser-html'), require('prettier/parser-babel')];
130
+ break;
131
+ default:
132
+ parser = 'babel';
133
+ }
134
+
135
+ // prettier.format是异步函数
136
+ const formatted_code = await prettier.format(code, {
137
+ parser: parser,
138
+ plugins: plugins,
139
+ semi: true,
140
+ singleQuote: true,
141
+ trailingComma: 'es5',
142
+ printWidth: 100,
143
+ tabWidth: 2,
144
+ useTabs: false,
145
+ bracketSpacing: true,
146
+ arrowParens: 'avoid'
147
+ });
148
+
149
+ return formatted_code;
150
+ } catch (error) {
151
+ // 格式化失败时返回原代码
152
+ console.warn(`格式化代码失败: ${error.message}`);
153
+ return code;
154
+ }
155
+ };
156
+
157
+ /**
158
+ * 转换为AMD模块
159
+ * @param {string} file_type 文件类型
160
+ * @param {string} text 代码
161
+ * @returns {Promise<string>} AMD模块代码
162
+ */
163
+ Static.prototype._toAmd = async function (file_type, text) {
164
+ let txt = text;
165
+ if (file_type === 'js') {
166
+ // 对JS文件,先检测是否为ES6代码,只有ES6代码才进行转换
167
+ if (this._isEs6Code(txt)) {
168
+ let code = this.convert.toAmd(txt);
169
+ // 格式化代码
170
+ return await this._formatCode(code, file_type);
171
+ } else {
172
+ // 格式化非ES6代码
173
+ return await this._formatCode(txt, file_type);
174
+ }
175
+ } else if (file_type === 'html' || file_type === 'vue') {
176
+ // 获取html或vue中script标签的代码,将ES6语法的代码转换为AMD模块
177
+ let match = txt.match(/<script[^>]*>([\s\S]*?)<\/script>/g);
178
+ if (match) {
179
+ for (let i = 0; i < match.length; i++) {
180
+ let script = match[i];
181
+ // 直接提取script标签内容,避免重复匹配
182
+ let inner_code = script.replace(/^<script[^>]*>([\s\S]*?)<\/script>$/, '$1').trim();
183
+ if (inner_code) {
184
+ // 先检测script标签内的代码是否为ES6语法
185
+ if (this._isEs6Code(inner_code)) {
186
+ let amd_code = this.convert.toAmd(inner_code);
187
+ // 移除type="module"属性,因为AMD模块不需要这个属性
188
+ let new_script = script.replace(/type\s*=\s*["']module["']/g, '').replace(inner_code, amd_code);
189
+ // 清理多余的空格
190
+ new_script = new_script.replace(/<script\s+>/, '<script>').replace(/<script\s+([^>]+)>/, '<script $1>');
191
+ txt = txt.replace(script, new_script);
192
+ }
193
+ // 如果不是ES6代码,保持原样不进行转换
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ // 格式化代码后返回,确保返回字符串而不是Promise
200
+ const result = await this._formatCode(txt, file_type);
201
+ return result;
202
+ };
203
+
204
+ /**
205
+ * 处理Vue模板部分
206
+ * @param {object} desc Vue组件描述符
207
+ * @returns {string} 编译后的render函数
208
+ */
209
+ Static.prototype._processTemplate = function (desc) {
210
+ if (!desc.template) return '';
211
+
212
+ const template_result = compile(desc.template.content, {
213
+ mode: 'module'
214
+ });
215
+
216
+ // 提取render函数代码,去除import语句和export语句
217
+ let render_function = template_result.code
218
+ .replace(/^import[^;]+;\s*/gm, '')
219
+ .replace(/^export[^;]+;\s*/gm, '')
220
+ .trim();
221
+
222
+ // 如果render函数包含函数定义,提取函数体
223
+ let render_match = render_function.match(/function\s+render\s*\([^)]*\)\s*{([\s\S]*?)}\s*$/);
224
+ if (render_match) {
225
+ render_function = `function render(_ctx, _cache) {${render_match[1]}}`;
226
+ }
227
+
228
+ return render_function;
229
+ };
230
+
231
+ /**
232
+ * 处理Vue脚本部分
233
+ * @param {object} desc Vue组件描述符
234
+ * @param {string} render_function render函数
235
+ * @returns {string} 处理后的脚本代码
236
+ */
237
+ Static.prototype._processScript = function (desc, render_function) {
238
+ if (!desc.script) {
239
+ return this._createDefaultComp(render_function);
240
+ }
241
+
242
+ let script_content = desc.script.content;
243
+
244
+ // 提取import语句
245
+ let imports = '';
246
+ let import_match = script_content.match(/import[^;]+;/g);
247
+ if (import_match) {
248
+ imports = import_match.join('\n') + '\n';
249
+ }
250
+
251
+ // 提取export default部分,保留完整的组件定义
252
+ let export_match = script_content.match(/export\s+default\s*({[\s\S]*?})\s*$/s);
253
+ if (export_match) {
254
+ return this._buildCompWithExport(export_match[1], imports, render_function);
255
+ } else {
256
+ return this._buildDefaultComp(imports, render_function);
257
+ }
258
+ };
259
+
260
+ /**
261
+ * 创建默认组件结构
262
+ * @param {string} render_function render函数
263
+ * @returns {string} 默认组件代码
264
+ */
265
+ Static.prototype._createDefaultComp = function (render_function) {
266
+ return `
267
+ export default {
268
+ name: 'VueComponent',
269
+ data() {
270
+ return {}
271
+ }${render_function ? `,\n render: ${render_function}` : ''}\n};
272
+ `;
273
+ };
274
+
275
+ /**
276
+ * 构建带有export的组件
277
+ * @param {string} code_lib 组件代码
278
+ * @param {string} imports import语句
279
+ * @param {string} render_function render函数
280
+ * @returns {string} 完整的组件代码
281
+ */
282
+ Static.prototype._buildCompWithExport = function (code_lib, imports, render_function) {
283
+ let proc_code = code_lib;
284
+
285
+ // 确保组件字符串是完整的对象结构
286
+ if (!proc_code.trim().endsWith('}')) {
287
+ proc_code = proc_code + '\n}';
288
+ }
289
+
290
+ // 如果render函数存在,将其添加到组件选项中
291
+ if (render_function) {
292
+ // 在最后一个属性之前插入render函数
293
+ if (proc_code.trim().endsWith('}')) {
294
+ proc_code = proc_code.replace(/}\s*$/, '');
295
+ // 确保最后一个属性后有逗号
296
+ if (!proc_code.trim().endsWith(',')) {
297
+ proc_code += ',';
298
+ }
299
+ proc_code += `\n render: ${render_function}\n}`;
300
+ }
301
+ }
302
+
303
+ return `${imports}
304
+ export default ${proc_code};
305
+ `;
306
+ };
307
+
308
+ /**
309
+ * 构建默认组件
310
+ * @param {string} imports import语句
311
+ * @param {string} render_function render函数
312
+ * @returns {string} 默认组件代码
313
+ */
314
+ Static.prototype._buildDefaultComp = function (imports, render_function) {
315
+ return `${imports}
316
+ export default {
317
+ name: 'VueComponent',
318
+ data() {
319
+ return {}
320
+ }${render_function ? `,\n render: ${render_function}` : ''}\n};
321
+ `;
322
+ };
323
+
324
+ /**
325
+ * 编译Vue组件
326
+ * @param {string} code Vue组件代码
327
+ * @returns {string} 可直接调用的JavaScript组件代码
328
+ */
329
+ Static.prototype._compileVue = function (code) {
330
+ let desc = parse(code).descriptor;
331
+ let render_function = this._processTemplate(desc);
332
+ return this._processScript(desc, render_function);
333
+ };
334
+
335
+ /**
336
+ * 发送文件
337
+ * @param {object} ctx Koa上下文对象
338
+ * @param {string} path 文件路径
339
+ */
340
+ Static.prototype._sendFile = async function (ctx, path) {
341
+ // koa-send会自动处理查询参数,直接传递路径
342
+ await send(ctx, path, this.config);
343
+ };
344
+
345
+ /**
346
+ * 运行AMD模块
347
+ * @param {string} path 文件路径
348
+ * @param {string} file_type 文件类型
349
+ * @returns {Promise<string>} AMD模块代码
350
+ */
351
+ Static.prototype._runAmd = async function (path, file_type) {
352
+ let type = file_type || this._getType(path);
353
+ let code = null;
354
+ let text = `.${path}`.fullname(this.config.root).loadText();
355
+ if (text) {
356
+ code = await this._toAmd(type, text);
357
+ }
358
+ return code;
359
+ };
360
+
361
+ /**
362
+ * 运行Vue组件
363
+ * @param {string} path 文件路径
364
+ * @returns {string} 编译后的代码
365
+ */
366
+ Static.prototype._runVue = function (path) {
367
+ let code = null;
368
+ let file = `.${path}`.fullname(this.config.root);
369
+ let text = file.loadText();
370
+ if (text) {
371
+ code = this._compileVue(text);
372
+ }
373
+ return code;
374
+ };
375
+
376
+ /**
377
+ * 返回响应
378
+ * @param {object} ctx Koa上下文对象
379
+ * @param {string} body 响应体
380
+ * @param {object} headers 响应头
381
+ * @returns {object} 响应对象
382
+ */
383
+ Static.prototype._return = function (ctx, body, headers) {
384
+ ctx.body = body;
385
+ if (headers) {
386
+ ctx.set(headers);
387
+ }
388
+ return { body, headers };
389
+ };
390
+
391
+ /**
392
+ * 初始化文件监听器
393
+ */
394
+ Static.prototype._initWatcher = function () {
395
+ try {
396
+ const chokidar = require('chokidar');
397
+
398
+ // 构建监听模式,只监听指定类型的文件
399
+ const watch_patterns = this.config.watch_files.map(ext =>
400
+ `${this.config.root}/**/*${ext}`
401
+ );
402
+
403
+ // 监听指定类型的文件
404
+ this._watcher = chokidar.watch(watch_patterns, {
405
+ ignored: /(^|[\/\\])\\./, // 忽略隐藏文件
406
+ persistent: true,
407
+ ignoreInitial: true // 忽略初始扫描事件
408
+ });
409
+
410
+ // 文件变化时的处理逻辑
411
+ this._watcher.on('all', (event, file_path) => {
412
+ this._handleFileChange(event, file_path);
413
+ });
414
+ } catch (error) {
415
+ console.error('文件监听初始化失败:', error.message);
416
+ }
417
+ };
418
+
419
+ /**
420
+ * 处理文件变化事件
421
+ * @param {string} event 事件类型
422
+ * @param {string} file_path 文件路径
423
+ */
424
+ Static.prototype._handleFileChange = function (event, file_path) {
425
+ // 构建缓存键(相对于静态目录的路径)
426
+ const relative_path = file_path.replace(this.config.root, '').replace(/^[\\\/]/, '');
427
+ const cache_key = this.config.key_prefix + relative_path;
428
+
429
+ // 根据事件类型处理缓存
430
+ switch (event) {
431
+ case 'change':
432
+ case 'add':
433
+ // 文件修改或新增,删除缓存,下次请求时重新生成
434
+ this._cache.del(cache_key);
435
+ break;
436
+ case 'unlink':
437
+ // 文件删除,删除缓存
438
+ this._cache.del(cache_key);
439
+ break;
440
+ }
441
+ };
442
+
443
+ /**
444
+ * 获取响应头
445
+ * @param {string} file_type 文件类型
446
+ * @returns {object} 响应头
447
+ */
448
+ Static.prototype._getHeaders = function (file_type) {
449
+ let headers = {};
450
+
451
+ // 缓存控制逻辑
452
+ if (this.config.max_age > 0) {
453
+ let cache_control = `max-age=${this.config.max_age}`;
454
+ if (this.config.immutable) {
455
+ cache_control += ', immutable';
456
+ }
457
+ headers['cache-control'] = cache_control;
458
+ }
459
+ switch (file_type) {
460
+ case 'js':
461
+ headers['content-type'] = 'application/javascript; charset=utf-8';
462
+ break;
463
+ case 'vue':
464
+ if (this.config.convert_amd) {
465
+ headers['content-type'] = 'text/html; charset=utf-8';
466
+ }
467
+ else if (this.config.compile_vue) {
468
+ headers['content-type'] = 'text/javascript; charset=utf-8';
469
+ }
470
+ break;
471
+ case 'html':
472
+ headers['content-type'] = 'text/html; charset=utf-8';
473
+ break;
474
+ default:
475
+ headers['content-type'] = 'text/plain; charset=utf-8';
476
+ break;
477
+ }
478
+ return headers;
479
+ };
480
+
481
+ /**
482
+ * 设置缓存
483
+ * @param {string} path 文件路径(包含查询参数)
484
+ * @param {object} ret 响应体和响应头
485
+ */
486
+ Static.prototype._setCache = async function (path, ret) {
487
+ // 使用完整路径(包含查询参数)作为缓存键
488
+ await this._cache.set(this.config.key_prefix + path, ret, this.config.cache_age);
489
+ };
490
+
491
+ /**
492
+ * 获取文件类型
493
+ * @param {string} path 文件路径
494
+ * @returns {string} 文件类型(不含点号)
495
+ */
496
+ Static.prototype._getType = function (path) {
497
+
498
+ // 使用mm_expand提供的extname方法获取扩展名
499
+ const ext = path.extname().toLowerCase();
500
+ if (ext) {
501
+ return ext.slice(1); // 去掉点号
502
+ }
503
+
504
+ // 备用方法:处理多扩展名情况
505
+ const basename = path.basename();
506
+ const parts = basename.split('.');
507
+
508
+ // 如果文件名包含多个点号,取最后一个作为扩展名
509
+ if (parts.length > 1) {
510
+ return parts[parts.length - 1];
511
+ }
512
+
513
+ return ''; // 无扩展名
514
+ };
515
+
516
+ /**
517
+ * 获取缓存
518
+ * @param {string} path 文件路径(包含查询参数)
519
+ * @returns {object} 响应体和响应头
520
+ */
521
+ Static.prototype._getCache = async function (path) {
522
+ // 使用完整路径(包含查询参数)作为缓存键
523
+ return await this._cache.get(this.config.key_prefix + path);
524
+ };
525
+
526
+ /**
527
+ * 删除缓存
528
+ * @param {string} path 文件路径(包含查询参数)
529
+ */
530
+ Static.prototype._delCache = async function (path) {
531
+ await this._cache.del(this.config.key_prefix + path);
532
+ };
533
+
534
+ /**
535
+ * 处理缓存逻辑
536
+ * @param {string} url 完整URL
537
+ * @param {object} ctx Koa上下文对象
538
+ * @returns {boolean} 是否命中缓存
539
+ */
540
+ Static.prototype._handleCache = async function (url, ctx) {
541
+ if (!this.config.cache) return false;
542
+
543
+ let ret = await this._getCache(url);
544
+ if (ret) {
545
+ this._return(ctx, ret.body, ret.headers);
546
+ return true;
547
+ }
548
+ return false;
549
+ };
550
+
551
+ /**
552
+ * 处理文件编译逻辑
553
+ * @param {string} path 文件路径
554
+ * @returns {string|null} 编译后的内容
555
+ */
556
+ Static.prototype._handleFileCompile = function (path) {
557
+ if (!path.startsWith(this.config.path)) return null;
558
+
559
+ let file_type = this._getType(path);
560
+ let { compile_vue, convert_amd } = this.config;
561
+
562
+ if (convert_amd && this.config.files.includes('.' + file_type)) {
563
+ return this._runAmd(path, file_type);
564
+ }
565
+ else if (compile_vue && file_type === 'vue') {
566
+ return this._runVue(path);
567
+ }
568
+
569
+ return null;
570
+ };
571
+
572
+ /**
573
+ * 处理编译后的文件响应
574
+ * @param {string} url 完整URL
575
+ * @param {string} path 文件路径
576
+ * @param {string} body 响应体
577
+ * @param {object} ctx Koa上下文对象
578
+ */
579
+ Static.prototype._handleCompiledResp = async function (url, path, body, ctx) {
580
+ let file_type = this._getType(path);
581
+ let headers = this._getHeaders(file_type);
582
+ let ret = this._return(ctx, body, headers);
583
+
584
+ // 只有当缓存启用时才设置缓存
585
+ if (this.config.cache) {
586
+ await this._setCache(url, ret);
587
+ }
588
+ };
589
+
590
+ /**
591
+ * 主处理函数
592
+ * @param {object} ctx Koa上下文对象
593
+ * @param {Function} next Koa中间件函数
594
+ */
595
+ Static.prototype.main = async function (ctx, next) {
596
+ const url = ctx.url; // 使用完整URL(包含查询参数)
597
+ const path = ctx.path; // 仅用于文件路径检查
598
+
599
+ // 检查缓存
600
+ if (await this._handleCache(url, ctx)) {
601
+ return;
602
+ }
603
+
604
+ // 处理文件编译
605
+ let body = this._handleFileCompile(path);
606
+ if (body) {
607
+ await this._handleCompiledResp(url, path, body, ctx);
608
+ return;
609
+ }
610
+
611
+ // 直接发送文件
612
+ try {
613
+ await this._sendFile(ctx, path);
614
+ }
615
+ catch {
616
+ await next();
617
+ }
618
+ };
619
+
620
+ /**
621
+ * 关闭文件监听器
622
+ */
623
+ Static.prototype.closeWatcher = function () {
624
+ if (this._watcher) {
625
+ this._watcher.close();
626
+ this._watcher = null;
627
+ }
628
+ };
629
+
630
+ /**
631
+ * 创建静态文件处理中间件
632
+ * @param {object} config 配置
633
+ * @returns {Function} Koa中间件函数
634
+ */
635
+ function statics(config) {
636
+ const statics = new Static(config);
637
+ return async function (ctx, next) {
638
+ await statics.main(ctx, next);
639
+ };
640
+ }
641
+
642
+ module.exports = { Static, statics };