@flun/html-template 4.0.10
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/.env +9 -0
- package/LICENSE +15 -0
- package/build.js +3 -0
- package/compile.js +349 -0
- package/copy-files.js +200 -0
- package/customize/account.js +726 -0
- package/customize/data.json +484 -0
- package/customize/functions.js +48 -0
- package/customize/hotReloadInjector.js +25 -0
- package/customize/routes.js +141 -0
- package/customize/users.json +44 -0
- package/customize/variables.js +70 -0
- package/dev-server.js +344 -0
- package/dev.js +4 -0
- package/f-CHANGELOG.md +4 -0
- package/f-README.md +485 -0
- package/index.d.ts +133 -0
- package/index.js +4 -0
- package/package.json +77 -0
- package/restoreDefaults.js +8 -0
- package/services/templateService.js +962 -0
- package/static/about.css +118 -0
- package/static/auth.js +27 -0
- package/static/constants.css +138 -0
- package/static/img/dark.png +0 -0
- package/static/img/favicon.ico +0 -0
- package/static/img/light.png +0 -0
- package/static/img/top.png +0 -0
- package/static/index.css +86 -0
- package/static/mouseOrTouch.js +156 -0
- package/static/public.css +288 -0
- package/static/script.css +318 -0
- package/static/script.js +392 -0
- package/static/styling.css +874 -0
- package/static/styling.js +933 -0
- package/static/themeImg.css +10 -0
- package/static/themeImg.js +19 -0
- package/static/themeModule.js +222 -0
- package/static/topImg.css +19 -0
- package/static/topImg.js +21 -0
- package/static/utils/browser13.js +270 -0
- package/static/utils/closebrackets.js +166 -0
- package/static/utils/css-lint.js +308 -0
- package/static/utils/custom-css-hint.js +876 -0
- package/static/utils/foldgutter.js +141 -0
- package/static/utils/match-highlighter.js +70 -0
- package/templates/about.html +236 -0
- package/templates/account/2fa.html +184 -0
- package/templates/account/forgot-password.html +226 -0
- package/templates/account/login.html +230 -0
- package/templates/account/profile.html +977 -0
- package/templates/account/register.html +224 -0
- package/templates/account/reset-password.html +205 -0
- package/templates/account/verify-email.html +163 -0
- package/templates/base.html +71 -0
- package/templates/footer-content.html +5 -0
- package/templates/index.html +140 -0
- package/templates/script.html +209 -0
- package/templates/test-include.html +11 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": 1775222838614,
|
|
4
|
+
"username": "admin",
|
|
5
|
+
"email": null,
|
|
6
|
+
"password": "$2b$10$u8LlXx6cbBkYYh1pnnjf3uTVVn2saj0daUb0b4QUDU9TOL1dmhg0G",
|
|
7
|
+
"emailVerified": false,
|
|
8
|
+
"emailVerificationToken": null,
|
|
9
|
+
"passwordResetToken": null,
|
|
10
|
+
"passwordResetExpires": null,
|
|
11
|
+
"twoFactorSecret": null,
|
|
12
|
+
"twoFactorEnabled": false,
|
|
13
|
+
"backupCodes": [],
|
|
14
|
+
"webauthnCredentials": [],
|
|
15
|
+
"webauthnEnabled": false,
|
|
16
|
+
"createdAt": 1775222838614,
|
|
17
|
+
"updatedAt": 1775222838614,
|
|
18
|
+
"passwordChangedAt": 1775222838614,
|
|
19
|
+
"pendingEmail": null,
|
|
20
|
+
"pendingEmailToken": null,
|
|
21
|
+
"pendingEmailExpires": null
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": 1777035892336,
|
|
25
|
+
"username": "lunjack123",
|
|
26
|
+
"email": "china@lunjack.com",
|
|
27
|
+
"password": "$2b$10$r4tmKt/j0blecpxy3QNL/u2Ht5y6RcHgSwtJ87CB.vYInwPWP5As2",
|
|
28
|
+
"emailVerified": true,
|
|
29
|
+
"emailVerificationToken": null,
|
|
30
|
+
"passwordResetToken": null,
|
|
31
|
+
"passwordResetExpires": null,
|
|
32
|
+
"twoFactorSecret": null,
|
|
33
|
+
"twoFactorEnabled": false,
|
|
34
|
+
"backupCodes": [],
|
|
35
|
+
"webauthnCredentials": [],
|
|
36
|
+
"webauthnEnabled": false,
|
|
37
|
+
"createdAt": 1777035892336,
|
|
38
|
+
"updatedAt": 1777035892336,
|
|
39
|
+
"passwordChangedAt": 1777035892336,
|
|
40
|
+
"pendingEmail": null,
|
|
41
|
+
"pendingEmailToken": null,
|
|
42
|
+
"pendingEmailExpires": null
|
|
43
|
+
}
|
|
44
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// customize/variables.js
|
|
2
|
+
export default {
|
|
3
|
+
variables: {
|
|
4
|
+
// ============= 基础信息变量 ============
|
|
5
|
+
year: new Date().getFullYear(),
|
|
6
|
+
timestamp: Date.now(), // 当前时间戳
|
|
7
|
+
baseUrl: '/',
|
|
8
|
+
nodeEnv: process.env.NODE_ENV || 'development', // 当前环境
|
|
9
|
+
serverTime: new Date().toLocaleString(), // 服务器当前时间
|
|
10
|
+
// ============ 自定义数据变量 ============
|
|
11
|
+
// 用户信息
|
|
12
|
+
user: {
|
|
13
|
+
isLoggedIn: true,
|
|
14
|
+
name: "张三",
|
|
15
|
+
membershipLevel: "VIP",
|
|
16
|
+
isGuest: false
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// 产品信息
|
|
20
|
+
product: {
|
|
21
|
+
stock: 5,
|
|
22
|
+
price: 99.99
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// 产品列表
|
|
26
|
+
products: [
|
|
27
|
+
{ name: "产品A", price: 100, stock: 10, isNew: true },
|
|
28
|
+
{ name: "产品B", price: 200, stock: 5, isNew: false },
|
|
29
|
+
{ name: "产品C", price: 300, stock: 0, isNew: true },
|
|
30
|
+
{ name: "产品D", price: 150, stock: 8, isNew: false },
|
|
31
|
+
{ name: "产品E", price: 250, stock: 3, isNew: true },
|
|
32
|
+
{ name: "产品F", price: 350, stock: 7, isNew: false }
|
|
33
|
+
],
|
|
34
|
+
|
|
35
|
+
// 团队成员信息
|
|
36
|
+
teamMembers: [
|
|
37
|
+
{
|
|
38
|
+
name: "李四",
|
|
39
|
+
position: "技术总监",
|
|
40
|
+
department: "技术部",
|
|
41
|
+
skills: ["JavaScript", "Node.js", "Vue"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "王五",
|
|
45
|
+
position: "设计师",
|
|
46
|
+
department: "设计部",
|
|
47
|
+
skills: ["UI设计", "UX设计", "原型设计"]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "赵六",
|
|
51
|
+
position: "产品经理",
|
|
52
|
+
department: "产品部",
|
|
53
|
+
skills: ["产品规划", "需求分析", "项目管理"]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
|
|
57
|
+
// 公司信息
|
|
58
|
+
companyInfo: {
|
|
59
|
+
"成立年份": "2015年",
|
|
60
|
+
"员工人数": "50+",
|
|
61
|
+
"总部地点": "北京",
|
|
62
|
+
"业务范围": "软件开发、技术咨询、产品设计",
|
|
63
|
+
"服务客户": "1000+"
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// 测试空数据
|
|
67
|
+
emptyArray: [], // 空数组
|
|
68
|
+
emptyObject: {} // 空对象
|
|
69
|
+
}
|
|
70
|
+
};
|
package/dev-server.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 开发服务器模块 - 动态模板渲染服务器
|
|
3
|
+
*
|
|
4
|
+
* 模块结构:
|
|
5
|
+
* 1. 依赖导入与服务器初始化
|
|
6
|
+
* 2. 服务器配置与端口管理(parseAndValidatePort,parseServerConfig)
|
|
7
|
+
* 3. 全局CORS中间件和静态资源配置(/static路径)
|
|
8
|
+
* 4. 服务器生命周期管理(printAvailablePages, startServer)
|
|
9
|
+
* 5. 请求页面路由处理(自动路由与模板渲染) —— 已在 startServer 内部动态添加
|
|
10
|
+
* 6. 热重载功能实现(文件监听、事件处理、服务器重启) —— setupHotReload, restartServer
|
|
11
|
+
* 7. 导出接口与启动执行(module.exports , startServer)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ==================== 1.依赖导入与服务器初始化 ====================
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import { constants, existsSync } from 'fs';
|
|
17
|
+
import http from 'http';
|
|
18
|
+
import { Server as socketIo } from 'socket.io';
|
|
19
|
+
import chokidar from 'chokidar';
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
21
|
+
import {
|
|
22
|
+
path, fsPromises, CWD, getAvailableTemplates, findEntryFile, validateTemplateFile, renderTemplate, processIncludes,
|
|
23
|
+
processVariables, loadUserFeatures, writtenFilesToIgnore, templatesAbsDir, templatesDir, staticDir, customizeDir,
|
|
24
|
+
accountDir, defaultPort, monitorFileWrites
|
|
25
|
+
} from './services/templateService.js';
|
|
26
|
+
import { injectScript } from './customize/hotReloadInjector.js';
|
|
27
|
+
|
|
28
|
+
let server, io, watcher, cachedPages = [], unmountMonitor = null;
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename),
|
|
30
|
+
app = express(), staticAbsDir = path.join(CWD, staticDir), customizeAbsDir = path.join(CWD, customizeDir),
|
|
31
|
+
|
|
32
|
+
// ==================== 工具函数 ====================
|
|
33
|
+
/**
|
|
34
|
+
* 创建带WebSocket的服务器
|
|
35
|
+
*/
|
|
36
|
+
createServerWithSocket = (app, hotReload) => {
|
|
37
|
+
server = http.createServer(app);
|
|
38
|
+
if (hotReload) {
|
|
39
|
+
io = new socketIo(server);
|
|
40
|
+
io.engine.on("headers", headers => headers["Content-Type"] = "text/html; charset=utf-8");
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 清理资源
|
|
46
|
+
*/
|
|
47
|
+
cleanupResources = () => {
|
|
48
|
+
if (watcher) watcher.close(), watcher = null;
|
|
49
|
+
if (io) io.close(), io = null;
|
|
50
|
+
if (unmountMonitor) unmountMonitor(); unmountMonitor = null;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 生成页面URL
|
|
55
|
+
*/
|
|
56
|
+
generateUrls = (page, port) => {
|
|
57
|
+
const baseUrl = `http://localhost:${port}`, url = `${baseUrl}/${page}`, needsEncoding = !/^[a-zA-Z0-9\-_.~/]+$/.test(page);
|
|
58
|
+
|
|
59
|
+
return { url, encodedUrl: `${baseUrl}/${encodeURI(page)}`, needsEncoding };
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 递归复制目录
|
|
64
|
+
*/
|
|
65
|
+
copyDir = async (src, dest) => {
|
|
66
|
+
await fsPromises.mkdir(dest, { recursive: true });
|
|
67
|
+
const entries = await fsPromises.readdir(src, { withFileTypes: true });
|
|
68
|
+
for (let entry of entries) {
|
|
69
|
+
const srcPath = path.join(src, entry.name), destPath = path.join(dest, entry.name);
|
|
70
|
+
if (entry.isDirectory()) await copyDir(srcPath, destPath);
|
|
71
|
+
else await fsPromises.copyFile(srcPath, destPath);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 确保登录模式所需文件存在(从包内复制缺失文件)
|
|
77
|
+
*/
|
|
78
|
+
ensureAccountFiles = async () => {
|
|
79
|
+
const userAccountJs = path.join(customizeAbsDir, 'account.js'),
|
|
80
|
+
userTemplatesAccount = path.join(CWD, templatesDir, accountDir);
|
|
81
|
+
// 检查并拷贝 customize/account.js
|
|
82
|
+
try {
|
|
83
|
+
await fsPromises.access(userAccountJs, constants.F_OK);
|
|
84
|
+
} catch {
|
|
85
|
+
console.log('检测到 account=true 但缺少 customize/account.js,正在从包内复制...');
|
|
86
|
+
const pkgCustomizeDir = path.join(__dirname, customizeDir),
|
|
87
|
+
defaultAccountJs = path.join(pkgCustomizeDir, 'account.js');
|
|
88
|
+
try {
|
|
89
|
+
await fsPromises.access(defaultAccountJs, constants.F_OK);
|
|
90
|
+
await fsPromises.mkdir(customizeAbsDir, { recursive: true });
|
|
91
|
+
await fsPromises.copyFile(defaultAccountJs, userAccountJs);
|
|
92
|
+
console.log('✅ 已复制 account.js');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error('❌ 默认 account.js 不存在或复制失败:', err.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 检查并拷贝 templates/account 目录
|
|
99
|
+
try {
|
|
100
|
+
await fsPromises.access(userTemplatesAccount, constants.F_OK);
|
|
101
|
+
} catch {
|
|
102
|
+
console.log('检测到 account=true 但缺少 templates/account 目录,正在从包内复制...');
|
|
103
|
+
const pkgTemplatesDir = path.join(__dirname, templatesDir),
|
|
104
|
+
defaultTemplatesAccount = path.join(pkgTemplatesDir, accountDir);
|
|
105
|
+
try {
|
|
106
|
+
await fsPromises.access(defaultTemplatesAccount, constants.F_OK);
|
|
107
|
+
await copyDir(defaultTemplatesAccount, userTemplatesAccount);
|
|
108
|
+
console.log('✅ 已复制 templates/account 目录');
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('❌ 默认 templates/account 目录不存在或复制失败:', err.message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// ==================== 2.服务器配置与端口管理 ====================
|
|
116
|
+
/**
|
|
117
|
+
* 解析并验证端口值
|
|
118
|
+
* @param {string|number} portValue - 端口值
|
|
119
|
+
* @param {string} source - 来源描述(用于错误消息)
|
|
120
|
+
* @returns {number} 有效的端口号或默认值
|
|
121
|
+
*/
|
|
122
|
+
parseAndValidatePort = (portValue, source) => {
|
|
123
|
+
const portNum = parseInt(portValue);
|
|
124
|
+
|
|
125
|
+
// 检查是否为有效数字且在有效端口范围内 (1-65535)
|
|
126
|
+
if (!isNaN(portNum) && portNum > 0 && portNum < 65536) return portNum;
|
|
127
|
+
console.warn(`警告: ${source} "${portValue}" 无效,已启用默认端口:${defaultPort}`);
|
|
128
|
+
return defaultPort;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 统一服务器配置解析函数
|
|
133
|
+
*
|
|
134
|
+
* 配置解析优先级:
|
|
135
|
+
* 端口:
|
|
136
|
+
* 1. 命令行参数 (--port 或 -p)
|
|
137
|
+
* 2. 函数参数 (options.port)
|
|
138
|
+
* 3. 环境变量 (process.env.PORT)
|
|
139
|
+
* 4. 默认值 (常量 defaultPort)
|
|
140
|
+
*
|
|
141
|
+
* 热重载:
|
|
142
|
+
* 1. 命令行参数 (--hot-reload/--no-hot-reload)
|
|
143
|
+
* 2. 函数参数 (options.hotReload)
|
|
144
|
+
* 3. 环境变量 (process.env.HOTRELOAD)
|
|
145
|
+
* 4. 默认值 (true)
|
|
146
|
+
*
|
|
147
|
+
* 登录模式:
|
|
148
|
+
* 1. 命令行参数 (--account/--no-account)
|
|
149
|
+
* 2. 函数参数 (options.account)
|
|
150
|
+
* 3. 环境变量 (process.env.ACCOUNT)
|
|
151
|
+
* 4. 默认值 (false)
|
|
152
|
+
*/
|
|
153
|
+
parseServerConfig = (options = {}) => {
|
|
154
|
+
let port, hotReload, account;
|
|
155
|
+
// 解析端口参数 - 优先级: 命令行 > 函数参数 > 环境变量 > 默认值
|
|
156
|
+
const args = process.argv.slice(2), portArgIndex = args.findIndex(arg => arg === '--port' || arg === '-p'),
|
|
157
|
+
portArgValue = portArgIndex !== -1 ? args[portArgIndex + 1] : null, { port: P, hotReload: H, account: A } = options;
|
|
158
|
+
|
|
159
|
+
if (portArgValue) port = parseAndValidatePort(portArgValue, '命令行参数');
|
|
160
|
+
else if (P !== undefined) port = parseAndValidatePort(P, '函数参数');
|
|
161
|
+
else if (process.env.PORT) port = parseAndValidatePort(process.env.PORT, '环境变量 PORT');
|
|
162
|
+
else port = defaultPort; // 默认端口
|
|
163
|
+
|
|
164
|
+
// 解析热重载参数 - 优先级: 命令行 > 函数参数 > 环境变量> 默认值
|
|
165
|
+
if (args.includes('--hot-reload')) hotReload = true;
|
|
166
|
+
else if (args.includes('--no-hot-reload')) hotReload = false;
|
|
167
|
+
else if (H !== undefined) hotReload = H;
|
|
168
|
+
else if (process.env.HOTRELOAD) hotReload = process.env.HOTRELOAD === 'true';
|
|
169
|
+
else hotReload = true; // 默认启用
|
|
170
|
+
|
|
171
|
+
// 解析登录模式参数 - 优先级: 命令行 > 函数参数 > 环境变量 > 默认值
|
|
172
|
+
if (args.includes('--account')) account = true;
|
|
173
|
+
else if (args.includes('--no-account')) account = false;
|
|
174
|
+
else if (A !== undefined) account = A;
|
|
175
|
+
else if (process.env.ACCOUNT) account = process.env.ACCOUNT === 'true';
|
|
176
|
+
else account = false; // 默认关闭
|
|
177
|
+
|
|
178
|
+
return { port, hotReload, account };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ==================== 3.全局CORS中间件和静态资源配置 ====================
|
|
182
|
+
app.use((req, res, next) => {
|
|
183
|
+
// 基本CORS头
|
|
184
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
185
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD');
|
|
186
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin, X-CSRF-Token');
|
|
187
|
+
res.setHeader('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
|
|
188
|
+
res.setHeader('Access-Control-Max-Age', '86400'); // 预检请求缓存24小时
|
|
189
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
190
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
191
|
+
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
|
|
192
|
+
// res.setHeader('Access-Control-Allow-Credentials', 'true');// 是否启用凭据(cookies、认证等)
|
|
193
|
+
|
|
194
|
+
// 处理预检请求(OPTIONS)
|
|
195
|
+
if (req.method === 'OPTIONS') {
|
|
196
|
+
res.setHeader('Content-Length', '0');
|
|
197
|
+
return res.status(204).end();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
next();
|
|
201
|
+
}), app.set('trust proxy', false);
|
|
202
|
+
app.use('/static', express.static(staticAbsDir));
|
|
203
|
+
|
|
204
|
+
// ==================== 4.服务器生命周期管理 ====================
|
|
205
|
+
/**
|
|
206
|
+
* 控制台输出可访问页面信息
|
|
207
|
+
* @param {string[]} pages - 有效的模板文件名集合
|
|
208
|
+
* @param {number} port - 服务器端口号
|
|
209
|
+
* @param {boolean} hotReload - 是否启用热重载
|
|
210
|
+
*/
|
|
211
|
+
const printAvailablePages = (pages, port, hotReload) => {
|
|
212
|
+
console.log(`开发服务器启动成功!\n访问地址: http://localhost:${port}`);
|
|
213
|
+
if (hotReload) console.log(`✅ 热重载功能已启用(监听目录->${templatesAbsDir},${staticDir},${customizeDir})`);
|
|
214
|
+
|
|
215
|
+
console.log('\n可访问页面:');
|
|
216
|
+
pages.sort().forEach(page => {
|
|
217
|
+
const { url, encodedUrl, needsEncoding } = generateUrls(page, port);
|
|
218
|
+
|
|
219
|
+
if (needsEncoding) console.log(` 原始路径: ${url} (需复制访问)\n 编码路径: ${encodedUrl} (直接访问)`);
|
|
220
|
+
else console.log(` 直接访问: ${url}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
console.log(`\n共发现 ${pages.length} 个可用模板`), console.log('-----------------------------------');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 服务器启动主函数
|
|
228
|
+
* >查看定义:@see {@link startServer}
|
|
229
|
+
* @async
|
|
230
|
+
* @param {Object} [options] - 配置对象
|
|
231
|
+
* @param {number} [options.port] - 可选端口号
|
|
232
|
+
* @param {boolean} [options.hotReload] - 是否启用热重载
|
|
233
|
+
* @param {boolean} [options.account] - 是否启用登录模式
|
|
234
|
+
*/
|
|
235
|
+
const startServer = async (options = {}) => {
|
|
236
|
+
try {
|
|
237
|
+
const config = parseServerConfig(options), { port: p, hotReload: h, account: acc } = config;
|
|
238
|
+
|
|
239
|
+
if (acc) await ensureAccountFiles(); // 如果启用登录模式,验证必要文件
|
|
240
|
+
await loadUserFeatures(app), cachedPages = await getAvailableTemplates(); // 加载用户自定义功能获取模板内容
|
|
241
|
+
|
|
242
|
+
// 添加核心模板渲染中间件
|
|
243
|
+
app.use(async (req, res, next) => {
|
|
244
|
+
try {
|
|
245
|
+
const decodedPath = decodeURIComponent(req.path);
|
|
246
|
+
if (decodedPath === '/') {
|
|
247
|
+
const entryFile = await findEntryFile(cachedPages);
|
|
248
|
+
return res.redirect(`/${entryFile}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const templateFile = decodedPath.endsWith('.html') ? decodedPath.slice(1) : `${decodedPath.slice(1)}.html`;
|
|
252
|
+
if (cachedPages.includes(templateFile)) {
|
|
253
|
+
let rendered = await renderTemplate(templateFile);
|
|
254
|
+
rendered = await processIncludes(rendered, templateFile);
|
|
255
|
+
rendered = processVariables(rendered, { currentUrl: decodedPath, query: req.query ? JSON.stringify(req.query) : '' });
|
|
256
|
+
|
|
257
|
+
if (io) rendered = injectScript(rendered); // 如果启用了热重载,注入客户端脚本
|
|
258
|
+
return res.type('html').send(rendered);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
next();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error(`处理请求时出错: ${error.message}`), console.error(error.stack);
|
|
264
|
+
next(error);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
for (const page of cachedPages) await validateTemplateFile(page, true); // 验证模板文件
|
|
268
|
+
|
|
269
|
+
if (h) setupHotReload();
|
|
270
|
+
printAvailablePages(cachedPages, p, h), createServerWithSocket(app, h);
|
|
271
|
+
|
|
272
|
+
server.listen(p, () => {
|
|
273
|
+
console.log(`服务器运行中,按 Ctrl+C 退出`), console.log('-----------------------------------');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return p;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error('服务器启动失败:', error.message), process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// ==================== 6.热重载功能实现 ====================
|
|
283
|
+
/**
|
|
284
|
+
* 设置文件监听和热重载功能
|
|
285
|
+
*/
|
|
286
|
+
setupHotReload = () => {
|
|
287
|
+
// 监听模板目录、静态文件目录和后端目录
|
|
288
|
+
const watchDirs = [templatesAbsDir, staticAbsDir, customizeAbsDir].filter(dir => existsSync(dir));
|
|
289
|
+
if (watchDirs.length === 0) return console.warn('[热重载] 没有可监听的目录');
|
|
290
|
+
|
|
291
|
+
unmountMonitor = monitorFileWrites(); // 启用持续文件写入监控并获取卸载函数
|
|
292
|
+
// 文件变更事件处理函数
|
|
293
|
+
const handleFileEvent = (event, filePath) => {
|
|
294
|
+
const normalizedPath = path.normalize(filePath);
|
|
295
|
+
if (writtenFilesToIgnore.includes(normalizedPath)) return; // 忽略文件
|
|
296
|
+
|
|
297
|
+
const isBackendFile = filePath.startsWith(customizeAbsDir);
|
|
298
|
+
if (isBackendFile) {
|
|
299
|
+
console.log(`检测到${event}了${normalizedPath}后端文件,[热重载] 执行服务器重启并刷新页面...`);
|
|
300
|
+
io.emit('hot-reload', 3500), setTimeout(() => restartServer(), 500); // 通知浏览器延迟刷新,延迟后重启服务器
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.log(`检测到${event}了${normalizedPath}前端文件,[热重载] 已刷新页面...`);
|
|
304
|
+
// 如果删除了HTML模板文件,从缓存中移除
|
|
305
|
+
if (event === '删除' && filePath.startsWith(templatesAbsDir) && filePath.endsWith('.html')) {
|
|
306
|
+
const templateName = path.relative(templatesAbsDir, filePath).replace(/\\/g, '/');
|
|
307
|
+
cachedPages = cachedPages.filter(page => page !== templateName);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
io.emit('hot-reload', 100); // 通知浏览器刷新
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// 设置监听器(忽略隐藏文件)
|
|
315
|
+
watcher = chokidar.watch(watchDirs, {
|
|
316
|
+
ignored: /(^|[\/\\])\../, persistent: true, ignoreInitial: true
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
watcher.on('change', (filePath) => handleFileEvent('更改', filePath))
|
|
320
|
+
.on('add', (filePath) => handleFileEvent('添加', filePath))
|
|
321
|
+
.on('unlink', (filePath) => handleFileEvent('删除', filePath))
|
|
322
|
+
.on('error', (error) => console.error('[热重载] 文件监听错误:', error));
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 重启服务器
|
|
327
|
+
*/
|
|
328
|
+
restartServer = async () => {
|
|
329
|
+
try {
|
|
330
|
+
const port = server.address().port;
|
|
331
|
+
cleanupResources();
|
|
332
|
+
server.close(async () => {
|
|
333
|
+
// ✅ 重新加载所有自定义功能(强制破坏缓存),重新获取模板列表,重建服务器并设置热重载
|
|
334
|
+
await loadUserFeatures(app, false, true), cachedPages = await getAvailableTemplates();
|
|
335
|
+
createServerWithSocket(app, true), server.listen(port, () => setupHotReload());
|
|
336
|
+
});
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('[热重载] 重启过程中发生错误:', error);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// ==================== 7.导出接口与启动执行 ====================
|
|
343
|
+
export { startServer };
|
|
344
|
+
if (process.argv[1] === __filename) startServer().catch(error => (console.error('服务器启动失败:', error), process.exit(1)));
|
package/dev.js
ADDED
package/f-CHANGELOG.md
ADDED