imean-service-engine-htmx-plugin 2.4.0 → 2.6.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.
@@ -1,509 +0,0 @@
1
- # Hono HTML 模板字符串最佳实践
2
-
3
- 本文档介绍在 HtmxAdminPlugin 中使用 Hono `html` 模板字符串开发自定义表单字段渲染器的最佳实践。
4
-
5
- ## 为什么使用 Hono `html` 模板字符串?
6
-
7
- ### 优势
8
-
9
- 1. **自动转义**:Hono 会自动转义 HTML 属性值,避免 XSS 攻击
10
- 2. **类型安全**:TypeScript 提供类型检查和代码提示
11
- 3. **语法高亮**:编辑器支持 HTML 语法高亮和格式化
12
- 4. **避免字符串拼接错误**:不需要手动拼接字符串,减少错误
13
- 5. **更好的可维护性**:代码结构清晰,易于理解和修改
14
-
15
- ### 与字符串拼接的对比
16
-
17
- **❌ 错误:使用字符串拼接**
18
-
19
- ```typescript
20
- const htmlContent = `
21
- <div class="container">
22
- <input name="${fieldName}" value="${value}" />
23
- </div>
24
- `;
25
- return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
26
- ```
27
-
28
- **问题**:
29
- - 需要手动转义 HTML 特殊字符
30
- - 容易出错,特别是处理引号和特殊字符时
31
- - 失去类型检查和语法高亮
32
- - 代码可读性差
33
-
34
- **✅ 正确:使用 Hono `html` 模板字符串**
35
-
36
- ```typescript
37
- import { html } from "hono/html";
38
-
39
- return html`
40
- <div class="container">
41
- <input name="${fieldName}" value="${value}" />
42
- </div>
43
- `;
44
- ```
45
-
46
- **优势**:
47
- - 自动转义,安全可靠
48
- - 类型检查和语法高亮
49
- - 代码清晰易读
50
-
51
- ## 核心原则
52
-
53
- ### 1. 使用 `html` 模板字符串生成所有 HTML
54
-
55
- 所有 HTML 都应该使用 `html` 模板字符串生成,避免手动字符串拼接:
56
-
57
- ```typescript
58
- import { html } from "hono/html";
59
-
60
- export function MyComponent(props: MyComponentProps) {
61
- return html`
62
- <div class="container">
63
- <input name="${props.fieldName}" value="${props.value}" />
64
- </div>
65
- `;
66
- }
67
- ```
68
-
69
- ### 2. `x-data` 属性的处理
70
-
71
- **重要**:不要使用 `raw()` 包装 `x-data` 属性值。
72
-
73
- **❌ 错误**:
74
-
75
- ```typescript
76
- import { html, raw } from "hono/html";
77
-
78
- return html`
79
- <div x-data='${raw(xDataContent)}'>
80
- <!-- ... -->
81
- </div>
82
- `;
83
- ```
84
-
85
- **✅ 正确**:
86
-
87
- ```typescript
88
- import { html } from "hono/html";
89
-
90
- const xDataContent = `{
91
- items: [],
92
- init() {
93
- // ...
94
- }
95
- }`;
96
-
97
- return html`
98
- <div x-data="${xDataContent}">
99
- <!-- ... -->
100
- </div>
101
- `;
102
- ```
103
-
104
- **原因**:Hono 会自动处理 `x-data` 属性值的转义,使用 `raw()` 会破坏转义机制。
105
-
106
- ### 3. JavaScript 字符串值的处理
107
-
108
- 在 `x-data` 中,使用 `JSON.stringify` 安全地插入字符串值:
109
-
110
- ```typescript
111
- const xDataContent = `{
112
- fieldName: ${JSON.stringify(fieldName)},
113
- placeholder: ${JSON.stringify(placeholder)},
114
- items: ${JSON.stringify(initialItems)},
115
- init() {
116
- // ...
117
- }
118
- }`;
119
- ```
120
-
121
- **为什么使用 `JSON.stringify`?**
122
- - 自动处理特殊字符(引号、换行符等)
123
- - 确保 JavaScript 语法正确
124
- - 避免手动转义错误
125
-
126
- ### 4. HTML 属性值的自动转义
127
-
128
- Hono 会自动转义 HTML 属性值,直接使用原始值即可:
129
-
130
- ```typescript
131
- return html`
132
- <input
133
- name="${fieldName}"
134
- value="${value}"
135
- data-testid="input-${fieldName}"
136
- />
137
- `;
138
- ```
139
-
140
- **不需要手动转义**:Hono 会自动处理 `&`、`<`、`>`、`"`、`'` 等特殊字符。
141
-
142
- ### 5. 避免字符串拼接
143
-
144
- **❌ 错误:在函数中拼接 HTML 字符串**
145
-
146
- ```typescript
147
- const generateFieldHTML = (field: ModelField): string => {
148
- return `
149
- <div class="field">
150
- <label>${field.label}</label>
151
- <input name="${field.name}" />
152
- </div>
153
- `;
154
- };
155
- ```
156
-
157
- **✅ 正确:使用 `html` 模板字符串**
158
-
159
- ```typescript
160
- const generateField = (field: ModelField) => {
161
- return html`
162
- <div class="field">
163
- <label>${field.label}</label>
164
- <input name="${field.name}" />
165
- </div>
166
- `;
167
- };
168
- ```
169
-
170
- ### 6. 条件渲染
171
-
172
- 使用三元表达式进行条件渲染:
173
-
174
- ```typescript
175
- return html`
176
- <div>
177
- ${field.required
178
- ? html`<span class="text-red-500">*</span>`
179
- : ""}
180
- <input name="${field.name}" ${field.required ? "required" : ""} />
181
- </div>
182
- `;
183
- ```
184
-
185
- ### 7. 数组渲染
186
-
187
- 使用 `map` 配合 `html` 模板字符串:
188
-
189
- ```typescript
190
- return html`
191
- <select>
192
- ${options.map(
193
- (option) => html`
194
- <option value="${option.value}">${option.label}</option>
195
- `
196
- )}
197
- </select>
198
- `;
199
- ```
200
-
201
- ### 8. 插入已生成的 HTML
202
-
203
- 如果必须插入已生成的 HTML 字符串,使用 `raw()`:
204
-
205
- ```typescript
206
- import { html, raw } from "hono/html";
207
-
208
- const fieldsHTML = fields.map((field) => generateFieldHTML(field)).join("");
209
-
210
- return html`
211
- <div>
212
- ${raw(fieldsHTML)}
213
- </div>
214
- `;
215
- ```
216
-
217
- **注意**:只有在确实需要插入已生成的 HTML 字符串时才使用 `raw()`,优先使用 `html` 模板字符串。
218
-
219
- ## 完整示例
220
-
221
- ### 示例 1:字符串数组编辑器
222
-
223
- ```typescript
224
- import { html } from "hono/html";
225
-
226
- export function StringArrayEditor(props: StringArrayEditorProps) {
227
- const { value, fieldName, placeholder = "请输入内容" } = props;
228
- const initialItems: string[] = value || [];
229
- const initialValueJson = JSON.stringify(initialItems);
230
-
231
- // 构建 x-data 字符串
232
- const xDataContent = `{
233
- items: ${initialValueJson},
234
- fieldName: ${JSON.stringify(fieldName)},
235
- placeholder: ${JSON.stringify(placeholder)},
236
- init() {
237
- const dataAttr = this.$el.getAttribute('data-initial-value');
238
- if (dataAttr) {
239
- try {
240
- const parsed = JSON.parse(dataAttr);
241
- if (Array.isArray(parsed)) {
242
- this.items = parsed;
243
- } else {
244
- this.items = [];
245
- }
246
- } catch (e) {
247
- console.error('Failed to parse initial value:', e);
248
- this.items = [];
249
- }
250
- }
251
- this.updateHiddenField();
252
- },
253
- updateHiddenField() {
254
- const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
255
- if (hiddenInput) {
256
- hiddenInput.value = JSON.stringify(this.items);
257
- }
258
- },
259
- addItem() {
260
- this.items.push('');
261
- this.updateHiddenField();
262
- },
263
- removeItem(index) {
264
- this.items.splice(index, 1);
265
- this.updateHiddenField();
266
- }
267
- }`;
268
-
269
- return html`
270
- <div
271
- x-data="${xDataContent}"
272
- data-initial-value="${initialValueJson}"
273
- x-init="init()"
274
- class="space-y-3"
275
- >
276
- <input
277
- type="hidden"
278
- name="${fieldName}"
279
- value=""
280
- data-testid="hidden-${fieldName}"
281
- />
282
- <div class="space-y-3">
283
- <div class="flex items-center justify-between">
284
- <span class="text-sm text-gray-600">
285
- 共 <span x-text="items.length">0</span> 项
286
- </span>
287
- <button
288
- type="button"
289
- x-on:click="addItem()"
290
- class="px-4 py-2 bg-blue-600 text-white rounded-lg"
291
- data-testid="${fieldName}-add-button"
292
- >
293
- 添加项
294
- </button>
295
- </div>
296
- <div class="space-y-2" x-show="items.length > 0">
297
- <template x-for="(item, index) in items" x-bind:key="index">
298
- <div class="flex items-center gap-2">
299
- <input
300
- type="text"
301
- x-bind:value="items[index] || ''"
302
- x-on:input="updateItem(index, $event.target.value)"
303
- class="flex-1 px-3 py-2 border rounded-lg"
304
- x-bind:data-testid="fieldName + '-input-' + index"
305
- />
306
- <button
307
- type="button"
308
- x-on:click="removeItem(index)"
309
- class="px-3 py-2 text-red-600"
310
- x-bind:data-testid="fieldName + '-remove-button-' + index"
311
- >
312
- 删除
313
- </button>
314
- </div>
315
- </template>
316
- </div>
317
- </div>
318
- </div>
319
- `;
320
- }
321
- ```
322
-
323
- ### 示例 2:对象编辑器(动态生成字段)
324
-
325
- ```typescript
326
- import { html } from "hono/html";
327
-
328
- export function ObjectEditor(props: ObjectEditorProps) {
329
- const { value, fieldName, objectSchema } = props;
330
- const fields = parseSchemaToFields(objectSchema);
331
- const initialValueJson = JSON.stringify(value || {});
332
-
333
- const xDataContent = `{
334
- obj: {},
335
- init() {
336
- const dataAttr = this.$el.getAttribute('data-initial-value');
337
- if (dataAttr) {
338
- try {
339
- this.obj = JSON.parse(dataAttr);
340
- } catch (e) {
341
- console.error('Failed to parse initial value:', e);
342
- this.obj = {};
343
- }
344
- }
345
- this.updateHiddenField();
346
- },
347
- updateHiddenField() {
348
- const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
349
- if (hiddenInput) {
350
- hiddenInput.value = JSON.stringify(this.obj);
351
- }
352
- },
353
- updateField(fieldName, value, fieldType, required) {
354
- // 更新逻辑
355
- this.updateHiddenField();
356
- }
357
- }`;
358
-
359
- // 生成字段组件
360
- const generateField = (field: ModelField) => {
361
- const fieldId = `${fieldName}-${field.name}`;
362
- const fieldNameVar = `obj.${field.name}`;
363
- const fieldNameForJs = JSON.stringify(field.name);
364
-
365
- return html`
366
- <div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
367
- <label
368
- for="${fieldId}"
369
- class="block text-sm font-semibold text-gray-700"
370
- >
371
- ${field.label}
372
- ${field.required ? html`<span class="text-red-500 ml-1">*</span>` : ""}
373
- </label>
374
- <input
375
- type="text"
376
- id="${fieldId}"
377
- x-bind:value="${fieldNameVar} || ''"
378
- x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${field.required})"
379
- class="w-full px-3 py-2 border rounded-lg"
380
- data-testid="${fieldName}-input-${field.name}"
381
- ${field.required ? "required" : ""}
382
- />
383
- </div>
384
- `;
385
- };
386
-
387
- return html`
388
- <div
389
- x-data="${xDataContent}"
390
- data-initial-value="${initialValueJson}"
391
- x-init="init()"
392
- class="space-y-4"
393
- >
394
- <input
395
- type="hidden"
396
- name="${fieldName}"
397
- value=""
398
- data-testid="hidden-${fieldName}"
399
- />
400
- <div class="space-y-4">
401
- ${fields.map((field) => generateField(field))}
402
- </div>
403
- </div>
404
- `;
405
- }
406
- ```
407
-
408
- ## Alpine.js 表达式中的字符串拼接
409
-
410
- 在 Alpine.js 表达式中(如 `x-bind:data-testid`),使用字符串拼接而不是模板字符串:
411
-
412
- **❌ 错误**:
413
-
414
- ```typescript
415
- x-bind:data-testid="`${fieldName}-item-${index}`"
416
- ```
417
-
418
- **✅ 正确**:
419
-
420
- ```typescript
421
- x-bind:data-testid="fieldName + '-item-' + index"
422
- ```
423
-
424
- **原因**:Alpine.js 表达式是 JavaScript 代码,模板字符串在运行时可能无法正确解析。
425
-
426
- ## 常见错误和解决方案
427
-
428
- ### 错误 1:在 `x-data` 中使用 `raw()`
429
-
430
- **错误**:
431
- ```typescript
432
- return html`
433
- <div x-data='${raw(xDataContent)}'>
434
- <!-- ... -->
435
- </div>
436
- `;
437
- ```
438
-
439
- **解决方案**:直接使用,不要包装 `raw()`:
440
- ```typescript
441
- return html`
442
- <div x-data="${xDataContent}">
443
- <!-- ... -->
444
- </div>
445
- `;
446
- ```
447
-
448
- ### 错误 2:手动转义 HTML
449
-
450
- **错误**:
451
- ```typescript
452
- const escapedValue = value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
453
- return html`<input value="${escapedValue}" />`;
454
- ```
455
-
456
- **解决方案**:让 Hono 自动转义:
457
- ```typescript
458
- return html`<input value="${value}" />`;
459
- ```
460
-
461
- ### 错误 3:字符串拼接生成 HTML
462
-
463
- **错误**:
464
- ```typescript
465
- const htmlContent = `<div class="${className}">${content}</div>`;
466
- return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
467
- ```
468
-
469
- **解决方案**:使用 `html` 模板字符串:
470
- ```typescript
471
- return html`<div class="${className}">${content}</div>`;
472
- ```
473
-
474
- ### 错误 4:在 Alpine.js 表达式中使用模板字符串
475
-
476
- **错误**:
477
- ```typescript
478
- x-bind:data-testid="`${fieldName}-item-${index}`"
479
- ```
480
-
481
- **解决方案**:使用字符串拼接:
482
- ```typescript
483
- x-bind:data-testid="fieldName + '-item-' + index"
484
- ```
485
-
486
- ## 检查清单
487
-
488
- 在开发自定义表单字段渲染器时,请确保:
489
-
490
- - [ ] 使用 `html` 模板字符串生成所有 HTML
491
- - [ ] 不在 `x-data` 属性上使用 `raw()`
492
- - [ ] 在 `x-data` 中使用 `JSON.stringify` 插入字符串值
493
- - [ ] 不在 HTML 属性值上手动转义
494
- - [ ] 避免字符串拼接,使用 `html` 模板字符串
495
- - [ ] 在 Alpine.js 表达式中使用字符串拼接而不是模板字符串
496
- - [ ] 条件渲染使用三元表达式
497
- - [ ] 数组渲染使用 `map` 配合 `html` 模板字符串
498
-
499
- ## 总结
500
-
501
- 使用 Hono `html` 模板字符串的最佳实践:
502
-
503
- 1. **始终使用 `html` 模板字符串**:避免字符串拼接
504
- 2. **信任自动转义**:让 Hono 处理 HTML 转义
505
- 3. **使用 `JSON.stringify`**:在 JavaScript 代码中安全插入字符串值
506
- 4. **不要使用 `raw()`**:除非确实需要插入已生成的 HTML
507
- 5. **保持代码清晰**:使用条件表达式和数组方法,而不是字符串拼接
508
-
509
- 遵循这些最佳实践,可以编写出更安全、更易维护、更少错误的代码。