佈景主題應用程式擴充功能
佈景主題應用程式擴充功能讓您將應用程式的功能直接新增到商家的店面中,無需編輯其佈景主題程式碼。透過 Online Store 2.0,商家使用佈景主題編輯器將您的應用程式區塊拖放到其頁面中——無需修改程式碼,應用程式解除安裝時也不會損壞佈景主題。這是將應用程式 UI 整合到 Shopify 店面的標準方式。
Online Store 2.0 和應用程式區塊
Online Store 2.0 引入了基於區段的架構,每個頁面都由區段組成,區段包含區塊。應用程式區塊是您的應用程式註冊的特殊類型區塊。商家透過佈景主題編輯器新增它們,就像原生 Shopify 區塊一樣。
有兩種類型的佈景主題應用程式擴充功能:
應用程式區塊
應用程式區塊是商家放置在區段內的可見 UI 元件。範例包括產品評價小工具、尺寸指南、信任徽章和倒數計時器。
- 在區段的區塊列表中渲染
- 商家控制相對於其他區塊的位置
- 可以新增到任何支援區塊的區段
- 透過佈景主題編輯器的設定面板設定
應用程式嵌入區塊
應用程式嵌入區塊在頁面層級全域注入,位於區段結構之外。它們非常適合需要在每個頁面上出現的功能,無論佈局如何。
- 在文件層級渲染(通常在
<head>或</body>之前) - 啟用後始終活躍——不綁定到特定區段
- 常用於聊天小工具、分析腳本、浮動按鈕和通知列
- 在佈景主題編輯器的應用程式嵌入面板中開啟/關閉
建立佈景主題應用程式擴充功能
# 產生佈景主題應用程式擴充功能
shopify app generate extension --template theme_app_extension --name product-reviews
這會建立:
extensions/product-reviews/
├── blocks/
│ ├── review-widget.liquid
│ └── review-summary.liquid
├── assets/
│ ├── reviews.css
│ └── reviews.js
├── locales/
│ └── en.default.json
├── snippets/
│ └── star-rating.liquid
└── shopify.extension.toml
擴充功能設定
# extensions/product-reviews/shopify.extension.toml
api_version = "2025-01"
[[extensions]]
type = "theme"
name = "Product Reviews"
handle = "product-reviews"
[[extensions.blocks]]
type = "review-widget"
name = "Review Widget"
target = "section"
[[extensions.blocks]]
type = "review-summary"
name = "Review Summary"
target = "section"
[[extensions.embeds]]
type = "review-popup"
name = "Review Request Popup"
使用 Liquid 建構應用程式區塊
應用程式區塊使用 Liquid 撰寫,Shopify 的模板語言。它們可以存取與佈景主題程式碼相同的 Liquid 物件,加上您的應用程式設定。
評價小工具區塊
{% comment %}
blocks/review-widget.liquid
顯示帶有星級評分和篩選的產品評價
{% endcomment %}
{% schema %}
{
"name": "Review Widget",
"target": "section",
"stylesheet": "reviews.css",
"javascript": "reviews.js",
"settings": [
{
"type": "range",
"id": "reviews_per_page",
"min": 3,
"max": 20,
"step": 1,
"default": 5,
"label": "每頁評價數"
},
{
"type": "select",
"id": "default_sort",
"label": "預設排序順序",
"options": [
{ "value": "newest", "label": "最新優先" },
{ "value": "highest", "label": "最高評分" },
{ "value": "lowest", "label": "最低評分" },
{ "value": "helpful", "label": "最有幫助" }
],
"default": "newest"
},
{
"type": "checkbox",
"id": "show_photos",
"label": "顯示評價照片",
"default": true
},
{
"type": "checkbox",
"id": "show_verified_badge",
"label": "顯示已驗證購買徽章",
"default": true
},
{
"type": "color",
"id": "star_color",
"label": "星星顏色",
"default": "#FFB800"
},
{
"type": "color",
"id": "star_empty_color",
"label": "空星顏色",
"default": "#E0E0E0"
}
]
}
{% endschema %}
<div
class="app-reviews-widget"
data-product-id="{{ product.id }}"
data-reviews-per-page="{{ block.settings.reviews_per_page }}"
data-default-sort="{{ block.settings.default_sort }}"
data-show-photos="{{ block.settings.show_photos }}"
style="
--star-color: {{ block.settings.star_color }};
--star-empty-color: {{ block.settings.star_empty_color }};
"
>
{% comment %} 摘要區段與彙總評分 {% endcomment %}
<div class="reviews-summary">
<div class="reviews-summary__average">
{% assign avg_rating = product.metafields.reviews.average_rating | default: 0 %}
{% assign review_count = product.metafields.reviews.count | default: 0 %}
<span class="reviews-summary__score">{{ avg_rating | round: 1 }}</span>
{% render 'star-rating', rating: avg_rating %}
<span class="reviews-summary__count">
{{ review_count }} {{ review_count | pluralize: 'review', 'reviews' }}
</span>
</div>
{% if review_count > 0 %}
<div class="reviews-summary__breakdown">
{% for i in (1..5) reversed %}
{% assign star_count = product.metafields.reviews[i | append: '_star_count'] | default: 0 %}
{% assign percentage = star_count | times: 100.0 | divided_by: review_count | round %}
<div class="reviews-breakdown__row">
<span class="reviews-breakdown__label">{{ i }} 星</span>
<div class="reviews-breakdown__bar">
<div
class="reviews-breakdown__fill"
style="width: {{ percentage }}%"
></div>
</div>
<span class="reviews-breakdown__count">{{ star_count }}</span>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% comment %} 篩選和排序控制項 {% endcomment %}
<div class="reviews-controls">
<div class="reviews-controls__filter">
<label for="reviews-rating-filter">依評分篩選:</label>
<select id="reviews-rating-filter" class="reviews-filter-select">
<option value="all">所有評分</option>
<option value="5">5 星</option>
<option value="4">4 星</option>
<option value="3">3 星</option>
<option value="2">2 星</option>
<option value="1">1 星</option>
</select>
</div>
</div>
{% comment %} 評價列表——由 JavaScript 填充 {% endcomment %}
<div class="reviews-list" id="reviews-list-{{ product.id }}">
<div class="reviews-loading">載入評價中...</div>
</div>
{% comment %} 撰寫評價 CTA {% endcomment %}
{% if customer %}
<div class="reviews-write">
<button
class="reviews-write__button"
data-product-id="{{ product.id }}"
data-customer-id="{{ customer.id }}"
>
撰寫評價
</button>
</div>
{% else %}
<div class="reviews-write">
<p>
<a href="{{ routes.account_login_url }}">登入</a>以撰寫評價。
</p>
</div>
{% endif %}
{% if block.settings.show_verified_badge %}
<div class="reviews-badge-info">
<svg class="reviews-verified-icon" width="16" height="16" viewBox="0 0 16 16">
<path d="M8 0L10 5.5L16 6L11.5 10L13 16L8 13L3 16L4.5 10L0 6L6 5.5Z" fill="currentColor"/>
</svg>
<span>已驗證購買的評價來自購買了此產品的顧客。</span>
</div>
{% endif %}
</div>
{% schema %} 區塊定義了當商家在佈景主題編輯器中選取您的應用程式區塊時出現的設定。使用描述性標籤和合理的預設值。為不明顯的設定提供 info 文字。將相關設定分組。佈景主題編輯器中的商家體驗是應用程式採用的關鍵差異化因素。
星級評分片段
{% comment %}
snippets/star-rating.liquid
渲染星級評分顯示
用法:{% render 'star-rating', rating: 4.5 %}
{% endcomment %}
<div class="star-rating" role="img" aria-label="{{ rating }} out of 5 stars">
{% for i in (1..5) %}
{% if rating >= i %}
<svg class="star-rating__star star-rating__star--full" width="20" height="20" viewBox="0 0 20 20">
<path d="M10 1L12.39 6.97L18.78 7.6L13.89 11.84L15.28 18.08L10 14.85L4.72 18.08L6.11 11.84L1.22 7.6L7.61 6.97L10 1Z" fill="var(--star-color, #FFB800)"/>
</svg>
{% elsif rating > i | minus: 1 %}
<svg class="star-rating__star star-rating__star--half" width="20" height="20" viewBox="0 0 20 20">
<defs>
<linearGradient id="half-{{ i }}">
<stop offset="50%" stop-color="var(--star-color, #FFB800)"/>
<stop offset="50%" stop-color="var(--star-empty-color, #E0E0E0)"/>
</linearGradient>
</defs>
<path d="M10 1L12.39 6.97L18.78 7.6L13.89 11.84L15.28 18.08L10 14.85L4.72 18.08L6.11 11.84L1.22 7.6L7.61 6.97L10 1Z" fill="url(#half-{{ i }})"/>
</svg>
{% else %}
<svg class="star-rating__star star-rating__star--empty" width="20" height="20" viewBox="0 0 20 20">
<path d="M10 1L12.39 6.97L18.78 7.6L13.89 11.84L15.28 18.08L10 14.85L4.72 18.08L6.11 11.84L1.22 7.6L7.61 6.97L10 1Z" fill="var(--star-empty-color, #E0E0E0)"/>
</svg>
{% endif %}
{% endfor %}
</div>
與中繼欄位整合
中繼欄位是在佈景主題擴充功能中儲存和擷取應用程式資料的主要方式。您的應用程式透過 Admin API 將資料寫入中繼欄位,而您的 Liquid 區塊讀取它們。
定義中繼欄位存取
// 在您的應用程式的 shopify.app.toml 中,宣告您的佈景主題擴充功能讀取哪些中繼欄位
[access.metafields]
[[access.metafields.read]]
namespace = "reviews"
key = "average_rating"
[[access.metafields.read]]
namespace = "reviews"
key = "count"
[[access.metafields.read]]
namespace = "reviews"
key = "data"
從您的應用程式寫入中繼欄位
// app/services/reviews.server.ts
export async function updateProductReviewMetafields(
admin: AdminApiContext,
productId: string,
reviews: Review[]
) {
const avgRating =
reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
const starCounts = [1, 2, 3, 4, 5].map(
(star) => reviews.filter((r) => r.rating === star).length
);
await admin.graphql(`
mutation SetReviewMetafields($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
id
namespace
key
value
}
userErrors {
field
message
}
}
}
`, {
variables: {
metafields: [
{
ownerId: productId,
namespace: 'reviews',
key: 'average_rating',
type: 'number_decimal',
value: String(avgRating.toFixed(2)),
},
{
ownerId: productId,
namespace: 'reviews',
key: 'count',
type: 'number_integer',
value: String(reviews.length),
},
...starCounts.map((count, index) => ({
ownerId: productId,
namespace: 'reviews',
key: `${index + 1}_star_count`,
type: 'number_integer',
value: String(count),
})),
],
},
});
}
在 Liquid 中讀取中繼欄位
{% comment %} 在您的應用程式區塊中存取中繼欄位 {% endcomment %}
{% assign avg = product.metafields.reviews.average_rating.value %}
{% assign count = product.metafields.reviews.count.value %}
{% if count > 0 %}
<div class="review-badge">
{% render 'star-rating', rating: avg %}
<span>({{ count }})</span>
</div>
{% endif %}
始終為中繼欄位使用您的應用程式保留的命名空間。如果另一個應用程式使用相同的命名空間和鍵,資料將會碰撞。Shopify 保留某些命名空間(如 global)。使用能清楚識別您的應用程式的命名空間,例如 my_app_reviews。
最佳實務
效能
- 最小化 JavaScript——只在區塊可見時載入 JS。使用
IntersectionObserver進行延遲載入。 - 使用 CSS 自訂屬性——讓商家自訂顏色而無需載入額外的樣式表。
- 避免阻擋主執行緒——所有 API 呼叫應該是非同步的。永遠不要使用同步 XHR。
- 積極快取——使用
localStorage存放不常變更的評價資料。
商家體驗
- 提供有意義的預設值——每個設定都應有合理的預設值,以便區塊立即看起來很好。
- 包含預覽資料——使用 Liquid 的
request.design_mode在佈景主題編輯器中顯示範例資料。 - 處理空狀態——沒有評價時顯示友善的訊息,而不是空白區域。
- 記錄設定——使用結構描述設定中的
info屬性來解釋每個選項的作用。
{% if request.design_mode %}
{% comment %} 在佈景主題編輯器中顯示範例資料 {% endcomment %}
<div class="reviews-preview">
{% render 'star-rating', rating: 4.5 %}
<p>預覽:127 則評價將在此顯示</p>
</div>
{% endif %}
乾淨解除安裝
當商家解除安裝您的應用程式時,您的佈景主題擴充功能區塊會自動從其佈景主題中移除。這是佈景主題應用程式擴充功能相較於舊版 Script Tag 或 Asset API 方法的核心優勢。然而,您仍應清除您的應用程式建立的任何中繼欄位,因為它們在解除安裝後仍然存在。處理 APP_UNINSTALLED webhook 以移除孤立的中繼欄位。
應用程式嵌入區塊範例
{% comment %}
blocks/review-popup.liquid(嵌入區塊)
顯示購後評價請求彈出視窗
{% endcomment %}
{% schema %}
{
"name": "Review Request Popup",
"target": "body",
"stylesheet": "review-popup.css",
"javascript": "review-popup.js",
"settings": [
{
"type": "range",
"id": "delay_days",
"min": 1,
"max": 14,
"step": 1,
"default": 7,
"label": "購買後顯示彈出視窗的天數",
"info": "顧客購買後多少天才提示評價"
},
{
"type": "text",
"id": "heading_text",
"label": "彈出視窗標題",
"default": "How are you enjoying your purchase?"
},
{
"type": "checkbox",
"id": "show_on_mobile",
"label": "在行動裝置上顯示",
"default": false,
"info": "彈出視窗在小螢幕上可能造成干擾"
}
]
}
{% endschema %}
<div
id="review-request-popup"
class="review-popup review-popup--hidden"
data-delay-days="{{ block.settings.delay_days }}"
data-show-mobile="{{ block.settings.show_on_mobile }}"
role="dialog"
aria-label="{{ block.settings.heading_text }}"
aria-hidden="true"
>
<div class="review-popup__overlay"></div>
<div class="review-popup__content">
<button class="review-popup__close" aria-label="Close">×</button>
<h2 class="review-popup__heading">{{ block.settings.heading_text }}</h2>
<div class="review-popup__body" id="review-popup-form">
<!-- 由 JavaScript 填充 -->
</div>
</div>
</div>
繼續閱讀 POS UI 擴充功能 以了解如何使用自訂 UI 元件擴展 Shopify 的銷售點應用程式,用於店內體驗。