@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/dev-server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 模块结构:
5
5
  * 1. 依赖导入与服务器初始化
6
- * 2. 服务器配置与端口管理(parseAndValidatePort,parseServerConfig)
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, findEntryFile, validateTemplateFile, renderTemplate, processIncludes,
23
- processVariables, loadUserFeatures, writtenFilesToIgnore, templatesAbsDir, templatesDir, staticDir, customizeDir,
24
- accountDir, defaultPort, monitorFileWrites
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
- server = http.createServer(app);
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
- next();
201
- }), app.set('trust proxy', false);
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
- console.log(`开发服务器启动成功!\n访问地址: http://localhost:${port}`);
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), { port: p, hotReload: h, account: acc } = config;
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
- if (acc) await ensureAccountFiles(); // 如果启用登录模式,验证必要文件
240
- await loadUserFeatures(app), cachedPages = await getAvailableTemplates(); // 加载用户自定义功能获取模板内容
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}`), console.error(error.stack);
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
- if (h) setupHotReload();
270
- printAvailablePages(cachedPages, p, h), createServerWithSocket(app, h);
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(p, () => {
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), process.exit(1);
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), setTimeout(() => restartServer(), 500); // 通知浏览器延迟刷新,延迟后重启服务器
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
- const port = server.address().port;
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
- await loadUserFeatures(app, false, true), cachedPages = await getAvailableTemplates();
335
- createServerWithSocket(app, true), server.listen(port, () => setupHotReload());
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({ port: 7296, hotReload: true, account: false }); // 默认参数:开发服务器端口7296,启用热更新,不启用登录系统;
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.2] - 2026-05-15 20:37
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 update @flun/html-template
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, defaultPort,
3
- writtenFilesToIgnore, getAvailableTemplates, findEntryFile, validateTemplateFile, renderTemplate, processIncludes,
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
- * import { startDevServer } from '@flun/html-template';
113
- * startDevServer({ port: 7296, hotReload: true, account: false }); // 默认参数:开发服务器端口7296,启用热更新,不启用登录系统;
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.2.2",
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": "^3.1.16",
59
- "@flun/mailer": "^2.1.14",
60
- "@flun/webauthn-server": "^2.0.22",
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 };