vite-plugin-react-shopify 1.0.1 → 2.0.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/README.md +222 -215
- package/dist/dev-server-index.html +278 -0
- package/dist/index.d.ts +23 -2
- package/dist/index.js +401 -127
- package/dist/runtime/index.d.ts +32 -0
- package/dist/runtime/index.js +73 -0
- package/package.json +14 -26
- package/dist/runtime/Liquid.client.d.ts +0 -6
- package/dist/runtime/Liquid.client.js +0 -7
- package/dist/runtime/Liquid.d.ts +0 -11
- package/dist/runtime/Liquid.js +0 -10
- package/dist/runtime/settings.d.ts +0 -8
- package/dist/runtime/settings.js +0 -44
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vite-plugin-react-shopify
|
|
2
2
|
|
|
3
|
-
使用 React 组件编写 Shopify 主题的 Section、Block 和 Template。在构建时通过 SSG(Static Site Generation)将 React 组件编译为 Shopify Liquid 文件,运行时由 React 进行水合(hydration)。
|
|
3
|
+
使用 React 组件编写 Shopify 主题的 Section、Block、Snippet 和 Template。在构建时通过 SSG(Static Site Generation)将 React 组件编译为 Shopify Liquid 文件,运行时由 React 进行水合(hydration)。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ import vitePluginShopify from "vite-plugin-react-shopify";
|
|
|
17
17
|
export default {
|
|
18
18
|
plugins: [
|
|
19
19
|
vitePluginShopify({
|
|
20
|
-
themeRoot: ".",
|
|
20
|
+
themeRoot: ".", // 主题根目录,默认 "./"
|
|
21
21
|
sourceCodeDir: "frontend", // 源码目录,默认 "frontend"
|
|
22
22
|
}),
|
|
23
23
|
],
|
|
@@ -27,19 +27,36 @@ export default {
|
|
|
27
27
|
```tsx
|
|
28
28
|
// frontend/sections/HelloWorld.tsx
|
|
29
29
|
import type { ShopifyMeta } from "vite-plugin-react-shopify";
|
|
30
|
+
import { useSectionSettings } from "vite-plugin-react-shopify/runtime";
|
|
30
31
|
|
|
31
32
|
export const shopifyMeta = {
|
|
32
33
|
name: "Hello World",
|
|
34
|
+
settings: [
|
|
35
|
+
{ type: "text", id: "title", label: "Title", default: "Hello, World!" },
|
|
36
|
+
],
|
|
33
37
|
presets: [{ name: "Hello World" }],
|
|
34
38
|
} satisfies ShopifyMeta;
|
|
35
39
|
|
|
36
40
|
export default function HelloWorld() {
|
|
37
|
-
|
|
41
|
+
const { value: title } = useSectionSettings("title");
|
|
42
|
+
return <h1>{title}</h1>;
|
|
38
43
|
}
|
|
39
44
|
```
|
|
40
45
|
|
|
41
46
|
构建后生成 `sections/react-hello-world.liquid`。
|
|
42
47
|
|
|
48
|
+
### 开发
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 终端 1: 启动 Vite 构建监听
|
|
52
|
+
pnpm dev # → vite build --watch
|
|
53
|
+
|
|
54
|
+
# 终端 2: 启动 Shopify 主题开发
|
|
55
|
+
shopify theme dev
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Vite 监听文件变化 → 增量构建 → 写入磁盘 → Shopify CLI 检测到变化 → 推送主题 → 编辑器热更新。
|
|
59
|
+
|
|
43
60
|
---
|
|
44
61
|
|
|
45
62
|
## 目录结构
|
|
@@ -51,178 +68,169 @@ my-theme/
|
|
|
51
68
|
│ │ └── HelloWorld.tsx
|
|
52
69
|
│ ├── blocks/
|
|
53
70
|
│ │ └── TextBlock.tsx
|
|
71
|
+
│ ├── snippets/
|
|
72
|
+
│ │ └── MySnippet.tsx
|
|
54
73
|
│ └── templates/
|
|
55
74
|
│ └── index.tsx
|
|
56
75
|
├── sections/ ← 生成的 Liquid + 原生 Liquid
|
|
57
76
|
├── blocks/
|
|
58
|
-
├── templates/
|
|
59
77
|
├── snippets/
|
|
60
|
-
|
|
78
|
+
├── templates/
|
|
61
79
|
└── assets/ ← Vite 构建产物(可通过 buildDir 配置子目录)
|
|
62
80
|
```
|
|
63
81
|
|
|
64
|
-
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Settings 和 Liquid 数据读取
|
|
85
|
+
|
|
86
|
+
### 核心 API
|
|
87
|
+
|
|
88
|
+
所有 Liquid 数据的读取通过统一的基础 hook `useLiquid(expr)`:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import {
|
|
92
|
+
useLiquid,
|
|
93
|
+
useLiquidValues,
|
|
94
|
+
useSectionSettings,
|
|
95
|
+
useBlockSettings,
|
|
96
|
+
useSnippetParams,
|
|
97
|
+
useBlockParams,
|
|
98
|
+
} from "vite-plugin-react-shopify/runtime";
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Hook | 用途 | 内部等价于 |
|
|
102
|
+
|------|------|-----------|
|
|
103
|
+
| `useLiquid(expr)` | 读取任意 Liquid 表达式 | — |
|
|
104
|
+
| `useLiquidValues({ key: expr })` | 批量读取多个表达式 | N 次 `useLiquid` |
|
|
105
|
+
| `useSectionSettings(key)` | Section setting | `useLiquid("section.settings.KEY")` |
|
|
106
|
+
| `useBlockSettings(key)` | Block setting | `useLiquid("block.settings.KEY")` |
|
|
107
|
+
| `useSnippetParams(key)` | Snippet param | `useLiquid("KEY")` |
|
|
108
|
+
| `useBlockParams(key)` | Block param | `useLiquid("KEY")` |
|
|
109
|
+
|
|
110
|
+
所有 hook 返回 `{ value: string | undefined }`(`useLiquidValues` 返回 `{ values: Record }`)。
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
export default function ProductBanner() {
|
|
114
|
+
const { value: title } = useSectionSettings("title");
|
|
115
|
+
const { value: price } = useLiquid("product.price");
|
|
116
|
+
const { values: p } = useLiquidValues({
|
|
117
|
+
name: "product.title",
|
|
118
|
+
desc: "product.description",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
<h1>{title}</h1>
|
|
124
|
+
<span>{price}</span>
|
|
125
|
+
<p>{p.name}</p>
|
|
126
|
+
<div dangerouslySetInnerHTML={{ __html: p.desc || "" }} />
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 工具函数
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { parseLiquidBoolean, parseLiquidNumber } from "vite-plugin-react-shopify/runtime";
|
|
136
|
+
|
|
137
|
+
// SSR-safe: Liquid 表达式字符串 → 默认值;客户端 → 实际解析
|
|
138
|
+
const count = parseLiquidNumber(s.initial, 0);
|
|
139
|
+
const show = parseLiquidBoolean(s.show_banner);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 水合流程
|
|
143
|
+
|
|
144
|
+
1. **SSR**:hook 返回 `{{ expr }}` 字符串,同时注册表达式到追踪器
|
|
145
|
+
2. **Liquid 组装**:生成统一 `<script type="application/json" data-ssg-liquid>` JSON bridge
|
|
146
|
+
3. **Shopify 渲染**:Liquid 引擎替换表达式为实际值,bridge JSON 包含序列化后的值
|
|
147
|
+
4. **客户端水合**:`LiquidDataProvider` 接收 bridge JSON → hook 通过 `useContext` 读取
|
|
65
148
|
|
|
66
149
|
---
|
|
67
150
|
|
|
68
151
|
## 组件约定
|
|
69
152
|
|
|
70
|
-
每个 React
|
|
153
|
+
每个 React 组件必须导出:
|
|
71
154
|
|
|
72
155
|
### 1. `shopifyMeta` — Shopify Schema 定义
|
|
73
156
|
|
|
74
157
|
```tsx
|
|
75
158
|
export const shopifyMeta = {
|
|
76
|
-
name: "组件名称",
|
|
77
|
-
type: "section", //
|
|
159
|
+
name: "组件名称",
|
|
160
|
+
type: "section", // 可选,覆盖目录推断
|
|
78
161
|
tag: "section", // 可选,外层 HTML 标签,默认 "div"
|
|
79
|
-
class: "custom-class", //
|
|
80
|
-
limit: 1,
|
|
81
|
-
max_blocks: 10,
|
|
82
|
-
settings: [...],
|
|
83
|
-
blocks: [...],
|
|
84
|
-
presets: [...],
|
|
85
|
-
enabled_on: {...}, // 可选,启用条件
|
|
86
|
-
disabled_on: {...}, // 可选,禁用条件
|
|
87
|
-
templates: [...], // 可选,适用的模板
|
|
162
|
+
class: "custom-class", // 可选,外层 CSS 类名
|
|
163
|
+
limit: 1,
|
|
164
|
+
max_blocks: 10,
|
|
165
|
+
settings: [...],
|
|
166
|
+
blocks: [...],
|
|
167
|
+
presets: [...],
|
|
88
168
|
} satisfies ShopifyMeta;
|
|
89
169
|
```
|
|
90
170
|
|
|
91
171
|
### 2. `default export` — React 组件
|
|
92
172
|
|
|
93
173
|
```tsx
|
|
94
|
-
export default function MyComponent(
|
|
174
|
+
export default function MyComponent() {
|
|
95
175
|
return <div>...</div>;
|
|
96
176
|
}
|
|
97
177
|
```
|
|
98
178
|
|
|
99
179
|
### 类型推断
|
|
100
180
|
|
|
101
|
-
文件名和目录决定目标类型:
|
|
102
|
-
|
|
103
181
|
| 源码路径 | 目标类型 | 生成文件 |
|
|
104
182
|
|----------|----------|----------|
|
|
105
183
|
| `frontend/sections/X.tsx` | `section` | `sections/react-x.liquid` |
|
|
106
184
|
| `frontend/blocks/X.tsx` | `block` | `blocks/react-x.liquid` |
|
|
107
185
|
| `frontend/templates/X.tsx` | `template` | `templates/page.react-x.liquid` |
|
|
108
|
-
|
|
109
|
-
可通过 `shopifyMeta.type` 覆盖。
|
|
186
|
+
| `frontend/snippets/X.tsx` | `snippet` | `snippets/react-x.liquid` |
|
|
110
187
|
|
|
111
188
|
---
|
|
112
189
|
|
|
113
190
|
## 设置 (Settings)
|
|
114
191
|
|
|
115
|
-
使用 `SettingSchema` 类型定义设置项。`SettingSchema` 是根据 `type` 字段判别(discriminated union)的联合类型 —— 每种 type 只接受 Shopify 文档中合法的字段,无效字段会在编译时报错:
|
|
116
|
-
|
|
117
192
|
```tsx
|
|
118
193
|
import type { SettingSchema } from "vite-plugin-react-shopify";
|
|
119
194
|
|
|
120
195
|
const settings = [
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
default: "Hello",
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
type: "select",
|
|
129
|
-
id: "layout",
|
|
130
|
-
label: "Layout",
|
|
131
|
-
default: "grid",
|
|
132
|
-
options: [
|
|
133
|
-
{ value: "grid", label: "Grid" },
|
|
134
|
-
{ value: "list", label: "List" },
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
type: "range",
|
|
139
|
-
id: "columns",
|
|
140
|
-
label: "Columns",
|
|
141
|
-
default: 3,
|
|
142
|
-
min: 1,
|
|
143
|
-
max: 6,
|
|
144
|
-
step: 1,
|
|
145
|
-
},
|
|
196
|
+
{ type: "text", id: "title", label: "Title", default: "Hello" },
|
|
197
|
+
{ type: "select", id: "layout", label: "Layout", default: "grid",
|
|
198
|
+
options: [{ value: "grid", label: "Grid" }, { value: "list", label: "List" }] },
|
|
199
|
+
{ type: "range", id: "columns", label: "Columns", default: 3, min: 1, max: 6, step: 1 },
|
|
146
200
|
] satisfies SettingSchema[];
|
|
147
|
-
|
|
148
|
-
// 也可以直接引用具体类型:
|
|
149
|
-
import type { SelectSetting, RangeSetting } from "vite-plugin-react-shopify";
|
|
150
201
|
```
|
|
151
202
|
|
|
152
203
|
### 基本输入类型
|
|
153
204
|
|
|
154
|
-
| 类型 | 额外字段 |
|
|
155
|
-
|
|
156
|
-
| `checkbox` |
|
|
157
|
-
| `number` | `placeholder` |
|
|
158
|
-
| `radio` | `options`(必填) |
|
|
159
|
-
| `range` | `min`(必填)、`max`(必填)、`step`、`unit` |
|
|
160
|
-
| `select` | `options`(必填) |
|
|
161
|
-
| `text` | `placeholder` |
|
|
162
|
-
| `textarea` | `placeholder` |
|
|
205
|
+
| 类型 | 额外字段 |
|
|
206
|
+
|------|----------|
|
|
207
|
+
| `checkbox` | `default`(`boolean`) |
|
|
208
|
+
| `number` | `placeholder` |
|
|
209
|
+
| `radio` | `options`(必填) |
|
|
210
|
+
| `range` | `min`(必填)、`max`(必填)、`step`、`unit` |
|
|
211
|
+
| `select` | `options`(必填) |
|
|
212
|
+
| `text` | `placeholder` |
|
|
213
|
+
| `textarea` | `placeholder` |
|
|
163
214
|
|
|
164
215
|
### 专用输入类型
|
|
165
216
|
|
|
166
|
-
|
|
167
|
-
|------|----------|
|
|
168
|
-
| `article` | —(不支持 `default`) |
|
|
169
|
-
| `article_list` | `limit` |
|
|
170
|
-
| `blog` | —(不支持 `default`) |
|
|
171
|
-
| `collection` | —(不支持 `default`) |
|
|
172
|
-
| `collection_list` | `limit` |
|
|
173
|
-
| `color` | — |
|
|
174
|
-
| `color_background` | — |
|
|
175
|
-
| `color_scheme` | — |
|
|
176
|
-
| `color_scheme_group` | `definition`(必填)、`role`(必填) |
|
|
177
|
-
| `font_picker` | —(`default` **必填** `string`) |
|
|
178
|
-
| `html` | `placeholder` |
|
|
179
|
-
| `image_picker` | —(不支持 `default`) |
|
|
180
|
-
| `inline_richtext` | — |
|
|
181
|
-
| `link_list` | —(`default` 限制为 `"main-menu"` / `"footer"`) |
|
|
182
|
-
| `liquid` | — |
|
|
183
|
-
| `metaobject` | `metaobject_type`(必填) |
|
|
184
|
-
| `metaobject_list` | `metaobject_type`(必填)、`limit` |
|
|
185
|
-
| `page` | —(不支持 `default`) |
|
|
186
|
-
| `product` | —(不支持 `default`) |
|
|
187
|
-
| `product_list` | `limit` |
|
|
188
|
-
| `richtext` | — |
|
|
189
|
-
| `text_alignment` | —(`default` 限制为 `"left"` / `"center"` / `"right"`) |
|
|
190
|
-
| `url` | — |
|
|
191
|
-
| `video` | —(不支持 `default`) |
|
|
192
|
-
| `video_url` | `accept`(必填)、`placeholder` |
|
|
193
|
-
|
|
194
|
-
### 侧边栏类型(非输入,仅显示信息)
|
|
195
|
-
|
|
196
|
-
`header`、`paragraph`、`line_break`。与输入类型不同,侧边栏类型不保存值,仅用于组织和描述设置项。使用 `content` 字段代替 `id`/`label`:
|
|
217
|
+
`article`、`article_list`、`blog`、`collection`、`collection_list`、`color`、`color_background`、`color_scheme`、`color_scheme_group`、`font_picker`、`html`、`image_picker`、`inline_richtext`、`link_list`、`liquid`、`metaobject`、`metaobject_list`、`page`、`product`、`product_list`、`richtext`、`text_alignment`、`url`、`video`、`video_url`
|
|
197
218
|
|
|
198
|
-
|
|
199
|
-
const settings = [
|
|
200
|
-
{ type: "header", content: "Typography", info: "Customize text styles below." },
|
|
201
|
-
{ type: "font_picker", id: "heading_font", label: "Heading font", default: "helvetica_n4" },
|
|
202
|
-
{ type: "paragraph", content: "Set your brand colors for buttons and accents." },
|
|
203
|
-
{ type: "color", id: "accent_color", label: "Accent color", default: "#000000" },
|
|
204
|
-
{ type: "line_break" },
|
|
205
|
-
{ type: "checkbox", id: "dark_mode", label: "Enable dark mode", default: false },
|
|
206
|
-
] satisfies SettingSchema[];
|
|
219
|
+
### 侧边栏类型
|
|
207
220
|
|
|
208
|
-
|
|
221
|
+
`header`、`paragraph`、`line_break`。使用 `content` 字段,不保存值。
|
|
209
222
|
|
|
210
223
|
---
|
|
211
224
|
|
|
212
225
|
## 预设 (Presets)
|
|
213
226
|
|
|
214
|
-
预设让组件可以在编辑器中通过"添加 Section/Block"面板直接添加:
|
|
215
|
-
|
|
216
227
|
```tsx
|
|
217
228
|
export const shopifyMeta = {
|
|
218
229
|
name: "Hero Banner",
|
|
219
230
|
presets: [
|
|
220
231
|
{ name: "Hero (Light)", category: "Banners" },
|
|
221
|
-
{
|
|
222
|
-
|
|
223
|
-
category: "Banners",
|
|
224
|
-
settings: { bg_color: "#000", text_color: "#fff" },
|
|
225
|
-
},
|
|
232
|
+
{ name: "Hero (Dark)", category: "Banners",
|
|
233
|
+
settings: { bg_color: "#000", text_color: "#fff" } },
|
|
226
234
|
],
|
|
227
235
|
} satisfies ShopifyMeta;
|
|
228
236
|
```
|
|
@@ -231,60 +239,56 @@ export const shopifyMeta = {
|
|
|
231
239
|
|
|
232
240
|
## 子 Block(嵌套)
|
|
233
241
|
|
|
234
|
-
Section 或 Block 可以声明接受的子 block 类型:
|
|
235
|
-
|
|
236
242
|
```tsx
|
|
237
243
|
export const shopifyMeta = {
|
|
238
244
|
name: "Group",
|
|
239
|
-
blocks: [{ type: "@theme" }],
|
|
245
|
+
blocks: [{ type: "@theme" }],
|
|
240
246
|
} satisfies ShopifyMeta;
|
|
241
247
|
```
|
|
242
248
|
|
|
243
249
|
- `"@theme"` 接受当前主题中所有已注册的 block
|
|
244
|
-
- `
|
|
245
|
-
- 插件会自动在生成的 Liquid 中插入 `{% content_for 'blocks' %}`
|
|
250
|
+
- 插件自动插入 `{% content_for 'blocks' %}`
|
|
246
251
|
|
|
247
252
|
---
|
|
248
253
|
|
|
249
|
-
##
|
|
254
|
+
## Snippet
|
|
250
255
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
```css
|
|
254
|
-
/* frontend/sections/Hero.module.css */
|
|
255
|
-
.hero {
|
|
256
|
-
display: grid;
|
|
257
|
-
padding: 2rem;
|
|
258
|
-
}
|
|
259
|
-
.hero-title {
|
|
260
|
-
font-size: 3rem;
|
|
261
|
-
font-weight: 700;
|
|
262
|
-
}
|
|
263
|
-
```
|
|
256
|
+
通过 `params` 传参(而非 `settings`):
|
|
264
257
|
|
|
265
258
|
```tsx
|
|
266
|
-
// frontend/
|
|
267
|
-
import
|
|
259
|
+
// frontend/snippets/ProductCard.tsx
|
|
260
|
+
import { useSnippetParams } from "vite-plugin-react-shopify/runtime";
|
|
268
261
|
|
|
269
|
-
export
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
262
|
+
export const shopifyMeta = {
|
|
263
|
+
type: "snippet",
|
|
264
|
+
name: "Product Card",
|
|
265
|
+
params: ["title", "price"],
|
|
266
|
+
} satisfies ShopifyMeta;
|
|
267
|
+
|
|
268
|
+
export default function ProductCard() {
|
|
269
|
+
const { value: title } = useSnippetParams("title");
|
|
270
|
+
const { value: price } = useSnippetParams("price");
|
|
271
|
+
return <div><h3>{title}</h3><span>{price}</span></div>;
|
|
275
272
|
}
|
|
276
273
|
```
|
|
277
274
|
|
|
278
|
-
|
|
275
|
+
调用方式:`{% render 'react-product-card', title: 'Hello', price: '$10' %}`
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## CSS 样式
|
|
280
|
+
|
|
281
|
+
使用普通 CSS 文件为组件添加样式。CSS 会在构建时被内联到 Liquid 的 `{% stylesheet %}` 块中。多个组件共享的 CSS 自动提取为 snippet:
|
|
279
282
|
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
.
|
|
283
|
-
._hero-title_abc123{font-size:3rem;font-weight:700}
|
|
284
|
-
{% endstylesheet %}
|
|
283
|
+
```css
|
|
284
|
+
/* frontend/sections/Hero.css */
|
|
285
|
+
.hero { display: grid; padding: 2rem; }
|
|
285
286
|
```
|
|
286
287
|
|
|
287
|
-
|
|
288
|
+
```tsx
|
|
289
|
+
import "./Hero.css";
|
|
290
|
+
export default function Hero() { return <div className="hero">...</div>; }
|
|
291
|
+
```
|
|
288
292
|
|
|
289
293
|
---
|
|
290
294
|
|
|
@@ -292,116 +296,119 @@ export default function Hero() {
|
|
|
292
296
|
|
|
293
297
|
```ts
|
|
294
298
|
vitePluginShopify({
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// === SSG 配置 ===
|
|
299
|
+
themeRoot: ".", // 主题根目录
|
|
300
|
+
sourceCodeDir: "frontend", // React 源码目录
|
|
301
|
+
snippetFile: "shopify-importmap.liquid",// importmap 片段文件名
|
|
302
|
+
buildDir: "assets", // 构建产物输出目录
|
|
303
|
+
debug: false, // 详细日志
|
|
302
304
|
ssg: {
|
|
303
|
-
directories: ["sections", "blocks", "templates"],
|
|
304
|
-
prefix: {
|
|
305
|
-
template: "page.react-",
|
|
306
|
-
section: "react-",
|
|
307
|
-
block: "react-",
|
|
305
|
+
directories: ["sections", "blocks", "templates", "snippets"],
|
|
306
|
+
prefix: {
|
|
307
|
+
template: "page.react-",
|
|
308
|
+
section: "react-",
|
|
309
|
+
block: "react-",
|
|
310
|
+
snippet: "react-",
|
|
308
311
|
},
|
|
309
|
-
outputName: "",
|
|
312
|
+
outputName: "", // 自定义输出模板
|
|
313
|
+
cssPrefix: "css", // 共享 CSS snippet 前缀
|
|
310
314
|
},
|
|
311
|
-
|
|
312
|
-
// === Import Map 配置 ===
|
|
313
315
|
importMap: {
|
|
314
|
-
react: "
|
|
315
|
-
reactDomClient: "
|
|
316
|
+
react: "{{ 'react.js' | asset_url }}",
|
|
317
|
+
reactDomClient: "{{ 'react-dom.js' | asset_url }}",
|
|
316
318
|
},
|
|
317
319
|
});
|
|
318
320
|
```
|
|
319
321
|
|
|
320
322
|
---
|
|
321
323
|
|
|
322
|
-
##
|
|
324
|
+
## 水合与编辑器事件
|
|
323
325
|
|
|
324
|
-
###
|
|
326
|
+
### 水合流程
|
|
325
327
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
| `frontend/blocks/MyBlock.tsx` | block | `blocks/react-my-block.liquid` |
|
|
330
|
-
| `frontend/templates/Index.tsx` | template | `templates/page.react-index.liquid` |
|
|
328
|
+
1. **SSG 预渲染**:React 组件渲染为含 Liquid 表达式的 HTML,Liquid 组装器生成 `data-ssg-liquid` JSON bridge
|
|
329
|
+
2. **Liquid 渲染**:Shopify 服务端替换表达式为实际值
|
|
330
|
+
3. **Hydration JS**:Vite 为每个组件生成独立 hydration chunk,读取 JSON bridge 并调用 `hydrateRoot`
|
|
331
331
|
|
|
332
|
-
|
|
333
|
-
- `HelloWorld` → `hello-world`
|
|
334
|
-
- `FAQSection` → `faq-section`
|
|
332
|
+
### 编辑器事件
|
|
335
333
|
|
|
336
|
-
|
|
334
|
+
| 事件 | 行为 |
|
|
335
|
+
|------|------|
|
|
336
|
+
| `shopify:section:load` | 重新 hydrate 组件 |
|
|
337
|
+
| `shopify:section:unload` | unmount React 根节点 |
|
|
337
338
|
|
|
338
|
-
|
|
339
|
-
ssg: {
|
|
340
|
-
outputName: "{target}s/{kebab}" // 按类型分目录,不添加前缀
|
|
341
|
-
}
|
|
342
|
-
```
|
|
339
|
+
---
|
|
343
340
|
|
|
344
|
-
|
|
341
|
+
## 调试
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
DEBUG=vite-plugin-shopify:* npx vite build
|
|
345
|
+
```
|
|
345
346
|
|
|
346
347
|
---
|
|
347
348
|
|
|
348
|
-
##
|
|
349
|
+
## 开发规范
|
|
349
350
|
|
|
350
|
-
|
|
351
|
+
> 详见 `docs/hydration-issues.md`
|
|
351
352
|
|
|
352
|
-
|
|
353
|
-
2. **Settings 透传**:Liquid 中 `<script type="application/json" data-ssg-props>` 包含 `{{ section.settings | json }}`
|
|
354
|
-
3. **Hydration JS**:Vite 为每个组件生成独立的 hydration chunk,读取 settings JSON 并调用 `hydrateRoot`
|
|
353
|
+
### JSX 子节点中相邻文本+表达式
|
|
355
354
|
|
|
356
|
-
|
|
355
|
+
**必须使用模板字面量包裹**:
|
|
357
356
|
|
|
358
|
-
|
|
357
|
+
```tsx
|
|
358
|
+
// ❌ 水合失败:相邻文本节点不匹配
|
|
359
|
+
<button>-{step}</button>
|
|
360
|
+
<li>title = {title}</li>
|
|
359
361
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
// ✅ 模板字面量
|
|
363
|
+
<button>{`-${step}`}</button>
|
|
364
|
+
<li>{`title = ${title}`}</li>
|
|
365
|
+
```
|
|
364
366
|
|
|
365
|
-
|
|
367
|
+
### useState 初始化
|
|
366
368
|
|
|
367
|
-
|
|
369
|
+
**不能依赖 `useLiquid` 返回值**:
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
// ❌ SSR 时 Number("{{ expr }}") = NaN → 不匹配
|
|
373
|
+
const [count, setCount] = useState(Number(s.initial) || 0);
|
|
368
374
|
|
|
369
|
-
|
|
375
|
+
// ✅ 固定默认值 + useEffect 同步
|
|
376
|
+
const [count, setCount] = useState(0);
|
|
377
|
+
useEffect(() => { setCount(parseLiquidNumber(s.initial, 0)); }, []);
|
|
378
|
+
```
|
|
370
379
|
|
|
371
|
-
###
|
|
380
|
+
### 条件渲染
|
|
372
381
|
|
|
373
|
-
|
|
382
|
+
**不用 `{value && <Element />}`**,用 `hidden` 属性:
|
|
374
383
|
|
|
375
384
|
```tsx
|
|
376
|
-
|
|
385
|
+
// ❌ SSR 时表达式字符串始终 truthy → 结构不匹配
|
|
386
|
+
{showBanner && <Banner />}
|
|
377
387
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<div>
|
|
381
|
-
<Liquid>{`{% if section.settings.show_title %}`}</Liquid>
|
|
382
|
-
<h1>Title</h1>
|
|
383
|
-
<Liquid>{`{% endif %}`}</Liquid>
|
|
384
|
-
</div>
|
|
385
|
-
);
|
|
386
|
-
}
|
|
388
|
+
// ✅ DOM 结构不变,仅切换属性
|
|
389
|
+
<section hidden={!parseLiquidBoolean(showBannerRaw)}>...</section>
|
|
387
390
|
```
|
|
388
391
|
|
|
389
|
-
###
|
|
392
|
+
### inline style 颜色值
|
|
393
|
+
|
|
394
|
+
**用 CSS 自定义属性代替内联颜色**:
|
|
390
395
|
|
|
391
|
-
|
|
396
|
+
```tsx
|
|
397
|
+
// ❌ 浏览器规范化 hex → rgb → 不匹配
|
|
398
|
+
<div style={{ backgroundColor: color }} />
|
|
392
399
|
|
|
393
|
-
|
|
394
|
-
{
|
|
400
|
+
// ✅ CSS 变量不归一化
|
|
401
|
+
<div style={{ "--accent": color } as React.CSSProperties} />
|
|
402
|
+
// CSS: .accent { background-color: var(--accent); }
|
|
395
403
|
```
|
|
396
404
|
|
|
397
405
|
---
|
|
398
406
|
|
|
399
407
|
## 注意事项
|
|
400
408
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
7. **构建产物默认输出到 `assets/`**:如需与其他静态资源隔离,可设置 `buildDir: "assets/build"` 将产物输出到子目录,然后在 `.gitignore` 中添加 `assets/build/` 忽略该目录
|
|
409
|
+
- **SSR 渲染生成 Liquid 模板**:构建时的 SSG 渲染将 settings/params 访问映射为 Liquid tag(如 `{{ section.settings.title }}`),由 Shopify 服务端解析为真实值
|
|
410
|
+
- **Template 类型不包裹**:`type: "template"` 的 HTML 直接输出,不添加 section/block 外层结构
|
|
411
|
+
- **Section 必须有预设才能通过编辑器添加**
|
|
412
|
+
- **构建产物默认输出到 `assets/`**:可通过 `buildDir` 配置子目录
|
|
413
|
+
- **Watch 模式**:`vite build --watch` 时自动关闭压缩并启用 inline sourcemap
|
|
414
|
+
- **Settings 按需追踪**:SSR 阶段追踪组件 render body 中实际访问的 Liquid 表达式,只将访问过的表达式注入 `data-ssg-liquid` JSON bridge
|