mm_statics 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -31
- package/index.js +30 -728
- package/lib/manager.js +290 -0
- package/lib/static.js +239 -0
- package/lib/static_handle.js +823 -0
- package/package.json +25 -14
- /package/{vue_compiler.js → lib/vue_compiler.js} +0 -0
package/index.js
CHANGED
|
@@ -1,733 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// 默认索引文件
|
|
15
|
-
index: 'index.html',
|
|
16
|
-
// 默认缓存过期时间
|
|
17
|
-
max_age: 7200,
|
|
18
|
-
// 静态文件缓存键前缀
|
|
19
|
-
prefix: 'static:',
|
|
20
|
-
// 缓存过期时间
|
|
21
|
-
cache_age: 7200,
|
|
22
|
-
// 是否开启缓存
|
|
23
|
-
cache: true,
|
|
24
|
-
// 是否开启不可变缓存
|
|
25
|
-
immutable: true,
|
|
26
|
-
// 是否隐藏静态文件
|
|
27
|
-
hidden: false,
|
|
28
|
-
// 是否格式化静态文件内容
|
|
29
|
-
format: true,
|
|
30
|
-
// 是否开启文件扩展名
|
|
31
|
-
extensions: false,
|
|
32
|
-
// 是否开启Brotli压缩
|
|
33
|
-
brotli: false,
|
|
34
|
-
// 是否压缩静态文件
|
|
35
|
-
gzip: false,
|
|
36
|
-
// 静态文件根目录
|
|
37
|
-
root: './static',
|
|
38
|
-
// 路由路径
|
|
39
|
-
path: '/',
|
|
40
|
-
// 是否编译Vue文件为JS
|
|
41
|
-
compile_vue: true,
|
|
42
|
-
// 是否编译Markdown文件为HTML
|
|
43
|
-
compile_md: true,
|
|
44
|
-
// 编译Markdown文件时,是否添加引入css和js文件
|
|
45
|
-
markdown_link: ['/css/common.css', '/css/markdown.css', '/js/markdown.js'],
|
|
46
|
-
// 需要转换的静态文件路径前缀
|
|
47
|
-
src_path: '/src',
|
|
48
|
-
// 需要转换的静态文件扩展名
|
|
49
|
-
files: ['.js', '.vue', '.html'],
|
|
50
|
-
// 是否转换ES6模块为AMD模块
|
|
51
|
-
convert_amd: false, // 修改为 false,避免 Vue 文件被错误转换为 AMD 格式
|
|
52
|
-
// 是否开启文件监听
|
|
53
|
-
watch: false,
|
|
54
|
-
// 监听的文件扩展名
|
|
55
|
-
watch_files: ['.js', '.css', '.html', '.vue', '.json', '.md']
|
|
56
|
-
};
|
|
57
|
-
|
|
1
|
+
const Static = require('./lib/static');
|
|
2
|
+
const StaticHandle = require('./lib/static_handle');
|
|
3
|
+
const Manager = require('./lib/manager');
|
|
4
|
+
const Admin = require('mm_corejs').Admin;
|
|
5
|
+
|
|
6
|
+
const admin = new Admin();
|
|
7
|
+
admin.setClass(Manager);
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
Static,
|
|
11
|
+
StaticHandle,
|
|
12
|
+
Manager,
|
|
13
|
+
admin,
|
|
58
14
|
/**
|
|
59
|
-
*
|
|
60
|
-
* @param {
|
|
15
|
+
* 创建静态文件中间件工厂函数
|
|
16
|
+
* @param {Object} config 静态文件配置
|
|
17
|
+
* @returns {Function} 可直接挂载到 Koa / mm_web_server 的中间件函数
|
|
61
18
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*/
|
|
76
|
-
Static.prototype.setConfig = function (config) {
|
|
77
|
-
Object.assign(this.config, config || {});
|
|
78
|
-
// 判断root是否绝对路径,不是则改成绝对路径
|
|
79
|
-
this.config.root = this.config.root.fullname();
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 初始化缓存
|
|
84
|
-
*/
|
|
85
|
-
Static.prototype._init = function () {
|
|
86
|
-
if (!$.cache) {
|
|
87
|
-
const { cacheAdmin } = require('mm_cache');
|
|
88
|
-
$.cache = cacheAdmin('sys', this.config);
|
|
89
|
-
}
|
|
90
|
-
this._cache = $.cache;
|
|
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
|
-
Static.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;
|
|
19
|
+
statics(config) {
|
|
20
|
+
const instance = new Static(config);
|
|
21
|
+
return instance.main.bind(instance);
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* 获取静态文件管理器实例
|
|
25
|
+
* @param {string} scope 作用域名称,为空时使用 os
|
|
26
|
+
* @param {Object} config 管理器配置
|
|
27
|
+
* @returns {Object} 静态文件管理器实例
|
|
28
|
+
*/
|
|
29
|
+
staticAdmin(scope, config) {
|
|
30
|
+
if (!scope) {
|
|
31
|
+
scope = 'os';
|
|
126
32
|
}
|
|
33
|
+
return admin.get(scope, config);
|
|
127
34
|
}
|
|
128
|
-
|
|
129
|
-
return false;
|
|
130
35
|
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 格式化代码
|
|
134
|
-
* @param {string} code 代码内容
|
|
135
|
-
* @param {string} file_type 文件类型
|
|
136
|
-
* @returns {Promise<string>} 格式化后的代码
|
|
137
|
-
*/
|
|
138
|
-
Static.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
|
-
// Static.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
|
-
Static.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
|
-
Static.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
|
-
* @param {object} ctx Koa上下文对象
|
|
289
|
-
* @param {string} path 文件路径
|
|
290
|
-
*/
|
|
291
|
-
Static.prototype._sendFile = async function (ctx, path) {
|
|
292
|
-
// koa-send会自动处理查询参数,直接传递路径
|
|
293
|
-
const config = { ...this.config };
|
|
294
|
-
|
|
295
|
-
// 为markdown文件设置正确的MIME类型
|
|
296
|
-
if (path.endsWith('.md')) {
|
|
297
|
-
config.setHeaders = (res) => {
|
|
298
|
-
// 当代码走到_sendFile路径时,说明文件没有被编译,应该返回原始markdown类型
|
|
299
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return await send(ctx, path, config);
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* 运行AMD模块
|
|
308
|
-
* @param {string} path 文件路径
|
|
309
|
-
* @param {string} file_type 文件类型
|
|
310
|
-
* @returns {Promise<string>} AMD模块代码
|
|
311
|
-
*/
|
|
312
|
-
Static.prototype._runAmd = async function (path, file_type) {
|
|
313
|
-
let type = file_type || this._getType(path);
|
|
314
|
-
let code = null;
|
|
315
|
-
let text = `.${path}`.fullname(this.config.root).loadText();
|
|
316
|
-
if (text) {
|
|
317
|
-
code = await this._toAmd(type, text);
|
|
318
|
-
}
|
|
319
|
-
return code;
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* 运行Vue组件
|
|
324
|
-
* @param {string} path 文件路径
|
|
325
|
-
* @returns {Promise<string>} 编译后的代码
|
|
326
|
-
*/
|
|
327
|
-
Static.prototype._runVue = async function (path) {
|
|
328
|
-
let code = null;
|
|
329
|
-
let file = `.${path}`.fullname(this.config.root);
|
|
330
|
-
let text = file.loadText();
|
|
331
|
-
if (text) {
|
|
332
|
-
code = await this._compileVue(text, file);
|
|
333
|
-
}
|
|
334
|
-
return code;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* 从Markdown内容中提取第一个一级标题
|
|
339
|
-
* @param {string} text Markdown文本内容
|
|
340
|
-
* @returns {string|null} 提取的标题,未找到时返回null
|
|
341
|
-
*/
|
|
342
|
-
Static.prototype._extractMarkdownTitle = function (text) {
|
|
343
|
-
if (!text || typeof text !== 'string') {
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// 匹配一级标题格式:# 标题
|
|
348
|
-
var title_regex = /^#\s+(.+)$/m;
|
|
349
|
-
var match = text.match(title_regex);
|
|
350
|
-
|
|
351
|
-
if (match && match[1]) {
|
|
352
|
-
return match[1].trim();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return null;
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* 处理Markdown文件转换
|
|
360
|
-
* @param {string} path 文件路径
|
|
361
|
-
* @returns {string|null} 转换后的HTML内容
|
|
362
|
-
*/
|
|
363
|
-
Static.prototype._runMarkdown = async function (path) {
|
|
364
|
-
let html = null;
|
|
365
|
-
let file = `.${path}`.fullname(this.config.root);
|
|
366
|
-
let text = file.loadText();
|
|
367
|
-
|
|
368
|
-
if (text) {
|
|
369
|
-
try {
|
|
370
|
-
// 使用marked将markdown转换为HTML
|
|
371
|
-
let markdown_html = marked.parse(text);
|
|
372
|
-
|
|
373
|
-
// 优先从Markdown内容中提取一级标题,后备使用文件名
|
|
374
|
-
var title_from_content = this._extractMarkdownTitle(text);
|
|
375
|
-
var title_from_file = path.split('/').pop().replace('.md', '');
|
|
376
|
-
var title = title_from_content || title_from_file;
|
|
377
|
-
// 构建外部资源链接
|
|
378
|
-
let links = '';
|
|
379
|
-
if (this.config.markdown_link && Array.isArray(this.config.markdown_link)) {
|
|
380
|
-
this.config.markdown_link.forEach(path => {
|
|
381
|
-
if (path.endsWith('.css')) {
|
|
382
|
-
links += ` <link rel="stylesheet" href="${path}">\n`;
|
|
383
|
-
} else if (path.endsWith('.js')) {
|
|
384
|
-
links += ` <script src="${path}"></script>\n`;
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// 构建完整的HTML结构
|
|
390
|
-
html = `<!DOCTYPE html>
|
|
391
|
-
<html>
|
|
392
|
-
<head>
|
|
393
|
-
<meta charset="UTF-8">
|
|
394
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
395
|
-
<title>${title}</title>
|
|
396
|
-
${links}
|
|
397
|
-
</head>
|
|
398
|
-
<body>
|
|
399
|
-
<div id="app" class="markdown-body">
|
|
400
|
-
${markdown_html}
|
|
401
|
-
</div>
|
|
402
|
-
</body>
|
|
403
|
-
</html>`;
|
|
404
|
-
|
|
405
|
-
// 如果启用了格式化,使用prettier格式化完整的HTML
|
|
406
|
-
if (this.config.format) {
|
|
407
|
-
try {
|
|
408
|
-
html = prettier.format(html, {
|
|
409
|
-
parser: 'html',
|
|
410
|
-
printWidth: 100,
|
|
411
|
-
tabWidth: 2
|
|
412
|
-
});
|
|
413
|
-
} catch (error) {
|
|
414
|
-
console.warn('HTML格式化失败,使用原始HTML:', error.message);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
} catch (error) {
|
|
418
|
-
console.error('Markdown转换错误:', error);
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return html;
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* 返回响应
|
|
428
|
-
* @param {object} ctx Koa上下文对象
|
|
429
|
-
* @param {string} body 响应体
|
|
430
|
-
* @param {object} headers 响应头
|
|
431
|
-
* @returns {object} 响应对象
|
|
432
|
-
*/
|
|
433
|
-
Static.prototype._return = function (ctx, body, headers) {
|
|
434
|
-
ctx.body = body;
|
|
435
|
-
if (headers) {
|
|
436
|
-
ctx.set(headers);
|
|
437
|
-
}
|
|
438
|
-
return { body, headers };
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* 初始化文件监听器
|
|
443
|
-
*/
|
|
444
|
-
Static.prototype._initWatcher = function () {
|
|
445
|
-
try {
|
|
446
|
-
const chokidar = require('chokidar');
|
|
447
|
-
|
|
448
|
-
// 构建监听模式,只监听指定类型的文件
|
|
449
|
-
const watch_patterns = this.config.watch_files.map(ext =>
|
|
450
|
-
`${this.config.root}/**/*${ext}`
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
// 监听指定类型的文件
|
|
454
|
-
this._watcher = chokidar.watch(watch_patterns, {
|
|
455
|
-
ignored: /(^|[\/\\])\\./, // 忽略隐藏文件
|
|
456
|
-
persistent: true,
|
|
457
|
-
ignoreInitial: true // 忽略初始扫描事件
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// 文件变化时的处理逻辑
|
|
461
|
-
this._watcher.on('all', (event, file_path) => {
|
|
462
|
-
this._handleFileChange(event, file_path);
|
|
463
|
-
});
|
|
464
|
-
} catch (error) {
|
|
465
|
-
console.error('文件监听初始化失败:', error.message);
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* 处理文件变化事件
|
|
471
|
-
* @param {string} event 事件类型
|
|
472
|
-
* @param {string} file_path 文件路径
|
|
473
|
-
*/
|
|
474
|
-
Static.prototype._handleFileChange = function (event, file_path) {
|
|
475
|
-
// 构建缓存键(相对于静态目录的路径)
|
|
476
|
-
const relative_path = file_path.replace(this.config.root, '').replace(/^[\\\/]/, '');
|
|
477
|
-
const cache_key = this.config.prefix + relative_path;
|
|
478
|
-
|
|
479
|
-
// 根据事件类型处理缓存
|
|
480
|
-
switch (event) {
|
|
481
|
-
case 'change':
|
|
482
|
-
case 'add':
|
|
483
|
-
// 文件修改或新增,删除缓存,下次请求时重新生成
|
|
484
|
-
this._cache.del(cache_key);
|
|
485
|
-
break;
|
|
486
|
-
case 'unlink':
|
|
487
|
-
// 文件删除,删除缓存
|
|
488
|
-
this._cache.del(cache_key);
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* 获取响应头
|
|
495
|
-
* @param {string} file_type 文件类型
|
|
496
|
-
* @returns {object} 响应头
|
|
497
|
-
*/
|
|
498
|
-
Static.prototype._getHeaders = function (file_type) {
|
|
499
|
-
let headers = {};
|
|
500
|
-
|
|
501
|
-
// 缓存控制逻辑
|
|
502
|
-
if (this.config.max_age > 0) {
|
|
503
|
-
let cache_control = `max-age=${this.config.max_age}`;
|
|
504
|
-
if (this.config.immutable) {
|
|
505
|
-
cache_control += ', immutable';
|
|
506
|
-
}
|
|
507
|
-
headers['cache-control'] = cache_control;
|
|
508
|
-
}
|
|
509
|
-
switch (file_type) {
|
|
510
|
-
case 'js':
|
|
511
|
-
headers['content-type'] = 'application/javascript; charset=utf-8';
|
|
512
|
-
break;
|
|
513
|
-
case 'vue':
|
|
514
|
-
if (this.config.convert_amd) {
|
|
515
|
-
headers['content-type'] = 'text/html; charset=utf-8';
|
|
516
|
-
}
|
|
517
|
-
else if (this.config.compile_vue) {
|
|
518
|
-
headers['content-type'] = 'application/javascript; charset=utf-8';
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
// 当不编译 Vue 文件时,设置为 JavaScript MIME 类型
|
|
522
|
-
headers['content-type'] = 'application/javascript; charset=utf-8';
|
|
523
|
-
}
|
|
524
|
-
break;
|
|
525
|
-
case 'html':
|
|
526
|
-
headers['content-type'] = 'text/html; charset=utf-8';
|
|
527
|
-
break;
|
|
528
|
-
case 'md':
|
|
529
|
-
if (this.config.compile_md) {
|
|
530
|
-
headers['content-type'] = 'text/html; charset=utf-8';
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
headers['content-type'] = 'text/plain; charset=utf-8';
|
|
534
|
-
}
|
|
535
|
-
break;
|
|
536
|
-
default:
|
|
537
|
-
headers['content-type'] = 'text/plain; charset=utf-8';
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
return headers;
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* 设置缓存
|
|
545
|
-
* @param {string} path 文件路径(包含查询参数)
|
|
546
|
-
* @param {object} ret 响应体和响应头
|
|
547
|
-
*/
|
|
548
|
-
Static.prototype._setCache = async function (path, ret) {
|
|
549
|
-
// 使用完整路径(包含查询参数)作为缓存键
|
|
550
|
-
await this._cache.set(this.config.prefix + path, ret, this.config.cache_age);
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* 获取文件类型
|
|
555
|
-
* @param {string} path 文件路径
|
|
556
|
-
* @returns {string} 文件类型(不含点号)
|
|
557
|
-
*/
|
|
558
|
-
Static.prototype._getType = function (path) {
|
|
559
|
-
|
|
560
|
-
// 使用mm_expand提供的extname方法获取扩展名
|
|
561
|
-
const ext = path.extname().toLowerCase();
|
|
562
|
-
if (ext) {
|
|
563
|
-
return ext.slice(1); // 去掉点号
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// 备用方法:处理多扩展名情况
|
|
567
|
-
const basename = path.basename();
|
|
568
|
-
const parts = basename.split('.');
|
|
569
|
-
|
|
570
|
-
// 如果文件名包含多个点号,取最后一个作为扩展名
|
|
571
|
-
if (parts.length > 1) {
|
|
572
|
-
return parts[parts.length - 1];
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return ''; // 无扩展名
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* 获取缓存
|
|
580
|
-
* @param {string} path 文件路径(包含查询参数)
|
|
581
|
-
* @returns {object} 响应体和响应头
|
|
582
|
-
*/
|
|
583
|
-
Static.prototype._getCache = async function (path) {
|
|
584
|
-
// 使用完整路径(包含查询参数)作为缓存键
|
|
585
|
-
return await this._cache.get(this.config.prefix + path);
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* 删除缓存
|
|
590
|
-
* @param {string} path 文件路径(包含查询参数)
|
|
591
|
-
*/
|
|
592
|
-
Static.prototype._delCache = async function (path) {
|
|
593
|
-
await this._cache.del(this.config.prefix + path);
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* 处理缓存逻辑
|
|
598
|
-
* @param {string} url 完整URL
|
|
599
|
-
* @param {object} ctx Koa上下文对象
|
|
600
|
-
* @returns {boolean} 是否命中缓存
|
|
601
|
-
*/
|
|
602
|
-
Static.prototype._handleCache = async function (url, ctx) {
|
|
603
|
-
if (!this.config.cache) return false;
|
|
604
|
-
|
|
605
|
-
let ret = await this._getCache(url);
|
|
606
|
-
if (ret) {
|
|
607
|
-
this._return(ctx, ret.body, ret.headers);
|
|
608
|
-
return true;
|
|
609
|
-
}
|
|
610
|
-
return false;
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* 处理文件编译逻辑
|
|
615
|
-
* @param {string} path 文件路径
|
|
616
|
-
* @returns {string|null} 编译后的内容
|
|
617
|
-
*/
|
|
618
|
-
Static.prototype._handleFileCompile = async function (path) {
|
|
619
|
-
let { src_path, compile_md, files, compile_vue, convert_amd } = this.config;
|
|
620
|
-
if (compile_md && path.extname() === '.md') {
|
|
621
|
-
return await this._runMarkdown(path);
|
|
622
|
-
} else if (!path.startsWith(src_path)) return null;
|
|
623
|
-
|
|
624
|
-
let file_type = this._getType(path);
|
|
625
|
-
|
|
626
|
-
if (convert_amd && files.includes('.' + file_type)) {
|
|
627
|
-
return await this._runAmd(path, file_type);
|
|
628
|
-
}
|
|
629
|
-
else if (compile_vue && file_type === 'vue') {
|
|
630
|
-
return await this._runVue(path);
|
|
631
|
-
}
|
|
632
|
-
return null;
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* 处理编译后的文件响应
|
|
637
|
-
* @param {string} url 完整URL
|
|
638
|
-
* @param {string} path 文件路径
|
|
639
|
-
* @param {string} body 响应体
|
|
640
|
-
* @param {object} ctx Koa上下文对象
|
|
641
|
-
*/
|
|
642
|
-
Static.prototype._handleCompiledResp = async function (url, path, body, ctx) {
|
|
643
|
-
let file_type = this._getType(path);
|
|
644
|
-
// 只有当启用了Markdown转换时,才将markdown文件类型设置为HTML
|
|
645
|
-
if (file_type === 'md' && this.config.compile_md) {
|
|
646
|
-
file_type = 'html';
|
|
647
|
-
}
|
|
648
|
-
let headers = this._getHeaders(file_type);
|
|
649
|
-
let ret = this._return(ctx, body, headers);
|
|
650
|
-
|
|
651
|
-
// 只有当缓存启用时才设置缓存
|
|
652
|
-
if (this.config.cache) {
|
|
653
|
-
await this._setCache(url, ret);
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* 获取真实路径
|
|
659
|
-
* @param {string} path 文件路径
|
|
660
|
-
* @returns {string} 真实路径
|
|
661
|
-
*/
|
|
662
|
-
Static.prototype._getRealPath = function (path) {
|
|
663
|
-
return path.replace(this.config.path, "/");
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* 将路由路径转换为物理路径
|
|
668
|
-
* @param {string} path 文件路径
|
|
669
|
-
* @returns {string} 根路径
|
|
670
|
-
*/
|
|
671
|
-
Static.prototype._pathToRoot = function (path) {
|
|
672
|
-
return ("." + path).fullname(this.config.root);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* 主处理函数
|
|
677
|
-
* @param {object} ctx Koa上下文对象
|
|
678
|
-
* @param {Function} next Koa中间件函数
|
|
679
|
-
*/
|
|
680
|
-
Static.prototype.main = async function (ctx, next) {
|
|
681
|
-
let url = ctx.url; // 使用完整URL(包含查询参数)
|
|
682
|
-
let p = ctx.path;
|
|
683
|
-
if (p.indexOf('.') === -1 || !p.startsWith(this.config.path)) {
|
|
684
|
-
await next();
|
|
685
|
-
return '';
|
|
686
|
-
}
|
|
687
|
-
let path = this._getRealPath(p);
|
|
688
|
-
|
|
689
|
-
// 检查缓存
|
|
690
|
-
if (await this._handleCache(url, ctx)) {
|
|
691
|
-
return this._pathToRoot(path);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// 处理文件编译
|
|
695
|
-
let body = await this._handleFileCompile(path);
|
|
696
|
-
if (body) {
|
|
697
|
-
await this._handleCompiledResp(url, path, body, ctx);
|
|
698
|
-
return this._pathToRoot(path);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// 直接发送文件
|
|
702
|
-
try {
|
|
703
|
-
return await this._sendFile(ctx, path);
|
|
704
|
-
}
|
|
705
|
-
catch {
|
|
706
|
-
await next();
|
|
707
|
-
}
|
|
708
|
-
return '';
|
|
709
|
-
};
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* 关闭文件监听器
|
|
713
|
-
*/
|
|
714
|
-
Static.prototype.closeWatcher = function () {
|
|
715
|
-
if (this._watcher) {
|
|
716
|
-
this._watcher.close();
|
|
717
|
-
this._watcher = null;
|
|
718
|
-
}
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* 创建静态文件处理中间件
|
|
723
|
-
* @param {object} config 配置
|
|
724
|
-
* @returns {Function} Koa中间件函数
|
|
725
|
-
*/
|
|
726
|
-
function statics(config) {
|
|
727
|
-
const statics = new Static(config);
|
|
728
|
-
return async function (ctx, next) {
|
|
729
|
-
await statics.main(ctx, next);
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
module.exports = { Static, statics };
|