@flun/html-template 4.2.2 → 4.3.0
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/compile.js +197 -190
- package/customize/hotReloadInjector.js +22 -22
- package/dev-server.js +95 -143
- package/dev.js +12 -1
- package/f-CHANGELOG.md +12 -2
- package/f-README.md +66 -7
- package/index.d.ts +24 -7
- package/package.json +4 -4
- package/services/middleware.js +31 -0
- package/services/templateService.js +147 -3
- package/static/auth.js +1 -1
package/dev-server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 模块结构:
|
|
5
5
|
* 1. 依赖导入与服务器初始化
|
|
6
|
-
* 2. 服务器配置与端口管理(
|
|
6
|
+
* 2. 服务器配置与端口管理(parseServerConfig)
|
|
7
7
|
* 3. 全局CORS中间件和静态资源配置(/static路径)
|
|
8
8
|
* 4. 服务器生命周期管理(printAvailablePages, startServer)
|
|
9
9
|
* 5. 请求页面路由处理(自动路由与模板渲染) —— 已在 startServer 内部动态添加
|
|
@@ -13,32 +13,57 @@
|
|
|
13
13
|
|
|
14
14
|
// ==================== 1.依赖导入与服务器初始化 ====================
|
|
15
15
|
import express from 'express';
|
|
16
|
-
import { constants, existsSync } from 'fs';
|
|
16
|
+
import { constants, existsSync, readFileSync } from 'fs';
|
|
17
17
|
import http from 'http';
|
|
18
|
+
import https from 'https';
|
|
18
19
|
import { Server as socketIo } from 'socket.io';
|
|
19
20
|
import chokidar from 'chokidar';
|
|
20
21
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
21
22
|
import {
|
|
22
|
-
path, fsPromises, CWD, getAvailableTemplates,
|
|
23
|
-
processVariables, loadUserFeatures, writtenFilesToIgnore, templatesAbsDir, templatesDir,
|
|
24
|
-
|
|
23
|
+
path, fsPromises, CWD, getAvailableTemplates, parseServerConfig, generateUrls, findEntryFile, validateTemplateFile,
|
|
24
|
+
renderTemplate, processIncludes, processVariables, loadUserFeatures, writtenFilesToIgnore, templatesAbsDir, templatesDir,
|
|
25
|
+
staticDir, customizeDir, accountDir, monitorFileWrites
|
|
25
26
|
} from './services/templateService.js';
|
|
27
|
+
import { corsMiddleware, trustProxySetting } from './services/middleware.js';
|
|
26
28
|
import { injectScript } from './customize/hotReloadInjector.js';
|
|
27
29
|
|
|
28
|
-
let server, io, watcher, cachedPages = [], unmountMonitor = null;
|
|
30
|
+
let server, io, watcher, cachedPages = [], unmountMonitor = null, currentHttpsConfig = null, currentHost = 'localhost';
|
|
29
31
|
const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename),
|
|
30
32
|
app = express(), staticAbsDir = path.join(CWD, staticDir), customizeAbsDir = path.join(CWD, customizeDir),
|
|
31
33
|
|
|
32
34
|
// ==================== 工具函数 ====================
|
|
33
35
|
/**
|
|
34
|
-
* 创建带WebSocket
|
|
36
|
+
* 创建带WebSocket的服务器(支持HTTP/HTTPS)
|
|
37
|
+
* @param {Express} app Express应用
|
|
38
|
+
* @param {boolean} hotReload 是否启用热重载
|
|
39
|
+
* @param {boolean} useHttps 是否使用HTTPS
|
|
40
|
+
* @param {string} keyPath HTTPS私钥路径(useHttps=true时必须)
|
|
41
|
+
* @param {string} certPath HTTPS证书路径(useHttps=true时必须)
|
|
42
|
+
* @returns {http.Server|https.Server} 创建的服务器实例
|
|
35
43
|
*/
|
|
36
|
-
createServerWithSocket = (app, hotReload) => {
|
|
37
|
-
|
|
44
|
+
createServerWithSocket = (app, hotReload, useHttps = false, keyPath = null, certPath = null) => {
|
|
45
|
+
let serverInstance;
|
|
46
|
+
if (useHttps) {
|
|
47
|
+
try {
|
|
48
|
+
// 确保文件存在
|
|
49
|
+
if (!existsSync(keyPath)) throw new Error(`私钥文件不存在: ${keyPath}`);
|
|
50
|
+
if (!existsSync(certPath)) throw new Error(`证书文件不存在: ${certPath}`);
|
|
51
|
+
const key = readFileSync(keyPath, 'utf8'), cert = readFileSync(certPath, 'utf8');
|
|
52
|
+
serverInstance = https.createServer({ key, cert }, app);
|
|
53
|
+
console.log(`🔒 HTTPS已启用,证书加载自: ${keyPath} 和 ${certPath}`);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`❌ HTTPS证书加载失败: ${err.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else serverInstance = http.createServer(app);
|
|
60
|
+
|
|
61
|
+
server = serverInstance;
|
|
38
62
|
if (hotReload) {
|
|
39
63
|
io = new socketIo(server);
|
|
40
64
|
io.engine.on("headers", headers => headers["Content-Type"] = "text/html; charset=utf-8");
|
|
41
65
|
}
|
|
66
|
+
return server;
|
|
42
67
|
},
|
|
43
68
|
|
|
44
69
|
/**
|
|
@@ -50,15 +75,6 @@ const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__fi
|
|
|
50
75
|
if (unmountMonitor) unmountMonitor(); unmountMonitor = null;
|
|
51
76
|
},
|
|
52
77
|
|
|
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
78
|
/**
|
|
63
79
|
* 递归复制目录
|
|
64
80
|
*/
|
|
@@ -110,95 +126,12 @@ const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__fi
|
|
|
110
126
|
console.error('❌ 默认 templates/account 目录不存在或复制失败:', err.message);
|
|
111
127
|
}
|
|
112
128
|
}
|
|
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
129
|
};
|
|
180
130
|
|
|
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
131
|
|
|
200
|
-
|
|
201
|
-
|
|
132
|
+
// ==================== 3.全局CORS中间件和静态资源配置 ====================
|
|
133
|
+
app.use(corsMiddleware);
|
|
134
|
+
app.set('trust proxy', trustProxySetting);
|
|
202
135
|
app.use('/static', express.static(staticAbsDir));
|
|
203
136
|
|
|
204
137
|
// ==================== 4.服务器生命周期管理 ====================
|
|
@@ -207,21 +140,27 @@ app.use('/static', express.static(staticAbsDir));
|
|
|
207
140
|
* @param {string[]} pages - 有效的模板文件名集合
|
|
208
141
|
* @param {number} port - 服务器端口号
|
|
209
142
|
* @param {boolean} hotReload - 是否启用热重载
|
|
143
|
+
* @param {boolean} useHttps - 是否使用HTTPS
|
|
144
|
+
* @param {string} host - 服务器主机名
|
|
210
145
|
*/
|
|
211
|
-
const printAvailablePages = (pages, port, hotReload) => {
|
|
212
|
-
|
|
146
|
+
const printAvailablePages = (pages, port, hotReload, useHttps, host) => {
|
|
147
|
+
const protocol = useHttps ? 'https' : 'http';
|
|
148
|
+
console.log(`开发服务器启动成功!\n访问地址: ${protocol}://${host}:${port}`);
|
|
213
149
|
if (hotReload) console.log(`✅ 热重载功能已启用(监听目录->${templatesAbsDir},${staticDir},${customizeDir})`);
|
|
214
150
|
|
|
151
|
+
if (useHttps && (host === 'localhost' || host === '127.0.0.1')) {
|
|
152
|
+
console.warn('⚠️ 警告: 使用 HTTPS 访问 localhost 会导致浏览器证书安全警告(自签名证书或证书域名不匹配)');
|
|
153
|
+
console.warn(' 请使用 --host 参数指定与证书 CN/SAN 匹配的域名,例如: --host book.123xyz.cn');
|
|
154
|
+
}
|
|
155
|
+
|
|
215
156
|
console.log('\n可访问页面:');
|
|
216
157
|
pages.sort().forEach(page => {
|
|
217
|
-
const { url, encodedUrl, needsEncoding } = generateUrls(page, port);
|
|
218
|
-
|
|
158
|
+
const { url, encodedUrl, needsEncoding } = generateUrls(page, port, useHttps, host);
|
|
219
159
|
if (needsEncoding) console.log(` 原始路径: ${url} (需复制访问)\n 编码路径: ${encodedUrl} (直接访问)`);
|
|
220
160
|
else console.log(` 直接访问: ${url}`);
|
|
221
161
|
});
|
|
222
|
-
|
|
223
162
|
console.log(`\n共发现 ${pages.length} 个可用模板`), console.log('-----------------------------------');
|
|
224
|
-
}
|
|
163
|
+
};
|
|
225
164
|
|
|
226
165
|
/**
|
|
227
166
|
* 服务器启动主函数
|
|
@@ -229,17 +168,33 @@ const printAvailablePages = (pages, port, hotReload) => {
|
|
|
229
168
|
* @async
|
|
230
169
|
* @param {Object} [options] - 配置对象
|
|
231
170
|
* @param {number} [options.port] - 可选端口号
|
|
171
|
+
* @param {string} [options.host] - 服务器主机名(默认localhost)
|
|
232
172
|
* @param {boolean} [options.hotReload] - 是否启用热重载
|
|
233
173
|
* @param {boolean} [options.account] - 是否启用登录模式
|
|
174
|
+
* @param {boolean} [options.https] - 是否启用HTTPS
|
|
175
|
+
* @param {string} [options.httpsKey] - HTTPS私钥路径(启用HTTPS时必须)
|
|
176
|
+
* @param {string} [options.httpsCert] - HTTPS证书路径(启用HTTPS时必须)
|
|
177
|
+
* @returns {Promise<number>} 启动成功后返回实际使用的端口号
|
|
234
178
|
*/
|
|
235
179
|
const startServer = async (options = {}) => {
|
|
236
180
|
try {
|
|
237
|
-
const config = parseServerConfig(options),
|
|
181
|
+
const config = parseServerConfig(options),
|
|
182
|
+
{ port, host, hotReload, account, httpsEnabled, httpsKeyPath, httpsCertPath } = config,
|
|
183
|
+
|
|
184
|
+
// 保存配置到文件(用于后续编译时沿用)
|
|
185
|
+
configToSave = {
|
|
186
|
+
port, host, https: httpsEnabled, httpsKey: httpsKeyPath, httpsCert: httpsCertPath
|
|
187
|
+
};
|
|
188
|
+
await fsPromises.writeFile(path.join(CWD, '.dev-config.json'), JSON.stringify(configToSave, null, 2), 'utf8');
|
|
238
189
|
|
|
239
|
-
|
|
240
|
-
|
|
190
|
+
currentHttpsConfig = { https: httpsEnabled, keyPath: httpsKeyPath, certPath: httpsCertPath };
|
|
191
|
+
currentHost = host;
|
|
241
192
|
|
|
242
|
-
|
|
193
|
+
if (account) await ensureAccountFiles();
|
|
194
|
+
await loadUserFeatures(app);
|
|
195
|
+
cachedPages = await getAvailableTemplates();
|
|
196
|
+
|
|
197
|
+
// 核心模板渲染中间件
|
|
243
198
|
app.use(async (req, res, next) => {
|
|
244
199
|
try {
|
|
245
200
|
const decodedPath = decodeURIComponent(req.path);
|
|
@@ -247,35 +202,34 @@ const startServer = async (options = {}) => {
|
|
|
247
202
|
const entryFile = await findEntryFile(cachedPages);
|
|
248
203
|
return res.redirect(`/${entryFile}`);
|
|
249
204
|
}
|
|
250
|
-
|
|
251
205
|
const templateFile = decodedPath.endsWith('.html') ? decodedPath.slice(1) : `${decodedPath.slice(1)}.html`;
|
|
252
206
|
if (cachedPages.includes(templateFile)) {
|
|
253
207
|
let rendered = await renderTemplate(templateFile);
|
|
254
208
|
rendered = await processIncludes(rendered, templateFile);
|
|
255
209
|
rendered = processVariables(rendered, { currentUrl: decodedPath, query: req.query ? JSON.stringify(req.query) : '' });
|
|
256
|
-
|
|
257
|
-
if (io) rendered = injectScript(rendered); // 如果启用了热重载,注入客户端脚本
|
|
210
|
+
if (io) rendered = injectScript(rendered);
|
|
258
211
|
return res.type('html').send(rendered);
|
|
259
212
|
}
|
|
260
|
-
|
|
261
213
|
next();
|
|
262
214
|
} catch (error) {
|
|
263
|
-
console.error(`处理请求时出错: ${error.message}
|
|
215
|
+
console.error(`处理请求时出错: ${error.message}`, error.stack);
|
|
264
216
|
next(error);
|
|
265
217
|
}
|
|
266
218
|
});
|
|
267
|
-
for (const page of cachedPages) await validateTemplateFile(page, true); // 验证模板文件
|
|
268
219
|
|
|
269
|
-
|
|
270
|
-
|
|
220
|
+
for (const page of cachedPages) await validateTemplateFile(page, true);
|
|
221
|
+
|
|
222
|
+
if (hotReload) setupHotReload();
|
|
223
|
+
createServerWithSocket(app, hotReload, httpsEnabled, httpsKeyPath, httpsCertPath);
|
|
224
|
+
printAvailablePages(cachedPages, port, hotReload, httpsEnabled, host);
|
|
271
225
|
|
|
272
|
-
server.listen(
|
|
226
|
+
server.listen(port, () => {
|
|
273
227
|
console.log(`服务器运行中,按 Ctrl+C 退出`), console.log('-----------------------------------');
|
|
274
228
|
});
|
|
275
|
-
|
|
276
|
-
return p;
|
|
229
|
+
return port;
|
|
277
230
|
} catch (error) {
|
|
278
|
-
console.error('服务器启动失败:', error.message)
|
|
231
|
+
console.error('服务器启动失败:', error.message);
|
|
232
|
+
process.exit(1);
|
|
279
233
|
}
|
|
280
234
|
},
|
|
281
235
|
|
|
@@ -284,38 +238,32 @@ const startServer = async (options = {}) => {
|
|
|
284
238
|
* 设置文件监听和热重载功能
|
|
285
239
|
*/
|
|
286
240
|
setupHotReload = () => {
|
|
287
|
-
// 监听模板目录、静态文件目录和后端目录
|
|
288
241
|
const watchDirs = [templatesAbsDir, staticAbsDir, customizeAbsDir].filter(dir => existsSync(dir));
|
|
289
242
|
if (watchDirs.length === 0) return console.warn('[热重载] 没有可监听的目录');
|
|
290
243
|
|
|
291
|
-
unmountMonitor = monitorFileWrites();
|
|
292
|
-
// 文件变更事件处理函数
|
|
244
|
+
unmountMonitor = monitorFileWrites();
|
|
293
245
|
const handleFileEvent = (event, filePath) => {
|
|
294
246
|
const normalizedPath = path.normalize(filePath);
|
|
295
|
-
if (writtenFilesToIgnore.includes(normalizedPath)) return;
|
|
247
|
+
if (writtenFilesToIgnore.includes(normalizedPath)) return;
|
|
296
248
|
|
|
297
249
|
const isBackendFile = filePath.startsWith(customizeAbsDir);
|
|
298
250
|
if (isBackendFile) {
|
|
299
251
|
console.log(`检测到${event}了${normalizedPath}后端文件,[热重载] 执行服务器重启并刷新页面...`);
|
|
300
|
-
io.emit('hot-reload', 3500)
|
|
301
|
-
|
|
302
|
-
else {
|
|
252
|
+
io.emit('hot-reload', 3500);
|
|
253
|
+
setTimeout(() => restartServer(), 500);
|
|
254
|
+
} else {
|
|
303
255
|
console.log(`检测到${event}了${normalizedPath}前端文件,[热重载] 已刷新页面...`);
|
|
304
|
-
// 如果删除了HTML模板文件,从缓存中移除
|
|
305
256
|
if (event === '删除' && filePath.startsWith(templatesAbsDir) && filePath.endsWith('.html')) {
|
|
306
257
|
const templateName = path.relative(templatesAbsDir, filePath).replace(/\\/g, '/');
|
|
307
258
|
cachedPages = cachedPages.filter(page => page !== templateName);
|
|
308
259
|
}
|
|
309
|
-
|
|
310
|
-
io.emit('hot-reload', 100); // 通知浏览器刷新
|
|
260
|
+
io.emit('hot-reload', 100);
|
|
311
261
|
}
|
|
312
262
|
};
|
|
313
263
|
|
|
314
|
-
// 设置监听器(忽略隐藏文件)
|
|
315
264
|
watcher = chokidar.watch(watchDirs, {
|
|
316
265
|
ignored: /(^|[\/\\])\../, persistent: true, ignoreInitial: true
|
|
317
266
|
});
|
|
318
|
-
|
|
319
267
|
watcher.on('change', (filePath) => handleFileEvent('更改', filePath))
|
|
320
268
|
.on('add', (filePath) => handleFileEvent('添加', filePath))
|
|
321
269
|
.on('unlink', (filePath) => handleFileEvent('删除', filePath))
|
|
@@ -323,16 +271,20 @@ const startServer = async (options = {}) => {
|
|
|
323
271
|
},
|
|
324
272
|
|
|
325
273
|
/**
|
|
326
|
-
*
|
|
274
|
+
* 重启服务器(保持HTTPS和主机名配置)
|
|
327
275
|
*/
|
|
328
276
|
restartServer = async () => {
|
|
329
277
|
try {
|
|
330
|
-
|
|
278
|
+
if (!currentHttpsConfig) return console.error('[热重载] 无法获取当前HTTPS配置,重启失败');
|
|
279
|
+
|
|
280
|
+
const port = server.address().port, { https, keyPath, certPath } = currentHttpsConfig, host = currentHost;
|
|
331
281
|
cleanupResources();
|
|
332
282
|
server.close(async () => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
createServerWithSocket(app, true
|
|
283
|
+
await loadUserFeatures(app, false, true);
|
|
284
|
+
cachedPages = await getAvailableTemplates();
|
|
285
|
+
createServerWithSocket(app, true, https, keyPath, certPath);
|
|
286
|
+
server.listen(port, () => setupHotReload());
|
|
287
|
+
printAvailablePages(cachedPages, port, true, https, host); // 重启后重新打印可访问页面
|
|
336
288
|
});
|
|
337
289
|
} catch (error) {
|
|
338
290
|
console.error('[热重载] 重启过程中发生错误:', error);
|
package/dev.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { startDevServer } from '@flun/html-template';
|
|
2
|
+
/**
|
|
3
|
+
* 如果需要启用https,请先安装 @flun/dns-auto-ssl,并在生成的示例文件(DnsAutoSSL.js)中配置相关参数,
|
|
4
|
+
* 然后将下面导入部分注释取消并使用这些配置项,或使用自己已有的证书路径和域名配置项进行替换;
|
|
5
|
+
*/
|
|
6
|
+
// import { domains, certPath, keyPath } from './DnsAutoSSL.js';
|
|
2
7
|
|
|
3
8
|
// 启动开发服务器
|
|
4
|
-
startDevServer({
|
|
9
|
+
startDevServer({
|
|
10
|
+
port: 7296, hotReload: true, account: false, // 默认参数:开发服务器端口7296,启用热更新,不启用登录系统;
|
|
11
|
+
// https: true,
|
|
12
|
+
// httpsKey: keyPath,
|
|
13
|
+
// httpsCert: certPath,
|
|
14
|
+
// host: domains[0],
|
|
15
|
+
});
|
package/f-CHANGELOG.md
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
# 变更日志
|
|
2
|
-
## [4.
|
|
2
|
+
## [4.3.0] - 2026-05-29 10:05
|
|
3
|
+
### 新增
|
|
4
|
+
- 开发服务器支持 HTTPS 协议,可通过示例文件: `dev.js` 中的 `https`、`httpsKey`、`httpsCert` 参数启用。
|
|
5
|
+
- 集成 `@flun/dns-auto-ssl` 自动生成受信任的证书,简化本地 HTTPS 配置(推荐方式)。
|
|
6
|
+
- 在配置选项中补充完整的 HTTPS 启用说明(含自定义证书与自动 SSL 两种方式)。
|
|
7
|
+
- 启动服务器时增加对证书文件存在性的校验,避免因路径错误导致服务崩溃。
|
|
8
|
+
- 控制台输出增加 HTTPS 协议访问提示,并在使用 localhost 访问 HTTPS 时给出警告。
|
|
9
|
+
### 修复:
|
|
10
|
+
- 修复个人资料页中硬件信息过长时导致删除按钮文字挤压样式改变的问题;
|
|
11
|
+
- 修复打包后运行项目时,自定义目录中的 hotReloadInjector.js 文件逻辑中对是否发送 IO 到页面判断的缺失,造成的错误提示;
|
|
3
12
|
### 优化:
|
|
4
|
-
-
|
|
13
|
+
- 将 account.js 中 cookie 的 secure 属性由固定 false 改为 'auto';
|
|
14
|
+
现在会根据请求是否为 HTTPS 自动设置 Secure 标志:HTTPS 下自动设为 true,HTTP 下保持 false,从而同时兼容本地开发环境和线上安全传输;
|
package/f-README.md
CHANGED
|
@@ -72,6 +72,7 @@ npm i @flun/html-template # 简写
|
|
|
72
72
|
npm i -g @flun/html-template # 简写
|
|
73
73
|
|
|
74
74
|
# flun其它npm家族安装包:
|
|
75
|
+
npm i @flun/dns-auto-ssl # https证书申请及配置自动续期
|
|
75
76
|
npm i @flun/env # .env 文件的环境变量调用
|
|
76
77
|
npm i @flun/mailer # 邮件发送
|
|
77
78
|
npm i @flun/windows # Window服务安装和管理
|
|
@@ -103,6 +104,7 @@ npm i @flun/webauthn-browser # 身份验证前端处理
|
|
|
103
104
|
```javascript
|
|
104
105
|
import { startDevServer } from '@flun/html-template';
|
|
105
106
|
startDevServer({ port: 7296, hotReload: true });
|
|
107
|
+
// 如需启用 HTTPS,请参考下文“配置选项 → 启用 HTTPS”章节
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
**build.js(ESM)**
|
|
@@ -177,6 +179,61 @@ import { compile } from '@flun/html-template';
|
|
|
177
179
|
compile({ outputDir: 'dist' }); // 默认参数:目录名 dist;
|
|
178
180
|
```
|
|
179
181
|
|
|
182
|
+
### 启用 HTTPS
|
|
183
|
+
|
|
184
|
+
开发服务器支持 HTTPS 协议,适用于需要安全连接、测试 PWA 或与第三方 API 交互的场景;
|
|
185
|
+
|
|
186
|
+
#### 方式一:使用自动 SSL 证书(推荐)
|
|
187
|
+
|
|
188
|
+
1. 安装自动 SSL 工具包:
|
|
189
|
+
```sh
|
|
190
|
+
npm i @flun/dns-auto-ssl --save-dev
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
2. 打开项目根目录下的 DnsAutoSSL.js,按提示配置你的域名等参数;
|
|
194
|
+
|
|
195
|
+
3. 在 `dev.js` 中取消注释相关配置项:
|
|
196
|
+
```javascript
|
|
197
|
+
import { startDevServer } from '@flun/html-template';
|
|
198
|
+
import { domains, certPath, keyPath } from './DnsAutoSSL.js';
|
|
199
|
+
|
|
200
|
+
startDevServer({
|
|
201
|
+
port: 7296,
|
|
202
|
+
hotReload: true,
|
|
203
|
+
https: true,
|
|
204
|
+
httpsKey: keyPath,
|
|
205
|
+
httpsCert: certPath,
|
|
206
|
+
host: domains[0]
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### 方式二:使用自定义证书
|
|
211
|
+
|
|
212
|
+
若已有证书文件(`.key` 和 `.crt`/`.pem`),直接指定路径:
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
startDevServer({
|
|
216
|
+
port: 7296,
|
|
217
|
+
https: true,
|
|
218
|
+
httpsKey: '你的私钥文件路径和文件名', // 例如: ./ssl/abc.key
|
|
219
|
+
httpsCert: '你的证书文件路径和文件名', // 例如: ./ssl/abc.crt
|
|
220
|
+
host: '你的域名' // 必须与证书中的 CN/SAN 一致
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### 命令行参数快速启用
|
|
225
|
+
|
|
226
|
+
也可通过命令行直接启用 HTTPS(需预先准备好证书文件):
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
node dev.js --https --https-key 你的私钥文件路径和文件名 --https-cert 你的证书文件路径和文件名 --host example.com
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> **注意**:
|
|
233
|
+
> - 使用 HTTPS 时,`host` 参数必须与证书中的域名(CN 或 SAN)完全一致。
|
|
234
|
+
> - 若用 `localhost` 访问 HTTPS,浏览器会提示不安全,请按控制台警告改用正确的域名访问。
|
|
235
|
+
> - 有关 `DnsAutoSSL.js` 的详细配置,请参考该文件内的注释或 `@flun/dns-auto-ssl` 文档。
|
|
236
|
+
|
|
180
237
|
## 模板标签使用指南
|
|
181
238
|
|
|
182
239
|
### 基础概念
|
|
@@ -212,7 +269,7 @@ HBuilder自定义代码块配置(HTML和js):
|
|
|
212
269
|
|
|
213
270
|
### 更新版本
|
|
214
271
|
```sh
|
|
215
|
-
npm
|
|
272
|
+
npm up @flun/html-template
|
|
216
273
|
```
|
|
217
274
|
|
|
218
275
|
### 恢复初始示例文件
|
|
@@ -248,6 +305,7 @@ initProject({ mode: 'overwrite', verbose: false }); // 覆盖所有包文件
|
|
|
248
305
|
initProject({ mode: 'skip-files', verbose: true }); // 跳过已存在文件并显示详细信息
|
|
249
306
|
initProject({ mode: 'skip-files', verbose: true, account: false }); // 跳过已存在文件,显示详细信息,并跳过恢复登录相关文件
|
|
250
307
|
```
|
|
308
|
+
|
|
251
309
|
---
|
|
252
310
|
|
|
253
311
|
## 工作流程
|
|
@@ -277,19 +335,19 @@ initProject({ mode: 'skip-files', verbose: true, account: false }); // 跳过已
|
|
|
277
335
|
- 模板结构验证与错误提示
|
|
278
336
|
- 用户功能热加载和页面热重载功能
|
|
279
337
|
|
|
280
|
-
### 登录系统支持
|
|
281
|
-
- 登录系统支持密码,2FA,硬件等验证;支持用户名或邮箱登录;
|
|
282
|
-
- 包含文件去重处理
|
|
283
|
-
- 按需生成Express服务入口
|
|
284
|
-
- 智能编译顺序控制
|
|
285
|
-
|
|
286
338
|
### 编译系统优势
|
|
287
339
|
- 分析模板依赖关系
|
|
288
340
|
- 包含文件去重处理
|
|
289
341
|
- 按需生成Express服务入口
|
|
290
342
|
- 智能编译顺序控制
|
|
343
|
+
- 继承开发阶段的配置(比如https启用,端口配置等等)
|
|
291
344
|
|
|
292
345
|
## 附加功能
|
|
346
|
+
### 登录系统支持
|
|
347
|
+
- 登录系统支持密码,2FA,硬件等验证;支持用户名或邮箱登录;
|
|
348
|
+
- 包含文件去重处理
|
|
349
|
+
- 按需生成Express服务入口
|
|
350
|
+
- 智能编译顺序控制
|
|
293
351
|
|
|
294
352
|
### 页面样式在线修改(需启用登录系统)
|
|
295
353
|
- 支持长按元素选择设置其各种属性样式(比如:边距,颜色,字体等等);
|
|
@@ -473,6 +531,7 @@ export default {
|
|
|
473
531
|
4. **路由不工作**:检查是否正确定义了 `setupRoutes` 导出
|
|
474
532
|
5. **找不到函数**: 1. 检查函数名是否正确,2. 确认文件在 `customize` 目录内,3. 确认使用了 `export const functions = {...}` 语法
|
|
475
533
|
6. **ESM 相关错误**:检查 `package.json` 是否包含 `"type": "module"`,或启动文件是否具有 `.mjs` 扩展名。
|
|
534
|
+
7. **HTTPS 证书错误**:请确保 `host` 参数与证书中的域名完全匹配,且证书未被吊销或过期。开发环境可使用 `@flun/dns-auto-ssl` 自动生成受信任的证书。
|
|
476
535
|
|
|
477
536
|
### 获取帮助
|
|
478
537
|
如果遇到问题,可以:
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
path, fsPromises, CWD, templatesDir, templatesAbsDir, staticDir, customizeDir, accountDir,
|
|
3
|
-
|
|
4
|
-
setCompilationMode, getIncludedFiles, processVariables, loadUserFeatures, monitorFileWrites
|
|
2
|
+
path, fsPromises, CWD, templatesDir, templatesAbsDir, staticDir, customizeDir, accountDir, writtenFilesToIgnore,
|
|
3
|
+
getAvailableTemplates, parseServerConfig, generateUrls, findEntryFile, validateTemplateFile, renderTemplate,
|
|
4
|
+
processIncludes, setCompilationMode, getIncludedFiles, processVariables, loadUserFeatures, monitorFileWrites
|
|
5
5
|
} from './services/templateService.js';
|
|
6
6
|
import { compileAllTemplates } from './compile.js';
|
|
7
7
|
import { runCopyFiles } from './copy-files.js';
|
|
@@ -27,6 +27,8 @@ import { injectScript } from './customize/hotReloadInjector.js';
|
|
|
27
27
|
*
|
|
28
28
|
* // 函数列表:
|
|
29
29
|
* getAvailableTemplates(); // 获取所有可用模板文件(排除 base.html)
|
|
30
|
+
* parseServerConfig(); // 解析服务器配置参数
|
|
31
|
+
* generateUrls(); // 生成服务器访问URL列表
|
|
30
32
|
* findEntryFile(); // 动态识别入口文件('@entry'标记 > 优先级列表 > 首字母排序)
|
|
31
33
|
* validateTemplateFile(); // 验证模板文件标签结构完整性
|
|
32
34
|
* renderTemplate(); // 核心模板渲染(处理 extends 继承与区块合并)
|
|
@@ -40,7 +42,7 @@ import { injectScript } from './customize/hotReloadInjector.js';
|
|
|
40
42
|
* >查看定义:@see
|
|
41
43
|
* - 常量:{@link path}、{@link fsPromises}、{@link CWD}、{@link templatesDir}、{@link templatesAbsDir}、{@link staticDir}、
|
|
42
44
|
*{@link customizeDir}、{@link accountDir}、{@link defaultPort}、{@link writtenFilesToIgnore}
|
|
43
|
-
* - 函数:{@link getAvailableTemplates}、{@link findEntryFile}、{@link validateTemplateFile}、{@link renderTemplate}、
|
|
45
|
+
* - 函数:{@link getAvailableTemplates}、{@link parseServerConfig}、{@link generateUrls}、{@link findEntryFile}、{@link validateTemplateFile}、{@link renderTemplate}、
|
|
44
46
|
*{@link processIncludes}、{@link setCompilationMode}、{@link getIncludedFiles}、{@link processVariables}、
|
|
45
47
|
*{@link loadUserFeatures}、{@link monitorFileWrites}
|
|
46
48
|
*/
|
|
@@ -109,8 +111,23 @@ declare module './customize/hotReloadInjector.js' {
|
|
|
109
111
|
* >
|
|
110
112
|
* @example
|
|
111
113
|
* // 启动服务器示例
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
+
* import { startDevServer } from '@flun/html-template';
|
|
115
|
+
*
|
|
116
|
+
* // 如果需要启用 https,请先安装 `@flun/dns-auto-ssl`,并在生成的示例文件 (DnsAutoSSL.js) 中配置相关参数,
|
|
117
|
+
* // 然后将下面导入部分注释取消并使用这些配置项,或使用自己已有的证书路径和域名配置项进行替换。
|
|
118
|
+
*
|
|
119
|
+
* // import { domains, certPath, keyPath } from './DnsAutoSSL.js';
|
|
120
|
+
*
|
|
121
|
+
* // 启动开发服务器
|
|
122
|
+
* startDevServer({
|
|
123
|
+
* port: 7296,
|
|
124
|
+
* hotReload: true,
|
|
125
|
+
* account: false, // 默认参数:不启用登录系统
|
|
126
|
+
* // https: true,
|
|
127
|
+
* // httpsKey: keyPath,
|
|
128
|
+
* // httpsCert: certPath,
|
|
129
|
+
* // host: domains[0],
|
|
130
|
+
* });
|
|
114
131
|
*
|
|
115
132
|
* // -----------------------------------------------
|
|
116
133
|
* // 恢复包示例文件
|
|
@@ -124,7 +141,7 @@ declare module './customize/hotReloadInjector.js' {
|
|
|
124
141
|
* // -----------------------------------------------
|
|
125
142
|
* // 编译模板示例
|
|
126
143
|
* import { compile } from '@flun/html-template';
|
|
127
|
-
* compile({outputDir: 'my-dist'}); // 可选参数:指定输出目录,默认为'dist'
|
|
144
|
+
* compile({ outputDir: 'my-dist' }); // 可选参数:指定输出目录,默认为'dist'
|
|
128
145
|
*/
|
|
129
146
|
declare module './index.js' {
|
|
130
147
|
export { compileAllTemplates as compile } from './compile.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flun/html-template",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "一个HTML模板工具包,提供开发服务器和模板编译功能,支持自定义标签和快捷输入,变量定义,包含文件引用,帮助开发者模块化处理HTML;",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"express": "^5.2.1",
|
|
56
56
|
"express-rate-limit": "^8.3.2",
|
|
57
57
|
"express-session": "^1.19.0",
|
|
58
|
-
"@flun/env": "
|
|
59
|
-
"@flun/mailer": "
|
|
60
|
-
"@flun/webauthn-server": "
|
|
58
|
+
"@flun/env": "*",
|
|
59
|
+
"@flun/mailer": "*",
|
|
60
|
+
"@flun/webauthn-server": "*",
|
|
61
61
|
"mysql2": "^3.20.0",
|
|
62
62
|
"otplib": "^13.4.0",
|
|
63
63
|
"qrcode": "^1.5.4",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description 公共中间件(如 CORS 设置等)
|
|
3
|
+
* @type {import('express').RequestHandler<
|
|
4
|
+
* import('express-serve-static-core').ParamsDictionary,
|
|
5
|
+
* unknown, // 响应体类型
|
|
6
|
+
* unknown, // 请求体类型
|
|
7
|
+
* import('qs').ParsedQs,
|
|
8
|
+
* Record<string, unknown> // locals 类型
|
|
9
|
+
* >}
|
|
10
|
+
*/
|
|
11
|
+
const corsMiddleware = (req, res, next) => {
|
|
12
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
13
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD');
|
|
14
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin, X-CSRF-Token');
|
|
15
|
+
res.setHeader('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
|
|
16
|
+
res.setHeader('Access-Control-Max-Age', '86400');
|
|
17
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
18
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
19
|
+
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
|
|
20
|
+
|
|
21
|
+
if (req.method === 'OPTIONS') {
|
|
22
|
+
res.setHeader('Content-Length', '0');
|
|
23
|
+
return res.status(204).end();
|
|
24
|
+
}
|
|
25
|
+
next();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** @type {boolean} */
|
|
29
|
+
const trustProxySetting = false;
|
|
30
|
+
|
|
31
|
+
export { corsMiddleware, trustProxySetting };
|