imean-service-engine-htmx-plugin 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +76 -65
- package/dist/index.d.ts +76 -65
- package/dist/index.js +1046 -284
- package/dist/index.mjs +1044 -283
- package/docs/README.md +22 -8
- package/docs/alpinejs-interactive-components.md +653 -0
- package/docs/hono-html-best-practices.md +509 -0
- package/package.json +14 -13
- package/docs/jsx-alpine-best-practices.md +0 -197
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# JSX + Alpine.js 最佳实践
|
|
2
|
-
|
|
3
|
-
## 问题分析
|
|
4
|
-
|
|
5
|
-
### 当前实现的问题
|
|
6
|
-
|
|
7
|
-
在 `BannerEditor` 组件中,我们遇到了一个设计问题:
|
|
8
|
-
|
|
9
|
-
1. **状态管理在 Alpine.js**:`banners` 数组由 Alpine.js 的 `x-data` 管理
|
|
10
|
-
2. **渲染在 JSX**:使用 React 的 `map` 渲染固定数量的占位符(最多10个)
|
|
11
|
-
3. **问题**:
|
|
12
|
-
- 如果用户添加超过10个banner,后面的不会显示
|
|
13
|
-
- 混合了 React 的渲染逻辑和 Alpine.js 的状态管理
|
|
14
|
-
- 不够动态,需要预先知道最大数量
|
|
15
|
-
|
|
16
|
-
### 正确的思路
|
|
17
|
-
|
|
18
|
-
既然状态由 Alpine.js 管理,渲染也应该由 Alpine.js 负责。我们应该:
|
|
19
|
-
1. 只提供一个模板
|
|
20
|
-
2. 让 Alpine.js 的 `x-for` 动态渲染列表
|
|
21
|
-
3. JSX 只负责提供静态结构
|
|
22
|
-
|
|
23
|
-
## 解决方案
|
|
24
|
-
|
|
25
|
-
### 方案 1:使用 `template` 标签 + `dangerouslySetInnerHTML`(推荐)
|
|
26
|
-
|
|
27
|
-
在 JSX 中,我们可以使用 `dangerouslySetInnerHTML` 来插入包含 `template` 标签的 HTML:
|
|
28
|
-
|
|
29
|
-
```tsx
|
|
30
|
-
<div
|
|
31
|
-
x-data="..."
|
|
32
|
-
dangerouslySetInnerHTML={{
|
|
33
|
-
__html: `
|
|
34
|
-
<template x-for="(banner, index) in banners">
|
|
35
|
-
<div class="banner-item">
|
|
36
|
-
<!-- banner 内容 -->
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
39
|
-
`
|
|
40
|
-
}}
|
|
41
|
-
/>
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**优点**:
|
|
45
|
-
- 完全由 Alpine.js 控制渲染
|
|
46
|
-
- 支持动态添加/删除,无数量限制
|
|
47
|
-
- 符合 Alpine.js 的最佳实践
|
|
48
|
-
|
|
49
|
-
**缺点**:
|
|
50
|
-
- 失去 JSX 的类型检查和语法高亮
|
|
51
|
-
- 需要手动转义 HTML
|
|
52
|
-
|
|
53
|
-
### 方案 2:使用包装容器 + Alpine.js 动态创建(当前实现)
|
|
54
|
-
|
|
55
|
-
使用一个容器,在 Alpine.js 的 `init()` 方法中动态创建 DOM:
|
|
56
|
-
|
|
57
|
-
```tsx
|
|
58
|
-
<div
|
|
59
|
-
x-data="{
|
|
60
|
-
banners: [],
|
|
61
|
-
init() {
|
|
62
|
-
// 从 data 属性读取初始值
|
|
63
|
-
// 动态创建 DOM 元素
|
|
64
|
-
}
|
|
65
|
-
}"
|
|
66
|
-
>
|
|
67
|
-
{/* 空的容器,由 Alpine.js 填充 */}
|
|
68
|
-
</div>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**优点**:
|
|
72
|
-
- 保持 JSX 结构
|
|
73
|
-
- 完全动态
|
|
74
|
-
|
|
75
|
-
**缺点**:
|
|
76
|
-
- 需要手动管理 DOM
|
|
77
|
-
- 代码复杂度高
|
|
78
|
-
|
|
79
|
-
### 方案 3:混合方案(当前实现,需要改进)
|
|
80
|
-
|
|
81
|
-
使用 JSX 渲染固定数量的占位符,用 `x-show` 控制显示:
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
{Array.from({ length: 10 }).map((_, index) => (
|
|
85
|
-
<div x-show={`banners.length > ${index}`}>
|
|
86
|
-
{/* 内容 */}
|
|
87
|
-
</div>
|
|
88
|
-
))}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**优点**:
|
|
92
|
-
- 保持 JSX 结构
|
|
93
|
-
- 简单直接
|
|
94
|
-
|
|
95
|
-
**缺点**:
|
|
96
|
-
- 有数量限制
|
|
97
|
-
- 不够动态
|
|
98
|
-
- 混合了两种渲染逻辑
|
|
99
|
-
|
|
100
|
-
## 推荐方案
|
|
101
|
-
|
|
102
|
-
### 对于动态列表:使用 `template` + `dangerouslySetInnerHTML`
|
|
103
|
-
|
|
104
|
-
```tsx
|
|
105
|
-
export function BannerEditor(props: BannerEditorProps) {
|
|
106
|
-
const { value, fieldName } = props;
|
|
107
|
-
const initialBanners: Banner[] = value || [];
|
|
108
|
-
const bannersJson = JSON.stringify(initialBanners);
|
|
109
|
-
|
|
110
|
-
// 构建模板 HTML
|
|
111
|
-
const templateHTML = `
|
|
112
|
-
<template x-for="(banner, index) in banners" :key="index">
|
|
113
|
-
<div class="border border-gray-200 rounded-lg p-4 space-y-3 bg-white">
|
|
114
|
-
<div class="flex items-center justify-between">
|
|
115
|
-
<span class="text-sm font-semibold text-gray-700">
|
|
116
|
-
Banner <span x-text="index + 1"></span>
|
|
117
|
-
</span>
|
|
118
|
-
<button
|
|
119
|
-
type="button"
|
|
120
|
-
x-on:click="removeBanner(index)"
|
|
121
|
-
class="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded"
|
|
122
|
-
>
|
|
123
|
-
删除
|
|
124
|
-
</button>
|
|
125
|
-
</div>
|
|
126
|
-
<!-- 更多内容 -->
|
|
127
|
-
</div>
|
|
128
|
-
</template>
|
|
129
|
-
`;
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<div
|
|
133
|
-
x-data={`{
|
|
134
|
-
banners: [],
|
|
135
|
-
init() {
|
|
136
|
-
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
137
|
-
if (dataAttr) {
|
|
138
|
-
this.banners = JSON.parse(dataAttr);
|
|
139
|
-
}
|
|
140
|
-
this.updateHiddenField();
|
|
141
|
-
},
|
|
142
|
-
// ... 其他方法
|
|
143
|
-
}`}
|
|
144
|
-
data-initial-value={bannersJson}
|
|
145
|
-
x-init="init()"
|
|
146
|
-
x-effect="updateHiddenField()"
|
|
147
|
-
>
|
|
148
|
-
<div
|
|
149
|
-
className="space-y-4"
|
|
150
|
-
x-show="banners.length > 0"
|
|
151
|
-
dangerouslySetInnerHTML={{ __html: templateHTML }}
|
|
152
|
-
/>
|
|
153
|
-
</div>
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### 对于简单交互:使用 JSX + Alpine.js 属性
|
|
159
|
-
|
|
160
|
-
对于不需要动态列表的场景,直接在 JSX 中使用 Alpine.js 属性:
|
|
161
|
-
|
|
162
|
-
```tsx
|
|
163
|
-
<div x-data="{ count: 0 }">
|
|
164
|
-
<button x-on:click="count++">点击</button>
|
|
165
|
-
<span x-text="count"></span>
|
|
166
|
-
</div>
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
## 最佳实践总结
|
|
170
|
-
|
|
171
|
-
1. **状态管理**:
|
|
172
|
-
- 简单状态:使用 Alpine.js 的 `x-data`
|
|
173
|
-
- 复杂状态:考虑使用 Alpine.js 的 Store 或外部状态管理
|
|
174
|
-
|
|
175
|
-
2. **渲染策略**:
|
|
176
|
-
- **动态列表**:使用 `template` + `x-for` + `dangerouslySetInnerHTML`
|
|
177
|
-
- **静态结构**:使用 JSX,配合 Alpine.js 属性(`x-show`, `x-model` 等)
|
|
178
|
-
- **条件渲染**:使用 `x-show` 或 `x-if`
|
|
179
|
-
|
|
180
|
-
3. **数据传递**:
|
|
181
|
-
- 使用 `data-*` 属性传递初始值
|
|
182
|
-
- 在 `x-init` 中读取并初始化
|
|
183
|
-
|
|
184
|
-
4. **属性命名**:
|
|
185
|
-
- JSX 中,包含点号的属性名需要使用方括号语法:`{...{ "x-model.number": value }}`
|
|
186
|
-
- 或者使用字符串作为属性名:`"x-model.number"={value}`
|
|
187
|
-
|
|
188
|
-
5. **避免混合**:
|
|
189
|
-
- 不要同时使用 React 的 `map` 和 Alpine.js 的 `x-for`
|
|
190
|
-
- 选择一种渲染方式并保持一致
|
|
191
|
-
|
|
192
|
-
## 注意事项
|
|
193
|
-
|
|
194
|
-
1. **安全性**:使用 `dangerouslySetInnerHTML` 时,确保内容已转义,避免 XSS 攻击
|
|
195
|
-
2. **性能**:对于大量数据,考虑虚拟滚动或分页
|
|
196
|
-
3. **可维护性**:如果模板 HTML 很长,考虑提取到单独的函数或文件
|
|
197
|
-
4. **类型安全**:使用 TypeScript 时,`dangerouslySetInnerHTML` 会失去类型检查,需要额外注意
|