vite-plugin-react-shopify 1.1.0 → 2.1.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 +205 -288
- package/dist/index.d.ts +35 -13
- package/dist/index.js +558 -359
- 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,14 +27,19 @@ 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
|
|
|
@@ -42,8 +47,6 @@ export default function HelloWorld() {
|
|
|
42
47
|
|
|
43
48
|
### 开发
|
|
44
49
|
|
|
45
|
-
使用 `vite build --watch` 进行本地开发。Vite 8 的 Rolldown 内置增量构建缓存,文件变更时仅重新构建受影响的模块。
|
|
46
|
-
|
|
47
50
|
```bash
|
|
48
51
|
# 终端 1: 启动 Vite 构建监听
|
|
49
52
|
pnpm dev # → vite build --watch
|
|
@@ -52,17 +55,7 @@ pnpm dev # → vite build --watch
|
|
|
52
55
|
shopify theme dev
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
Vite 监听文件变化 → 增量构建 → 写入磁盘 → Shopify CLI 检测到变化 → 推送主题 → 编辑器热更新。
|
|
56
|
-
|
|
57
|
-
```json
|
|
58
|
-
// package.json
|
|
59
|
-
{
|
|
60
|
-
"scripts": {
|
|
61
|
-
"dev": "vite build --watch",
|
|
62
|
-
"build": "vite build"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
```
|
|
58
|
+
Vite 监听文件变化 → 增量构建 → 写入磁盘 → Shopify CLI 检测到变化 → 推送主题 → 编辑器热更新。
|
|
66
59
|
|
|
67
60
|
---
|
|
68
61
|
|
|
@@ -75,178 +68,169 @@ my-theme/
|
|
|
75
68
|
│ │ └── HelloWorld.tsx
|
|
76
69
|
│ ├── blocks/
|
|
77
70
|
│ │ └── TextBlock.tsx
|
|
71
|
+
│ ├── snippets/
|
|
72
|
+
│ │ └── MySnippet.tsx
|
|
78
73
|
│ └── templates/
|
|
79
74
|
│ └── index.tsx
|
|
80
75
|
├── sections/ ← 生成的 Liquid + 原生 Liquid
|
|
81
76
|
├── blocks/
|
|
82
|
-
├── templates/
|
|
83
77
|
├── snippets/
|
|
84
|
-
|
|
78
|
+
├── templates/
|
|
85
79
|
└── assets/ ← Vite 构建产物(可通过 buildDir 配置子目录)
|
|
86
80
|
```
|
|
87
81
|
|
|
88
|
-
|
|
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` 读取
|
|
89
148
|
|
|
90
149
|
---
|
|
91
150
|
|
|
92
151
|
## 组件约定
|
|
93
152
|
|
|
94
|
-
每个 React
|
|
153
|
+
每个 React 组件必须导出:
|
|
95
154
|
|
|
96
155
|
### 1. `shopifyMeta` — Shopify Schema 定义
|
|
97
156
|
|
|
98
157
|
```tsx
|
|
99
158
|
export const shopifyMeta = {
|
|
100
|
-
name: "组件名称",
|
|
101
|
-
type: "section", //
|
|
159
|
+
name: "组件名称",
|
|
160
|
+
type: "section", // 可选,覆盖目录推断
|
|
102
161
|
tag: "section", // 可选,外层 HTML 标签,默认 "div"
|
|
103
|
-
class: "custom-class", //
|
|
104
|
-
limit: 1,
|
|
105
|
-
max_blocks: 10,
|
|
106
|
-
settings: [...],
|
|
107
|
-
blocks: [...],
|
|
108
|
-
presets: [...],
|
|
109
|
-
enabled_on: {...}, // 可选,启用条件
|
|
110
|
-
disabled_on: {...}, // 可选,禁用条件
|
|
111
|
-
templates: [...], // 可选,适用的模板
|
|
162
|
+
class: "custom-class", // 可选,外层 CSS 类名
|
|
163
|
+
limit: 1,
|
|
164
|
+
max_blocks: 10,
|
|
165
|
+
settings: [...],
|
|
166
|
+
blocks: [...],
|
|
167
|
+
presets: [...],
|
|
112
168
|
} satisfies ShopifyMeta;
|
|
113
169
|
```
|
|
114
170
|
|
|
115
171
|
### 2. `default export` — React 组件
|
|
116
172
|
|
|
117
173
|
```tsx
|
|
118
|
-
export default function MyComponent(
|
|
174
|
+
export default function MyComponent() {
|
|
119
175
|
return <div>...</div>;
|
|
120
176
|
}
|
|
121
177
|
```
|
|
122
178
|
|
|
123
179
|
### 类型推断
|
|
124
180
|
|
|
125
|
-
文件名和目录决定目标类型:
|
|
126
|
-
|
|
127
181
|
| 源码路径 | 目标类型 | 生成文件 |
|
|
128
182
|
|----------|----------|----------|
|
|
129
183
|
| `frontend/sections/X.tsx` | `section` | `sections/react-x.liquid` |
|
|
130
184
|
| `frontend/blocks/X.tsx` | `block` | `blocks/react-x.liquid` |
|
|
131
185
|
| `frontend/templates/X.tsx` | `template` | `templates/page.react-x.liquid` |
|
|
132
|
-
|
|
133
|
-
可通过 `shopifyMeta.type` 覆盖。
|
|
186
|
+
| `frontend/snippets/X.tsx` | `snippet` | `snippets/react-x.liquid` |
|
|
134
187
|
|
|
135
188
|
---
|
|
136
189
|
|
|
137
190
|
## 设置 (Settings)
|
|
138
191
|
|
|
139
|
-
使用 `SettingSchema` 类型定义设置项。`SettingSchema` 是根据 `type` 字段判别(discriminated union)的联合类型 —— 每种 type 只接受 Shopify 文档中合法的字段,无效字段会在编译时报错:
|
|
140
|
-
|
|
141
192
|
```tsx
|
|
142
193
|
import type { SettingSchema } from "vite-plugin-react-shopify";
|
|
143
194
|
|
|
144
195
|
const settings = [
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
default: "Hello",
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
type: "select",
|
|
153
|
-
id: "layout",
|
|
154
|
-
label: "Layout",
|
|
155
|
-
default: "grid",
|
|
156
|
-
options: [
|
|
157
|
-
{ value: "grid", label: "Grid" },
|
|
158
|
-
{ value: "list", label: "List" },
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
type: "range",
|
|
163
|
-
id: "columns",
|
|
164
|
-
label: "Columns",
|
|
165
|
-
default: 3,
|
|
166
|
-
min: 1,
|
|
167
|
-
max: 6,
|
|
168
|
-
step: 1,
|
|
169
|
-
},
|
|
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 },
|
|
170
200
|
] satisfies SettingSchema[];
|
|
171
|
-
|
|
172
|
-
// 也可以直接引用具体类型:
|
|
173
|
-
import type { SelectSetting, RangeSetting } from "vite-plugin-react-shopify";
|
|
174
201
|
```
|
|
175
202
|
|
|
176
203
|
### 基本输入类型
|
|
177
204
|
|
|
178
|
-
| 类型 | 额外字段 |
|
|
179
|
-
|
|
180
|
-
| `checkbox` |
|
|
181
|
-
| `number` | `placeholder` |
|
|
182
|
-
| `radio` | `options`(必填) |
|
|
183
|
-
| `range` | `min`(必填)、`max`(必填)、`step`、`unit` |
|
|
184
|
-
| `select` | `options`(必填) |
|
|
185
|
-
| `text` | `placeholder` |
|
|
186
|
-
| `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` |
|
|
187
214
|
|
|
188
215
|
### 专用输入类型
|
|
189
216
|
|
|
190
|
-
|
|
191
|
-
|------|----------|
|
|
192
|
-
| `article` | —(不支持 `default`) |
|
|
193
|
-
| `article_list` | `limit` |
|
|
194
|
-
| `blog` | —(不支持 `default`) |
|
|
195
|
-
| `collection` | —(不支持 `default`) |
|
|
196
|
-
| `collection_list` | `limit` |
|
|
197
|
-
| `color` | — |
|
|
198
|
-
| `color_background` | — |
|
|
199
|
-
| `color_scheme` | — |
|
|
200
|
-
| `color_scheme_group` | `definition`(必填)、`role`(必填) |
|
|
201
|
-
| `font_picker` | —(`default` **必填** `string`) |
|
|
202
|
-
| `html` | `placeholder` |
|
|
203
|
-
| `image_picker` | —(不支持 `default`) |
|
|
204
|
-
| `inline_richtext` | — |
|
|
205
|
-
| `link_list` | —(`default` 限制为 `"main-menu"` / `"footer"`) |
|
|
206
|
-
| `liquid` | — |
|
|
207
|
-
| `metaobject` | `metaobject_type`(必填) |
|
|
208
|
-
| `metaobject_list` | `metaobject_type`(必填)、`limit` |
|
|
209
|
-
| `page` | —(不支持 `default`) |
|
|
210
|
-
| `product` | —(不支持 `default`) |
|
|
211
|
-
| `product_list` | `limit` |
|
|
212
|
-
| `richtext` | — |
|
|
213
|
-
| `text_alignment` | —(`default` 限制为 `"left"` / `"center"` / `"right"`) |
|
|
214
|
-
| `url` | — |
|
|
215
|
-
| `video` | —(不支持 `default`) |
|
|
216
|
-
| `video_url` | `accept`(必填)、`placeholder` |
|
|
217
|
-
|
|
218
|
-
### 侧边栏类型(非输入,仅显示信息)
|
|
219
|
-
|
|
220
|
-
`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`
|
|
221
218
|
|
|
222
|
-
|
|
223
|
-
const settings = [
|
|
224
|
-
{ type: "header", content: "Typography", info: "Customize text styles below." },
|
|
225
|
-
{ type: "font_picker", id: "heading_font", label: "Heading font", default: "helvetica_n4" },
|
|
226
|
-
{ type: "paragraph", content: "Set your brand colors for buttons and accents." },
|
|
227
|
-
{ type: "color", id: "accent_color", label: "Accent color", default: "#000000" },
|
|
228
|
-
{ type: "line_break" },
|
|
229
|
-
{ type: "checkbox", id: "dark_mode", label: "Enable dark mode", default: false },
|
|
230
|
-
] satisfies SettingSchema[];
|
|
219
|
+
### 侧边栏类型
|
|
231
220
|
|
|
232
|
-
|
|
221
|
+
`header`、`paragraph`、`line_break`。使用 `content` 字段,不保存值。
|
|
233
222
|
|
|
234
223
|
---
|
|
235
224
|
|
|
236
225
|
## 预设 (Presets)
|
|
237
226
|
|
|
238
|
-
预设让组件可以在编辑器中通过"添加 Section/Block"面板直接添加:
|
|
239
|
-
|
|
240
227
|
```tsx
|
|
241
228
|
export const shopifyMeta = {
|
|
242
229
|
name: "Hero Banner",
|
|
243
230
|
presets: [
|
|
244
231
|
{ name: "Hero (Light)", category: "Banners" },
|
|
245
|
-
{
|
|
246
|
-
|
|
247
|
-
category: "Banners",
|
|
248
|
-
settings: { bg_color: "#000", text_color: "#fff" },
|
|
249
|
-
},
|
|
232
|
+
{ name: "Hero (Dark)", category: "Banners",
|
|
233
|
+
settings: { bg_color: "#000", text_color: "#fff" } },
|
|
250
234
|
],
|
|
251
235
|
} satisfies ShopifyMeta;
|
|
252
236
|
```
|
|
@@ -255,60 +239,56 @@ export const shopifyMeta = {
|
|
|
255
239
|
|
|
256
240
|
## 子 Block(嵌套)
|
|
257
241
|
|
|
258
|
-
Section 或 Block 可以声明接受的子 block 类型:
|
|
259
|
-
|
|
260
242
|
```tsx
|
|
261
243
|
export const shopifyMeta = {
|
|
262
244
|
name: "Group",
|
|
263
|
-
blocks: [{ type: "@theme" }],
|
|
245
|
+
blocks: [{ type: "@theme" }],
|
|
264
246
|
} satisfies ShopifyMeta;
|
|
265
247
|
```
|
|
266
248
|
|
|
267
249
|
- `"@theme"` 接受当前主题中所有已注册的 block
|
|
268
|
-
- `
|
|
269
|
-
- 插件会自动在生成的 Liquid 中插入 `{% content_for 'blocks' %}`
|
|
250
|
+
- 插件自动插入 `{% content_for 'blocks' %}`
|
|
270
251
|
|
|
271
252
|
---
|
|
272
253
|
|
|
273
|
-
##
|
|
254
|
+
## Snippet
|
|
274
255
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
```css
|
|
278
|
-
/* frontend/sections/Hero.module.css */
|
|
279
|
-
.hero {
|
|
280
|
-
display: grid;
|
|
281
|
-
padding: 2rem;
|
|
282
|
-
}
|
|
283
|
-
.hero-title {
|
|
284
|
-
font-size: 3rem;
|
|
285
|
-
font-weight: 700;
|
|
286
|
-
}
|
|
287
|
-
```
|
|
256
|
+
通过 `params` 传参(而非 `settings`):
|
|
288
257
|
|
|
289
258
|
```tsx
|
|
290
|
-
// frontend/
|
|
291
|
-
import
|
|
259
|
+
// frontend/snippets/ProductCard.tsx
|
|
260
|
+
import { useSnippetParams } from "vite-plugin-react-shopify/runtime";
|
|
292
261
|
|
|
293
|
-
export
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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>;
|
|
299
272
|
}
|
|
300
273
|
```
|
|
301
274
|
|
|
302
|
-
|
|
275
|
+
调用方式:`{% render 'react-product-card', title: 'Hello', price: '$10' %}`
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## CSS 样式
|
|
280
|
+
|
|
281
|
+
使用普通 CSS 文件为组件添加样式。CSS 会在构建时被内联到 Liquid 的 `{% stylesheet %}` 块中。多个组件共享的 CSS 自动提取为 snippet:
|
|
303
282
|
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
.
|
|
307
|
-
._hero-title_abc123{font-size:3rem;font-weight:700}
|
|
308
|
-
{% endstylesheet %}
|
|
283
|
+
```css
|
|
284
|
+
/* frontend/sections/Hero.css */
|
|
285
|
+
.hero { display: grid; padding: 2rem; }
|
|
309
286
|
```
|
|
310
287
|
|
|
311
|
-
|
|
288
|
+
```tsx
|
|
289
|
+
import "./Hero.css";
|
|
290
|
+
export default function Hero() { return <div className="hero">...</div>; }
|
|
291
|
+
```
|
|
312
292
|
|
|
313
293
|
---
|
|
314
294
|
|
|
@@ -316,182 +296,119 @@ export default function Hero() {
|
|
|
316
296
|
|
|
317
297
|
```ts
|
|
318
298
|
vitePluginShopify({
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// === 构建配置 ===
|
|
326
|
-
hash: false, // 产物文件名是否包含 content hash,默认 false
|
|
327
|
-
|
|
328
|
-
// === 调试 ===
|
|
329
|
-
debug: false, // 启用详细日志输出
|
|
330
|
-
|
|
331
|
-
// === SSG 配置 ===
|
|
299
|
+
themeRoot: ".", // 主题根目录
|
|
300
|
+
sourceCodeDir: "frontend", // React 源码目录
|
|
301
|
+
snippetFile: "shopify-importmap.liquid",// importmap 片段文件名
|
|
302
|
+
buildDir: "assets", // 构建产物输出目录
|
|
303
|
+
debug: false, // 详细日志
|
|
332
304
|
ssg: {
|
|
333
|
-
directories: ["sections", "blocks", "templates"],
|
|
334
|
-
prefix: {
|
|
335
|
-
template: "page.react-",
|
|
336
|
-
section: "react-",
|
|
337
|
-
block: "react-",
|
|
305
|
+
directories: ["sections", "blocks", "templates", "snippets"],
|
|
306
|
+
prefix: {
|
|
307
|
+
template: "page.react-",
|
|
308
|
+
section: "react-",
|
|
309
|
+
block: "react-",
|
|
310
|
+
snippet: "react-",
|
|
338
311
|
},
|
|
339
|
-
outputName: "",
|
|
312
|
+
outputName: "", // 自定义输出模板
|
|
313
|
+
cssPrefix: "css", // 共享 CSS snippet 前缀
|
|
340
314
|
},
|
|
341
|
-
|
|
342
|
-
// === Import Map 配置 ===
|
|
343
315
|
importMap: {
|
|
344
|
-
react: "
|
|
345
|
-
reactDomClient: "
|
|
316
|
+
react: "{{ 'react.js' | asset_url }}",
|
|
317
|
+
reactDomClient: "{{ 'react-dom.js' | asset_url }}",
|
|
346
318
|
},
|
|
347
319
|
});
|
|
348
320
|
```
|
|
349
321
|
|
|
350
322
|
---
|
|
351
323
|
|
|
352
|
-
##
|
|
353
|
-
|
|
354
|
-
### 默认命名规则
|
|
355
|
-
|
|
356
|
-
| 源码文件 | 目标类型 | 生成文件 |
|
|
357
|
-
|----------|----------|----------|
|
|
358
|
-
| `frontend/sections/MySection.tsx` | section | `sections/react-my-section.liquid` |
|
|
359
|
-
| `frontend/blocks/MyBlock.tsx` | block | `blocks/react-my-block.liquid` |
|
|
360
|
-
| `frontend/templates/Index.tsx` | template | `templates/page.react-index.liquid` |
|
|
361
|
-
|
|
362
|
-
- 组件文件名通过 `toKebabCase` 转换为 kebab-case:
|
|
363
|
-
- `HelloWorld` → `hello-world`
|
|
364
|
-
- `FAQSection` → `faq-section`
|
|
365
|
-
- JS 产物默认使用稳定文件名(如 `assets/build/hello-world.js`),设置 `hash: true` 后启用 content hash(如 `assets/build/hello-world-aBc123.js`)
|
|
366
|
-
|
|
367
|
-
### 自定义命名(`outputName`)
|
|
368
|
-
|
|
369
|
-
```ts
|
|
370
|
-
ssg: {
|
|
371
|
-
outputName: "{target}s/{kebab}" // 按类型分目录,不添加前缀
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
支持占位符:`{type}`、`{kebab}`、`{pascal}`、`{target}`。
|
|
376
|
-
|
|
377
|
-
---
|
|
378
|
-
|
|
379
|
-
## 水合(Hydration)与编辑器事件
|
|
324
|
+
## 水合与编辑器事件
|
|
380
325
|
|
|
381
326
|
### 水合流程
|
|
382
327
|
|
|
383
|
-
1. **SSG
|
|
384
|
-
2. **
|
|
385
|
-
3. **Hydration JS**:Vite
|
|
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`
|
|
386
331
|
|
|
387
332
|
### 编辑器事件
|
|
388
333
|
|
|
389
|
-
Hydration 脚本自动监听 Shopify 编辑器的 Section 生命周期事件:
|
|
390
|
-
|
|
391
334
|
| 事件 | 行为 |
|
|
392
335
|
|------|------|
|
|
393
|
-
| `shopify:section:load` |
|
|
394
|
-
| `shopify:section:unload` |
|
|
395
|
-
|
|
396
|
-
当用户在编辑器中修改 setting 时:Shopify 重新渲染 Section HTML → 触发 `unload`(清理旧 root)→ 触发 `load`(用新 props 重新 hydrate)。
|
|
336
|
+
| `shopify:section:load` | 重新 hydrate 组件 |
|
|
337
|
+
| `shopify:section:unload` | unmount React 根节点 |
|
|
397
338
|
|
|
398
339
|
---
|
|
399
340
|
|
|
400
|
-
##
|
|
401
|
-
|
|
402
|
-
### Liquid 组件
|
|
403
|
-
|
|
404
|
-
在 JSX 中嵌入原始 Liquid 代码:
|
|
405
|
-
|
|
406
|
-
```tsx
|
|
407
|
-
import { Liquid } from "vite-plugin-react-shopify/runtime";
|
|
341
|
+
## 调试
|
|
408
342
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
<div>
|
|
412
|
-
<Liquid>{`{% if section.settings.show_title %}`}</Liquid>
|
|
413
|
-
<h1>Title</h1>
|
|
414
|
-
<Liquid>{`{% endif %}`}</Liquid>
|
|
415
|
-
</div>
|
|
416
|
-
);
|
|
417
|
-
}
|
|
343
|
+
```bash
|
|
344
|
+
DEBUG=vite-plugin-shopify:* npx vite build
|
|
418
345
|
```
|
|
419
346
|
|
|
420
|
-
|
|
347
|
+
---
|
|
421
348
|
|
|
422
|
-
|
|
349
|
+
## 开发规范
|
|
423
350
|
|
|
424
|
-
|
|
425
|
-
{% render 'shopify-importmap' %}
|
|
426
|
-
```
|
|
351
|
+
> 详见 `docs/hydration-issues.md`
|
|
427
352
|
|
|
428
|
-
|
|
353
|
+
### JSX 子节点中相邻文本+表达式
|
|
429
354
|
|
|
430
|
-
|
|
355
|
+
**必须使用模板字面量包裹**:
|
|
431
356
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
357
|
+
```tsx
|
|
358
|
+
// ❌ 水合失败:相邻文本节点不匹配
|
|
359
|
+
<button>-{step}</button>
|
|
360
|
+
<li>title = {title}</li>
|
|
435
361
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
});
|
|
362
|
+
// ✅ 模板字面量
|
|
363
|
+
<button>{`-${step}`}</button>
|
|
364
|
+
<li>{`title = ${title}`}</li>
|
|
440
365
|
```
|
|
441
366
|
|
|
442
|
-
###
|
|
367
|
+
### useState 初始化
|
|
443
368
|
|
|
444
|
-
|
|
445
|
-
|
|
369
|
+
**不能依赖 `useLiquid` 返回值**:
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
// ❌ SSR 时 Number("{{ expr }}") = NaN → 不匹配
|
|
373
|
+
const [count, setCount] = useState(Number(s.initial) || 0);
|
|
374
|
+
|
|
375
|
+
// ✅ 固定默认值 + useEffect 同步
|
|
376
|
+
const [count, setCount] = useState(0);
|
|
377
|
+
useEffect(() => { setCount(parseLiquidNumber(s.initial, 0)); }, []);
|
|
446
378
|
```
|
|
447
379
|
|
|
448
|
-
|
|
380
|
+
### 条件渲染
|
|
449
381
|
|
|
450
|
-
|
|
382
|
+
**不用 `{value && <Element />}`**,用 `hidden` 属性:
|
|
451
383
|
|
|
452
|
-
|
|
384
|
+
```tsx
|
|
385
|
+
// ❌ SSR 时表达式字符串始终 truthy → 结构不匹配
|
|
386
|
+
{showBanner && <Banner />}
|
|
453
387
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
vite-plugin-shopify:ssg:compiler found 5 entries to compile
|
|
457
|
-
vite-plugin-shopify:ssg:compiler entry counter has 1 CSS files
|
|
458
|
-
vite-plugin-shopify:ssg:compiler entry hello-world has 1 CSS files
|
|
459
|
-
vite-plugin-shopify:ssg:compiler generated shared CSS snippet react-css-SharedCard (used by 2 entries)
|
|
460
|
-
vite-plugin-shopify:ssg:compiler compiling counter (type=section, css inline=0, css snippets=1)
|
|
461
|
-
vite-plugin-shopify:ssg:compiler bundling counter via esbuild
|
|
462
|
-
vite-plugin-shopify:ssg:compiler esbuild bundle took 53ms
|
|
463
|
-
...
|
|
464
|
-
[vite-plugin-shopify] Starting SSG compilation...
|
|
465
|
-
[vite-plugin-shopify] Compiled 5 entries
|
|
466
|
-
[vite-plugin-shopify] SSG compilation complete
|
|
388
|
+
// ✅ DOM 结构不变,仅切换属性
|
|
389
|
+
<section hidden={!parseLiquidBoolean(showBannerRaw)}>...</section>
|
|
467
390
|
```
|
|
468
391
|
|
|
469
|
-
###
|
|
392
|
+
### inline style 颜色值
|
|
470
393
|
|
|
471
|
-
|
|
472
|
-
|------|----------|--------|
|
|
473
|
-
| `info` | 始终可见 | 构建阶段摘要、完成计数 |
|
|
474
|
-
| `warn` | 始终可见 | 缺少依赖、跳过的组件 |
|
|
475
|
-
| `error` | 始终可见 | 编译失败的组件及堆栈 |
|
|
476
|
-
| `debug` | 仅 debug 模式 | 入口扫描结果、CSS 分发、esbuild 耗时、配置详情 |
|
|
394
|
+
**用 CSS 自定义属性代替内联颜色**:
|
|
477
395
|
|
|
478
|
-
|
|
396
|
+
```tsx
|
|
397
|
+
// ❌ 浏览器规范化 hex → rgb → 不匹配
|
|
398
|
+
<div style={{ backgroundColor: color }} />
|
|
479
399
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
400
|
+
// ✅ CSS 变量不归一化
|
|
401
|
+
<div style={{ "--accent": color } as React.CSSProperties} />
|
|
402
|
+
// CSS: .accent { background-color: var(--accent); }
|
|
403
|
+
```
|
|
484
404
|
|
|
485
405
|
---
|
|
486
406
|
|
|
487
407
|
## 注意事项
|
|
488
408
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
7. **构建产物默认输出到 `assets/`**:如需与其他静态资源隔离,可设置 `buildDir: "assets/build"` 将产物输出到子目录,然后在 `.gitignore` 中添加 `assets/build/` 忽略该目录
|
|
496
|
-
8. **Watch 模式自动关闭压缩**:`vite build --watch` 时自动设置 `minify: false` 并启用 inline sourcemap,方便在浏览器中调试。生产构建(`vite build`)则使用正常压缩
|
|
497
|
-
9. **增量构建依赖 Rolldown 缓存**:watch 模式下 Rolldown 的 `ScanStageCache` 自动缓存模块图,文件变更时仅重新扫描变更模块,大幅加速构建。首次启动执行全量构建,后续为增量构建
|
|
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
|