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