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 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
- return <h1>Hello, World!</h1>;
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
- │ └── shopify-importmap.liquid ← 自动生成的 importmap
78
+ ├── templates/
61
79
  └── assets/ ← Vite 构建产物(可通过 buildDir 配置子目录)
62
80
  ```
63
81
 
64
- 源码放在 `frontend/` 下,按 Shopify 类型分目录。构建后生成对应的 `.liquid` 文件到主题根目录的 `sections/`、`blocks/`、`templates/` 下。
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: "组件名称", // 必填,在 Shopify 编辑器中显示的名称
77
- type: "section", // 可选,覆盖目录推断的类型
159
+ name: "组件名称",
160
+ type: "section", // 可选,覆盖目录推断
78
161
  tag: "section", // 可选,外层 HTML 标签,默认 "div"
79
- class: "custom-class", // 可选,外层附加的 CSS 类名
80
- limit: 1, // 可选,每页最大数量
81
- max_blocks: 10, // 可选,最大子 block 数
82
- settings: [...], // 可选,设置项
83
- blocks: [...], // 可选,接受的子 block 类型
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(props) {
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
- type: "text",
123
- id: "title",
124
- label: "Title",
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
- | 类型 | 额外字段 | `default` |
155
- |------|----------|-----------|
156
- | `checkbox` | — | 可选 `boolean` |
157
- | `number` | `placeholder` | 可选 `number` |
158
- | `radio` | `options`(必填) | 可选 `string` |
159
- | `range` | `min`(必填)、`max`(必填)、`step`、`unit` | **必填** `number` |
160
- | `select` | `options`(必填) | 可选 `string` |
161
- | `text` | `placeholder` | 可选 `string` |
162
- | `textarea` | `placeholder` | 可选 `string` |
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
- ```tsx
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
- Setting 的 `id` 会作为 React 组件的 prop 名传入。Shopify 编辑器修改 setting 后,水合层会读取 `{{ section.settings | json }}` 将最新值传给组件。设置值通过 `useShopifySettings()` hook 读取。
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
- name: "Hero (Dark)",
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" }], // 接受所有主题 block
245
+ blocks: [{ type: "@theme" }],
240
246
  } satisfies ShopifyMeta;
241
247
  ```
242
248
 
243
249
  - `"@theme"` 接受当前主题中所有已注册的 block
244
- - `"类型名"` 只接受特定类型的 block(如 `"text"` 匹配 `blocks/text.liquid`)
245
- - 插件会自动在生成的 Liquid 中插入 `{% content_for 'blocks' %}`
250
+ - 插件自动插入 `{% content_for 'blocks' %}`
246
251
 
247
252
  ---
248
253
 
249
- ## CSS 样式
254
+ ## Snippet
250
255
 
251
- 使用 CSS Module 为组件添加样式。CSS 会在构建时被内联到生成的 Liquid 的 `{% stylesheet %}` 块中,由 Shopify 统一注入到 `<head>`,避免 FOUC 和额外 HTTP 请求。
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/sections/Hero.tsx
267
- import styles from "./Hero.module.css";
259
+ // frontend/snippets/ProductCard.tsx
260
+ import { useSnippetParams } from "vite-plugin-react-shopify/runtime";
268
261
 
269
- export default function Hero() {
270
- return (
271
- <div className={styles["hero"]}>
272
- <h1 className={styles["hero-title"]}>Title</h1>
273
- </div>
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
- 生成的 Liquid 中会包含:
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
- ```liquid
281
- {% stylesheet %}
282
- ._hero_abc123{display:grid;padding:2rem}
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
- > **注意**:SSG 预渲染的 HTML 使用未经过 Vite hash 处理的类名。浏览器加载后 React 水合会替换为正确的 hash 类名。这是 CSS Module + SSG 的已知权衡。
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
- themeRoot: ".", // 主题根目录
297
- sourceCodeDir: "frontend", // React 源码目录(相对于 themeRoot)
298
- snippetFile: "shopify-importmap.liquid", // importmap 片段文件名
299
- buildDir: "assets", // Vite 构建产物输出目录(相对于 themeRoot)
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-", // templates → page.react-xxx.liquid
306
- section: "react-", // sections → react-xxx.liquid
307
- block: "react-", // blocks → react-xxx.liquid
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: "https://esm.sh/react@19",
315
- reactDomClient: "https://esm.sh/react-dom@19/client",
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
- | `frontend/sections/MySection.tsx` | section | `sections/react-my-section.liquid` |
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
- - 组件文件名通过 `toKebabCase` 转换为 kebab-case:
333
- - `HelloWorld` → `hello-world`
334
- - `FAQSection` → `faq-section`
332
+ ### 编辑器事件
335
333
 
336
- ### 自定义命名(`outputName`)
334
+ | 事件 | 行为 |
335
+ |------|------|
336
+ | `shopify:section:load` | 重新 hydrate 组件 |
337
+ | `shopify:section:unload` | unmount React 根节点 |
337
338
 
338
- ```ts
339
- ssg: {
340
- outputName: "{target}s/{kebab}" // 按类型分目录,不添加前缀
341
- }
342
- ```
339
+ ---
343
340
 
344
- 支持占位符:`{type}`、`{kebab}`、`{pascal}`、`{target}`。
341
+ ## 调试
342
+
343
+ ```bash
344
+ DEBUG=vite-plugin-shopify:* npx vite build
345
+ ```
345
346
 
346
347
  ---
347
348
 
348
- ## 水合(Hydration)与编辑器事件
349
+ ## 开发规范
349
350
 
350
- ### 水合流程
351
+ > 详见 `docs/hydration-issues.md`
351
352
 
352
- 1. **SSG 预渲染**:构建时 React 组件渲染为静态 HTML,嵌入 `<div data-ssg-hydrate>`
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
- Hydration 脚本自动监听 Shopify 编辑器的 Section 生命周期事件:
357
+ ```tsx
358
+ // ❌ 水合失败:相邻文本节点不匹配
359
+ <button>-{step}</button>
360
+ <li>title = {title}</li>
359
361
 
360
- | 事件 | 行为 |
361
- |------|------|
362
- | `shopify:section:load` | Section 被添加或重新渲染时,重新 hydrate 组件 |
363
- | `shopify:section:unload` | Section 被删除或即将重新渲染时,unmount React 根节点 |
362
+ // 模板字面量
363
+ <button>{`-${step}`}</button>
364
+ <li>{`title = ${title}`}</li>
365
+ ```
364
366
 
365
- 当用户在编辑器中修改 setting 时:Shopify 重新渲染 Section HTML → 触发 `unload`(清理旧 root)→ 触发 `load`(用新 props 重新 hydrate)。
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
- ### Liquid 组件
380
+ ### 条件渲染
372
381
 
373
- JSX 中嵌入原始 Liquid 代码:
382
+ **不用 `{value && <Element />}`**,用 `hidden` 属性:
374
383
 
375
384
  ```tsx
376
- import { Liquid } from "vite-plugin-react-shopify/runtime";
385
+ // SSR 时表达式字符串始终 truthy → 结构不匹配
386
+ {showBanner && <Banner />}
377
387
 
378
- export default function Section() {
379
- return (
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
- ### Import Map
392
+ ### inline style 颜色值
393
+
394
+ **用 CSS 自定义属性代替内联颜色**:
390
395
 
391
- 插件自动生成 `snippets/shopify-importmap.liquid`,包含 React 和 ReactDOM 的 CDN import map。在主题 `layout/theme.liquid` 的 `<head>` 中引入:
396
+ ```tsx
397
+ // ❌ 浏览器规范化 hex → rgb → 不匹配
398
+ <div style={{ backgroundColor: color }} />
392
399
 
393
- ```liquid
394
- {% render 'shopify-importmap' %}
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
- 1. **React / ReactDOM 不打包进 bundle**:通过 import map CDN 加载,避免重复打包和体积膨胀
402
- 2. **CSS Module hash 差异**:SSG 预渲染的 HTML 使用原始类名,水合后才会替换为 hash 类名。如需完美匹配,建议使用全局 CSS 而非 CSS Module,或接受短暂的样式跳跃
403
- 3. **SSG 使用默认 props 渲染**:构建时的预渲染使用组件的默认 prop 值,编辑器中修改 setting 后会通过水合更新
404
- 4. **Template 类型不包裹**:`type: "template"` 的组件 HTML 直接输出,不添加 section/block 外层结构,适用于整页模板
405
- 5. **Section 必须有预设才能通过编辑器添加**:没有 `presets` section 需要手动在 JSON 模板中引用,编辑器无法直接添加
406
- 6. **`{% content_for 'blocks' %}` 自动插入**:当 `shopifyMeta.blocks` 非空时,插件自动在生成的 Liquid 中插入子 block 渲染标签
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