Shopify 生态系统
Shopify 基本概况
- 电商平台Shopify的技术架构演进思路:http://www.uml.org.cn/zjjs/201809101.asp
- Shopify Workshops: https://workshops.shopify.dev/
- 开发文档:https://shopify.dev/
- API文档:https://shopify.dev/docs/api
- GitHub: https://github.com/Shopify
- Shopify CLI: https://github.com/Shopify/cli
- Shopify帮助中心: https://help.shopify.com/
- shopify app store: https://apps.shopify.com/categories/
支付系统
创建一个测试订单
使用 Shopify Bogus Gateway 模拟支付。
- https://help.shopify.com/zh-CN/manual/checkout-settings/test-orders
您可以在结账时使用以下代码代替信用卡号:
1 - 模拟已批准的交易
2 - 模拟已拒绝的交易
3 - 模拟网关故障
# CVS
任何三位数字都可以用作“信用卡安全码”,将来的任何到期日都可以使用。
Shopify Plus
Shopify Plus 品牌获得了编写自己的函数并在自定义应用程序中将它们分发到其商店的独家能力。
技巧
产品详情URL直接加后缀".json",就可以查看产品详情的JSON数据信息
# shopify后台产品详情
https://admin.shopify.com/store/ysungod-001/products/7527308886155
# JSON数据
https://admin.shopify.com/store/ysungod-001/products/7527308886155.json
第02章 Shopify CLI环境
Shopify CLI 是一个命令行界面工具,可帮助您构建 Shopify 应用程序和主题。它可以快速生成 Shopify 应用程序、模版和自定义店面。您还可以使用它来自动执行许多常见的开发任务。
VSCode配置插件
- Shopify Liquid
- Liquid
Shopify模板检查器
- chrome插件: Shopify Theme Inspector for chrome
MacOS安装配置
当您使用 Homebrew 安装 Shopify CLI 时,Homebrew 会为您安装 Node.js、Ruby 和 Git。
brew tap shopify/shopify
brew install shopify-cli
# 插件版本
shopify version # Current Shopify CLI version: 3.48.2
- MacOS安装Ruby,版本需在2.8以上
brew install ruby
ruby -v # 查看版本
# 使用 rvm 更新 Ruby 版本 |
curl -sSL https://get.rvm.io | bash -s stable --ruby # 安装 rvm
rvm install ruby --latest # 关闭终端并重新打开安装
rvm list # 插件已经安装的列表
# 以上过程可能会出现 curl: (6) Could not resolve host: get.rvm.io,可能致因,解决办法如下:
curl -L https://get.rvm.io --noproxy | bash -s stable --ruby --autolibs=enable --auto-dotfiles
curl -L https://get.rvm.io | bash -s stable --ruby --autolibs=enable --auto-dotfiles
# 使用 rbenv 更新 Ruby 版本
brew install rbenv
rbenv install -l # 在尝试安装 Ruby 之前,请检查您的构建环境是否具有必要的工具和库
rbenv install -L # 在尝试安装 Ruby 之前,请检查您的构建环境是否具有必要的工具和库
rbenv install --list # 查看可用版本
rbenv install 3.1.2 # 安装指定版本
# rbenv 中的 Ruby 版本有三个不同的作用域:全局(global),本地(local),当前终端(shell),查找版本的优先级是 当前终端 > 本地 > 全局。
rbenv global 3.1.2 # 设置全局版本,全局版本是在没有找到"当前终端"或"本地"作用域的设置时执行。
rbenv local 3.1.2 # 设置本地版本,针对各个项目,通过项目文件夹中的 .rbenv-version 这个文件进行管理
rbenv shell 3.1.2 # 设置当前终端版本
rbenv global system # 恢复使用系统自带Ruby版本
rbenv versions # 获取你已安装的所有版本的列表
rbenv version
# 问题一:rbenv version与ruby -v的版本号不一致?
> /Users/apple/.rbenv/shims | ~/.zshrc | ~/.bash_profile
# 解决方案:将以下代码加入到vim ~/.zshrc文件中
export PATH="/Users/自己文件夹名称/.rbenv/shims:$PATH"
export PATH="/Users/自己文件夹名称/.rbenv/bin:$PATH"
# Mac本地路径
export PATH="/Users/apple/.rbenv/shims:$PATH"
export PATH="/Users/apple/.rbenv/bin:$PATH"
# 执行环境变量
echo $PATH
# 查看版本,即可成功 ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc) [x86_64-linux]
ruby -v
Linux安装配置
# 基础依赖库
> sudo yum install curl
> sudo yum group install "Development Tools"
> sudo yum install ruby
> sudo yum install ruby-devel
> sudo yum install git
# 安装 @shopify/cli @shopify/theme
> npm install -g @shopify/cli @shopify/theme
/root/.local/share/pnpm/global/5:
+ @shopify/cli 3.50.0
+ @shopify/theme 3.50.0
win安装配置
# 安装node.js
> https://nodejs.org
# 安装nvm
> https://github.com/coreybutler/nvm-windows/releases
# 安装Git
> https://github.com/git/git/releases
ssh-keygen -t rsa -C "zhangxiaobin211@gmail.com"
# 安装7-Zip
> https://www.7-zip.org/
# 安装Ruby版本管理器rbenv-for-windows
> https://github.com/ccmywish/rbenv-for-windows
# 安装@shopify/cli
> npm install -g @shopify/cli @shopify/theme
# 测试
> shopify theme dev
# 问题一: An error occurred while installing wdm (0.1.1), and Bundler cannot continue.
> 因为 wdm 需要编译原生扩展,但缺少必要的编译工具。
Shopify CLI 命令
> shopify version # 查看版本
> shopify login # 登录shopify
> shopify upgrade # 更新依赖项
> shopify logs # 显示详细日志
# 升级最新版本
npm install -g @shopify/cli@latest
Shopify CLI 应用命令
# 应用或应用扩展
# 在运行与应用程序相关的 CLI 命令时设置默认配置
# https://shopify.dev/docs/api/shopify-cli/app/app-config-use
> npm run shopify app config use --verbose
Themes模板系统
Shopify主题是使用Shopify的主题模板语言Liquid以及HTML,CSS,JavaScript和JSON构建,主题控制商家在线商店的组织、功能和风格。主题代码使用特定于 Shopify 主题的文件的标准目录结构以及图像、样式表和脚本等支持资产进行组织。
- Theme文档:https://shopify.dev/docs/storefronts/themes
- 主题模板列表:https://themes.shopify.com/
- Shopify 主题:https://shopify.dev/themes
- Shopify帮助中心:https://help.shopify.com/zh-CN
- 模板文档:https://help.shopify.com/zh-CN/manual/online-store/themes

shopify CLI 主题命令
自定义主题
二次开发已经创建存在的商家主题模板。
# 创建主题目录,进入目录
> cd feierfitness_shopify
# 注意,如何查看要连接的商店store-name
> 登录shopify Partner: https://admin.shopify.com/store/sharksportsonline/
> 其中,https://admin.shopify.com/store/后面的名称sharksportsonline即为商店store-name
# 连接到商店
shopify theme list --store {store-name}
# 获得商家商店的访问权限,运行shopify theme list或shopify theme pull,会出现一下信息,然后Enter即可
To run this command, log in to Shopify Partners.
👉 Press any key to open the login page on your browser
# 然后会自动打开网址,类似于https://accounts.shopify.com/select?rid=72f6af07-44bf-4628-8524-3522200de6ce,然后选择一个账户登录,出现以下即可。
✔ Logged in.
# 列出商店中的模版及其 ID 和状态。
> shopify theme list
----------------------------------------------------------------------------------
name role id
----------------------------------------------------------------------------------
Refresh [live] #134670680285
Dawn [unpublished] #130184446173
Refresh-ysungod@163.com [unpublished] #136840085725
Dawn 的更新版副本 [unpublished] #134013321437
----------------------------------------------------------------------------------
# 步骤一: 从 Shopify 检索主题文件
> shopify theme pull
# 步骤二: 选择yes
It doesn’t seem like you’re running this command in a theme directory.
? Are you sure you want to proceed? (You chose: yes)
# 步骤三: 选择需要迭代更新的模板,选好后,按Enter键确认即可开始下载模板
? Select a theme to pull from feierfitness.myshopify.com (Choose with ↑ ↓ ⏎, filter with 'f', enter option with 'e')
1. Refresh-Lastest [live]
2. Eshen-Test-Locale [unpublished]
3. Refresh [unpublished]
4. copy-2023-02-11 [unpublished]
5. Dawn 的更新版副本 [unpublished]
6. Dawn—20220719 [unpublished]
7. Updated copy of Dawn [unpublished]
8. Ride 的副本 [unpublished]
9. Ride [unpublished]
> 10. Refresh-Lastest-2023-10-11 [unpublished]
11. Dawn [unpublished]
# 主题检查
> shopify theme check
# 本地开发实时预览,运行该指令,出现本地预览地址
> shopify theme dev
# http://127.0.0.1:9292
# http://localhost:9292/
# 主题分发
# 将您的本地模板文件上传到 Shopify,覆盖远程版本(如果指定),如果您不想使用更改更新商店中的现有模板,则可以使用 --unpublish 标志将模板作为新的未发布模板上传到模板库。
# 通过将主题目录捆绑为 ZIP 文件,然后商家可以在其 Shopify 后台中上传该文件。
> shopify theme push
# 发布更新的主题(谨慎)
> shopify theme publish
# 将本地账户连接注销,常用于切换账户登录
> shopify auth logout
# 查看本地当前商店信息
> shopify theme info
------------------------------------------------------
THEME CONFIGURATION
------------------------------------------------------
Store feierfitness.myshopify.com
Development Theme ID #137068282077
------------------------------------------------------
TOOLING AND SYSTEM
------------------------------------------------------
Shopify CLI 3.48.2 💡 Version 3.49.7 available!
OS darwin-amd64
Shell /bin/zsh
Node version v18.16.0
Ruby version 3.1.2
------------------------------------------------------
Client network socket disconnected before secure TLS connection was established
# ############################# #
# 连接不上的问题 TLS
╭─ error ──────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ request to │
│ https://ysungod.myshopify.com/admin/api/unstable/themes.json?fields=id%2Cname%2Crole%2Cprocessing │
│ failed, reason: Client network socket disconnected before secure TLS connection was established │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯
# ############################# #
> cd /usr/local/Cellar/shopify-cli/3.56.2/libexec/lib/node_modules/@shopify/cli/node_modules/@shopify/cli-kit
# 在当前目录中安装 https-proxy-agent
> npm install https-proxy-agent
# 修改 dist/public/node/http.js 文件中的fetch 函数
- dist/public/node/http.js
import { HttpsProxyAgent } from "https-proxy-agent";
export async function fetch(url, init) {
// 可以保留 console.log("fetching: Proxy"); 以确认你修改的路径是否正确
console.log("fetching: Proxy");
// 可以用变量,也可以直接写代理地址
// const proxy_url = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
const proxy_url = "http://127.0.0.1:10010";
const agent = new HttpsProxyAgent(proxy_url);
return runWithTimer("cmd_all_timing_network_ms")(() =>
debugLogResponseInfo({
url: url.toString(),
request: nodeFetch(url, { ...init, agent }),
})
);
}
Shopify GitHub集成
Shopify GitHub 集成允许您使用 Shopify 登录名连接 GitHub 帐户或组织。此连接可帮助您进行和跟踪对在线商店模板代码的更改。它还可以帮助您与其他开发人员协作并实时共享进度。
- Shopify GitHub集成: https://shopify.dev/docs/themes/tools/github/getting-started
Shopify GitHub 集成使您能够执行以下任务
从与您的 GitHub 帐户关联的任何组织或仓库自动提取和推送主题代码
连接存储库中的一个或多个分支,轻松开发和测试新的主题功能或活动
通过提交到分支使模版保持最新,并跟踪在 Shopify 后台(包括代码编辑器和模版编辑器)中进行的编辑
将分支连接到未发布或已发布的主题
Shopify for GitHub
使用 Shopify for GitHub 更快地构建主题和自定义店面。连接任何存储库并推送代码以自动部署更改。使用分支构建和预览新功能和营销活动,而不会影响您的实时店面。
步骤一
步骤二
步骤三
步骤四
可能出现的异常错误
取消链接,重新建立新链接。

辅助工具
- Chrome插件Shopify 模版检查器: https://chrome.google.com/webstore/detail/shopify-theme-inspector-f/fndnankcflemoafdeboboehphmiijkgp
主题检查
主题检查是主题和主题应用扩展中的 Liquid 和 JSON 的 linter。它检测错误并强制执行 Shopify 主题和液体最佳实践。
- 在Visual Studio Code安装Shopify Liquid插件
- 检查参考: https://shopify.dev/docs/themes/tools/theme-check/checks
创建主题(新主题模板)
Shopify CDN
Shopify 服务器 全局资源
即存储在Shopify 服务器的公共资源,例如一些公共库地址,使用Shopify部署在CDN上的公共资源,可以节省资源带宽。
{% # global_asset_url %}
{{ 'lightbox.js' | global_asset_url | script_tag }}
{{ 'lightbox.css' | global_asset_url | stylesheet_tag }}
{% # shopify_asset_url %}
{{ 'option_selection.js' | shopify_asset_url }}
Shopify 后台 Files管理
{% # 引用图片,可以指定大小 %}
{{ 'red.jpg' | file_img_url }}
{{ 'red.jpg' | file_img_url: 'large' }}
{{ 'disclaimer.pdf' | file_url }}
- size 参数允许您指定图像的尺寸,最大为 5760 x 5760 像素。可以仅指定宽度和/或仅指定高度,还可以使用以下命名大小
----------------------------------------------------------------------------------
名字 尺寸
----------------------------------------------------------------------------------
pico 16x16 px
icon 32x32 px
thumb 50x50 px
small 100x100 px
compact 160x160 px
medium 240x240 px
large 480x480 px
grande 600x600 px
original 默认为图片原始尺寸
master 默认为图片原始尺寸
----------------------------------------------------------------------------------
文件资源的引用与路径
- Shopify服务器CDN公共资源的路径
{% # %}
- 图片
{% # %}
- CSS样式表
{% # %}
- javascript脚本
{% # %}
- 其他文件
{% # %}
主题性能最佳实践
- SHOPIFY分析器: https://analyze.speedboostr.com/
- 店铺速度: https://help.shopify.com/en/manual/online-store/store-speed
- 提升店铺速度: https://help.shopify.com/zh-CN/manual/online-store/web-performance/improving-web-performance
从Shopify数据库获取店铺数据
Theme目录结构工程
不支持列出的子目录以外的子目录。
- sections groups目录中文件添加到template目录
- sections目录中文件添加到sections groups或template
- snippets目录中文件添加到sections
├─ assets # 该目录包含主题中使用的所有资产,包括图像、CSS 和 JavaScript 文件。
├─ theme.png
├─ theme.css
└─ theme.js
├─ config #
├─ settings_data.json # 控制主题编辑器的主题设置区域的组织和内容。
└─ settings_schema.json # 包含从 中的设置中保存的值settings_schema.json。
├─ layout # 使用布局文件托管重复的主题元素,如页眉和页脚。
├─ theme.liquid
└─ theme.json
├─ locales #
├─ sections # 可以添加到 JSON 模板和版块组中的可重复使用、可自定义的内容模块。
├─ snippets # 可重用、可自定义的[代码片段],可以添加到分区,并删除和重新排序。
└─ templates #
├─ 404.json
├─ article.json
├─ article.liquid
├─ blog.json
├─ cart.json
├─ collection.json
├─ list-collections.json
├─ password.json
├─ product.json
├─ ...
└─ customers
├─ account
├─ activate_account
├─ addresses
├─ login
├─ lorder
└─ ...
assets与资源
静态资源,包括图片、样式与脚本等,可以应用于layout、template、section等文件中的CSS 和 JavaScript 定义,最终托管在 Shopify CDN 上。
- layout/theme.liquid 引用示例
asset_url: 主题目录assets路径。
CSS样式与动态类名
<link rel="stylesheet" href="{{ 'search.css' | asset_url }}">
{% # 引用 assets目录中静态资源 .css %}
{{ 'theme.css' | asset_url | stylesheet_tag }}
{% # CSS样式表 %}
<style type="text/css">
Element {}
.button__cell { background: {{ shop.email_accent_color }}; }
</style>
{% # CSS样式表 %}
<style type="text/css">
.{{ section.id }} {
position: relative;
}
</style>
{% # CSS样式表 每个section只能有一个标记。有多个将导致错误。仅当您的版块或应用区块要安装在多个主题或商店中时,您才需要使用这些标记。 %}
{% stylesheet %}
Element {}
{% endstylesheet %}
多端适配方案
/** 默认移动端 */
/** 非移动端 */
@media screen and (min-width: 990px) {
}
<h1 class="{{ section.id }}">你好</h1>
CSS命名
因为shopify theme不能使用LESS或SASS等动态CSS语言,导致编写样式比较冗杂,因此,有效命名机制对于编写类型与维护来说相当便捷。
- 一级:[文件夹]-[缩写]-标签
- 二级:[文件夹]-[缩写]-[见CSS最佳实践]-标签
<section class="section-activity-current-layout">
<h3 class="sections-sacl-h3">Limited time sales activities</h3>
</section>
<style type="text/css">
/** 默认移动端 */
.section-activity-current-layout {
position: relative;
}
.sections-sacl-h3 {
}
/** 非移动端 */
@media screen and (min-width: 990px) {
.section-activity-current-layout {
}
}
</style>
javascript脚本
<script src="{{ 'grid-image-viewer.js' | asset_url }}"></script>
<script src="{{ 'grid-image-viewer.js' | asset_url }}" async></script>
<script src="{{ 'grid-image-viewer.js' | asset_url }}" defer></script>
{% # 引用 assets目录中静态资源 .js %}
{{ 'jquery.min.js' | asset_url | script_tag }}
{% # javascript脚本 %}
<script type="text/javascript">
//
</script>
{% # javascript脚本 每个section只能有一个标记。有多个将导致错误。仅当您的版块或应用区块要安装在多个主题或商店中时,您才需要使用这些标记。 %}
{% javascript %}
function name {}
{% endjavascript %}
javascript脚本使用变量
{% # 代码 ID %}
{% capture googleID %}
{% if googole_target_id != blank %}
{{ googole_target_id }}
{% else %}
"G-DF03C49CM9"
{% endif %}
{% endcapture %}
{% # 使用变量 %}
console.info('BUDUG: ', Date.now(), {{ googleID }});
HTML 过滤器
- asset_url:返回主题 assets 目录中文件的 CDN URL。
- asset_img_url: 返回主题目录assets中图像 的CDN URL。
- global_asset_url:
{% # 返回主题的 assets目录 中文件的 CDN URL %}
{{ 'cart.js' | asset_url }}
{% # 脚本 %}
{{ 'cart.js' | asset_url | script_tag }}
{% # 样式表 %}
{{ 'base.css' | asset_url | stylesheet_tag }}
{% # 预加载标记 %}
{{ 'cart.js' | asset_url | preload_tag: as: 'script' }}
{{ 'cart.js' | asset_url | preload_tag: as: 'script', type: 'text/javascript' }}
{% # 图像 返回 assets目录中图像的 CDN URL %}
{{ 'red-and-black-bramble-berries.jpg' | asset_img_url }}
{{ 'red-and-black-bramble-berries.jpg' | asset_img_url: 'large' }}
托管文件 过滤器
托管文件筛选器返回Shopify 后台上传存储的图像资源的 URL。
- file_img_url:Shopify 后台上传存储的图像资源
- file_url:Shopify 后台上传存储的资源
{% # 返回Shopify CDN文件库中的图像 URL,可以指定大小 %}
{% # 内容-文件库 https://admin.shopify.com/store/ysunlight/content/files?selectedView=all %}
{{ 'potions-header.png' | file_img_url }}
{{ 'potions-header.png' | file_img_url: 'large' }}
{% # %}
{{ 'disclaimer.pdf' | file_url }}
Shopify托管服务器上的公共资源
- Shopify 服务器 CDN URL:https://shopify.dev/docs/api/liquid/filters/asset_img_url
{% # 返回全局资产的 CDN URL。 %}
{{ 'lightbox.js' | global_asset_url | script_tag }}
{{ 'lightbox.css' | global_asset_url | stylesheet_tag }}
{% # 返回全球可访问的 Shopify 资产的 CDN URL。 %}
{{ 'option_selection.js' | shopify_asset_url }}
config 基础配置
配置文件在主题编辑器的主题设置区域中定义设置,并存储它们的值。
settings_data.json
- 功能:此设置旨在使用户能够在不编辑模板代码的情况下自定义其店面的外观。
- 局限性: 文件大小不能超过 1.5MB。
- 约束: 作为模版开发者,您不应添加此设置,也不应在设置此设置后编辑此设置的值。相反,您应该使用专用的 CSS 资产和样式表 Liquid 标签,并使用主题设置在这些区域引入 CSS 的自定义选项。
{
// 包含当前保存在主题编辑器中的所有设置值。
"current": {
"favicon": "shopify://shop_images/yosuda-logo.png",
"sections": {
"block_order": ["announcement-bar-0"],
"header": {
"type": "header",
"settings": {
"menu": "home-menu",
"menu_type_desktop": "dropdown",
}
},
"settings": {
"color_scheme": "inverse",
"newsletter_enable": false
}
}
},
// 包含每个主题样式的对象。
"presets": {
"Default": {
"colors_solid_button_labels": "#ffffff",
"colors_accent_1": "#4770db"
}
}
}
settings_schema.json
- 此文件中的设置转换为全局主题设置,可以通过 Liquid语法中settings对象访问。
[
{
"name": "theme_info",
// 主题的名称
"theme_name": "Refresh",
// 主题的作者
"theme_author": "Shopify",
// 主题的版本号
"theme_version": "1.0.0",
"theme_documentation_url": "https://help.shopify.com/manual/online-store/themes",
"theme_support_email": "",
"theme_support_url": "https://support.shopify.com/"
},
{
"name": "t:settings_schema.colors.name",
"settings": [
{
"type": "header",
"content": "t:settings_schema.colors.settings.header__1.content"
},
{
"type": "color",
"id": "colors_solid_button_labels",
"default": "#FFFFFF",
"label": "t:settings_schema.colors.settings.colors_solid_button_labels.label",
"info": "t:settings_schema.colors.settings.colors_solid_button_labels.info"
}
]
}
]
- Liquid对象访问
允许您从 settings_schema.json 文件访问主题的所有设置。
{% if settings.favicon != blank %}
<link rel="icon" type="image/png" href="{{ settings.favicon | image_url: width: 32, height: 32 }}">
{% endif %}
layout(布局)
布局是 Liquid 文件,使您能够在一个位置包含应在多个页面类型上重复的内容。例如,布局是包含元素中可能需要的任何内容的好地方,以及页眉和页脚的节组。
- 此文件夹中必须存在文件,才能将模版上传到 Shopify。
theme.liquid
<!doctype html>
<html>
<head>
...
{% # 它将 Shopify 所需的所有脚本动态加载到文档头中。reCAPTCHA、Shopify 应用等功能需要这些脚本。 %}
{{ content_for_header }}
...
</head>
<body>
...
{% # 动态输出每个模板的内容 %}
{{ content_for_layout }}
...
</body>
</html>
locales(区域设置)
templates(模板)
主题模板支持JSON 和 Liquid两种文件类型,可以在主题中使用两种不同的模板文件类型。如果要在模板中使用sections功能,则应使用 JSON 模板。
- 在线商店中的每个页面类型都有一个关联的模板类型。
- 对于要呈现的任何页面类型,必须具有匹配的模板。
- 您可以使用模板添加对页面类型有意义的功能。例如,您可以向产品模板添加额外的产品推荐,或向文章模板添加评论表单。
全局变量
{{ template }}
{{ template.name }} {{ template.directory }} {{ template.suffix }}
功能特征
- 在线商店的整体布局由模板控制。
- 您的模板使用模板来指定您的商店中显示的内容类型。
- 您的主题中最多可以有 1000 个 JSON 模板,涵盖所有模板类型。
- 每个模板最多可包含 25 个分区,并且每个分区最多可包含 50 个块。
模板结构
在线商店功能
分区和块
模板:模板由一组分区构成,这些分区经配置可为在线商店提供一致的外观。每个类别最多可以包含 50 个模板。
页面
JSON 模板: JSON 模板仅接受具有固定架构和接受属性列表的 JSON。
主题模板目录
├─ 404
├─ article
├─ blog
├─ cart # 购物车
├─ collection # 产品系列
├─ customers # 客户
│ ├─ account
│ ├─ activate_account
│ ├─ addresses
│ ├─ login
│ ├─ order
│ ├─ register
│ └─ reset_password
├─ gift_card.liquid
├─ index
├─ list-collections # 产品系列列表页,其中列出了存储区的所有产品系列 /collections
├─ page # 呈现商店的页面,例如“关于我们”和“联系我们”。
├─ password
├─ product # 产品
├─ robots.txt.liquid # robots.txt文件配置 robots.txt 文件位于 Shopify 商店的主域名的根目录
│ ├─ https://help.shopify.com/zh-CN/manual/promoting-marketing/seo/editing-robots-txt
│ ├─ https://help.shopify.com/zh-CN/manual/promoting-marketing/seo/hide-a-page-from-search-engines
│ └─ https://help.shopify.com/zh-CN/manual/promoting-marketing/seo/seo-overview
├─ search
└─ metaobject
模板嵌套分区
Liquid模板
Liquid 模板没有固定的架构。Liquid 模板可以访问任何全局 Liquid 对象,以及与模板关联的对象。
{% comment %}
{% endcomment %}
{% # CSS样式表 %}
<style type="text/css">
/** 多端复用样式 */
/** 默认移动端 320px */
@media only screen and (min-width: 375px) {
/**/
}
@media only screen and (min-width: 480px) {
/**/
}
@media only screen and (min-width: 640px) {
/**/
}
@media only screen and (min-width: 750px) {
/**/
}
/** 非移动端,有些模板遗留问题,所以是990px尺寸界限来适配非移动端 */
@media screen and (min-width: 990px) {
/**/
}
@media screen and (min-width: 1180px) {
/**/
}
@media screen and (min-width: 1366px) {
/**/
}
@media screen and (min-width: 1440px) {
/**/
}
@media screen and (min-width: 1680px) {
/**/
}
/*需要兼顾适配大屏的场景*/
@media screen and (min-width: 1920px) {
/**/
}
</style>
<section class="section-name">
</section>
{% # javascript脚本 %}
<script type="text/javascript">
</script>
{% # 自定义数据属性 %}
{% schema %}
{
"settings": []
}
{% endschema %}
JSON模板
JSON 模板仅接受具有固定架构和接受属性列表的 JSON 文件。需要在template模板中使用sections时,则需要使用JSON模板。JSON 模板为商家提供了更大的灵活性来添加、删除和重新排序sections。
JSON模板:https://shopify.dev/docs/themes/architecture/templates/json-templates
JSON 模板是存储要呈现的部分列表及其关联设置的数据文件。
JSON 模板最多可以呈现 25 个section,每个section最多可以有 50 个snippets。
一个主题最多可以包含 1,000 个 JSON 模板。达到限制后,无法创建新的 JSON 模板。
无法在 JSON theme templates 之间共享section数据,因此每个section都必须具有在模板中唯一的 ID。
JSON模板结构
{
"layout": "", // <String | Boolean>,[选填],呈现模板时要使用的布局的文件名。
"wrapper": "div#div_id.div_class[attribute-one=value]", // String,[选填],可以使用<div>、<main>、<section>
// 数据结构与settings_data.json中sections相同
"sections": { // Object,<必填>,
"<sectionID>": {
"id": "", // {String},[选填],不允许在模板中使用重复的 ID
"type": "<SectionType>", // {String},<必填>,需要导入的sections目录中的文件名称,省略.liquid
"disabled": false, // 是否隐藏,隐藏后不会呈现
"settings": {
"<SettingID>": "<SettingValue>", //
},
"blocks": {
// 块的唯一 ID。仅接受字母数字字符。
"<BlockID>": {
"type": "<BlockType>", // <必填>,要呈现的块的类型,
"settings": {
"<SettingID>": "<SettingValue>"
}
},
},
"block_order": [] // {Array<string>},块 ID 数组,按应呈现的方式排序。对象中必须存在 ID,并且不允许使用重复的 ID。
}
},
"order": [] // {Array<string>},<必填>,section ID 的数组,按其呈现顺序列出。ID 必须存在于对象中。不允许重复。
}
- example.json示例
{
"sections": {
"debug-test": {
"type": "debug-test"
}
}
}
- example.json输出结果
<div id="shopify-section-template--17571246276866__debug-test" class="shopify-section"><div>Hello</div>
</div>
- example.json示例二
{
"wrapper": "div#div_id.div_class[attribute-one=value]",
"sections": {
"main": {
"type": "product"
}
},
"order": [
"main"
]
}
- example.json输出结果
<div id="div_id" class="div_class" attribute-one="value">
<!-- product.json sections -->
</div>
- example.json示例三
- example.json输出结果
- example.json示例四
- example.json输出结果
sections(分区)
在线商店中的每个页面类型都有一个关联的模板类型。您可以使用模板添加对页面类型有意义的功能。 模板使用分区来创建所需的布局。大多数分区由块组成,块用于提供特定功能,例如标头、文本、单张图片、拼贴图片或链接。 在模板中使用分区和块可为排列商店内容提供更大的灵活性,使您无需编辑代码即可控制在线商店的外观。每个模板最多可有 25 个sections。
严重警告
sections文件夹中的文件不能互相嵌套,或者引用。
- sections文件夹目录
├─ footer.liquid
├─ header.liquid
├─
├─
├─
├─
├─
├─
├─
├─
└─
- 代码示例
{% # 每个section只能有一个标记。有多个将导致错误。 %}
{% stylesheet %}
.slideshow-wrapper {
// your styles
}
{% endstylesheet %}
{% # 每个section只能有一个javascript标记。有多个将导致错误。 %}
{% javascript %}
document.querySelector('.slideshow').slideshow();
{% endjavascript %}
- 分区嵌套块
Section schema
定义section的各种属性。每个section只能有一个标记,该标记必须仅包含使用内容中列出的属性的有效 JSON。该标签可以放置在节文件中的任何位置,但不能嵌套在另一个 Liquid 标签中。
- 允许商家使用对象自定义版块,通过Liquid语法中section对象来访问。
严重警告
拥有多个标签,或将其放置在另一个 Liquid 标签中,将导致错误。
Section schema: https://shopify.dev/docs/themes/architecture/sections/section-schema
schema属性
{% schema %}
{
name: , {% # %}
tag: , {% # %}
class: , {% # %}
limit: , {% # 限制该section分区可以添加到模板或分区组的次数 %}
settings: [ {% # %}
{
"type": "checkbox",
"id": "show_announcement", {% # section.settings.show_announcement %}
"label": "Show announcement",
"default": true
}
],
blocks: [ {% # %}
{
"type": "",
"name": "",
"limit": "",
"settings": {}
}
],
max_blocks: , {% # 每个section限制为 最大数50 个块。 %}
presets: { {% # %}
},
default: , {% # %}
locales: { {% # 多语言,访问{{ 'sections.[name].title' | t }} %}
"en": {
"title": "Slideshow"
},
"fr": {
"title": "Diaporama"
}
},
"enabled_on": { {% # 将分区限制为特定模板和分区组 %}
},
"disabled_on": { {% # 防止在特定模板和分区组中使用分区 %}
}
}
{% endschema %}
- tag属性
该属性仅仅接受article、aside、div、footer、header、section。
- settings属性
- 动态源
{% schema %}
{
settings: [
{
"type": "text",
"id": "featured_product_title",
"label": "Featured product title",
"default": "Featuring: {{ product.title }}"
}
]
}
{% endschema %}
Liquid中section对象
- section.blocks
{% for block in section.blocks %}
{% case block.type %}
{% when 'slide' %}
<div class="slide" {{ block.shopify_attributes }}>
{{ block.settings.image | image_url: width: 2048 | image_tag }}
</div>
...
{% endcase %}
{% endfor %}
- section.settings
- 访问设置示例
// Settings
Message: {{ settings.message }}
// Section
Message: {{ section.settings.message }}
// Block
Message: {{ block.settings.message }}
{% schema %}
{
"type": "text",
"id": "message",
"label": "Message",
"default": "Hello!"
}
{% endschema %}
snippets(块)
该目录包含 Liquid 文件,这些文件托管较小的可重用代码片段,可以将要在主题中重复使用的 Liquid 和 HTML 存储在代码段中。
- 块是构成页面模板中的分区的可自定义模块。您可以使用块来添加文本、图片、链接、按钮等。
- 限制性:每个section最多可包含 50 个snippets。
重要通知
snippets目录中文件可以使用render 'name'嵌套snippets目录中文件,但是不能互相嵌套。
页面路由结构
在内部, Shopify 有自己的路由表,该路由表根据用户请求的 URL 确定显示哪个模板。
模板与页面URL映射关系
/thisisntarealurl → 404.liquid
/blogs/{blog-name}/{article-id-handle} → article.liquid
/blogs/{blog-name} → blog.liquid
/cart → cart.liquid
/collections → list-collections.liquid
/collections/{collection-handle} → collection.liquid
/collections/{collection-handle}/{tag} → collection.liquid
/ → index.liquid
/pages/{page-handle} → page.liquid
/products → list-collections.liquid
/products/{product-handle} → product.liquid
/search?q={search-term} → search.liquid
当前URL渲染哪个模板
您需要做的就是添加到您的文件并开始浏览您的商店。此全局 Shopify 变量将输出当前呈现的模板减去扩展名。
{{ template }} theme.liquid
<div style="position:fixed;top:0;left:0;z-index:10000;background:#00f;width:200px;padding:10px;">Current template: {{ template }}.liquid</div>
- 代码工程中"/template"中的所有页面,也是路由
- 域名/pages/...: "在线商店" -> "页面",即页面中的所有页面,就是访问路由
https://admin.shopify.com/store/sharksportsonline/pages
路由变量
- 路由对象
{
"account_addresses_url": "/account/addresses",
"account_login_url": "/account/login",
"account_logout_url": "/account/logout",
"account_recover_url": "/account/recover",
"account_register_url": "/account/register",
"account_url": "/account",
"all_products_collection_url": "/collections/all",
"cart_add_url": "/cart/add",
"cart_change_url": "/cart/change",
"cart_clear_url": "/cart/clear",
"cart_update_url": "/cart/update",
"cart_url": "/cart",
"collections_url": "/collections",
"predictive_search_url": "/search/suggest",
"product_recommendations_url": "/recommendations/products",
"root_url": "/",
"search_url": "/search"
}
- 具体引用示例
<a href="{{ routes.account_url }}">
{{ 'customer.account.return' | t }}
</a>
Admin后台管理与代码编辑
添加产品
添加产品可以选择该产品的UI模板。

添加产品系列
添加产品系列可以选择该产品系列的UI模板。

添加页面
添加页面可以选择该页面的UI模板。

添加博客文章
添加博客文章可以选择该博客文章的UI模板。

创建各类模板
创建各类模板可以为产品、产品系列、博客分类、博客文章等提供自定义的UI模板。

Dawn语言
Dawn 是一个超轻量级、移动优先的主题,它使用原子组件、最小的 JavaScript 和一组固执己见的功能。它最大限度地提高了商家的灵活性,同时将复杂性降至最低。
第三方插件集成
gtag.js集成
hso-hello-google-analytics.liquid
{% comment %} {% render 'hso-hello-google-analytics' ga_ids: "" ga_ids: [] gtagId: "" %} {% endcomment %}
{% comment %} {% assign ga_ids = 'AA-787676,BB-JSHFJHJ' | split: ',' %}
{% render 'hso-hello-google-analytics' ga_ids: ga_ids %} {% endcomment %}
{% # 代码 ID %}
{% liquid
assign init_ga_ids = 'G-H15VBDMTBJ'
if ga_ids != blank and ga_ids != empty
assign init_ga_ids = ga_ids
endif
# 如果init_ga_ids为字符串,则转化为数组赋值给ga_ids_list
assign ga_ids_list = init_ga_ids
if init_ga_ids[0] == blank or init_ga_ids[0] == empty
assign ga_ids_list = init_ga_ids | split: ' '
endif
# 如果不传gtagId,则默认取ga_ids_list数组第一个值
assign scriptID = gtagId
if gtagId == blank or gtagId == empty
assign scriptID = ga_ids_list[0]
endif
%}
<!-- HGA app content start -->
<!-- Global site tag (gtag.js) - Google Analytics -->
{% comment %} <script async src="https://www.googletagmanager.com/gtag/js?id={{ scriptID }}"></script> {% endcomment %}
<script>
var scriptLink = "https://www.googletagmanager.com/gtag/js?id={{ scriptID }}";
window.addEventListener("load", function () {
window.setTimeout(function() {
doLoadScript(scriptLink, { async: true });
}, 500);
});
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
{% for ga_id in ga_ids_list %}
// console.info('日志 config: ', `{{ ga_id }}`);
gtag('config', `{{ ga_id }}`);
{% endfor %}
</script>
<!-- HGA app content end -->
代码兼容后台自定义编辑
开发者在实现业务代码的过程中,功能模块可以在后台主题编辑器中自定义,这样方便非开发者在基于开发者实现的功能模块上进行自定义。

Section schema可配置项
文档:https://shopify.dev/docs/storefronts/themes/architecture/sections/section-schema
name:主题编辑器中显示的节标题。
tag:标记,当前板块所在的HTML元素容器,默认包装在
<div>
容器中。可选:article、aside、div、footer、header、section。class:类,会包装在一个类为.的容器中
limit:限制添加次数,默认情况下,可以将部分添加到模板或部分组的次数没有限制。
settings:可以创建特定版块的设置,以允许商家使用该对象自定义版块。每个分区中的所有分区设置 ID 必须是唯一的。在某个部分中具有重复的 ID 将导致错误。
blocks:块是可重复使用的内容模块,可以在部分内添加、删除和重新排序。
max_blocks:
presets:
default:
locales:
enabled_on:
disabled_on:
{% schema %}
{
"name": "可配置项",
"settings": [],
}
{% endschema %}
blocks语法(太重要)
您可以为section创建块。块是可重复使用的内容模块,可以在部分内添加、删除和重新排序。
语法
-------------------------------------------------------------------------------------------
属性 描述 必填
-------------------------------------------------------------------------------------------
type 块类型。这是一个自由格式的字符串,可用作标识符。您可以通过块对象的属性访问此值。type 是的
name 块名称,将在主题编辑器中显示为块标题。 是的
limit 可以使用的此类块数。 不
settings 您希望块的任何输入或侧边栏设置。某些设置可能用作主题编辑器中块的标题。 不
-------------------------------------------------------------------------------------------
代码示例
<section>
{%- for block in section.blocks -%}
{%- liquid
assign product = block.settings.product
-%}
<p>{{ product.title }}</p>
{% endfor %}
</section>
{% schema %}
{
"name": "可配置项",
"blocks": [
{
"type": "actives_product",
"name": "产品",
"settings": [
{
"type": "product",
"id": "product",
"label": "产品"
},
{
"type": "image_picker",
"id": "image",
"label": "折扣图片"
}
]
}
]
}
{% endschema %}
标准属性
即在sections/xxx.liquid中通过
{% schema %}{% endschema %}
进行设置的配置。
-------------------------------------------------------------------------------------------
属性 描述 必填
-------------------------------------------------------------------------------------------
type 设置类型,可以是任何基本或专用输入设置类型。 是的
id 用于访问设置值的设置 ID。 是的
label 设置标签,将显示在主题编辑器中。 是的
default 设置的默认值。 不
info 有关设置的信息性文本的选项。 不
-------------------------------------------------------------------------------------------
type:基本类型
- checkbox
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "checkbox",
"id": "",
"label": ""
}
]
}
{% endschema %}
- number
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "number",
"id": "",
"label": ""
}
]
}
{% endschema %}
- radio
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "radio",
"id": "",
"label": ""
}
]
}
{% endschema %}
- range
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "range",
"id": "",
"label": ""
}
]
}
{% endschema %}
- select
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "select",
"id": "",
"label": ""
}
]
}
{% endschema %}
- text
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "text",
"id": "",
"label": ""
}
]
}
{% endschema %}
- textarea
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "textarea",
"id": "",
"label": ""
}
]
}
{% endschema %}
type:专用类型
- article
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "article",
"id": "",
"label": ""
}
]
}
{% endschema %}
- blog
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "blog",
"id": "",
"label": ""
}
]
}
{% endschema %}
- collection
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "collection",
"id": "",
"label": ""
}
]
}
{% endschema %}
- collection_list
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "collection_list",
"id": "",
"label": ""
}
]
}
{% endschema %}
- color
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "color",
"id": "",
"label": ""
}
]
}
{% endschema %}
- color_background
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "color_background",
"id": "",
"label": ""
}
]
}
{% endschema %}
- color_scheme
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "color_scheme",
"id": "",
"label": ""
}
]
}
{% endschema %}
- color_scheme_group
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "color_scheme_group",
"id": "",
"label": ""
}
]
}
{% endschema %}
- font_picker
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "font_picker",
"id": "",
"label": ""
}
]
}
{% endschema %}
- html
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "html",
"id": "",
"label": ""
}
]
}
{% endschema %}
- image_picker
<div class="section-debug">
<img
class="pc-img"
src="{{ section.settings.banner_image_pc | img_url: 'master' }}"
alt=""
>
<img
class="wap-img"
src="{{ section.settings.banner_image_wap | img_url: 'master' }}"
alt=""
>
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "image_picker",
"id": "banner_image_pc",
"label": "电脑端背景图"
},
{
"type": "image_picker",
"id": "banner_image_wap",
"label": "移动端背景图"
}
]
}
{% endschema %}
- inline_richtext
<h2>{{ section.settings.heading }}</h2>
{% schema %}
{
"name": "DEBUG调试模块",
"settings": [
{
"type": "inline_richtext",
"id": "heading",
"label": "标题",
"default": "Heading标题"
}
]
}
{% endschema %}
- link_list
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "link_list",
"id": "",
"label": ""
}
]
}
{% endschema %}
- liquid
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "liquid",
"id": "",
"label": ""
}
]
}
{% endschema %}
- metaobject
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "metaobject",
"id": "",
"label": ""
}
]
}
{% endschema %}
- metaobject_list
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "metaobject_list",
"id": "",
"label": ""
}
]
}
{% endschema %}
- page
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "page",
"id": "",
"label": ""
}
]
}
{% endschema %}
- product
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "product",
"id": "",
"label": ""
}
]
}
{% endschema %}
- product_list
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "product_list",
"id": "",
"label": ""
}
]
}
{% endschema %}
- richtext
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "richtext",
"id": "",
"label": ""
}
]
}
{% endschema %}
- text_alignment
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "text_alignment",
"id": "",
"label": ""
}
]
}
{% endschema %}
- url
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "url",
"id": "",
"label": ""
}
]
}
{% endschema %}
- video
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "video",
"id": "",
"label": ""
}
]
}
{% endschema %}
- video_url
<div class="section-debug">
{{ section.settings. }}
</div>
{% schema %}
{
"name": "配置示例",
"settings": [
{
"type": "video_url",
"id": "",
"label": ""
}
]
}
{% endschema %}
经典示例
templates/index.json
可以不用配置,当在后台配置的时候,会自动生成数据保存在这个文件中。
{
"sections": {
"test": {
"type": "test",
// 这是CMS后台自定义进入的视图编辑器中,页面位置的左侧显示的模块名称
"name": "类型模块名称", // 如果这里没有声明name,则取sections/test.liquid中schema的name值
"settings": {
"heading": "Save 20% Instantly: Mix & Match Bestsellers",
"subtitle": "Enjoy 20% off any combination of products across the store. Custom fitness bundles below RECOMMEND."
},
// 配置产品数据
"blocks": {
"actives_product_nFiiWd": {
"type": "actives_product",
"settings": {
"product": "fed-weighted-eco-friendly-dumbbells",
"image": "shopify://shop_images/user_centric.webp"
}
},
"actives_product_BzfHp8": {
"type": "actives_product",
"settings": {
"product": "lynx-5-in-1-adjustable-dumbbell-weight-set"
}
}
},
"block_order": [
"actives_product_nFiiWd",
"actives_product_BzfHp8"
]
}
}
}
sections/test.liquid
<h1>{{ section.settings.heading }}</h1>
<p>{{ section.settings.subtitle }}</p>
<section>
{%- for block in section.blocks -%}
{%- liquid
assign product = block.settings.product
-%}
<p>{{ product.title }}</p>
{% endfor %}
</section>
{% schema %}
{
"name": "后台CMS显示的模块名称", // 如果templates/index.json中的test没有声明name,则取sections/test.liquid中schema的name值
"settings": [
{
"type": "inline_richtext",
"id": "heading",
"label": "标题"
},
{
"type": "inline_richtext",
"id": "subtitle",
"label": "副标题"
}
],
"blocks": [
{
"type": "actives_product",
"name": "产品",
"settings": [
{
"type": "product",
"id": "product",
"label": "产品"
},
{
"type": "image_picker",
"id": "image",
"label": "折扣图片"
}
]
}
]
}
{% endschema %}
- schema 说明文档
{
"type": "inline_richtext", // 类型
"id": "inline", // ID,使用section.settings[id]调用
"default": "my <i>inline</i> <b>text</b>", // 默认值,在templates/模板中设置settings为空时显示
"label": "Inline rich text" // 标签,在CMS后台显示
}
Liquid模板语言
Liquid 模板接受标准的 HTML 和 Liquid,用于动态输出对象及其属性。Liquid 模板可以访问任何全局 Liquid 对象,以及与模板关联的对象。Liquid 模板语言是 Shopify 主题的支柱,用于在店面上加载动态内容。扩展 Liquid 对象以使用元字段存储和呈现自定义数据。
Liquid 模板语言用于构建Shopify主题。
Liquid 是一种基于 Ruby 的开源模板语言,被数千个其他项目用来混合静态 HTML 和动态 Liquid 标签。Liquid 基于 Ruby 的语法。
Shopify Liquid:https://shopify.dev/api/liquid
基础语法
{% assign name = "HELLO!" %}
{{ name }}
{% # 注释内容 %}
{% comment %}
注释内容
{% endcomment %}
创建变量
{% # 创建任何基本类型、对象或对象属性的变量 %}
{% assign variable_name = value %}
{{ variable_name }}
{% # 创建具有字符串值的新变量 %}
{% capture variable %}
value
{% endcapture %}
{%- assign up_title = product.title | upcase -%}
{%- assign down_title = product.title | downcase -%}
{%- assign show_up_title = true -%}
{%- capture title -%}
{% if show_up_title -%}
Upcase title: {{ up_title }}
{%- else -%}
Downcase title: {{ down_title }}
{%- endif %}
{%- endcapture %}
{{ title }}
{% # 递减: 创建一个默认值为 -1 的新变量,每次后续调用都会减少 1 %}
{% decrement variable_name %}
{% # 增加: 创建一个默认值为 0 的新变量,每次后续调用都会增加 1 %}
{% increment variable_name %}
liquid表达式
{% liquid
expression
%}
{% # 代码 ID %}
{% liquid
assign init_ga_ids = 'G-DF03C49CM9'
if ga_ids != blank and ga_ids != empty
assign init_ga_ids = ga_ids
endif
# 如果init_ga_ids为字符串,则转化为数组赋值给ga_ids_list
assign ga_ids_list = init_ga_ids
if init_ga_ids[0] == blank or init_ga_ids[0] == empty
assign ga_ids_list = init_ga_ids | split: ' '
endif
# 如果不传gtagId,则默认取ga_ids_list数组第一个值
assign scriptID = gtagId
if gtagId == blank or gtagId == empty
assign scriptID = ga_ids_list[0]
endif
%}
<!-- HGA app content start -->
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ scriptID }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
{% for ga_id in ga_ids_list %}
gtag('config', `{{ ga_id }}`);
{% endfor %}
</script>
<!-- HGA app content end -->
{% liquid
# Show a message that's customized to the product type
assign product_type = product.type | downcase
assign message = ''
case product_type
when 'health'
assign message = 'This is a health potion!'
when 'love'
assign message = 'This is a love potion!'
else
assign message = 'This is a potion!'
endcase
echo message
%}
页面主题标签
{% # 指定要使用的布局 %}
{% layout name %}
{% layout 'full-width' %}
{% layout none %}
{% # 呈现一个分区,即加载sections中的文件,一般在templates中使用 %}
{% section 'name' %}
render呈现代码段或应用块
{% # 导入snippets目录文件 %}
{% # 呈现代码段或应用块 要呈现的代码段的名称,不带扩展名。.liquid %}
{% render 'filename' %}
{% # 您可以使用该参数为数组中的每个项目呈现一个代码段。您还可以提供一个可选参数,以便能够在代码段内的迭代中引用当前项。 %}
{% render 'filename' for array as item %}
{% # 将变量传递给代码段,将变量指定为参数,以便将外部变量传递给代码段。 %}
{% render 'filename', variable: value %}
{% # 您可以使用该参数将单个对象传递给代码段。您还可以提供可选参数来指定自定义名称以引用代码段中的对象。如果不使用该参数指定自定义名称,则可以使用代码段文件名引用对象。 %}
{% render 'filename' with object as name %}
<div class="swiper-slide">
{% liquid
assign class_uuid = section.id | append: 'slide-1'
render 'product-card-pack-bundle' class_uuid: class_uuid
%}
</div>
数据类型
----------------------------------------------------------------------------------
type 类型 说明
----------------------------------------------------------------------------------
string 字符串 任何字符系列,用单引号或双引号括起来
number 数字 包括浮点数和整数
boolean 布尔
nil 未定义的值,返回的标记或输出不会向页面打印任何内容
array 数组
empty 空 已定义但没有值
----------------------------------------------------------------------------------
代码示例
{% unless pages.about-us == empty %}
...
{% endunless %}
{% if settings.featured_potions_title != blank %}
{{ settings.featured_potions_title }}
{% else %}
No value for this setting has been selected.
{% endif %}
{% unless pages.recipes == empty %}
{{ pages.recipes.content }}
{% else %}
No page with this handle exists.
{% endunless %}
检查变量类型
{% # 代码 ID %}
{% capture googleTargetID %}
{% if googole_target_id != blank %}
{{ googole_target_id }}
{% else %}
{% # 通用性ID %}
"G-DF03C49CM9"
{% endif %}
{% endcapture %}
{% assign variable_name = [0] %}
{% capture configList %}
{% # 判断是否为数组 %}
{% if googleTargetID.first != blank and googleTargetID.first != empty %}
{{ googleTargetID }}
{% # 判断是否为非数组: string nil %}
{% else %}
[{{ googleTargetID | split: '-' }}]
{% endif %}
{% endcapture %}
运算符
括号不是 Liquid 标签中的有效字符。
----------------------------------------------------------------------------------
运算符 功能
----------------------------------------------------------------------------------
== 等于
!= 不等于
> 大于
< 小于
>= 大于或等于
<= 小于或等于
or 或,有一个为true即可
and 和,所有同时为true即可
contains 检查字符串是否包含在字符串或数组中
----------------------------------------------------------------------------------
条件判断
{% # if...else %}
{% if product.tags contains 'healing' %}
...
{% elsif %}
...
{% else %}
...
{% endif %}
{% # unless 除非满足条件,否则要呈现的表达式。 %}
{% unless condition %}
expression
{% endunless %}
{% # case...when %}
{% case variable %}
{% when first_value %}
first_expression
{% when second_value %}
second_expression
{% else %}
third_expression
{% endcase %}
{% unless section.settings.hide_variants and variant_images contains media.src and forloop.index != 1 %}
HTML标记
HTML 标签使用 Shopify 特定的属性呈现 HTML 元素。
{% # 为数组中的每个项目生成 HTML 表行 %}
{% tablerow variable in array %}
expression
{% endtablerow %}
{% # 标页数 %}
{% paginate array by page_size %}
{% for item in array %}
forloop_content
{% endfor %}
{% endpaginate %}
迭代标记
{% assign numbers = '1,2,3,4,5' | split: ',' %}
{% # for...in %}
{% for item in numbers %}
{{ item }}
{% endfor %}
{% for variable in array limit: number %}
{{ item }}
{% endfor %}
{% for variable in array offset: number %}
{{ item }}
{% endfor %}
{% # 范围 %}
{% for variable in (number..number) %}
expression
{% endfor %}
{% for variable in array reversed %}
expression
{% endfor %}
{% # forloop %}
<div {% if forloop.index == 1 %} class="active" {% endif %}>
{% for variable in array %}
first_expression
{% else %}
second_expression
{% endfor %}
{% # break: 阻止 for 循环迭代 %}
{% for i in (1..5) -%}
{%- if i == 4 -%}
{% break %}
{%- else -%}
{{ i }}
{%- endif -%}
{%- endfor %}
{% # continue: 导致 for 循环跳到下一次迭代 %}
{% for i in (1..5) -%}
{%- if i == 4 -%}
{% continue %}
{%- else -%}
{{ i }}
{%- endif -%}
{%- endfor %}
{% #
循环一组字符串,并为 for 循环的每次迭代一次输出一个字符串
cycle 如果在同一模板中包含具有相同参数的多个标签,则每组标签将被视为同一组。
这意味着标签可以输出提供的任何字符串,而不是总是从第一个字符串开始。
为此,您可以为每个标签指定一个组名称。
%}
{% for i in (1..4) -%}
{% cycle 'one', 'two', 'three' %}
{%- endfor %}
过滤器
液体过滤器用于修改变量和对象的输出。要将过滤器应用于输出,请在输出的大括号分隔符内添加过滤器和任何过滤器参数,前面是管道字符 |。
{% # product.title -> Health potion %}
{{ product.title | upcase | remove: 'HEALTH' }}
{{ item.content | highlight: search.terms }}
{{ 'Shopify' | link_to: 'https://www.shopify.com', class: 'link-class' }}
{{ 'cart.js' | asset_url | preload_tag: as: 'script' }}
{{ 'cart.js' | asset_url | script_tag }}
Array 过滤器
{% # size 返回字符串或数组的大小 %}
{{ collection.title | size }}
{{ collection.products | size }}
{% if collection.products.size >= 10 %}
{% endif %}
{% liquid
# Liquid 代码块
%}
{%- assign variants_images = product.variants | map: 'featured_image' -%}
{%- for variant in product.variants -%}
{% assign variant_image = variant.featured_image | json %}
{% liquid
assign variant_image_exclude = ""
for item in variants_images
assign _item = item | json
if _item contains variant_image
continue
else
assign variant_image_exclude = _item
endif
endfor
%}
<section class="section-exhibition-power-layout">
<div class="section-exhibition-power-tab">
<div class="section-exhibition-power-tab-wrapper">
{%- for variant in product.variants -%}
{% if variant.title == 'Halo' %}
<div data-tab="left" name="{{ variant.title }}" class="com-flex h center active">{{ variant.title }}</div>
{%- endif -%}
{% if variant.title == 'NebulaX' %}
<div data-tab="right" name="{{ variant.title }}" class="com-flex h center">{{ variant.title }}</div>
{%- endif -%}
{%- endfor -%}
</div>
</div>
<div class="section-exhibition-power-content">
{%- assign variants_images = product.variants | map: 'featured_image' -%}
{%- for variant in product.variants -%}
{% assign variant_image = variant.featured_image | json %}
{% liquid
assign variant_image_exclude = ""
for item in variants_images
assign _item = item | json
if _item contains variant_image
continue
else
assign variant_image_exclude = _item | remove: "\"
endif
endfor
%}
<div class="section-exhibition-power-wrapper">
<grid-image-viewer>
<div class="grid-image-layout">
{%- for media in product.media -%}
{% assign image_url = media | image_url %}
{% if variant_image_exclude contains image_url %}
{% else %}
<div class="grid-image-wrapper">
<img
src="{{ media | image_url }}"
loading="lazy"
alt=""
/>
</div>
{% endif %}
{%- endfor -%}
</div>
</grid-image-viewer>
</div>
{%- endfor -%}
</div>
</section>
各种数组处理
<div class="swiper-slide">
{% liquid
assign products = "" | split: ""
for product in collections.all.products limit: 6
if forloop.index0 >= 1 and forloop.index0 <= 4
assign products_list = product | split: ""
assign products = products | concat: products_list
endif
if forloop.index0 >= 4
break
endif
endfor
assign class_uuid = section.id | append: 'slide-1'
render 'product-card-pack-bundle-free', class_uuid: class_uuid, products: products
%}
</div>
字符串 过滤器
{{ product_title | upcase }}
{% liquid
# Liquid 代码块
assign stringCtn = "字符串: "
assign lastCtn = stringCtn | append: "内容"
%}
本地化 过滤器
{% # 本地化语言 translate: string | t 返回string %}
{{ 'products.product.price.sale_price' | t }}
Math 过滤器
数学筛选器对数字执行数学运算。您可以将数学筛选器应用于数字、返回数字的变量或元字段。
{% # 加减乘除 %}
{{ 4 | divided_by: 2 }} {% # 除 %}
{{ 4 | minus: 2 }} {% # 减 %}
{{ 2 | plus: 2 }} {% # 加 %}
{{ 2 | times: 2 }} {% # 乘 %}
{% liquid
# Liquid 代码块
%}
Media 过滤器
{% # %}
{{ product | image_url: width: 200 | image_tag }}
Metafield 过滤器
{% # %}
货币 过滤器
{% # $10.00 %}
{{ product.price | money }}
{% # $10.00 CAD %}
{{ product.price | money_with_currency }}
{% # 10.00 %}
{{ product.price | money_without_currency }}
{% # $10 %}
{{ product.price | money_without_trailing_zeros }}
字体Fonts过滤器
{{ settings.type_header_font | font_face }}
{{ settings.type_header_font | font_face: font_display: 'swap' }}
{{ settings.type_header_font | font_url }}
{{ settings.type_header_font | font_url: 'woff' }}
引用对象数据
有些对象可以全局访问,有些只在某些上下文中可用。请参阅特定对象引用以查找其访问范围。
- 存储资源,例如集合或产品及其属性
- 用于支持 Shopify 主题的标准内容,例如content_for_header
- 可用于构建交互性的功能元素,例如paginate和search
<div class=”product-page”>
<div class=”product-image”>
{{ product.featured_image | image_url: width: 400 | image_tag }}
</div>
<div class=”product-title”>
{{ product.title }}
</div>
<div class=”product-price”>
{{ product.price | money }}
</div>
</div>
全局对象
可以直接在任何 Liquid 主题文件中访问该对象,不包括 checkout.liquid 和 Liquid 资产文件。
{% # 页面标题 %}
{{ page_title }}
{% # 获取每个国家与其子区域数据 %}
{{ all_country_option_tags }}
{% # cart 对象 %}
{{ }}
{% # %}
{{ }}
{% # %}
{{ }}
{% # %}
{{ }}
{% # %}
{{ }}
{% # %}
{{ }}
{% # %}
{{ }}
{% # %}
{{ }}
{% # %}
{{ }}
{% # %}
{{ }}
店铺: shop.
名称: {{ shop.name }}
域名: {{ shop.domain }}
邮箱: {{ shop.email }}
电话: {{ shop.phone }}
描述: {{ shop.description }}
地址: {{ shop.address.summary }}
国家: {{ shop.address.country }}
城市: {{ shop.address.city }}
{{ shop.brand.logo.src }}
品牌 数据
短信通知 变量
短信通知,需要照常在订单属性前面加上 order,其他的通知变量再在相关变量前面加上相关对象即可。
collection
{{ /* 结果为 null */ }}
{{ collection | json }}
collections返回数据
{% for collection in collections %}
{{- collection.title | link_to: collection.url }}
{% # 当前集合的产品数量 %}
{% if collection.products.size > 0 %}
{% endif %}
{{ collection.all_products_count }}
{{ collection.products.size }}
{% endfor %}
{% for product in collections['sale-potions'].products %}
{{- product.title | link_to: product.url }}
{% endfor %}
{{ /* 结果为如下示例 */ }}
{{ collections | json }}
[
{
"id": 501727822113,
"handle": "all-product",
"updated_at": "2025-09-10T04:18:57-07:00",
"published_at": "2025-07-15T20:10:01-07:00",
"sort_order": "best-selling",
"template_suffix": "",
"published_scope": "web",
"title": "All Product",
"body_html": ""
},
{
"id": 503004823841,
"handle": "bcan-equipments",
"title": "Bcan Equipments",
"updated_at": "2025-09-05T02:16:20-07:00",
"body_html": "",
"published_at": "2025-08-14T01:19:14-07:00",
"sort_order": "best-selling",
"template_suffix": "",
"disjunctive": false,
"rules": [
{ "column": "tag", "relation": "equals", "condition": "BCAN Equipments" }
],
"published_scope": "web"
},
{
"id": 501036024097,
"handle": "cardio-equipment",
"title": "Cardio Equipment",
"updated_at": "2025-09-07T06:23:39-07:00",
"body_html": "",
"published_at": "2025-06-27T01:27:25-07:00",
"sort_order": "manual",
"template_suffix": "",
"disjunctive": false,
"rules": [
{ "column": "tag", "relation": "equals", "condition": "Cardio Equipment" }
],
"published_scope": "web"
}
]
产品 模块
在模板或模板内部的某个部分中包含产品数据,可以通过
{{ product | json }}
输出具体内容。
注意事项
请注意产品的单属性与多属性取值的区别问题,product对象中的一级属性,取的值是属性variants数组中的第一个对象值的属性。
- https://shopify.dev/docs/api/liquid/objects/product
- https://shopify.dev/docs/api/liquid/objects/variant
产品列表
这里获取的数据是:https://admin.shopify.com/store/sharksportsonline/products?selectedView=all
<div class="sections-sacl-product">
{% for product in collections['Barbell'].products limit: 3 %}
<div class="com-flex-column sections-sacl-p-item">
<div class="com-flex sections-sacl-pi-content">
<div class="com-flex-center sections-sacl-pic-viewer">
{{ product | image_url: width: 110 | image_tag }}
</div>
<div class="com-flex-column sections-sacl-pic-wrapper">
<h3 class="sections-sacl-picw-h3">ONLY LEAVE</h3>
<div class="com-flex h center sections-sacl-picw-wrapper">
<label class="sections-sacl-picw-w-label">47</label>
<span class="sections-sacl-picw-w-span">piece</span>
</div>
</div>
</div>
<div class="sections-sacl-pi-footer">{{ product.title }}</div>
{% assign current_variant = product.selected_or_first_available_variant %}
<span class="variant-sku">{{ current_variant.sku }}</span>
</div>
{% endfor %}
</div>
{% # 'sale-potions'是产品集合中的某一个类别名称,需要根据后台管理系统来灵活制定 %}
{% for product in collections['sale-potions'].products %}
{{- product.title | link_to: product.url }}
{% endfor %}
{% # 输出HTML结构 %}
<a href="/products/health-potion" title="">Health potion</a>
<a href="/products/invisibility-potion" title="">Invisibility potion</a>
{% # 写法一 %}
{%- liquid
assign count = 0
for product in collections.all.products
{% # 状态:可以购买 %}
if product.available == true
assign count = count | plus: 1
endif
endfor
-%}
{% # 写法二 %}
{% assign count = 0 %}
{% for product in collections.all.products %}
{% # 状态:可以购买 %}
{% if product.available == true %}
{% assign count = count | plus: 1 %}
{% endif %}
{% endfor %}
{{ count }}
- 产品列表数据
{{ collections.all.products | json }}
[
{
"id": 10065379787041,
"title": "FED Weighted Eco-Friendly Dumbbells: Suitable for Strength Training in Home, Office, and Gym Settings",
"handle": "fed-weighted-eco-friendly-dumbbells",
"description": "",
"published_at": "2025-08-05T03:29:39-07:00",
"created_at": "2025-07-18T02:22:44-07:00",
"vendor": "FED Fitness",
"type": "",
"tags": ["Test/Insurance"],
"price": 15999, // 现价
"price_min": 15999,
"price_max": 15999,
"available": true,
"price_varies": false,
"compare_at_price": null, // 原价
"compare_at_price_min": 0,
"compare_at_price_max": 0,
"compare_at_price_varies": false,
"variants": [
{
"id": 51494401835297,
"title": "Default Title",
"option1": "Default Title",
"option2": null,
"option3": null,
"sku": null,
"requires_shipping": true,
"taxable": true,
"featured_image": null,
"available": true,
"name": "FED Weighted Eco-Friendly Dumbbells: Suitable for Strength Training in Home, Office, and Gym Settings",
"public_title": null,
"options": ["Default Title"],
"price": 15999,
"weight": 0,
"compare_at_price": null,
"inventory_management": "shopify",
"barcode": "",
"requires_selling_plan": false,
"selling_plan_allocations": [],
"quantity_rule": { "min": 1, "max": null, "increment": 1 }
}
],
"images": [
"//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/1.png?v=1752830534",
"//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/3_4eb9aeaa-7850-45db-97f1-b3ae8011ce6a.jpg?v=1752830533",
"//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/4_8882fb99-5b0d-47bb-944c-9edeb69fa24e.jpg?v=1752830534",
"//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/5_6405d547-de33-4952-83af-43960dc608dd.png?v=1752830534",
"//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/2.png?v=1752830535"
],
"featured_image": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/1.png?v=1752830534",
"options": ["Title"],
"media": [
{
"alt": null,
"id": 41829157110049,
"position": 1,
"preview_image": {
"aspect_ratio": 1.0,
"height": 662,
"width": 662,
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/1.png?v=1752830534"
},
"aspect_ratio": 1.0,
"height": 662,
"media_type": "image",
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/1.png?v=1752830534",
"width": 662
},
{
"alt": null,
"id": 41829157077281,
"position": 2,
"preview_image": {
"aspect_ratio": 1.0,
"height": 783,
"width": 783,
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/3_4eb9aeaa-7850-45db-97f1-b3ae8011ce6a.jpg?v=1752830533"
},
"aspect_ratio": 1.0,
"height": 783,
"media_type": "image",
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/3_4eb9aeaa-7850-45db-97f1-b3ae8011ce6a.jpg?v=1752830533",
"width": 783
},
{
"alt": null,
"id": 41829157142817,
"position": 3,
"preview_image": {
"aspect_ratio": 1.0,
"height": 1600,
"width": 1600,
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/4_8882fb99-5b0d-47bb-944c-9edeb69fa24e.jpg?v=1752830534"
},
"aspect_ratio": 1.0,
"height": 1600,
"media_type": "image",
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/4_8882fb99-5b0d-47bb-944c-9edeb69fa24e.jpg?v=1752830534",
"width": 1600
},
{
"alt": null,
"id": 41829157208353,
"position": 4,
"preview_image": {
"aspect_ratio": 1.333,
"height": 433,
"width": 577,
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/5_6405d547-de33-4952-83af-43960dc608dd.png?v=1752830534"
},
"aspect_ratio": 1.333,
"height": 433,
"media_type": "image",
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/5_6405d547-de33-4952-83af-43960dc608dd.png?v=1752830534",
"width": 577
},
{
"alt": null,
"id": 41829157830945,
"position": 5,
"preview_image": {
"aspect_ratio": 1.0,
"height": 1600,
"width": 1600,
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/2.png?v=1752830535"
},
"aspect_ratio": 1.0,
"height": 1600,
"media_type": "image",
"src": "//u6p6gbtwpyvnt09l-93956571425.shopifypreview.com/cdn/shop/files/2.png?v=1752830535",
"width": 1600
}
],
"requires_selling_plan": false,
"selling_plan_groups": [],
"content": ""
}
]
产品属性
// 'sale-potions'为某个集合的handle
{% for product in collections['sale-potions'].products %}
{{- product.title | link_to: product.url }}
{% endfor %}
{% # 获取产品链接 %}
{{ request.origin | append: product.url }}
<div class="ysun-scroll-wrapper">
{% for product in collections.all.products %}
{% liquid
assign allow = true
if product.title == 'Feier Product Accessory'
assign allow = false
elsif product.title == 'Green Shipping Protection'
assign allow = false
else
assign allow = true
endif
%}
{% if allow == true %}
<div class="section-index-catalog-view">
<a href="{{ request.origin | append: product.url }}" target="_blank">
<img src="{{ product.featured_image | img_url: 'grande' }}" alt="" loading="lazy" alt="{{ product.title }}" />
<p>{{ product.title }}</p>
</a>
</div>
{% endif %}
{% endfor %}
</div>
{
/**
* 当前产品是否可用,需满足以下条件
* 库存数量 variant.inventory_quantity > 0,
* 库存跟踪数量 variant.inventory_policy被设置
*/
"available": true,
"sku": "",
"tags": ["", "", "", ""],
"id": "",
"title": "",
"description": "",
"price": 0, // 价格,整数与小数一起显示,去掉了小数点
"price_min": 0,
"price_max": 0,
// 如果产品的多属性价格不同,则返回true。否则返回 false。
"price_varies": false,
"compare_at_price": 0, // Compare-at price 原价,整数与小数一起显示,去掉了小数点
"compare_at_price_max": 0,
"compare_at_price_min": 0,
// 如果多属性比较的产品价格不同,则返回 true。否则返回 false。
"compare_at_price_varies": false,
"first_available_variant": {}, // 第一个属性
"selected_or_first_available_variant": {},
"metafields": {},
// 媒体文件
"media": [
{
"id": "",
"preview_image": {
"aspect_ratio": "",
"width": 0,
"height": 0,
},
"media_type":"image",
"src": ""
}
],
// 多属性,其值会遍历呈现多属性对象,注意打印调试 {{ product.variants | json }}
"variants": [
{
"id": "",
"sku": "",
"barcode": "ISBN"
}
]
}
// A product variant 属性
{
"metafields": {},
}
{
"id": 8435818561757,
"title": "FEIER Strength Station",
"handle": "feier-strength-station",
"description": "<div>\n<div data-line=\"true\" data-line-index=\"0\" data-zone-id=\"0\"><span>Stylish-designed all-in-one small home gym equipment with exceptional Flexibility, up to 110 LBs(NebulaX) resistance for single-side. The combined FEIER Smart APP tracks your exercise data and provides the courses to follow. The small body can meet your whole-body muscle training needs with resistance adjustment up to 36 levels (NebulaX).</span></div>\n</div>",
"published_at": "2024-03-31T23:25:02-07:00",
"created_at": "2024-03-29T02:09:37-07:00",
"vendor": "Feier",
"type": "",
"tags": [],
"price": 69900,
"price_min": 69900,
"price_max": 119999,
"available": false,
"price_varies": true,
"compare_at_price": 80000,
"compare_at_price_min": 80000,
"compare_at_price_max": 19928400,
"compare_at_price_varies": true,
"variants": [
{
"id": 44752505143517,
"title": "Halo",
"option1": "Halo",
"option2": null,
"option3": null,
"sku": "",
"requires_shipping": true,
"taxable": true,
"featured_image": {
"id": 40701848486109,
"product_id": 8435818561757,
"position": 1,
"created_at": "2024-04-01T00:29:54-07:00",
"updated_at": "2024-04-01T00:29:56-07:00",
"alt": null,
"width": 1000,
"height": 1000,
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100.Forcardio_cec7244c-28dc-4ede-955e-5900ffa74a73.jpg?v=1711956596",
"variant_ids": [
44752505143517
]
},
"available": false,
"name": "FEIER Strength Station - Halo",
"public_title": "Halo",
"options": [
"Halo"
],
"price": 69900,
"weight": 0,
"compare_at_price": 80000,
"inventory_management": "shopify",
"barcode": "",
"featured_media": {
"alt": null,
"id": 33342780637405,
"position": 1,
"preview_image": {
"aspect_ratio": 1,
"height": 1000,
"width": 1000,
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100.Forcardio_cec7244c-28dc-4ede-955e-5900ffa74a73.jpg?v=1711956596"
}
},
"requires_selling_plan": false,
"selling_plan_allocations": [],
"quantity_rule": {
"min": 1,
"max": null,
"increment": 1
}
},
{
"id": 44752505176285,
"title": "NebulaX",
"option1": "NebulaX",
"option2": null,
"option3": null,
"sku": "",
"requires_shipping": true,
"taxable": true,
"featured_image": {
"id": 40701848387805,
"product_id": 8435818561757,
"position": 2,
"created_at": "2024-04-01T00:29:54-07:00",
"updated_at": "2024-04-01T00:29:56-07:00",
"alt": null,
"width": 1000,
"height": 1000,
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100forfatlose_e9b7c121-b7bb-42ff-8681-c2f9086ed5b6.jpg?v=1711956596",
"variant_ids": [
44752505176285
]
},
"available": false,
"name": "FEIER Strength Station - NebulaX",
"public_title": "NebulaX",
"options": [
"NebulaX"
],
"price": 119999,
"weight": 0,
"compare_at_price": 19928400,
"inventory_management": "shopify",
"barcode": "",
"featured_media": {
"alt": null,
"id": 33342780670173,
"position": 2,
"preview_image": {
"aspect_ratio": 1,
"height": 1000,
"width": 1000,
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100forfatlose_e9b7c121-b7bb-42ff-8681-c2f9086ed5b6.jpg?v=1711956596"
}
},
"requires_selling_plan": false,
"selling_plan_allocations": [],
"quantity_rule": {
"min": 1,
"max": null,
"increment": 1
}
}
],
"images": [
"//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100.Forcardio_cec7244c-28dc-4ede-955e-5900ffa74a73.jpg?v=1711956596",
"//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100forfatlose_e9b7c121-b7bb-42ff-8681-c2f9086ed5b6.jpg?v=1711956596",
"//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100thateasytofold.convenientfolding_spacesaving_808f5d44-6c4d-428b-9650-ada4029dc48c.jpg?v=1711956596",
"//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100withshockabsorption_4cbc249e-b628-461d-b86f-24351d060ba5.jpg?v=1711956596"
],
"featured_image": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100.Forcardio_cec7244c-28dc-4ede-955e-5900ffa74a73.jpg?v=1711956596",
"options": [
"FEIER Strength Station"
],
"media": [
{
"alt": null,
"id": 33342780637405,
"position": 1,
"preview_image": {
"aspect_ratio": 1,
"height": 1000,
"width": 1000,
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100.Forcardio_cec7244c-28dc-4ede-955e-5900ffa74a73.jpg?v=1711956596"
},
"aspect_ratio": 1,
"height": 1000,
"media_type": "image",
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100.Forcardio_cec7244c-28dc-4ede-955e-5900ffa74a73.jpg?v=1711956596",
"width": 1000
},
{
"alt": null,
"id": 33342780670173,
"position": 2,
"preview_image": {
"aspect_ratio": 1,
"height": 1000,
"width": 1000,
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100forfatlose_e9b7c121-b7bb-42ff-8681-c2f9086ed5b6.jpg?v=1711956596"
},
"aspect_ratio": 1,
"height": 1000,
"media_type": "image",
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100forfatlose_e9b7c121-b7bb-42ff-8681-c2f9086ed5b6.jpg?v=1711956596",
"width": 1000
},
{
"alt": null,
"id": 33342780702941,
"position": 3,
"preview_image": {
"aspect_ratio": 1,
"height": 1000,
"width": 1000,
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100thateasytofold.convenientfolding_spacesaving_808f5d44-6c4d-428b-9650-ada4029dc48c.jpg?v=1711956596"
},
"aspect_ratio": 1,
"height": 1000,
"media_type": "image",
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100thateasytofold.convenientfolding_spacesaving_808f5d44-6c4d-428b-9650-ada4029dc48c.jpg?v=1711956596",
"width": 1000
},
{
"alt": null,
"id": 33342780735709,
"position": 4,
"preview_image": {
"aspect_ratio": 1,
"height": 1000,
"width": 1000,
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100withshockabsorption_4cbc249e-b628-461d-b86f-24351d060ba5.jpg?v=1711956596"
},
"aspect_ratio": 1,
"height": 1000,
"media_type": "image",
"src": "//www.feierfitness.com/cdn/shop/files/feiertreadmillstar100withshockabsorption_4cbc249e-b628-461d-b86f-24351d060ba5.jpg?v=1711956596",
"width": 1000
}
],
"requires_selling_plan": false,
"selling_plan_groups": [],
"content": "<div>\n<div data-line=\"true\" data-line-index=\"0\" data-zone-id=\"0\"><span>Stylish-designed all-in-one small home gym equipment with exceptional Flexibility, up to 110 LBs(NebulaX) resistance for single-side. The combined FEIER Smart APP tracks your exercise data and provides the courses to follow. The small body can meet your whole-body muscle training needs with resistance adjustment up to 36 levels (NebulaX).</span></div>\n</div>"
}
产品数据
- 产品集合
这里获取的数据是https://admin.shopify.com/store/sharksportsonline/collections?selectedView=all
- collections对象取值必须按照示例图,才能生效。
collections['power-tower'].products_count

{% # 获取指定产品数据 %}
{% assign handle = 'my-product-handle' %}
{% assign product = products.handle %}
{% assign someProduct = all_products.some-handle %}
{% # 当前产品系列中的产品数量 %}
{{ collections['Barbell'].products_count }}
{% for collection in collections %}
{{ collection.title | link_to: collection.url }}
{% endfor %}
{% # 输出HTML结构 %}
<a href="/collections/ingredients" title="">Ingredients</a>
<a href="/collections/potions" title="">Potions</a>
产品元字段


{% # module.metafields.namespace.key %}
{{ product.metafields.activity | json }}
{{ product.metafields.activity.status }}
{% # 浏览记录: 评分、浏览量 %}
{{ product.metafields.reviews | json }}
产品销量
{% # 写法一 %}
{%- liquid
assign count = 0
for product in collections.all.products
{% # 状态:不可以购买 %}
if product.available == false
assign count = count | plus: 1
endif
endfor
-%}
{% # 写法二 %}
{% assign count = 0 %}
{% for product in collections.all.products %}
{% # 状态:不可以购买 %}
{% if product.available == false %}
{% assign count = count | plus: 1 %}
{% endif %}
{% endfor %}
{{ count }}
单属性时更新和显示库存水平
{% assign inventory_quantity = 0 %}
{% assign current_variant = product.selected_or_first_available_variant %}
{% if current_variant.available %}
{% assign inventory_quantity = current_variant.inventory_quantity %}
{% endif %}
{% assign current_variant = product.selected_or_first_available_variant %}
<div class="inventoryNote form__label">
{% if current_variant.available %}
{% if current_variant.inventory_quantity > 0 and current_variant.inventory_quantity <= 10 %}We have {{ current_variant.inventory_quantity }} in stock
{% elsif current_variant.inventory_quantity > 10 %}We have more than 10 in stock
{% endif %}
{% endif %}
</div>
多属性时切换更新和显示库存水平
{% # 步骤一 theme.liquid %}
<script type="text/javascript">
var variantStock = {};
</script>
{% # 步骤二 main-product.liquid %}
<script type="text/javascript">
{% for variant in product.variants %}
variantStock[{{- variant.id -}}] = {{ variant.inventory_quantity }};
{% endfor %}
</script>
{% # 步骤三 global.js %}
<script type="text/javascript">
toggleAddButton(disable = true, text, modifyClass = true) {
const productForm = document.getElementById(`product-form-${this.dataset.section}`);
if (!productForm) return;
const addButton = productForm.querySelector('[name="add"]');
const addButtonText = productForm.querySelector('[name="add"] > span');
const inventoryNote = document.querySelector('.inventoryNote');
const inventoryHtml = `We have ${variantStock[this.currentVariant.id]} in stock`;
const inventryHighHtml = `We have more than 10 in stock`;
if (!addButton) return;
if (disable) {
addButton.setAttribute('disabled', 'disabled');
if (text) addButtonText.textContent = text;
inventoryNote.innerHTML = "";
} else {
addButton.removeAttribute('disabled');
addButtonText.textContent = window.variantStrings.addToCart;
if (variantStock[this.currentVariant.id] > 0 && variantStock[this.currentVariant.id] <= 10) {
inventoryNote.textContent = inventoryHtml;
} else if (variantStock[this.currentVariant.id] > 10) {
inventoryNote.textContent = inventryHighHtml;
}
}
if (!modifyClass) return;
}
</script>
统计多维属性总库存
{% assign inventory_quantity_total = 0 %}
{% for variant in product.variants %}
{% assign inventory_quantity_total = variant.inventory_quantity | plus: inventory_quantity_total %}
{% endfor %}
产品页面上售出的商品数量
{% comment %}
Show current amount sold for a single product (with default variant).
This assumes that you have created a metafield.
Message will show if variant is sold out.
{% endcomment %}
{% if product.variants.first.inventory_quantity > 0 %}
{% assign productStartCount = product.metafields.stock.initial | times:1 %}
{% if productStartCount > 0 %}
{% assign productInventory = product.variants.first.inventory_quantity %}
<p>{{ productStartCount | minus:productInventory }} sold. Only {{ productInventory }} remain.</p>
{% endif %}
{% else %}
<p>Uh Oh! All sold out...</p>
{% endif %}
{% comment %}
Show current amount sold for a single product (with default variant).
This assumes that you have created a metafield
{% endcomment %}
{% assign productStartCount = product.metafields.stock.initial | times:1 %}
{% if productStartCount > 0 %}
{% assign productInventory = product.variants.first.inventory_quantity %}
<p>{{ productStartCount | minus:productInventory }} sold. Only {{ productInventory }} remain.</p>
{% endif %}
剩余的库存数量
订单 模块
订单属性
订单列表
购物车 模块
购物车属性
商品数量: {{ cart.item_count }}
{{ cart.original_total_price }}
总折扣: {{ cart.total_discount }}
总价: {{ cart.total_price }}
{{ cart.total_weight }}
{{ cart.items_subtotal_price }}
购物车列表
{% for line_item in cart.items %}
{% if line_item.original_price > line_item.final_price %}
<s>{{ line_item.original_price | money }}</s>
{% endif %}
{{ line_item.final_price | money }})
{% if line_item.line_level_discount_allocations.size > 0 %}
Discounts:
<ul>
{% for discount_allocation in line_item.line_level_discount_allocations %}
<li>
{{ discount_allocation.discount_application.title }}-{{ discount_allocation.amount | money }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
结账 checkout
{% assign total_item_count = 0 %}
{% for item in checkout.line_items %}
{% assign item_quantity = item.quantity | times: 1 %}
{% assign total_item_count = total_item_count | plus: item_quantity %}
{% endfor %}
{{ total_item_count }}
折扣 模块
可以针对商店中的产品、产品系列或多属性为客户提供金额折扣。
折扣类型
- 产品折扣:产品降价金额、买X得Y
- 订单折扣:订单降价金额
- 运费折扣:免运费
折扣码匹配规则
折扣码组合规则
折扣码互斥规则
- 一个订单,如果订单折扣与产品折扣没有设置组合,且订单折扣小于产品折扣,则订单折扣失效。如果订单折扣大于产品折扣,则订单折扣有效。
折扣属性
用户 模块
用户属性
{{ customer.addresses_count }}
邮件: {{ customer.email }}
{{ customer.first_name }}
{{ customer.last_name }}
姓名: {{ customer.name }}
博客 Blogs
Ajax API
Ajax API提供了一套轻量级的REST API端点,用于开发Shopify主题。所有 API 响应都返回 JSON 格式的数据。
注意事项
Ajax API只能由Shopify托管的主题使用。您不能在 Shopify 自定义店面上使用 Ajax API。Ajax API 不能用于读取任何客户或订单数据,也不能更新任何商店数据。如果需要更广泛的访问权限,请检查管理 API。
Ajax API提供接口列表
- 购物车 cart - 更新购物车行项目、属性和备注。
- 产品 product - 获取有关目录中任何产品的信息。
- 产品推荐 Product Recommendations - 在产品页面上显示推荐的产品。
- 预测搜索 Predictive Search - 在买家键入搜索查询时向他们推荐产品、产品系列、页面和文章。
代码示例
var cartContents = fetch(window.Shopify.routes.root + 'cart.js')
.then(response => response.json())
.then(data => { return data });
购物车
获取购物车数据
let formData = {
'items': [{
'id': 36110175633573,
'quantity': 2
}]
};
var cartContents = fetch(window.Shopify.routes.root + 'cart.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => { return data });
// 商品选项
if (cartContents?.items) {
//
}
"Add to cart"按钮功能
具体见:snippets/card-product.liquid
<button type="submit" name="add" class="quick-add__submit">Add to cart</button>
使用端点将一个或多个变体添加到购物车
let formData = {
'items': [{
'id': 36110175633573,
'quantity': 2
}]
};
fetch(window.Shopify.routes.root + 'cart/add.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => {
return response.json();
})
.catch((error) => {
console.error('Error:', error);
});
- 添加订单项属性
- 更新订单项数量
let updates = {
794864053: 2,
794864233: 3
};
fetch(window.Shopify.routes.root + 'cart/update.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ updates })
})
.then(response => {
return response.json();
})
.catch((error) => {
console.error('Error:', error);
});
- 更新购物车备注
{
note: 'This is a note about my order'
}
- 更新 cart 属性
{
attributes: {
'Gift wrap': 'Yes'
}
}
var formData = new FormData();
formData.append("attributes[Gift wrap]", "Yes");
fetch(window.Shopify.routes.root + 'cart/update.js', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => console.log(data));
产品
product(产品),
- 获取产品数据
fetch(window.Shopify.routes.root + 'products/red-rain-coat.js')
.then(response => response.json())
.then(product => alert('The title of this product is ' + product.title));
产品推荐
Product Recommendations(产品推荐),
- 获取产品推荐数据
预测搜索
Predictive Search(预测搜索),
- 获取预测搜索数据
邮件通知 变量
订单属性
一个订单的许多属性可以直接通过使用 Liquid、模板和自定义脚本获取。
{{ id }} {% # 订单在系统范围内的唯一 ID %}
{{ email }} {% # %}
{{ name }} {% # %}
{{ order_name }} {% # %}
{{ order_number }} {% # %}
{{ shop.name }} {% # %}
{{ item_count }} {% # 所有商品的数量总和 %}
{{ }} {% # %}
{{ }} {% # %}
{{ }} {% # %}
{{ }} {% # %}
订单项目属性
line_items 或 subtotal_line_items 列表中的每个 line 都具有以下属性,具体查看 订单项目属性。
重要通知
subtotal_line_items返回了用于计算订单的subtotal_price的订单项目。subtotal_line_items排除小费订单项目。
{{ subtotal_line_items.size }} {% # 订单中产品数量 %}
{% for line in subtotal_line_items %}
{{ line.quantity }} {% # %}
{{ line.quantity }} {% # 产品数量 %}
{{ line.product.metafields }} {% # 产品级别的元字段 line.product.metafields.NAMESPACE.KEY %}
{{ line.applied_discounts }} {% # 应用于此产品的折扣列表 %}
{{ }} {% # %}
{{ }} {% # %}
{{ }} {% # %}
{{ }} {% # %}
{{ }} {% # %}
{{ }} {% # %}
{{ }} {% # %}
{{ }} {% # %}
{% endfor %}
<table style="width:100%;">
{% for line in subtotal_line_items %}
<tr>
<td>
{% if line.image %}
<img src="{{ line | img_url: 'compact_cropped' }}" />
{% endif %}
</td>
<td>
{% if line.product.title %}
{% assign line_title = line.product.title %}
{% else %}
{% assign line_title = line.title %}
{% endif %}
</td>
<td>
</td>
</tr>
{% endfor %}
</table>
草稿订单属性
付款时间表属性
付款状态属性
订单项目属性
退款属性
<table style="width:100%;">
{% for line in return.line_items %}
<tr>
<td>
{% if line.image %}
<img src="{{ line.line_item | img_url: 'compact_cropped' }}" />
{% endif %}
</td>
</tr>
{% endfor %}
</table>
发货属性
<table style="width:100%;">
{% for fulfillment_line_item in fulfillment.fulfillment_line_items %}
{% assign line = fulfillment_line_item.line_item %}
<tr>
<td>
{% if line.image %}
<img src="{{ line.line_item | img_url: 'compact_cropped' }}" />
{% endif %}
</td>
</tr>
{% endfor %}
</table>
配送属性
折扣属性
订阅属性
电子邮件通知属性
自定义邮件内容
{% # 问候语 %}
{% capture email_hello_title %}
{% if requires_shipping %}
{% case delivery_method %}
{% when 'pick-up' %}
You’ll receive an email when your order is ready for pickup.
{% when 'local' %}
Hi, dear {{ customer.first_name }}
{% else %}
Hi, dear {{ customer.first_name }}
{% endcase %}
{% if delivery_instructions != blank %}
<p><b>Delivery information:</b> {{ delivery_instructions }}</p>
{% endif %}
{% endif %}
{% endcapture %}
{% # 原始默认邮件主体内容 %}
{% capture email_body_content %}
{% endcapture %}
{% # 单个产品 %}
{% if subtotal_line_items.size == 1 %}
{% assign line = subtotal_line_items[0] %}
{% # 特定产品类型 %}
{% assign product_email_content_type = line.product.metafields.custom.product_email_content_type %}
{% # 折扣码 %}
{% assign product_disaccount_code = line.product.metafields.custom.product_disaccount_code %}
{% # 折扣价格 %}
{% assign product_order_activity_price = line.product.metafields.custom.product_order_activity_price %}
{% # 特定产品 %}
{% if product_email_content_type == 1 %}
{% # 内容区 start %}
<table class="row section">
<tr>
<td class="section__cell">
<center>
<table class="container">
<tr>
<td>
{{ email_hello_title }}
<p style="color: #777; line-height: 150%; font-size: 16px; margin: 0;">
Your voucher code is <strong>{{ product_disaccount_code }}</strong>. You can start using it from the 15th, Nov.
It is applicable when your order value is greater than <strong>{{ product_order_activity_price }}</strong>. It can be combined with Black Friday & Cyber Monday discount codes: BF10 (10% off order) and BF15 (15% off order).
Please do not share this code with anyone else. This code is your privilege. See you on the 15th.
<br>
Happy shopping and happy training!
</p>
</td>
</tr>
</table>
</center>
</td>
</tr>
</table>
{% # 内容区 end %}
{% # 正常产品 %}
{% else %}
{% # 内容区 start %}
{{ email_body_content }}
{% # 内容区 end %}
{% endif %}
{% # 多个产品 %}
{% else %}
{{ email_body_content }}
{% for line in subtotal_line_items %}
{% # 特定产品类型 %}
{% assign product_email_content_type = line.product.metafields.custom.product_email_content_type %}
{% # 折扣码 %}
{% assign product_disaccount_code = line.product.metafields.custom.product_disaccount_code %}
{% # 折扣价格 %}
{% assign product_order_activity_price = line.product.metafields.custom.product_order_activity_price %}
{% # 特定产品 %}
{% if product_email_content_type == 1 %}
<table class="row section" style="border-top:1px solid #e5e5e5;">
<tr>
<td class="section__cell">
<center>
<table class="container">
<tr>
<td>
<p style="color: #777; line-height: 150%; font-size: 16px; margin: 0;">
Your voucher code is <strong>{{ product_disaccount_code }}</strong>. You can start using it from the 15th, Nov.
It is applicable when your order value is greater than <strong>{{ product_order_activity_price }}</strong>. It can be combined with Black Friday & Cyber Monday discount codes: BF10 (10% off order) and BF15 (15% off order).
Please do not share this code with anyone else. This code is your privilege. See you on the 15th.
<br>
Happy shopping and happy training!
</p>
</td>
</tr>
</table>
</center>
</td>
</tr>
</table>
{% endif %}
{% endfor %}
{% endif %}
Checkout 结账业务
应用程序开发人员可以使用不同的方法来自定义 Checkout 的外观和功能,包括heckout UI extensions, Branding API, Shopify Functions, and Post-purchase checkout extensions。
- 最新技术文档: https://workshops.shopify.dev/workshops/checkout-ui-custom-field#0
- Shopify Plus: https://help.shopify.com/zh-CN/manual/intro-to-shopify/pricing-plans/plans-features
- 自定义 Shopify 结帐: https://shopify.dev/docs/apps/checkout
Checkout Branding API
通过更改收银台的品牌颜色等,定制收银台外观。
Post-purchase extensions
在购买后页面中添加新内容,例如在客户结账后显示产品优惠,或在客户结账时捕获其他信息。
- Post-purchase checkout extensions: https://shopify.dev/docs/api/checkout-extensions/post-purchase
Web pixel extension
收集买家行为数据,以衡量和优化营销活动绩效以及网店的转化渠道。
结账规则


checkout.liquid
checkout.liquid: https://shopify.dev/docs/themes/architecture/layouts/checkout-liquid
基本目录
└── theme
├── layout
| ├── theme.liquid
| └── checkout.liquid
├── templates
Checkout Extensibility
checkout.liquid 文件提供可自定义的结账功能,让您能够更好地控制商店的品牌营销。2024年8月13日,checkout.liquid 将不再可用于信息、发货和付款页面。Shopify Plus商家现在可以改为使用Checkout Extensibility来自定义这些页面。如果您自定义结账流程中的信息、发货或付款页面,请于 2024 年 8 月 13 日之前升级到 Checkout Extensibility。
- Checkout Extensibility: https://help.shopify.com/zh-CN/manual/checkout-settings/checkout-extensibility/checkout-upgrade

Checkout UI extensions
结帐 UI 扩展允许您在结帐流程的定义点添加自定义工作流和功能,包括产品信息、运输、付款、订单摘要和商店付款。
- Checkout UI extensions: https://shopify.dev/docs/api/checkout-ui-extensions
- ui-extensions: https://github.com/Shopify/ui-extensions
- API: https://shopify.dev/docs/api/checkout-ui-extensions/2023-07/apis
- UI组件:https://shopify.dev/docs/api/checkout-ui-extensions/2023-07/components
开发调试部署流程
参考 Shopify Extension的具体流程即可。
参考项目示例
开发代码示例
import {
reactExtension,
Banner,
BlockStack,
Checkbox,
Text,
useInstructions,
useTranslate,
useApi
} from "@shopify/ui-extensions-react/checkout";
export default reactExtension("purchase.checkout.block.render", () => (
<Extension />
));
function Extension() {
const translate = useTranslate();
const { extension } = useApi();
const instructions = useInstructions();
if (!instructions.attributes.canUpdateAttributes) {
return (
<Banner title="checkout-ui" status="warning">
{translate("attributeChangesAreNotSupported")}
</Banner>
)
}
return (
<BlockStack border={"dotted"} padding={"tight"}>
<Banner title="checkout-ui-title"></Banner>
<Banner title="checkout-ui-title">
{translate("welcome", {
target: <Text emphasis="italic">{extension.target}</Text>,
})}
</Banner>
<Checkbox onChange={onCheckboxChange}>
{translate("iWouldLikeAFreeGiftWithMyOrder")}
</Checkbox>
</BlockStack>
);
async function onCheckboxChange(isChecked) {
// 4. Call the API to modify checkout
const result = await applyAttributeChange({
key: "requestedFreeGift",
type: "updateAttribute",
value: isChecked ? "yes" : "no",
});
console.log("applyAttributeChange result", result);
}
}
扩展目标
目标表示结账 UI 扩展的显示位置。
- 扩展目标提供了商家可以插入自定义内容的位置,静态附加信息目标与核心结账功能(如联系信息、配送方式和订单摘要订单项)相关联。 动态扩展目标可以在结账过程中的任何时间点显示,并且无论哪些结账功能可用,都将始终呈现。 例如,用于从客户那里捕获订单备注的字段。
import {
reactExtension,
Banner,
} from '@shopify/ui-extensions-react/checkout';
export default reactExtension(
'purchase.checkout.block.render',
() => <Extension />,
);
function Extension() {
return <Banner>Your extension</Banner>;
}
/**
* Address
*
*/
/**
* Block
*
*/
/**
* Footer
*
*/
/**
* Header
*
*/
/**
* Information
*
*/
/**
* Local Pickup
*
*/
/**
* Navigation
*
*/
/**
* Order Summary
*
*/
/**
* Payments
*
*/
/**
* Pickup Points
*
*/
/**
* Shipping
*
*/
扩展点展示位置
Checkout locations
收银台是买家购买商品的地方。结账包括信息、运输和付款步骤,以及订单摘要和 Shop Pay。
Thank you locations
成功提交结账后,将立即向买家显示感谢页面。
Order status locations
当买家返回到已完成的结账页面以获取订单更新时,会向他们显示订单状态页面。
Static extension points
静态扩展点(Static extension points),静态扩展点会在大多数核心结账功能(例如联系信息、运输方式和订单摘要行项目)之前或之后立即呈现。
- 静态扩展点: https://shopify.dev/docs/api/checkout-ui-extensions/2023-04/extension-points-overview#static-extension-points
- 扩展目标列表: https://shopify.dev/docs/api/checkout-ui-extensions/2024-07/targets
配置示例
在shopify.extension.toml仅仅配置静态扩展点。
- shopify.extension.toml
[[extensions.targeting]]
module = "./src/Glove.jsx"
target = "purchase.checkout.delivery-address.render-after"
- Glove.jsx
import {
reactExtension,
Banner,
BlockStack,
Checkbox,
Text,
useApi,
useInstructions,
useTranslate
} from "@shopify/ui-extensions-react/checkout";
// 1. Choose an extension target
export default reactExtension("purchase.checkout.delivery-address.render-after", () => (
<Extension />
));
function Extension() {
console.log("静态扩展点: ", Date.now());
const translate = useTranslate();
const { extension, applyAttributeChang } = useApi();
const instructions = useInstructions();
const toggleServiceFunc = function() {
console.log("切换: ", Date.now());
}
// 3. Render a UI
return (
<BlockStack>
<Banner title="purchase.checkout.delivery-address.render-after">
白手套服务
</Banner>
</BlockStack>
);
}
Dynamic extension points
动态扩展点(Dynamic Extension Points)允许开发者在结算页面的特定位置插入自定义组件。
动态扩展点语法: https://shopify.dev/docs/api/checkout-ui-extensions/2023-04/apis/extensionpoints
Checkout::Dynamic::Render 的显示位置由 Shopify 确定。
配置示例
同时在shopify.extension.toml配置静态扩展点与动态扩展点。
- shopify.extension.toml
[[extensions.targeting]]
target = "purchase.checkout.delivery-address.render-after"
module = "./src/select.jsx"
[[extensions.targeting]]
target = "purchase.checkout.reductions.render-after"
module = "./src/fee.jsx"
- 扩展点一:./src/select.jsx
import {
reactExtension,
Grid,
View,
Checkbox,
Select,
Stepper,
useApi,
} from "@shopify/ui-extensions-react/checkout";
import { useState, useEffect } from "react";
export default reactExtension(
"purchase.checkout.delivery-address.render-after",
() => <Extension />
);
function Extension() {
const { storage, applyAttributeChange, attributes, cost } = useApi();
const allApi = useApi();
console.log("allApi: ", allApi);
const currencyCode =
cost?.subtotalAmount?.current?.currencyCode?.toLowerCase();
const currency = currencyCode === "cny" ? "¥" : "$";
const [checked, setChecked] = useState(false);
const [floorData, setFloorData] = useState({
value: "",
required: false,
error: "",
});
const [minuteData, setMinuteData] = useState({
value: "",
required: false,
error: "",
});
const minuteList = [
{ value: "under", label: "under 30 minutes" },
{ value: "over", label: "over 30 minutes" },
];
// 初始化白手套
function initService() {
if (attributes.current.length) {
const wgsDataItem = attributes.current.filter(
(item) => item.key === "wgs-data"
)?.[0];
const value = JSON.parse(wgsDataItem?.value || "{}");
const { action, channel, floor, minute, event_type } = value;
if (
channel !== "checkout" ||
event_type !== "white_glove_select" ||
action !== "calc-total"
)
return;
console.log("value: ", value);
setChecked(true);
setFloorData({ value: floor, required: false, error: "" });
setMinuteData({ value: minute, required: false, error: "" });
// 同步价格
calcPriceModel(floor, minute);
return;
}
setFloorData({ value: "", required: false, error: "" });
setMinuteData({ value: "", required: false, error: "" });
clearPriceUIFunc();
}
// 清除价格显示
const clearPriceUIFunc = function () {
const data = { checked: false };
storage.write("own-checkout-white-glove-service", JSON.stringify(data));
};
useEffect(() => {
initService();
}, []);
// 同步数据到Function
function updateDataToFunction(key, data = {}) {
const wgsDataString =
typeof data === "string" ? data : JSON.stringify(data);
const applyAttributeData = {
type: "updateAttribute",
key: key,
value: wgsDataString,
};
applyAttributeChange(applyAttributeData);
}
// 更新订单总价
const updateOrderTotalFunc = function (floor, minute, value) {
const wgsData = {
channel: "checkout",
event_type: "white_glove_select",
action: "calc-total",
fee: value,
floor: floor,
minute: minute,
};
const minuteItems = minuteList.filter((item) => item.value === minute);
const minuteItem = minuteItems[0];
updateDataToFunction("ROC选择服务", "已选择");
updateDataToFunction("ROC费用", currency + value);
updateDataToFunction("Floor(楼层)", floor);
updateDataToFunction("Time(时间)", minuteItem.label);
updateDataToFunction("wgs-data", wgsData);
// console.log(
// "1-1更新订单总价: ",
// "楼层: ",
// floor,
// "服务时间: ",
// minute,
// "总价: ",
// value
// );
};
// 同步价格
const calcPriceModel = function (floor, minute) {
if (!floor || !minute) {
clearPriceUIFunc();
return;
}
const _floor = Number(floor);
// 计算价格
// const base_fee = 105;
const base_fee = 85;
const service_fee = minute === "over" ? 50 : 0;
const floor_fee = _floor > 2 ? (_floor - 2) * 30 : 0;
const total_fee = base_fee + service_fee + floor_fee;
// 同步价格显示
const data = { checked: true, price: total_fee };
storage.write("own-checkout-white-glove-service", JSON.stringify(data));
// 更新订单总价
updateOrderTotalFunc(floor, minute, total_fee);
};
// 选择白手套服务
const serviceChange = async () => {
const changeChecked = !checked;
setChecked(changeChecked);
if (changeChecked) {
setFloorData((prevErrors) => ({
...prevErrors,
required: true,
error: "Floor is required",
}));
setMinuteData((prevErrors) => ({
...prevErrors,
required: true,
error: "Service time is required",
}));
return;
}
setFloorData({ value: "", required: false, error: "" });
setMinuteData({ value: "", required: false, error: "" });
// 清除价格显示
const data = { checked: changeChecked };
storage.write("own-checkout-white-glove-service", JSON.stringify(data));
const wgsData = {
channel: "checkout",
event_type: "white_glove_select",
action: "destroy",
};
updateDataToFunction("wgs-data", wgsData);
};
const inputFloorChange = function (value) {
if (!value && value != "0") {
clearPriceUIFunc();
setFloorData((prevErrors) => ({
...prevErrors,
value: "",
required: true,
error: "Floor is required and must be a number greater than 1",
}));
return;
}
const _value = Number(value);
if (Number.isNaN(_value)) {
clearPriceUIFunc();
setFloorData((prevErrors) => ({
...prevErrors,
required: true,
error: "The filled content must be a number",
}));
return;
}
if (_value < 1) {
clearPriceUIFunc();
setFloorData((prevErrors) => ({
...prevErrors,
required: true,
error: "The filled content must be a number greater than 1",
}));
return;
}
setFloorData((prevErrors) => ({
...prevErrors,
value: _value,
required: false,
error: "",
}));
// 同步价格
calcPriceModel(_value, minuteData.value);
};
const minuteValueChange = function (value) {
setMinuteData((prevErrors) => ({
...prevErrors,
value: value,
required: false,
error: "",
}));
// 同步价格
calcPriceModel(floorData.value, value);
};
// 白手套: Add White Glove Service
return (
<View>
<Checkbox
id="checkbox"
name="checkbox"
checked={checked}
onChange={serviceChange}
>
Add ROC (Room Of Choice)
</Checkbox>
{checked && (
<Grid columns={["30%", "fill"]}>
<View padding="tight">
<Stepper
label="Select floor"
required={floorData.required}
error={floorData.error}
value={floorData.value}
min={1}
max={200}
onChange={inputFloorChange}
/>
</View>
<View padding="tight">
<Select
label="Select service time"
required={minuteData.required}
error={minuteData.error}
options={minuteList}
value={minuteData.value}
onChange={minuteValueChange}
/>
</View>
</Grid>
)}
</View>
);
}
- 扩展点二: ./src/fee.jsx
import {
reactExtension,
Grid,
View,
useApi,
useStorage
} from "@shopify/ui-extensions-react/checkout";
import { useState, useEffect } from 'react';
export default reactExtension("purchase.checkout.reductions.render-after", () => (
<MyCheckoutExtension />
));
function MyCheckoutExtension() {
const { read } = useStorage();
const [checked, setChecked] = useState(false);
const [price, setPrice] = useState("");
useEffect(() => {
const interval = setInterval(async () => {
const readData = await read("own-checkout-white-glove-service");
const data = JSON.parse(readData || "{}");
setPrice(data.price || "");
setChecked(data.checked || false);
}, 1000);
return () => clearInterval(interval);
}, []);
const { localization } = useApi();
const isoCode = localization?.currency?.current?.isoCode?.toLowerCase();
const currency = isoCode === "cny" ? "¥": "$";
if (!checked) return null;
return (
<Grid columns={['fill', 'auto']}>
<View>White Glove Fee</View>
<View>{currency}{price}</View>
</Grid>
);
}
配置文件
创建结账 UI 扩展时,将在结账 UI 扩展目录中自动生成该文件。它包含扩展的配置,其中包括扩展名称、扩展目标、元字段、功能和设置定义。
- Shopify.extension.toml
api_version = "2023-07"
[[extensions]]
type = "ui_extension"
name = "My checkout extension"
handle = "checkout-ui"
# 扩展点与相关文件
[[extensions.targeting]]
target = "purchase.checkout.block.render"
module = "./Checkout.jsx"
# 指定入口文件
[build.inputs]
main = "./src/index.jsx" # 入口文件路径
# 配置开发时的入口文件路径
[development.inputs]
main = "./src/index.jsx" # 入口文件路径
@shopify/ui-extensions-react/checkout
import { } from "@shopify/ui-extensions-react/checkout";
扩展 API
API 使结账 UI 扩展能够获取有关结账或相关对象的信息并执行操作。例如,您可以使用 API 来检索客户购物车中的内容,以便提供相关产品。
import {
// Addresses: 地址
// Analytics: Web像素
// Attributes: 购物车与结账
// Buyer Identity: 买家身份
useCustomer,
useEmail,
usePhone,
// Buyer Journey: 买家事务
// Cart Instructions: 购物车说明
// Cart Lines: 购物车
useCartLines,
useApplyCartLinesChange,
// Checkout Token: 结账令牌
useCheckoutToken,
// Cost: 费用
useSubtotalAmount,
useTotalShippingAmount,
useTotalTaxAmount,
useTotalAmount,
// Customer Privacy: 客户隐私
useCustomerPrivacy,
// Delivery: 交货与运输
useDeliveryGroups,
useDeliveryGroup,
// Discounts: 折扣
// Extension: 扩展
// Gift Cards: 礼品卡
// Localization: 本地化
useTranslate,
useCurrency,
// Metafields: 元字段
// Note: 备注
// Order: 订单
useOrder,
// Payments: 付款
useSelectedPaymentOptions,
// Session Token: 回话
// Settings: 商家设置
useSettings,
// Shop: 店铺数据
// Storage: 存储
// Storefront API: 店面操作
// useApi():
useApi,
// useSubscription():
useSubscription,
} from '@shopify/ui-extensions-react/checkout';
import { useEffect, useState } from 'react';
export default reactExtension(
'purchase.checkout.delivery-address.render-before',
() => <Extension />,
);
function Extension() {
const {
analytics,
appMetafields,
appliedGiftCards,
applyAttributeChange,
applyCartLinesChange,
applyDiscountCodeChange,
applyGiftCardChange,
applyMetafieldChange,
applyNoteChange,
applyShippingAddressChange,
applyTrackingConsentChange,
attributes,
availablePaymentOptions,
billingAddress,
buyerIdentity,
buyerJourney,
checkoutSettings,
checkoutToken,
cost,
customerPrivacy,
deliveryGroups,
discountAllocations,
discountCodes,
experimentalIsShopAppStyle,
extension,
extensionPoint,
i18n,
instructions,
lines,
localization,
metafields,
note,
query,
selectedPaymentOptions,
sessionToken,
settings,
shippingAddress,
shop,
storage,
ui,
version
} = useApi();
const { id, myshopifyDomain, name, storefrontUrl } = shop;
const { read, write, delete } = storage;
const translate = useTranslate();
const currency = useCurrency();
const { countryCode } = useShippingAddress();
const { banner_title } = useSettings();
const options = useSelectedPaymentOptions();
// 本地化货币
const balance = 9.99;
const formattedBalance = i18n.formatCurrency(balance);
// 本地化号码
const points = 10000;
const formattedPoints = i18n.formatNumber(points);
// 订单
const order = useOrder();
const { id } = useSubscription(orderConfirmation);
// 价格
const subtotalAmount = cost?.subtotalAmount?.current?.amount || 0;
const totalAmount = cost?.totalAmount?.current?.amount || 0;
const totalShippingAmount = cost?.totalShippingAmount?.current?.amount || 0;
const totalTaxAmount = cost?.totalTaxAmount?.current?.amount || 0;
// 监听结算变化
useEffect(() => {
// 订阅 checkoutSettings 的变化
const unsubscribe = checkoutSettings.subscribe((newSettings) => {
console.log('Checkout settings updated:', newSettings);
// 这里你可以根据新的 settings 做出相应的处理
const whiteGloveService = newSettings.customAttributes?.white_glove_service;
if (whiteGloveService === 'yes') {
// 用户选择了白手套服务,执行相关逻辑
console.log('White glove service is selected.');
}
});
// 清理订阅
return () => unsubscribe();
}, [checkoutSettings]);
// 同步价格
const calcPriceModel = function(floor, minute) {
// 添加
applyCartLinesChange({
type: 'addCartLine',
// 假设白手套服务有一个特定的 ID,shopify后台的产品变体ID
merchandiseId: "gid://shopify/ProductVariant/44328672887008",
quantity: 1,
// 示例一
customAttributes: [
floorItem,
minuteItem
],
// 示例二
customAttributes: [
{key: "white-glove-service", value: "199"}
]
price: 199,
});
// 删除
applyCartLinesChange({
type: 'removeCartLine',
id: "gid://shopify/ProductVariant/44328672887008",
});
}
const [productsData, setProductsData] = useState();
useEffect(() => {
async function queryApi() {
// Request a new (or cached) session token from Shopify
const token = await sessionToken.get();
console.log('sessionToken.get()', token);
const apiResponse = await fetchWithToken(token);
// Use your response
console.log('API response', apiResponse);
}
function fetchWithToken(token) {
const result = fetch(
'https://myapp.com/api/session-token',
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
return result;
}
queryApi();
}, [sessionToken]);
useEffect(() => {
query(
`query ($first: Int!) {
products(first: $first) {
nodes {
id
title
}
}
}`,
{
variables: {first: 5},
},
)
.then(({data, errors}) => setProductsData(data))
.catch(console.error);
}, [query]);
if (countryCode !== 'CA') {
return (
<List>
{data?.products?.nodes.map((node) => (
<ListItem key={node.id}>
{node.title}
</ListItem>
))}
</List>
);
}
}
UI 组件
结账 UI 扩展使用受支持的 UI 组件声明其界面。
- UI组件: https://shopify.dev/docs/api/checkout-ui-extensions/2024-07#ui-components
- UI组件列表: https://shopify.dev/docs/api/checkout-ui-extensions/2024-07/components
import {
reactExtension,
BlockStack,
InlineStack,
Button,
Image,
Text,
} from '@shopify/ui-extensions-react/checkout';
export default reactExtension(
'purchase.checkout.block.render',
() => <Extension />,
);
function Extension() {
return (
<InlineStack>
<Image source="/url/for/image" />
<BlockStack>
<Text size="large">Heading</Text>
<Text size="small">Description</Text>
</BlockStack>
<Button
onPress={() => {
console.log('button was pressed');
}}
>
Button
</Button>
</InlineStack>
);
}
React Hooks 钩子函数
在一些操作前进行逻辑拦截或验证,如果不符合,则禁止继续操作。
白手套服务案例
白手套项目white-glove-fee
- extensions/white-glove-fee/src/select.jsx
import {
reactExtension,
Grid,
View,
Checkbox,
Select,
Stepper,
useApi
} from "@shopify/ui-extensions-react/checkout";
import { useState, useEffect } from 'react';
export default reactExtension("purchase.checkout.delivery-address.render-after", () => (
<Extension />
));
function Extension() {
const {
cost,
checkoutSettings,
checkoutToken,
storage,
applyAttributeChange,
applyMetafieldChange,
instructions
} = useApi();
const allApi = useApi();
const checkoutId = checkoutToken?.current || "";
const subtotalAmount = cost?.subtotalAmount?.current?.amount || 0;
const totalAmount = cost?.totalAmount?.current?.amount || 0;
const totalShippingAmount = cost?.totalShippingAmount?.current?.amount || 0;
const totalTaxAmount = cost?.totalTaxAmount?.current?.amount || 0;
const [checked, setChecked] = useState(false);
const [floorData, setFloorData] = useState({ value: "", required: false, error: "" });
const [minuteData, setMinuteData] = useState({ value: "", required: false, error: "" });
const minuteList = [
{ value: 'under', label: 'under 30 minutes' },
{ value: 'over', label: 'over 30 minutes' }
];
console.log("allApi: ", allApi);
console.log("instructions: ", instructions);
// 清除价格显示
const clearPriceUIFunc = function() {
const data = { checked: false };
storage.write("own-checkout-white-glove-service", JSON.stringify(data));
}
useEffect(() => {
setFloorData({ value: "", required: false, error: "" });
setMinuteData({ value: "", required: false, error: "" });
clearPriceUIFunc();
}, []);
const date = new Date();
function getDateString() {
const year = date.getFullYear(); // 年(0000)
const month = date.getMonth(); // 月份(0-11)
const day = date.getDate(); // 天数(1--31)
const hours = date.getHours(); // 0 ~ 23
const minutes = date.getMinutes(); // 0 ~ 59
const seconds = date.getSeconds(); // 0 ~ 59
return year + "-" + (month + 1) + day + " " + hours + ":" + minutes + ":" + seconds
}
applyAttributeChange({
type: 'updateAttribute',
key: "white-glove-fee",
value: "初始值: " + getDateString()
});
applyMetafieldChange({
type: 'updateMetafield',
namespace: 'custom',
key: 'white_glove_service',
value: "初始值: " + getDateString()
});
// 更新订单总价
const updateOrderTotalFunc = function(floor, minute, value) {
applyAttributeChange({
type: 'updateAttribute',
key: "white-glove-fee-total",
value: value,
});
console.log("更新订单总价: ", "楼层: ", floor, "服务时间: ", minute, "总价: ", value);
}
// 同步价格
const calcPriceModel = function(floor, minute) {
if (!floor || !minute) {
clearPriceUIFunc();
return;
}
const _floor = Number(floor);
// 计算价格
const base_fee = 105;
const service_fee = minute === "over" ? 50 : 0;
const floor_fee = _floor > 2 ? (_floor - 2) * 30 : 0;
const total_fee = base_fee + service_fee + floor_fee;
// 同步价格显示
const data = { checked: true, price: total_fee };
storage.write("own-checkout-white-glove-service", JSON.stringify(data));
// 更新订单总价
updateOrderTotalFunc(floor, minute, total_fee);
}
// 选择白手套服务
const serviceChange = async () => {
const changeChecked = !checked;
setChecked(changeChecked);
if (changeChecked) {
setFloorData((prevErrors) => ({ ...prevErrors, required: true, error: "Floor is required" }));
setMinuteData((prevErrors) => ({ ...prevErrors, required: true, error: "Service time is required" }));
return;
}
setFloorData({ value: "", required: false, error: "" });
setMinuteData({ value: "", required: false, error: "" });
// 清除价格显示
const data = { checked: changeChecked };
storage.write("own-checkout-white-glove-service", JSON.stringify(data));
};
const inputFloorChange = function(value) {
if (!value && value != "0") {
clearPriceUIFunc();
setFloorData((prevErrors) => ({ ...prevErrors, value: "", required: true, error: "Floor is required and must be a number greater than 1" }));
return;
}
const _value = Number(value);
if (Number.isNaN(_value)) {
clearPriceUIFunc();
setFloorData((prevErrors) => ({ ...prevErrors, required: true, error: "The filled content must be a number" }));
return;
}
if (_value < 1) {
clearPriceUIFunc();
setFloorData((prevErrors) => ({ ...prevErrors, required: true, error: "The filled content must be a number greater than 1" }));
return;
}
setFloorData((prevErrors) => ({ ...prevErrors, value: _value, required: false, error: "" }));
// 同步价格
calcPriceModel(_value, minuteData.value);
}
const minuteValueChange = function(value) {
setMinuteData((prevErrors) => ({ ...prevErrors, value: value, required: false, error: "" }));
// 同步价格
calcPriceModel(floorData.value, value);
}
return (
<View>
<Checkbox
id="checkbox"
name="checkbox"
checked={checked}
onChange={serviceChange}
>Add White Glove Service</Checkbox>
{checked && (
<Grid columns={['30%', 'fill']}>
<View padding="tight">
<Stepper
label="Select floor"
required={floorData.required}
error={floorData.error}
value={floorData.value}
min={1}
max={200}
onChange={inputFloorChange}
/>
</View>
<View padding="tight">
<Select
label="Select service time"
required={minuteData.required}
error={minuteData.error}
options={minuteList}
value={minuteData.value}
onChange={minuteValueChange}
/>
</View>
</Grid>
)}
</View>
);
}
- extensions/white-glove-fee/src/calc.jsx
import {
reactExtension,
Grid,
View,
useApi
} from "@shopify/ui-extensions-react/checkout";
import { useState, useEffect } from 'react';
export default reactExtension("purchase.checkout.reductions.render-after", () => (
<MyCheckoutExtension />
));
function MyCheckoutExtension() {
const { storage, localization } = useApi();
const [checked, setChecked] = useState(true);
const [price, setPrice] = useState("");
useEffect(() => {
const interval = setInterval(async () => {
const readData = await storage.read("own-checkout-white-glove-service");
const data = JSON.parse(readData || "{}");
setPrice(data.price || "");
setChecked(data.checked || false);
}, 1000);
return () => clearInterval(interval);
}, []);
const isoCode = localization?.currency?.current?.isoCode?.toLowerCase();
const currency = isoCode === "cny" ? "¥": "$";
if (!checked) return null;
return (
<Grid columns={['fill', 'auto']}>
<View>White Glove Fee</View>
<View>{currency}{price}</View>
</Grid>
);
}
- shopify.extension.toml
# Learn more about configuring your checkout UI extension:
# https://shopify.dev/api/checkout-extensions/checkout/configuration
# The version of APIs your extension will receive. Learn more:
# https://shopify.dev/docs/api/usage/versioning
api_version = "2024-07"
[[extensions]]
name = "white-glove-fee"
handle = "white-glove-fee"
type = "ui_extension"
# Controls where in Shopify your extension will be injected,
# and the file that contains your extension’s source code. Learn more:
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/extension-targets-overview
extension_points = [
{ target = "purchase.checkout.delivery-address.render-after", module = "./src/select.jsx" },
{ target = "purchase.checkout.reductions.render-after", module = "./src/calc.jsx" }
]
[build]
command = "npm run build"
[extensions.capabilities]
# Gives your extension access to directly query Shopify’s storefront API.
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#api-access
api_access = true
# Gives your extension access to make external network calls, using the
# JavaScript `fetch()` API. Learn more:
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#network-access
network_access = true
# Loads metafields on checkout resources, including the cart,
# products, customers, and more. Learn more:
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#metafields
# [[extensions.metafields]]
# namespace = "my_namespace"
# key = "my_key"
# [[extensions.metafields]]
# namespace = "my_namespace"
# key = "my_other_key"
# Defines settings that will be collected from merchants installing
# your extension. Learn more:
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#settings-definition
# [extensions.settings]
# [[extensions.settings.fields]]
# key = "banner_title"
# type = "single_line_text_field"
# name = "Banner title"
# description = "Enter a title for the banner"
Shopify Extension
仅扩展程序应用是没有嵌入式应用页面的自定义应用。由于它们完全由扩展程序组成,因此仅扩展程序的应用程序可以托管在 Shopify 上。仅扩展程序应用具有 Shopify 填充的应用 URL,您可以选择更改该网址,以构建嵌入式应用页面或在 Shopify 应用商店中列出您的应用。
注意事项
应用扩展不是应用。这是一种机制,可让应用程序向多个 Shopify 用户界面的某些定义部分添加功能。使用扩展程序的应用必须遵守与不使用扩展程序的应用相同的身份验证要求和速率限制。
Shopify CLI 初始化扩展
请注意,某些扩展必须使用 Shopify CLI 进行配置。在命令行中初始化您的应用程序(如果失败,注意看看是否需要翻墙,或者注意使用nvm切换对应node版本,这里可以试试v18.16.0)
# 创建新应用,并安装 Shopify CLI 以及构建 Shopify 应用程序所需的所有依赖项
> npm init @shopify/app@latest # 这一步估计会失败,或许是因为网络问题
? Your project name?
✔ shopify-app-project
? Get started building your app:
> Start with Remix (recommended)
Start by adding your first extension
# 选择"Start by adding your first extension" 会持续出现异常,估计网络问题,暂无具体的解决方案。
# Start by adding your first extension 创建仅限扩展程序的应用?https://shopify.dev/docs/apps/app-extensions/extension-only-apps
# Downloading template from https://github.com/Shopify/shopify-app-template-none ...
# 然后下载安装依赖项 Dwonlaoding Installing dependencies ...
# 进入扩展目录
> cd shopify-app-project
> shopify app generate extension
# 出现如下信息
? Which organization is this work for?
✔ 深圳市联讯云科技有限公司
Before proceeding, your project needs to be associated with an app.
? Create this project as a new app on Shopify?
> (y) Yes, create it as a new app
(n) No, connect it to an existing app
? Which existing app is this for?
> test
Before proceeding, your project needs to be associated with an app.
? Type of extension? Type to search...
Admin
Admin action
Admin block (preview for dev stores only)
Product configuration
Subscription UI
Analytics
Web pixel
Automations
Flow action
Flow template
Flow trigger
Discounts and checkout # 折扣与结账
Checkout UI
Post-purchase UI
Cart and checkout validation - Function
Cart transformer - Function
Delivery customization - Function
Discount orders - Function
Discount products - Function
Discount shipping - Function
Fulfillment constraints - Function
Local pickup delivery option generators — Function
Payment customization - Function
Online store
Theme app extension
Point-of-Sale
POS UI
# 扩展类型
? Type of extension?
✔ Checkout UI
# 扩展程序名称
? Name your extension:
✔ checkout-ui
# 语言规则
? What would you like to work in?
✔ JavaScript React
# 然后生成中
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
Installing dependencies ...
# 成功提示
╭─ success ────────────────────────────────────────────────────────────────────╮
│ │
│ Your extension was created in extensions/checkout-ui. │
│ │
│ Next steps │
│ • To preview this extension along with the rest of the project, run │
│ `shopify app dev` │
│ │
│ Reference │
│ • For more details, see the docs [1] │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
[1] https://shopify.dev/api/checkout-extensions/checkout/configuration
问题反馈与解决措施
- 问题一:: error: RPC failed; curl 7 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 60
# 由于大文件造成的提交或者拉取失败,curl的postBuffer默认值太小,增大缓存配置
> git config --global https.postBuffer 1048576000
> git config --global http.postBuffer 1048576000
- 问题二:Your cache folder contains root-owned files,previous versions of npm which has since been addressed
> sudo chown -R 501:20 "/Users/apple/.npm"
- 问题三:Git error: RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 54
# 对于 errno 54 这个错误,经常是 http 或者 https 协议都无法正常提交。必须改为 ssh 方式来提交代码。也就是必须使用公私钥的方式进行账号验证,并提交代码。
# 登录到GitHub,在Accounting settings中选择SSH key, 点击Add SSH key
- 问题四:error: RPC failed; curl 18 transfer closed with outstanding read data remaining
# 可能是公司网络连接GitHub比较慢,下载的时候总是超时断开导致拉取失败。
# 增加最低速度连接时间
git config --global http.lowSpeedLimit 0
git config --global http.lowSpeedTime 999999
- 问题五:fatal: unable to access 'https://github.com/Shopify/extensions-templates/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443
# 暂无详细方案,待寻找中...
> /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 问题六:Failed to connect to github.com port 443: Operation timed out
工程结构示例
├─ extensions
│ ├─ checkout-ui
│ │ ├─ dist
│ │ ├─ locales
│ │ │ ├─ en.default.json
│ │ │ └─ fr.json
│ │ ├─ src
│ │ │ ├─ Checkout.jsx
│ │ ├─ package.json
│ │ └─ shopify.extension.toml
│ └─
├─ node_modules
├─ .graphqlrc.js
├─ package.json
└─ shopify.app.toml
shopify.app.toml
配置应用信息,即shopify-app-project。
- 应用配置: https://shopify.dev/docs/apps/tools/cli/configuration
- 访问范围: https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration#access_scopes
- Shopify API 访问范围:https://shopify.dev/docs/api/usage/access-scopes
- 应用程序配置:https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration
- 受保护的客户数据: https://shopify.dev/docs/apps/launch/protected-customer-data
# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
client_id = "86bda4aceba0f475d23d00c1db929460"
name = "shopify-app-project"
handle = "shopify-app-project"
application_url = "https://shopify.dev/apps/default-app-home"
embedded = true
[build]
include_config_on_deploy = true
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = ""
[auth]
redirect_urls = [ "https://shopify.dev/apps/default-app-home/api/auth" ]
[webhooks]
api_version = "2024-07"
[pos]
embedded = false
# 访问范围
scopes = "read_checkouts,write_checkouts,read_discounts,write_discounts,read_orders,write_orders,read_products,write_products,unauthenticated_read_checkouts,unauthenticated_write_checkouts,read_cart_transforms,write_cart_transforms,read_customers"
启动本地开发服务器
> cd test-extension
> npm run dev
# 出现如下信息
? Which organization is this work for?
✔ 深圳市联讯云科技有限公司
Before proceeding, your project needs to be associated with an app.
? Create this project as a new app on Shopify?
✔ No, connect it to an existing app
? Which existing app is this for?
✔ test (shopify.app.toml)
# 选择shopify partners后台中的商店
? Which store would you like to use to view your project?
> Eshen-test
ysunlight
ysunlight-test
ysungod
# 选择店铺后出现如下信息
╭─ info ───────────────────────────────────────────────────────────────────────╮
│ │
│ Using shopify.app.toml: │
│ │
│ • Org: 深圳市联讯云科技有限公司 │
│ • App: test │
│ • Dev store: ysungod.myshopify.com │
│ • Update URLs: Not yet configured │
│ │
│ You can pass `--reset` to your command to reset your app configuration. │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
# 如果失败,则出现以下信息(可能网络问题,尤其是VNP)
╭─ error ──────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Unknown error connecting to your store │
│ │
│ To investigate the issue, examine this stack trace: │
│ at fetchApiVersions (Users/apple/.nvm/versions/node/v18.16.0/lib/node_modules/@shopify/c │
│ li/dist/index.js:185640) │
│ at processTicksAndRejections (node:internal/process/task_queues:95) │
│ at async supportedApiVersions (Users/apple/.nvm/versions/node/v18.16.0/lib/node_modules/ │
│ @shopify/cli/dist/index.js:185613) │
│ at async fetchLatestSupportedApiVersion (Users/apple/.nvm/versions/node/v18.16.0/lib/nod │
│ e_modules/@shopify/cli/dist/index.js:185609) │
│ at async adminRequest2 (Users/apple/.nvm/versions/node/v18.16.0/lib/node_modules/@shopif │
│ y/cli/dist/index.js:185604) │
│ at async fetchProductVariant (Users/apple/.nvm/versions/node/v18.16.0/lib/node_modules/@ │
│ shopify/cli/dist/index.js:203895) │
│ at async buildCartURLIfNeeded (Users/apple/.nvm/versions/node/v18.16.0/lib/node_modules/ │
│ @shopify/cli/dist/index.js:203919) │
│ at async setupPreviewableExtensionsProcess (Users/apple/.nvm/versions/node/v18.16.0/lib/ │
│ node_modules/@shopify/cli/dist/index.js:208609) │
│ at async setupDevProcesses (Users/apple/.nvm/versions/node/v18.16.0/lib/node_modules/@sh │
│ opify/cli/dist/index.js:209932) │
│ at async dev2 (Users/apple/.nvm/versions/node/v18.16.0/lib/node_modules/@shopify/cli/dis │
│ t/index.js:210392) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
# 如果成功,则出现以下信息
✔ Created extension checkout-ui.
10:38:54 │ graphiql │ GraphiQL server started on port 3457
10:38:54 │ checkout-ui │ Bundling UI extension checkout-ui...
10:38:55 │ checkout-ui │ Parsed locales for extension checkout-ui at
C:/devpt/shopifys/ysungod-extension/extensions/checkout-ui
10:38:55 │ checkout-ui │ checkout-ui successfully built
10:38:55 │ checkout-ui │ Parsed locales for extension checkout-ui at
C:/devpt/shopifys/ysungod-extension/extensions/checkout-ui
10:38:56 │ checkout-ui │ Draft updated successfully for extension: checkout-ui
───────────────────────────────────────────────────────────────────────────────────────
› Press d │ toggle development store preview: ✔ on
› Press g │ open GraphiQL (Admin API) in your browser
› Press p │ preview in your browser
› Press q │ quit
Preview URL: https://twice-cologne-frequencies-greensboro.trycloudflare.com/extensions/dev-console
GraphiQL URL: http://localhost:3457/graphiql
# 在浏览器中打开预览链接 Preview URL
Developer Console
Install your app to see changes live. Share preview links with teammates or clients.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Handle Preview link Mobile Status
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
ysungod-checkout-ui-extension App home View mobile
checkout-ui purchase.checkout.block.render Connected
────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# 单击Preview link中的App home,可进入安装扩展页面,安装后,即可在Checkout Extensibility或其他页面看到实时效果。
# 单击 Preview link,进入前台店铺主页 https://ysungod.myshopify.com/
打开网址:https://yesterday-sub-bookmarks-excitement.trycloudflare.com/extensions/dev-console,出现如下界面。
- project name: shopify-app-project
- extension name: checkout-ui

- 然后单击:purchase.checkout.block.render,即可进入Checkout Extensibility页面,这时可以看到UI效果。
本地开发调试

支持扩展列表
----------------------------------------------------------------------------------
Extension type value in the TOML file --template flag value in the generate command
----------------------------------------------------------------------------------
# developer preview
Checkout UI ui_extension checkout_ui
Admin action ui_extension admin_action
# developer preview
Admin block ui_extension admin_block
Product configuration ui_extension product_configuration
Shopify Flow trigger flow_trigger flow_trigger
Shopify Flow action flow_action flow_action
Order discount function order_discounts
Product discount function product_discounts
# developer preview
Shipping discount function shipping_discounts
Delivery customization function delivery_customization
Payment customization function payment_customization
# beta
Order routing location rule function order_routing_location_rule
Cart and checkout validation function cart_checkout_validation
Cart transform function cart_transform
Fulfillment constraints function fulfillment_constraints
# beta
Post-purchase UI post_purchase_ui post_purchase_ui
Product subscription subscription_ui subscription_ui
Web pixel web_pixel web_pixel
Shopify POS UI pos_ui_extension pos_ui
Theme app extensions theme_app_extension theme_app_extension
----------------------------------------------------------------------------------
在开发商店中安装应用
在服务器运行的情况下,打开上一步终端输出的App URL部分中的 URL。

测试扩展应用效果
# 步骤一
> 进入到合作伙伴管理后台,单击"应用" -> 选择"所有应用" -> 选择指定应用,进入应用详情
# 步骤二
> 单击"测试你的应用" -> 单击"选择商店",即可安装。
远程扩展预览选择

实时UI预览效果

shopify.extension.toml
配置应用扩展信息,即white-service/extensions/checkout-ui。
# Learn more about configuring your checkout UI extension:
# https://shopify.dev/api/checkout-extensions/checkout/configuration
# The version of APIs your extension will receive. Learn more:
# https://shopify.dev/docs/api/usage/versioning
api_version = "2024-07"
# 应用扩展基础信息
[[extensions]]
name = "checkout-ui"
description = "A UI extension"
handle = "checkout-ui"
type = "ui_extension"
# Controls where in Shopify your extension will be injected,
# and the file that contains your extension’s source code. Learn more:
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/extension-targets-overview
# 目标: 表示将注入结帐 UI 扩展的位置
[[extensions.targeting]]
target = "purchase.checkout.block.render"
module = "./src/Checkout.jsx"
[[extensions.targeting]]
target = "purchase.checkout.shipping-option-item.render-after"
module = "./ShippingOptions.jsx"
# 多个扩展点
extension_points = [
{ target = "purchase.checkout.delivery-address.render-after", module = "./src/select.jsx" },
{ target = "purchase.checkout.reductions.render-after", module = "./src/calc.jsx" }
]
# 功能
[extensions.capabilities]
# Gives your extension access to directly query Shopify’s storefront API.
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#api-access
# 允许扩展查询 Storefront API
api_access = true
# Gives your extension access to make external network calls, using the
# JavaScript `fetch()` API. Learn more:
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#network-access
# 允许您在扩展中使用 JavaScript fetch() API 进行外部网络调用。如果添加此属性,则需要在合作伙伴仪表板中完成网络访问请求。
# 允许分机进行外部网络调用
network_access = true
# 声明您的扩展可能会使用 buyerJourney 拦截来阻止结账进度以强制执行验证。此功能无法保证,并且需要商家在结账编辑器中配置扩展时允许此功能。
# 声明您的扩展可能会阻止买方的进度
block_progress = true
# Loads metafields on checkout resources, including the cart,
# products, customers, and more. Learn more:
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#metafields
# 功能
# 允许扩展程序收集买家对特定策略(如短信营销)的同意
[extensions.capabilities.collect_buyer_consent]
sms_marketing = true
customer_privacy = true
# 扩展点
extension_points = [
'Checkout::Dynamic::Render'
]
# 元字段
[[extensions.metafields]]
namespace = "my_namespace"
key = "my_key"
[[extensions.metafields]]
namespace = "my-namespace"
key = "my-other-key"
# Defines settings that will be collected from merchants installing
# your extension. Learn more:
# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#settings-definition
[extensions.settings]
[[extensions.settings.fields]]
key = "banner_title"
type = "single_line_text_field"
name = "Banner title"
description = "Enter a title for the banner"
[[extensions.settings.fields]]
key = "field_key_2"
type = "number_integer"
name = "field-name-2"
[[extensions.settings.fields.validations]]
name = "min"
value = "5"
目标 [[extensions.targeting]]
目标是一个标识符,用于指定您将代码注入 Shopify API 或 Shopify 平台的其他部分的位置。
- Checkout UI 目标: https://shopify.dev/docs/api/checkout-ui-extensions/unstable/targets
- Customer Account UI 目标: https://shopify.dev/docs/api/customer-account-ui-extensions/targets
- Admin UI 目标: https://shopify.dev/docs/api/admin-extensions/extension-targets
生成扩展程序
创建和发布应用版本
发布应用版本将替换提供给已安装应用的商店的当前活动版本。应用程序用户可能需要几分钟时间才能升级到新版本。
# 要查找先前版本的名称
> shopify app versions list
> shopify app versions list [flags]
# 发布到应用
> npm run deploy
> npm run deploy --version x.x.x --message "message"
# 出现以下信息
> shopify-app-project@1.0.0 deploy
> shopify app deploy
╭─ info ───────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Using shopify.app.toml: │
│ │
│ • Org: 深圳市联讯云科技有限公司 │
│ • App: shopify-app-project │
│ • Include config: Yes │
│ │
│ You can pass `--reset` to your command to reset your app configuration. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
? Release a new version of shopify-app-project?
┃ Configuration:
┃ No changes
┃
┃ Extensions:
┃ + checkout-ui (new)
> (y) Yes, release this new version
(n) No, cancel
# 选择 y
? Release a new version of shopify-app-project?
✔ Yes, release this new version
✔ Created extension checkout-ui.
Releasing a new app version as part of shopify-app-project
checkout-ui │ Bundling UI extension checkout-ui...
checkout-ui │ checkout-ui successfully built
╭─ success ────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ New version released to users. │
│ │
│ shopify-app-project-2 │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
# 如果要创建版本,但避免将其发布给用户,请运行带有标志的命令。deploy--no-release
> npm run deploy -- --no-release
# 使用 Shopify CLI 发布现有应用版本,同时同步给所有用户
> shopify app release --version=VERSION
> shopify app release --version=basic-extension-9
可能会出现的异常错误

部署和发布
为应用添加功能后,您需要将这些功能提供给商家。应用配置和扩展是通过应用版本部署。
Shopify App Store分发




最后一步,特别注意,否则不会生效
- 步骤一:复制安装连接

- 步骤二:打开shopify商店后台,单击"设置",单击"结账",在配置区域中单击"自定义"。

- 步骤三:在自定义结账中,单击"应用"安装应用扩展。

- 步骤四:打开店铺网站,在结账页即可看到应用扩展。

Shopify Functions
Shopify 使用 将输入作为 JSON 传递给您的函数,而您的函数使用 将输出作为 JSON 写入 Shopify。
- Shopify Functions: https://shopify.dev/docs/apps/functions
- Shopify Functions API: https://shopify.dev/docs/api/functions
- 配置参数: https://shopify.dev/docs/api/functions/configuration
- 堆叠上下文 - Shopify 开发教程: https://www.youtube.com/@stackingcontext
清除缓存
run.graphql与run.js经常出现修改不生效,是因为缓存问题
# 更新graphql类型
> cd extensions\cart-checkout-validation
> npm run typegen
# 重新安装Shopify Functions
本地开发环境
# 初始化应用,具体参考Shopify Extension
> npm init @shopify/app@latest
# 切换至应用工程目录
> cd project_name
> shopify app generate extension
# 出现如下信息
Discounts and checkout
Checkout UI
Post-purchase UI
Cart and checkout validation - Function
Cart transformer - Function
Delivery customization - Function
> Discount orders - Function
Discount products - Function
Discount shipping - Function
Discounts allocator — Function
Fulfillment constraints - Function
Local pickup delivery option generators — Function
Payment customization - Function
Pickup point delivery option generators — Function
删除 Shopify Functions
直接删除extensions目录中的指定Shopify Functions即可,然后执行
npm run deploy
即可。
工程结构
# extension
├─ src
│ ├─
│ └─ index.js
├─ input.graphql
├─ schema.graphql
├─ metadata.json
├─ schema.graphql
└─ shopify.extension.toml
基础指令
> cd extensions/order-discount
# 手动输入指令调试
> echo '{"cart":{"lines": []}}' | shopify app function run --export=run
# 手动引入文件调试
cat input.json | shopify app function run --export=run
# Compile a function to wasm.
> shopify app function build
# Run a function locally for testing.
> shopify app function run
# Fetch the latest GraphQL schema for a function.
> shopify app function schema
# Generate GraphQL types for a JavaScript function.
> shopify app function typegen
标量类型
- Boolean:
- Date:
- DateTime:
- DateTimeWithoutTimezone:
- Decimal:
- Float:
- Handle:
- ID:
- Int:
- JSON:
- String:
- TimeWithoutTimezone:
- Void:
Function input输入
函数输入是一个 JSON 对象,它是您定义的 GraphQL 输入查询的结果。输入查询允许您选择函数所需的特定数据,例如 cart line product 数据或元字段。
- 具体输入数据结构: extensions/order-discount/schema.graphql
input.graphql
每当你更改了 的内容时,请确保在 extensions 文件夹中运行此命令
npm run typegen
,以使你的 index.js 文件知道新类型。
> cd /Users/apple/Downloads/shopify-test/basic-extension
> cd extensions/order-discount
# /Users/apple/Downloads/shopify-test/basic-extension/extensions/order-discount
> npm run typegen
query RunInput {
cart {
# 存在多个key,所以需要使用别名,如果单个则不用
whiteGloveFee: attribute(key: "white-glove-fee") {
value
}
whiteGloveService: attribute(key: "white_glove_service") {
value
}
# 单个key
attribute(key: "white_glove_service") {
value
}
lines {
id
quantity
cost {
amountPerQuantity {
amount
currencyCode
}
}
# Access the cart line attribute to decide if we should add a warranty
warrantyAdded: attribute(key: "Warranty Added") {
value
}
merchandise {
__typename
... on ProductVariant {
id
title
product {
# Access the metafield value to determine the cost of the warranty
warrantyCostPercentage: metafield(namespace: "$app:optional-add-ons", key: "function-configuration") {
type
value
}
}
}
}
}
}
cartTransform {
# Access the variant ID that represents the warranty product
warrantyVariantID: metafield(namespace: "$app:optional-add-ons", key: "function-configuration") {
value
}
}
}
函数所有者
Shopify 函数属于 Shopify 数据模型中的对象,并且可以影响对象的行为。与函数关联的对象称为函数所有者。例如,Discount API 函数的所有者是 a discount。
Function logic
函数逻辑是用任何可以编译满足函数需求的 WebAssembly 模块的语言编写的。函数模板和客户端库可用于 JavaScript 和 Rust。
index.js
export * from './run';
run.js
run.js
Function output输出
函数输出是一个 JSON 文档,用于描述您希望 Shopify 执行的操作。
- 具体输出数据结构: extensions/order-discount/generated/api.ts
shopify.extension.toml
创建函数时,会在函数扩展目录中自动生成该文件,更新文件后需要使用
npm run dev
重新启动扩展。该文件包含扩展的配置,其中包括扩展名称、类型、API 版本、UI 路径、生成配置和查询变量的元字段。
Shopify GraphiQL App
query {
shopifyFunctions(first:20) {
edges {
node {
id
title
apiType
inputQuery
__typename
description
useCreationUi
}
}
nodes {
id
title
}
}
shop {
id
name
}
}
函数日志
- 日志显示渠道一:合作伙伴管理后台 - 应用 - 所有应用 - 当前应用 - 扩展 - 当前扩展 - 查看日志
- 日志显示渠道二:触发事件 - 本地控制台 - 显示日志
-------------------------------------------------------------------------------
日期 (Asia/Shanghai) 状态 目标 商店
-------------------------------------------------------------------------------
2024年9月4日 10:45:09 Success成功 purchase.validation.run ysungod-001
-------------------------------------------------------------------------------
运行详细信息
资源限制
输入 (STDIN)
输出 (STDOUT)
日志 (STDERR)
Type of Function
Cart and checkout validation - Function
重要通知
已正常在shopify partners的应用扩展的日志记录中展示。
触发操作
- 单击购物车的订单数量加减操作按钮,即可触发
位置一:在购物车的订单数量加减控件下面提示信息
位置二:在结算页的顶部提示信息
操作步骤
# 后台 - Custom data - 产品 - 添加定义
名称: Max Amount
命名空间和密钥: custom.max_amount
integer:证书
最小值:1
# 后台 - 产品 - 详情 - 元字段 - Max Amount: 2
# 创建App
> npm init @shopify/app@latest
# 切换至应用工程目录
> cd project_name
> shopify app generate extension
# 更新graphql类型
> cd extensions\cart-checkout-validation
> npm run typegen
# 部署到服务器但不同步给用户
> npm run deploy -- --no-release
# 更新配置 shopify.app.toml
> npm run shopify app config use
# 本地运行
npm run dev
# 安装程序
https://ysungod-001.myshopify.com/admin/oauth/redirect_from_cli?client_id=71bc277641c4ef56116b9af25f9b6dea
# 结账规则
> 后台 - 设置 - 结账 - 结账规则 - 添加规则 - 选择该Function - 激活即可
- run.graphql
// 初始化默认
query RunInput {
cart {
lines {
quantity
}
}
}
query RunInput {
cart {
lines {
quantity
merchandise {
... on ProductVariant {
product {
handle
max_orders: metafield(
namespace: "custom",
key: "max_amount"
) {
value
}
}
}
}
}
}
}
- index.js
export * from './run';
- run.js
// @ts-check
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log(": ", JSON.stringify(input));
// 默认
const errors = input.cart.lines
.filter(({ quantity }) => quantity > 1)
.map(() => ({
localizedMessage: "Not possible to order more than one of each",
target: "cart",
}));
const errors = [];
input.cart.lines.forEach(lineItem => {
const { quantity, merchandise } = lineItem;
const max = Number.parseInt(merchandise?.product?.max_orders?.value);
if (max && quantity > max) {
errors.push({
localizeMessage: `Can't order more than ${merchandise?.product?.max_orders?.value} of ${merchandise?.product?.handle}`,
target: "cart"
});
}
});
return {
errors
}
};
- FunctionRunResult输出结构
Cart transformer - Function
编写修改购物车行项目的定价和促销的函数。
- run.graphql
// 初始化默认
query RunInput {
cart {
lines {
id
quantity
}
}
}
query RunInput {
cart {
attribute(key: "white-glove-fee") {
value
}
__typename
lines {
id
quantity
merchandise {
__typename
... on ProductVariant {
id
title
sku
weight
weightUnit
metafield(key: "") {
jsonValue
type
value
__typename
}
product {
id
title
vendor
metafield(key: "") {
jsonValue
type
value
__typename
}
}
}
}
cost {
amountPerQuantity {
amount
currencyCode
}
compareAtAmountPerQuantity {
amount
currencyCode
}
subtotalAmount {
amount
currencyCode
}
totalAmount {
amount
currencyCode
}
}
}
}
}
- index.js
export * from './run';
- run.js
// @ts-check
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @type {FunctionRunResult}
*/
const NO_CHANGES = {
operations: [],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log("Cart transformer: ", JSON.stringify(input));
const cartLineIds = input.cart.lines.map((line) => line.id);
const cartLineId = cartLineIds[0];
const ENTITY_CHANGES = {
operations: [
{
update: {
cartLineId: cartLineId,
image: {
url: "https://cdn.shopify.com/s/files/1/0642/7466/1515/files/59031454b0366_610.jpg?v=1725679779"
},
price: {
adjustment: {
fixedPricePerUnit: {
amount: 11
}
}
},
title: "白手套服务"
}
}
]
};
return ENTITY_CHANGES;
};
- FunctionRunResult输出结构
/** The run target result. */
export type FunctionRunResult = {
/** Cart operations to run on Cart. */
operations: Array<CartOperation>;
};
/** An operation to apply to the Cart. */
export type CartOperation =
/** A cart line expand operation. */
{ expand: ExpandOperation; merge?: never; update?: never; }
| /** A cart line merge operation. */
{ expand?: never; merge: MergeOperation; update?: never; }
| /** A cart line update operation. Only stores on the Shopify Plus plan can use apps with update operations. */
{ expand?: never; merge?: never; update: UpdateOperation; };
/** A cart line expand operation. */
export type ExpandOperation = {
/** The cart line id to expand. */
cartLineId: Scalars['ID'];
/** The cart items to expand. */
expandedCartItems: Array<ExpandedItem>;
/** The image of the group. */
image?: InputMaybe<ImageInput>;
/** The price adjustment to the group. */
price?: InputMaybe<PriceAdjustment>;
/** Title override. If title is not provided, variant title is used. */
title?: InputMaybe<Scalars['String']>;
};
/** A cart line merge operation. */
export type MergeOperation = {
/** The CartLine attributes to be added to the parent line. */
attributes?: InputMaybe<Array<AttributeOutput>>;
/** The list of cart lines to merge. */
cartLines: Array<CartLineInput>;
/** The image of the group. */
image?: InputMaybe<ImageInput>;
/** The product variant that models the group of lines. */
parentVariantId: Scalars['ID'];
/** The price adjustment to the group. */
price?: InputMaybe<PriceAdjustment>;
/** The name of the group of lines to merge. If it isn't specified, it will use the parent_variant' name. */
title?: InputMaybe<Scalars['String']>;
};
/** A cart line update operation. Only stores on the Shopify Plus plan can use apps with update operations. */
export type UpdateOperation = {
/** The ID of the cart line. */
cartLineId: Scalars['ID'];
/** The image override for the cart line item. */
image?: InputMaybe<ImageInput>;
/** The price adjustment for the cart line item. */
price?: InputMaybe<UpdateOperationPriceAdjustment>;
/** The title override for the cart line item. If not provided, the variant title is used. */
title?: InputMaybe<Scalars['String']>;
};
- Shopify GraphiQL App
要激活您的函数,请使用 cartTransformCreate 更改在您安装应用程序的商店中创建一个 cartTransform 对象。
query {
shopifyFunctions(first: 25) {
nodes {
app {
title
}
apiType
title
id
}
}
}
// 结果
{
"app": {
"title": "white-glove-service"
},
"apiType": "cart_transform",
"title": "cart-transformer",
"id": "cf1bdb3f-ac94-406e-8306-3bc0abe1cf5d"
}
// 更新
mutation {
cartTransformCreate(
functionId: "d634f3b0-b7e6-4bbc-b442-4a27b5e0a8a9",
blockOnFailure: false
) {
cartTransform {
id
functionId
}
userErrors {
field
message
}
}
}
- shopify.app.toml
scopes = "read_checkouts,write_checkouts,read_discounts,write_discounts,read_orders,write_orders,read_products,write_products,unauthenticated_read_checkouts,unauthenticated_write_checkouts,read_cart_transforms,write_cart_transforms,read_customers"
Delivery customization - Function
- run.graphql
query RunInput {
deliveryCustomization {
metafield(
namespace: "$app:delivery-customization",
key: "function-configuration"
) {
value
}
}
}
// 结果
{
"app": {
"title": "function-001"
},
"apiType": "cart_transform",
"title": "cart-transformer",
"id": "502f5abd-a23f-41e1-b5d4-3bd270a24658"
}
mutation {
cartTransformCreate(
functionId: "502f5abd-a23f-41e1-b5d4-3bd270a24658",
blockOnFailure: false
) {
cartTransform {
id
functionId
}
userErrors {
field
message
}
}
}
- index.js
export * from './run';
- run.js
// @ts-check
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @type {FunctionRunResult}
*/
const NO_CHANGES = {
operations: [],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log(": ", JSON.stringify(input));
const configuration = JSON.parse(
input?.deliveryCustomization?.metafield?.value ?? "{}"
);
return NO_CHANGES;
};
- FunctionRunResult输出结构
Discount orders - Function
重要通知
已正常在shopify partners的应用扩展的日志记录中展示。
触发时机
- 添加产品到购物车
- 进入结算页
- 购物车单项产品添加删减数量
run.graphql
// 初始化默认
query RunInput {
discountNode {
metafield(
namespace: "$app:order-discount",
key: "function-configuration"
) {
value
}
}
}
query RunInput {
cart {
lines {
id
quantity
merchandise {
__typename
... on ProductVariant {
id
title
sku
weight
weightUnit
product {
id
title
}
}
}
}
cost {
subtotalAmount {
amount
currencyCode
__typename
}
totalAmount {
amount
currencyCode
__typename
}
totalDutyAmount {
amount
currencyCode
__typename
}
totalTaxAmount {
amount
currencyCode
__typename
}
__typename
}
}
}
- index.js
export * from './run';
- run.js
// @ts-check
import { DiscountApplicationStrategy } from "../generated/api";
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @type {FunctionRunResult}
*/
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: []
};
const MIN_UNIQUE_PRODUCTS = 2;
const DISCOUNT_PER_PRODUCT = 5;
const MAX_DISCOUNT = 20;
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log("Discount orders1-1: ", JSON.stringify(input));
const configuration = JSON.parse(
input?.discountNode?.metafield?.value ?? "{}"
);
const uniqueProducts = input.cart.lines
// @ts-ignore
.map((line) => line.merchandise.product.id);
if (uniqueProducts.length < MIN_UNIQUE_PRODUCTS) {
return EMPTY_DISCOUNT;
}
const currencyCode = input.cart?.cost?.subtotalAmount?.currencyCode?.toLowerCase();
const currency = currencyCode === "cny" ? "¥" : "$";
const targets = input.cart.lines
.map(line => {
return {
productVariant: {
id: line.merchandise.id
}
};
});
const fixedAmount = 99;
const ENTITY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [
{
message: `${discount}% off for buying ${uniqueDiscountProducts} unique products`,
value: {
// percentage: {
// value: discount
// }
fixedAmount: {
amount: fixedAmount
}
},
targets: targets
}
]
};
return ENTITY_DISCOUNT;
};
- FunctionRunResult输出结构
export type FunctionRunResult = {
discountApplicationStrategy: DiscountApplicationStrategy;
discounts: Array<Discount>;
};
export enum DiscountApplicationStrategy {
First = 'FIRST',
Maximum = 'MAXIMUM'
}
/** The discount to be applied. */
export type Discount = {
/** The condition to apply the discount. */
conditions?: InputMaybe<Array<Condition>>;
/** The discount message. */
message?: InputMaybe<Scalars['String']>;
/**
* The targets of the discount.
*
* The value is validated against: targets should contain only one type of `Target`.
* Only a list that contains either a single `OrderSubtotalTarget` or one or more
* `ProductVariantTarget`s is valid.
*/
targets: Array<Target>;
/** The value of the discount. */
value: Value;
};
/** The discount value. */
export type Value =
/** A fixed amount value. */
{ fixedAmount: FixedAmount; percentage?: never; }
| /** A percentage value. */
{ fixedAmount?: never; percentage: Percentage; };
/** A fixed amount value. */
export type FixedAmount = {
/**
* The fixed amount value of the discount, in the currency of the cart.
*
* The amount must be greater than or equal to 0.
*/
amount: Scalars['Decimal'];
};
/** A percentage value. */
export type Percentage = {
/**
* The percentage value.
*
* The value is validated against: >= 0 and <= 100.
*/
value: Scalars['Decimal'];
};
/**
* A target of a discount.
*
* A discount can either have a single `OrderSubtotalTarget`, or one or more `ProductVariantTarget`s.
*/
export type Target =
/** If used, the discount targets the entire order subtotal after product discounts are applied. */
{ orderSubtotal: OrderSubtotalTarget; productVariant?: never; }
| /**
* A discount [Target](https://shopify.dev/api/functions/reference/product-discounts/graphql/common-objects/target) that can apply to any cart lines for a specific product variant, up to an
* optional quantity limit.
*/
{ orderSubtotal?: never; productVariant: ProductVariantTarget; };
/** If used, the discount targets the entire order subtotal after product discounts are applied. */
export type OrderSubtotalTarget = {
/**
* The list of excluded product variant IDs. Cart lines for these product variants are excluded from the order
* subtotal calculation when calculating the maximum value of the discount.
*/
excludedVariantIds: Array<Scalars['ID']>;
};
/**
* A discount [Target](https://shopify.dev/api/functions/reference/product-discounts/graphql/common-objects/target) that can apply to any cart lines for a specific product variant, up to an
* optional quantity limit.
*/
export type ProductVariantTarget = {
/** The ID of the targeted product variant. */
id: Scalars['ID'];
/**
* The maximum number of line item units to be discounted.
* The default value is `null`, which represents the total quantity of the matching line items.
*
* The value is validated against: > 0.
*/
quantity?: InputMaybe<Scalars['Int']>;
};
/** The condition to apply the discount. */
export type Condition =
/** The condition for checking the minimum subtotal amount of the order. */
{ orderMinimumSubtotal: OrderMinimumSubtotal; productMinimumQuantity?: never; productMinimumSubtotal?: never; }
| /** The condition for checking the minimum quantity of a product. */
{ orderMinimumSubtotal?: never; productMinimumQuantity: ProductMinimumQuantity; productMinimumSubtotal?: never; }
| /** The condition for checking the minimum subtotal amount of the product. */
{ orderMinimumSubtotal?: never; productMinimumQuantity?: never; productMinimumSubtotal: ProductMinimumSubtotal; };
/** The condition for checking the minimum subtotal amount of the order. */
export type OrderMinimumSubtotal = {
/** Variant IDs with a merchandise line price that's excluded to calculate the minimum subtotal amount of the order. */
excludedVariantIds: Array<Scalars['ID']>;
/** The minimum subtotal amount of the order. */
minimumAmount: Scalars['Decimal'];
/**
* The target type of the condition.
*
* The value is validated against: = "ORDER_SUBTOTAL".
*/
targetType: TargetType;
};
/** The condition for checking the minimum quantity of a product. */
export type ProductMinimumQuantity = {
/** Variant IDs with a merchandise line price that's included to calculate the minimum quantity of the product. */
ids: Array<Scalars['ID']>;
/** The minimum quantity of a product. */
minimumQuantity: Scalars['Int'];
/**
* The target type of the condition.
*
* The value is validated against: = "PRODUCT_VARIANT".
*/
targetType: TargetType;
};
/** The target type of a condition. */
export enum TargetType {
/** The target is the subtotal of the order. */
OrderSubtotal = 'ORDER_SUBTOTAL',
/** The target is a product variant. */
ProductVariant = 'PRODUCT_VARIANT'
}
/** The condition for checking the minimum quantity of a product. */
export type ProductMinimumQuantity = {
/** Variant IDs with a merchandise line price that's included to calculate the minimum quantity of the product. */
ids: Array<Scalars['ID']>;
/** The minimum quantity of a product. */
minimumQuantity: Scalars['Int'];
/**
* The target type of the condition.
*
* The value is validated against: = "PRODUCT_VARIANT".
*/
targetType: TargetType;
};
/** The condition for checking the minimum subtotal amount of the product. */
export type ProductMinimumSubtotal = {
/** Variant IDs with a merchandise line price that's included to calculate the minimum subtotal amount of a product. */
ids: Array<Scalars['ID']>;
/** The minimum subtotal amount of the product. */
minimumAmount: Scalars['Decimal'];
/**
* The target type of the condition.
*
* The value is validated against: = "PRODUCT_VARIANT".
*/
targetType: TargetType;
};
- Shopify GraphiQL App
// 检索店铺已安装的Shopify Functions应用
// Returns the Shopify Functions for apps installed on the shop.
query {
shopifyFunctions(first: 50) {
edges {
node {
id
title
}
}
}
}
// 检索结果
{
"data": {
"shopifyFunctions": {
"edges": [
{
"node": {
"id": "05e50d6b-b047-409c-9c72-1f7fc9fcc525",
"title": "order-discount"
}
}
]
}
}
}
// 更新-创建折扣
// 注意配置shopify.app.toml中scopes权限
mutation {
discountAutomaticAppCreate(automaticAppDiscount: {
title: "define-order-function",
functionId: "05e50d6b-b047-409c-9c72-1f7fc9fcc525",
startsAt: "2024-09-05T00:00:00"
}) {
automaticAppDiscount {
discountId
}
userErrors {
code
message
}
}
}
- shopify.extension.toml
api_version = "2024-07"
[[extensions]]
name = "t:name"
handle = "order-discount"
type = "function"
description = "t:description"
[[extensions.targeting]]
target = "purchase.order-discount.run"
input_query = "src/run.graphql"
export = "run"
[extensions.build]
command = ""
path = "dist/function.wasm"
[extensions.ui.paths]
create = "/"
details = "/"
- shopify.app.toml
scopes = "read_checkouts,write_checkouts,read_discounts,write_discounts,read_orders,write_orders,read_products,write_products,unauthenticated_read_checkouts,unauthenticated_write_checkouts,read_cart_transforms,write_cart_transforms,read_customers"
Discount products - Function
针对添加到购物车的每一款结算产品进行折扣
重要通知
已正常在shopify partners的应用扩展的日志记录中展示。
触发操作
- 在结算页输入discountCodeAppCreate创建的折扣码后即可触发,其他折扣码不生效。
设置产品
shipify后台 - 产品 - 产品详情 - 标签 - "Limited Edition"
- run.graphql
// 初始化默认
query RunInput {
discountNode {
metafield(
namespace: "$app:product-discount",
key: "function-configuration"
) {
value
}
}
}
// 示例
query RunInput {
cart {
lines {
merchandise {
... on ProductVariant {
id
product {
hasAnyTag(tags: ["Limited Edition"])
}
}
__typename
}
}
}
}
- index.js
export * from './run';
- run.js
// @ts-check
import { DiscountApplicationStrategy } from "../generated/api";
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @type {FunctionRunResult}
*/
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log("Discount products: ", JSON.stringify(input));
/**
* @type {{
* quantity: number
* percentage: number
* }}
*/
const configuration = JSON.parse(
input?.discountNode?.metafield?.value ?? "{}"
);
if (!configuration.quantity || !configuration.percentage) {
return EMPTY_DISCOUNT;
}
const targets = input.cart.lines
.filter((line) => {
if (line.merchandise.__typename == "ProductVariant") {
const hasLimitEditionTag = line.merchandise.product.hasAnyTag;
return hasLimitEditionTag === false;
}
})
.map(line => {
return {
productVariant: {
id: line.merchandise.id
}
};
});
if (!targets.length) {
console.error("No cart lines qualify for volume discount.");
return EMPTY_DISCOUNT;
}
const DISCOUNTED_ITEMS = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [
{
targets: targets,
value: {
percentage: {
// 符合条件的产品,折扣10%的优惠
value: 10
}
},
message: "10% OFF"
}
]
};
return DISCOUNTED_ITEMS;
};
FunctionRunResult输出结构
Shopify GraphiQL App
// 检索店铺已安装的Shopify Functions应用
// Returns the Shopify Functions for apps installed on the shop.
query {
shopifyFunctions(first: 50) {
nodes {
id
app {
title
}
apiType
title
}
}
}
// 检索结果
{
"data": {
"shopifyFunctions": {
"nodes": [
{
"id": "61a67747-add2-41e5-a1d3-ee9ebbad1ae2",
"app": {
"title": "functions-test"
},
"apiType": "product_discounts",
"title": "product-discount"
}
]
}
}
}
// 更新-创建折扣
// 注意配置shopify.app.toml中scopes权限
mutation {
discountCodeAppCreate(codeAppDiscount: {
code: "CUSTOM_PRODUCT_DISCOOUNT",
title: "Tag Excluded discount",
functionId: "61a67747-add2-41e5-a1d3-ee9ebbad1ae2",
startsAt: "2024-09-04T00:00:00"
}) {
codeAppDiscount {
discountId
}
userErrors {
field
message
}
}
}
- shopify.app.toml
scopes = "read_checkouts,write_checkouts,read_discounts,write_discounts,read_orders,write_orders,read_products,write_products,unauthenticated_read_checkouts,unauthenticated_write_checkouts,read_cart_transforms,write_cart_transforms,read_customers"
Discount shipping - Function
- run.graphql
// 初始化默认
query RunInput {
discountNode {
metafield(namespace: "$app:shipping-discount", key: "function-configuration") {
value
}
}
}
- index.js
export * from './run';
- run.js
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @type {FunctionRunResult}
*/
const EMPTY_DISCOUNT = {
discounts: [],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log(": ", JSON.stringify(input));
const configuration = JSON.parse(
input?.discountNode?.metafield?.value ?? "{}"
);
return EMPTY_DISCOUNT;
};
- FunctionRunResult输出结构
Discounts allocator — Function
- run.graphql
// 初始化默认
query RunInput {
shop {
metafield(namespace: "$app:discounts-allocator", key: "function-configuration") {
value
}
}
}
- index.js
export * from './run';
- run.js
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
const EMPTY_RESULT = {
displayableErrors: [],
lineDiscounts: [],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log(": ", JSON.stringify(input));
return EMPTY_RESULT
}
- FunctionRunResult输出结构
Fulfillment constraints - Function
- run.graphql
// 初始化默认
query RunInput {
cart {
deliverableLines {
id
}
}
fulfillmentConstraintRule {
metafield(namespace: "$app:fulfillment-constraints", key: "function-configuration") {
value
}
}
}
- index.js
export * from './run';
- run.js
// @ts-check
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @type {FunctionRunResult}
*/
const NO_CHANGES = {
operations: [],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log(": ", JSON.stringify(input));
const configuration = JSON.parse(
input?.fulfillmentConstraintRule?.metafield?.value ?? "{}"
);
return NO_CHANGES;
};
- FunctionRunResult输出结构
Local pickup delivery option generators — Function
- index.js
// @ts-check
/**
* @typedef {import("../generated/api").InputQuery} InputQuery
* @typedef {import("../generated/api").FunctionResult} FunctionResult
*/
/**
* @type {FunctionResult}
*/
const DELIVERY_OPTION = {
operations: [
{
add: {
title: "Main St.",
cost: 1.99,
pickupLocation: {
locationHandle: "2578303",
pickupInstruction: "Usually ready in 24 hours."
}
}
}
],
};
export default /**
* @param {InputQuery} input
* @returns {FunctionResult}
*/
(input) => {
console.log(": ", JSON.stringify(input));
const configuration = JSON.parse(
input?.deliveryOptionGenerator?.metafield?.value ?? "{}"
);
return DELIVERY_OPTION;
};
- FunctionRunResult输出结构
Payment customization - Function
- run.graphql
// 初始化默认
query RunInput {
paymentCustomization {
metafield(namespace: "$app:payment-customization", key: "function-configuration") {
value
}
}
}
- index.js
export * from './run';
- run.js
// @ts-check
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @type {FunctionRunResult}
*/
const NO_CHANGES = {
operations: [],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log(": ", JSON.stringify(input));
const configuration = JSON.parse(
input?.paymentCustomization?.metafield?.value ?? "{}"
);
return NO_CHANGES;
};
- FunctionRunResult输出结构
Pickup point delivery option generators — Function
- run.graphql
// 初始化默认
query RunInput {
fetchResult {
status
body
}
}
- index.js
export * from './run';
export * from './fetch';
- run.js
export function run(input) {
console.log(": ", JSON.stringify(input));
const { fetchResult } = input;
const status = fetchResult?.status;
const body = fetchResult?.body;
let operations = [];
if (status === 200 && body) {
const { deliveryPoints } = JSON.parse(body);
operations = buildPickupPointDeliveryOptionOperations(deliveryPoints);
}
return { operations };
}
function buildPickupPointDeliveryOptionOperations(externalApiDeliveryPoints) {
return externalApiDeliveryPoints
.map(externalApiDeliveryPoint => ({ add: buildPickupPointDeliveryOption(externalApiDeliveryPoint) }));
}
function buildPickupPointDeliveryOption(externalApiDeliveryPoint) {
return {
cost: null,
pickupPoint: {
externalId: externalApiDeliveryPoint.pointId,
name: externalApiDeliveryPoint.pointName,
provider: buildProvider(),
address: buildAddress(externalApiDeliveryPoint),
businessHours: buildBusinessHours(externalApiDeliveryPoint),
},
};
}
function buildProvider() {
return {
name: "Shopify Javascript Demo",
logoUrl: "https://cdn.shopify.com/s/files/1/0628/3830/9033/files/shopify_icon_146101.png?v=1706120545",
};
}
function buildAddress(externalApiDeliveryPoint) {
let location = externalApiDeliveryPoint.location;
let addressComponents = location.addressComponents;
let geometry = location.geometry.location;
let administrativeArea = addressComponents.administrativeArea;
return {
address1: `${addressComponents.streetNumber} ${addressComponents.route}`,
address2: null,
city: addressComponents.locality,
country: addressComponents.country,
countryCode: addressComponents.countryCode,
latitude: geometry.lat,
longitude: geometry.lng,
phone: null,
province: administrativeArea.name,
provinceCode: administrativeArea.code,
zip: addressComponents.postalCode,
};
}
// Transforms the opening hours of a delivery point into a vector of `BusinessHours` objects.
// Each day's opening hours are represented using a `BusinessHours` object as follows:
// "Monday: 9:00 AM – 5:00 PM" is transformed to {day: "MONDAY", periods: [{opening_time: "09:00:00", closing_time: "17:00:00"}]}
// "Tuesday: Closed" is transformed to {day: "TUESDAY", periods: []}
function buildBusinessHours(externalApiDeliveryPoint) {
if(!externalApiDeliveryPoint.openingHours) {
return null;
}
return externalApiDeliveryPoint.openingHours.weekdayText
.map(day => {
let dayParts = day.split(": ");
let dayName = dayParts[0].toUpperCase();
if (dayParts[1] === "Closed") {
return { day: dayName, periods: [] };
} else {
let openingClosingTimes = dayParts[1].split(" – ");
return {
day: dayName,
periods: [{
openingTime: formatTime(openingClosingTimes[0]),
closingTime: formatTime(openingClosingTimes[1]),
}],
};
}
});
}
// Converts a time string from 12-hour to 24-hour format.
// Example: "9:00 AM" => "09:00:00", "5:00 PM" => "17:00:00"
function formatTime(time) {
let timeParts = time.split(' ');
let hourMin = timeParts[0].split(':');
let hour = parseInt(hourMin[0]);
let min = hourMin[1];
let period = timeParts[1];
let hourIn24Format = period === 'AM' ? (hour === 12 ? 0 : hour) : (hour === 12 ? hour : hour + 12);
return `${hourIn24Format.toString().padStart(2, '0')}:${min}:00`;
}
- fetch.graphql
query FetchInput {
deliveryAddress{
countryCode
longitude
latitude
}
}
- fetch.js
export function fetch(input) {
let { countryCode, longitude, latitude } = input.deliveryAddress;
if (longitude && latitude && countryCode === 'CA') {
return {
request: buildExternalApiRequest(latitude, longitude),
};
}
return { request: null };
}
function buildExternalApiRequest(latitude, longitude) {
// The latitude and longitude parameters are included in the URL for demonstration purposes only. They do not influence the result.
let url = `https://cdn.shopify.com/s/files/1/0628/3830/9033/files/pickup-points-external-api-v2.json?v=1714588690&lat=${latitude}&lon=${longitude}`
return {
method: 'GET',
url,
headers: [{
name: "Accept",
value: "application/json; charset=utf-8"
}],
body: null,
policy: {
readTimeoutMs: 500,
},
};
}
- FunctionRunResult输出结构
Delivery Customization API
投放自定义 API,在结账时对买家可用的配送选项进行重命名、重新排序和排序。
Extension target
purchase.delivery-customization.run
- index.js
- run.js
- run.graphql
// 初始化默认
- FunctionRunResult输出结构
Order Discount API
订单折扣 API,创建应用于购物车中所有商品的新型折扣。
Extension target
purchase.order-discount.run
- index.js
export * from './run';
- run.js
// @ts-check
import { DiscountApplicationStrategy } from "../generated/api";
/**
* @typedef {import("../generated/api").RunInput} RunInput
* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
*/
/**
* @type {FunctionRunResult}
*/
const EMPTY_DISCOUNT = {
// DiscountApplicationStrategy.All
// DiscountApplicationStrategy.First
// DiscountApplicationStrategy.Maximum
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [
{
message: "",
conditions: [
{
orderMinimumSubtotal: {
excludedVariantIds: [],
minimumAmount:
},
productMinimumQuantity: {
},
productMinimumSubtotal: {
}
}
],
// 必填
target: [],
// 必填
value: ""
}
],
};
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
console.log("shopify functions:", Date.now());
console.log(input);
const configuration = JSON.parse(
input?.discountNode?.metafield?.value ?? "{}"
);
return EMPTY_DISCOUNT;
};
FunctionRunResult输出结构
run.graphql
// 初始化默认
query RunInput {
discountNode {
metafield(namespace: "$app:order-discount", key: "function-configuration") {
value
}
}
}
Product Discount API
产品折扣 API,创建应用于购物车中特定产品或产品多属性的新折扣类型。
Extension target
purchase.product-discount.run
- index.js
- run.js
FunctionRunResult输出结构
run.graphql
Payment Customization API
支付定制 API,对买家在结账时可用的付款方式进行重命名、重新排序和排序。
Extension target
purchase.payment-customization.run
- index.js
- run.js
FunctionRunResult输出结构
run.graphql
Cart Transform API
购物车转换 API,展开购物车订单项并更新购物车订单项的显示方式。
Extension target
purchase.cart-transform.run
- index.js
- run.js
FunctionRunResult输出结构
run.graphql
Cart and Checkout Validation API
购物车和结账验证 API,提供您自己的购物车验证和结账。
Extension target
purchase.validation.run
- index.js
- run.js
FunctionRunResult输出结构
run.graphql
Fulfillment Constraints API
履行约束 API,提供您自己的逻辑,说明 Shopify 应如何履行和分配订单。
Extension target
purchase.fulfillment-constraint-rule.run
- index.js
- run.js
FunctionRunResult输出结构
run.graphql
App Bridge
使用 Shopify App Bridge 构建的应用性能更高、更灵活,并且与 Shopify 后台无缝集成。您可以将 Shopify App Bridge 与 Polaris 结合使用,以提供与 Shopify 后台其余部分相匹配的一致且直观的用户体验。
注意事项
App Bridge 组件不会呈现为应用组件层次结构的一部分。它们是与 Shopify 后台通信的 JavaScript 消息的类似 React 的包装器。Shopify 后台执行 UI 渲染。

Polaris
- Shopify polaris:https://polaris.shopify.com/
GraphQL
GraphQL 是一种 API 查询语言,也是一种使用现有数据完成这些查询的运行时。向你的 API 发出一个 GraphQL 请求就能准确获得你想要的数据,不多不少。 GraphQL 查询总是返回可预测的结果。使用 GraphQL 的应用可以工作得又快又稳,因为控制数据的是应用,而不是服务器。
中文官网: https://graphql.cn/
Schema 和类型: https://graphql.cn/learn/schema/
GitHub: https://github.com/graphql
Shopify GraphiQL 应用程序: https://shopify-graphiql-app.shopifycloud.com/login
GraphQL 服务器
一个 GraphQL 查询在被验证后,GraphQL 服务器会将之执行,并返回与请求的结构相对应的结果,该结果通常会是 JSON 的格式。
GraphQL的核心机制
- 描述需要查询的数据
type Project {
name: String
tagline: String
contributors: [User]
}
- 询问想要获取的数据
{
project(name: "GraphQL") {
tagline
}
}
- 返回实际想要的数据
{
"project": {
"tagline": "A query language for APIs"
}
}
查询语法
// 作为操作类型的关键字 query 以及操作名称 HeroNameAndFriends
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
// 参数
{
human(id: "1000") {
name
height
}
}
// 别名
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
// 默认变量
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
// 指令
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
// 变更(Mutations)
// 内联片段(Inline Fragments)
变更(Mutations)
GraphQL 的大部分讨论集中在数据获取,但是任何完整的数据平台也都需要一个改变服务端数据的方法。
标量类型
- Int:有符号的 32 位整数。
- Float:有符号双精度浮点值。
- StringUTF-8 字符序列。
- Boolean:或。truefalse
- ID:ID 标量类型表示唯一标识符,通常用于重新获取对象或用作缓存的键。ID 类型的序列化方式与 String 相同;但是,将其定义为 表示它不是人类可读的。
Shopify App 应用
通过集成到 Shopify 的管理员、在线商店、结帐等中的应用程序,从而扩展Shopify的核心功能。
Shopify CLI 初始化应用
# 方法一: 会持续出现异常错误问题,需要有足够的耐心去反复尝试,估计与网络问题有关,但是暂无具体的解决方案。
> npm init @shopify/app@latest
# 方法二
> shopify app init

启动本地开发服务器
Shopify CLI 使用云耀斑以创建一个隧道,使你的应用能够使用 HTTPS URL 进行访问。
# 进入 应用程序
cd app-ysunlight-test
npm run dev
# 可能会出现一下信息
Before proceeding, your project needs to be associated with an app.
? Create this project as a new app on Shopify? # 在合伙伙伴后台中的应用存在可用的扩展APP
✔ No, connect it to an existing app
? Which existing app is this for? # 是否是当前扩展?
✔ app-ysunlight
# 打开在开发商店中安装应用的预览URL
Preview URL: https://ysunlight-test.myshopify.com/admin/oauth/redirect_from_cli?client_id=35919bed8f5d6fad03fa765d54d555e6
› Press p │ preview in your browser
# 退出
› Press q │ quit
在开发商店中测试应用

本地修改代码

实时同步商店

管理历史版本
# 查看历史版本
shopify app versions list
# 出现如下信息
╭─ info ───────────────────────────────────────────────────────────────────────╮
│ │
│ Using shopify.app.toml: │
│ │
│ • Org: 深圳市联讯云科技有限公司 │
│ • App: test │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
VERSION STATUS MESSAGE DATE CREATED CREATED BY
─────── ──────── ─────── ─────────────────── ──────────
test-1 ★ active 2024-07-15 08:02:10
View all 1 app versions in the Partner Dashboard ( https://partners.shopify.com/2562207/apps/141226737665/versions )
部署与发布
# 发布
shopify app release -- --version=VERSION
构建仅限扩展程序的应用
仅限扩展程序的应用是指没有嵌入应用页面的应用。因为它们完全由扩展程序组成,所以您可以在 Shopify 上托管仅限扩展程序的应用程序。
技术文档: https://shopify.dev/docs/apps/build/app-extensions/build-extension-only-app
具体参考:Shopify Extension 扩展
Shopify商店与应用
Shopify 合作伙伴
- 合作伙伴入口: https://www.shopify.com/partners
创建店铺



创建应用


后台管理系统

扩展工程结构
- 文件目录一
├─ shopify-app
├─ web
│ ├─ frontend # 前端项目
│ │ ├─ assets
│ │ ├─ components
│ │ ├─ hooks
│ │ ├─ pages
│ │ ├─ App.jsx
│ │ ├─ dev_embed.js
│ │ ├─ Routes.jsx
│ │ ├─ shopify.web.toml
│ │ └─ vite.config.js
│ ├─ helpers
│ ├─ middleware
│ ├─ app_installations.js
│ ├─ index.js
│ ├─ package.json
│ └─ shopify.web.toml
├─ package.json
└─ shopify.app.toml
- 文件目录二
└── <App name>
├── shopify.app.toml
├── package.json
├── node_modules/
| └── ...
├── web/
| ├── shopify.web.toml
| └── ...
├── extensions/
| ├── my-ui-extension
| | ├── shopify.extension.toml
| | ├── package.json
| | └── ...
| ├── my-function-extension
| | ├── shopify.extension.toml
| | ├── package.json
| | └── ...
| ├── my-theme-extension
| | ├── shopify.extension.toml
| | ├── package.json
| | └── ...
| └── ...
└── .env
应用配置文件
# 在应用的根目录中生成一个配置文件
shopify app config link
# 将配置文件更改推送到 Shopify,这些更改将立即生效
shopify app config push
相关技术栈
- Express构建后端。
- Vitest测试 express 后端。
- Vite构建了React前端。
- React Router用于路由。我们用基于文件的路由来包装它。
- React Query查询 Admin API。
- Shopify API 库将 OAuth 添加到 Express 后端。这允许用户安装应用程序并授予范围权限。
- App Bridge React在前端向 API 请求添加身份验证,并在嵌入式 App 的 iFrame 之外呈现组件。
- Polaris React是一个强大的设计系统和组件库,可帮助开发人员为 Shopify 商家构建高质量、一致的体验。
- 自定义挂钩向 Admin API 发出经过身份验证的请求。
- 基于文件的路由使创建新页面更容易。
Web Pixel API
使用 Shopify 像素代码管理器来管理跟踪客户事件的像素代码,使用 Web 像素应用扩展来收集行为数据,以便进行营销活动优化和分析。
- 官网: https://shopify.dev/docs/api/pixels
- Web Pixels API: https://shopify.dev/docs/api/web-pixels-api
客户事件
APP pixels
由营销和收集数据应用安装。
- 代码示例
import { register } from '@shopify/web-pixels-extension';
register((api) => {
api.analytics.subscribe('page_viewed', (event) => {
console.log(`Event Name is: ${event.name}`);
api.browser.cookie.set('my_user_id', 'ABCX123');
console.log(api.settings);
});
});
Custom pixels
由开发人员在像素代码管理器中手动添加。
- 代码示例
analytics.subscribe("checkout_completed", event => {
pixel("track", "event_name", event.data);
});
标准事件
analytics.subscribe("all_standard_events", event => {
console.log("Event data ", event?.data);
});
cart_viewed
checkout_address_info_submitted
checkout_completed
当访客完成购买时,事件会记录下来。每次结账都会触发一次,通常在“谢谢”页面上。但是,对于追加销售和发布购买,该事件会在第一个追加销售优惠页面上触发。不会在“谢谢”页面上再次触发该事件。如果应该触发事件的页面无法加载,则根本不会触发该事件。
analytics.subscribe("checkout_completed", event => {
pixel("track", "event_name", event.data);
console.log(event.data);
});
// 返回数据
{
"checkout": {
"buyerAcceptsEmailMarketing": false,
"buyerAcceptsSmsMarketing": false,
"attributes": [
{
"key": "ROC选择服务",
"value": "已选择",
"__typename": "NoteAttribute"
},
{
"key": "ROC费用",
"value": "¥135",
"__typename": "NoteAttribute"
},
{
"key": "Floor(楼层)",
"value": "1",
"__typename": "NoteAttribute"
},
{
"key": "Time(时间)",
"value": "over 30 minutes",
"__typename": "NoteAttribute"
},
{
"key": "wgs-data",
"value": "{\"channel\":\"checkout\",\"event_type\":\"white_glove_select\",\"action\":\"calc-total\",\"fee\":135,\"floor\":1,\"minute\":\"over\"}",
"__typename": "NoteAttribute"
}
],
"billingAddress": {
"address1": "这是测试单",
"address2": "梵蒂冈电饭锅电饭锅的说法个",
"city": "梵蒂冈电饭锅",
"country": "CN",
"countryCode": "CN",
"firstName": "gdfg",
"lastName": "dfgdfgdf",
"phone": null,
"province": "BJ",
"provinceCode": "BJ",
"zip": "100000"
},
"token": "0acae1cecc38440ee96c2e2b2d92fed1",
"currencyCode": "CNY",
"discountApplications": [],
"discountsAmount": {
"amount": 0,
"currencyCode": "CNY"
},
"email": "ysungod@163.com",
"phone": null,
"lineItems": [
{
"discountAllocations": [],
"id": "42685879943307",
"quantity": 1,
"title": "The Collection Snowboard: Liquid (¥749.95)",
"variant": {
"id": "42685879943307",
"image": {
"src": "https://cdn.shopify.com/s/files/1/0642/7466/1515/files/BaiduShurufa_2024-9-5_23-29-37_f79a7208-33ab-40f9-8edd-25fc779e8f14_64x64.png?v=1725550991"
},
"price": {
"amount": 884.95,
"currencyCode": "CNY"
},
"product": {
"id": "7527309148299",
"title": "The Collection Snowboard: Liquid (¥749.95)",
"vendor": "Hydrogen Vendor",
"type": "snowboard",
"untranslatedTitle": "The Collection Snowboard: Liquid",
"url": "/products/the-collection-snowboard-liquid"
},
"sku": null,
"title": null,
"untranslatedTitle": ""
},
"finalLinePrice": {
"amount": 884.95,
"currencyCode": "CNY"
},
"sellingPlanAllocation": null,
"properties": []
}
],
"localization": {
"country": {
"isoCode": "CN"
},
"language": {
"isoCode": "en-US"
},
"market": {
"id": "gid://shopify/Market/37770199179",
"handle": "cn"
}
},
"order": {
"id": "5520670130315",
"customer": {
"id": "7531332206731",
"isFirstOrder": true
}
},
"delivery": {
"selectedDeliveryOptions": [
{
"cost": {
"amount": 4.95,
"currencyCode": "CNY"
},
"costAfterDiscounts": {
"amount": 4.95,
"currencyCode": "CNY"
},
"description": null,
"handle": "0acae1cecc38440ee96c2e2b2d92fed1-86e57d7c84fa3117cd30dbbc4fef8b80",
"title": "Standard",
"type": "shipping"
}
]
},
"shippingAddress": {
"address1": "这是测试单",
"address2": "梵蒂冈电饭锅电饭锅的说法个",
"city": "梵蒂冈电饭锅",
"country": "CN",
"countryCode": "CN",
"firstName": "gdfg",
"lastName": "dfgdfgdf",
"phone": null,
"province": "BJ",
"provinceCode": "BJ",
"zip": "100000"
},
"subtotalPrice": {
"amount": 884.95,
"currencyCode": "CNY"
},
"shippingLine": {
"price": {
"amount": 4.95,
"currencyCode": "CNY"
}
},
"smsMarketingPhone": null,
"totalTax": {
"amount": 115.04,
"currencyCode": "CNY"
},
"totalPrice": {
"amount": 1004.94,
"currencyCode": "CNY"
},
"transactions": [
{
"amount": {
"amount": 1004.94,
"currencyCode": "CNY"
},
"gateway": "bogus",
"paymentMethod": {
"type": "creditCard",
"name": "BOGUS"
}
}
]
}
}
checkout_contact_info_submitted
checkout_shipping_info_submitted
checkout_started
collection_viewed
page_viewed
payment_info_submitted
product_added_to_cart
product_removed_from_cart
product_viewed
search_submitted
自定义事件
DOM事件
DOM 事件仅在 Checkout 可扩展性中可用。它们在店面上不可用。
Pixel隐私
Built for Shopify
借助 Shopify Functions、Checkout Extensibility和Embedded App Improvements(嵌入式应用程序)改进,现在可以快速灵活地针对各种业务需求定制 Shopify。
预购、预订与先试后买
购买选项应用程序为商家和客户提供各种销售和购买产品的方式,超越了“立即购买、立即付款、立即发货”的体验。例如,客户可以在专辑发行之前下订单,或者下订单订阅以每月收到一袋咖啡豆。
- 预购及更多: https://www.shopify.com/editions/summer2022/dev#pre-orders
- 自定义购买选项: https://shopify.dev/docs/apps/build/purchase-options
- Try Before You Buy (TBYB): https://shopify.dev/docs/apps/build/purchase-options/deferred
Remix
Remix 是一个基于 React 和 Node 的全栈框架,以解决开发者在用 React 开发时面临的一些棘手问题。
GraphQL Admin API
Admin API 可让您构建扩展和增强 Shopify 后台的应用程序和集成,所有 GraphQL Admin API 查询都需要有效的 Shopify 访问令牌。管理 API 允许您读取和写入 Shopify 资源数据,包括产品、客户、订单、库存、履行等。关于适用于 Node 的 Shopify 管理员 API 库。通过支持身份验证、graphql 代理、webhook 加速开发。
重要通知
一些较新的平台功能可能仅在 GraphQL 中可用。
相关特征
- 是否需要认证:是
- API格式:GraphQL or REST
- 通过 OAuth 为 Admin API创建在线或离线访问令牌。
- 向REST API发出请求。
- 向GraphQL API发出请求。
- 注册/处理 webhook。
使用条件
- 声明访问范围: https://shopify.dev/api/usage/access-scopes
- 身份验证: https://shopify.dev/apps/auth
- 速率限制: https://shopify.dev/api/usage/rate-limits
- 经过身份验证的访问范围: https://shopify.dev/docs/api/usage/access-scopes
安装配置
> yarn add @shopify/shopify-api
访问令牌
API接口
import Shopify from '@shopify/shopify-api';
//
Shopify.Auth
//
Shopify.Session
//
Shopify.Context
//
Shopify.Utils
//
Shopify.Clients
//
Shopify.Webhooks
//
Shopify.Errors
- 代码示例
const Shopify = require("@shopify/shopify-api");
const { shopifyApi, GraphqlClient } = Shopify;
const client = new GraphqlClient(
shop: "ysungod.myshopify.com",
accessToken: ""
);
const queryString = `{
products (first: 3) {
edges {
node {
id
title
}
}
}
}`
const products = await client.query({
data: queryString,
});
REST Admin API
Admin API 可让您构建扩展和增强 Shopify 后台的应用程序和集成,所有 REST Admin API 查询都需要有效的 Shopify 访问令牌。管理 API 允许您读取和写入 Shopify 资源数据,包括产品、客户、订单、库存、履行等。
相关特征
- 是否需要认证:是
- API格式:GraphQL or REST
认证机制
const client = new shopify.clients.Rest({session});
const response = client.get({path: 'shop'});
const productId = "11235813213455";
const product = await client.get({
path: `products/${productId}`,
query: {id: 1, title: "title"}
});
代码示例
const session = await Shopify.Utils.loadCurrentSession(
req,
res
);
const client = new Shopify.Clients.Rest(
session.shop,
session.accessToken
);
const product = await client.get({
path: `products/${productId}`,
query: {id: 1, title: "title"}
});
Storefront API
Storefront API用于将Shopify 购买体验扩展到超出 Shopify 内置销售渠道(如在线商店或 Shopify POS)的 Web、移动和游戏环境。
注意事项
Storefront API 是未经身份验证的公共 API,也就是说,您商店的任何访客都能看到您向该应用公开的所有数据。只有可以接受这样的风险,您才应使用 Storefront API,并且您只应授予专有应用所需数据类型的访问权限。
相关特征
- 是否需要认证:否
- API格式:GraphQL
import {
useShopQuery,
gql,
useShop,
fetchSync,
CacheLong,
useQuery,
useUrl, // 检索服务器或客户端组件中的当前 URL。
useServerProps,
} from '@shopify/hydrogen';
const QUERY = gql`
`;
export default function Example() {
const url = useUrl(); // 用于客户端组件
const {locale} = useShop();
const things = fetchSync('https://3p.api.com/things.json', {
method: 'post',
preload: true,
cache: CacheLong()
}).json();
const {data} = useShopQuery({
query: QUERY, // <String>[必填], GraphQL 查询的字符串。
variables: { // <Object>[选填],GraphQL 查询的变量对象。
handle: 'frontpage',
},
cache: { // <Object>[选填],缓存策略可帮助您确定要设置的缓存控制标头。
},
preload: false, // <Boolean>[选填],是否预加载请求。仅当CachingStrategyis not时才默认为 true CacheNone。指定false禁用或使用'*'为所有请求预加载查询。
});
const {data} = useQuery(['unique', 'key'], async () => {
const response = await fetch('https://my.api.com/data.json', {
headers: {
accept: 'application/json',
},
});
return await response.json();
});
return (
<section>
<div>{JSON.stringify(data)}</div>
<p>{locale}</p>
</section>
);
}
产品(products)
集合(collections)
客户(customers)
购物车(carts)
结账(checkouts)
其他商店资源(other store resources)
prisma ORM
下一代 Node.js 和 TypeScript ORM,Prisma 凭借其直观的数据模型、自动迁移、类型安全和自动完成功能,在使用数据库时将开发人员体验提升到一个新的水平。
Custom storefronts
自定义店面是一种构建无头的模型,其中店面的前端和后端相互独立。你构建前端。商家使用 Shopify 的商务引擎来支持他们的定制店面体验。

Hydrogen框架
Hydrogen 是一个基于 React 的框架,用于构建动态的、由 Shopify 提供支持的自定义店面。它包括您开始使用所需的结构、组件和工具,以便您可以花时间设计和设计使您的品牌独一无二的功能。
- 官网:https://shopify.dev/
- Hydrogen:https://shopify.dev/custom-storefronts/hydrogen
- GitHub:https://github.com/Shopify/hydrogen/
- shopify-cli:https://github.com/shopify/cli
安装配置
npm init @shopify/hydrogen
pnpm create @shopify/create-hydrogen
架构设计与工作原理
Hydrogen 包括一个框架,该框架提供了一组用于构建网站的最佳实践和脚手架。
Demo Store工程结构
├─public
├─src
│ ├─assets
│ ├─components
│ ├─lib
│ ├─routes
│ ├─styles
│ └─App.server.tsx
├─hydrogen.config.ts
├─package.json
└─vite.config.ts
hydrogen.config.js
import { defineConfig, CookieSessionStorage, HydrogenRequest } from '@shopify/hydrogen/config';
import hydrogen from '@shopify/hydrogen/plugin';
export default defineConfig({
// 指定查找服务器组件和 API 处理程序的路径。
routes: '/src/routes',
routes: {
files: '/path/to/routes',
basePath: '/',
},
// 包含您的应用程序连接到 Storefront API 所需的所有信息。
shopify: {
defaultCountryCode: 'US',
defaultLanguageCode: 'EN',
storeDomain: 'hydrogen-preview.myshopify.com',
storefrontToken: '3b580e70970c4528da70c98e097c2fa0',
storefrontApiVersion: '2022-07',
},
shopify: (request: HydrogenRequest) => {
const url = new URL(request.normalizedUrl);
},
// 允许您在 Hydrogen 店面中配置会话支持。
session: CookieSessionStorage('__session', {
path: '/',
httpOnly: true,
secure: import.meta.env.PROD,
sameSite: 'Strict',
maxAge: 60 * 60 * 24 * 30,
}),
// 允许您从Hydrogen 应用程序中的服务器发送分析数据。
serverAnalyticsConnectors: {},
// 该log实用程序的默认行为映射到全局console对象。
logger: {},
// 默认情况下,所有正在开发的 Hydrogen 店面都启用严格模式。
strictMode: true,
// 默认情况下,Hydrogen 使用x-powered-by: Shopify-Hydrogen标头响应。
poweredByHeader: true,
});
App.server.tsx
import {Router, FileRoutes, Route} from '@shopify/hydrogen';
function App() {
const esRoutes = import.meta.globEager('./custom-routes/es/**/*.server.jsx');
const enRoutes = import.meta.globEager('./custom-routes/en/**/*.server.jsx');
return (
<Suspense fallback={<LoadingFallback />}>
<ShopifyProvider>
<CartProvider>
<Router>
<FileRoutes />
<FileRoutes basePath="/es/" routes={esRoutes} />
<FileRoutes basePath="/en/" routes={enRoutes} />
<Route path="*" page={<NotFound />} />
</Router>
</CartProvider>
</ShopifyProvider>
</Suspense>
);
}
function NotFound() {
return <h1>Not found</h1>;
}
服务器组件(.server.jsx)
在服务器上获取数据和呈现内容的组件。它们的依赖项不在客户端捆绑包中。服务器组件不包括任何客户端交互。只有服务器组件可以调用Storefront API。
- 权限限制
- 服务器组件无法访问仅限客户端的功能,例如状态。
- 可能不使用状态,因为它们(概念上)在服务器上每个请求只执行一次。所以useState()不useReducer()支持。
- 可能不使用渲染生命周期(效果)。所以useEffect()不useLayoutEffect()支持。
- 不得使用仅限浏览器的 API,例如 DOM(除非您在服务器上对它们进行 polyfill)。
- 不得使用依赖于状态或效果的自定义钩子,或依赖于仅浏览器 API 的实用程序函数。
客户端组件(.client.jsx)
在客户端呈现的组件。客户端组件包括客户端状态交互。
- 权限限制
- 客户端组件无法访问仅服务器功能,例如文件系统,并且只能导入其他客户端组件。
- 不能从客户端组件导入和渲染服务器组件。
- 可能无法导入服务器组件或调用服务器挂钩/实用程序,因为这些仅适用于服务器。
- 不得使用仅限服务器的数据源。
共享式组件
在服务器和客户端上呈现的组件。共享组件不以.client.jsx或结尾.server.jsx。
- 权限限制
- 不能使用状态。
- 不得使用效果等渲染生命周期挂钩。
- 不得使用仅限浏览器的 API。
- 不得使用依赖于状态、效果或浏览器 API 的自定义挂钩或实用程序。
- 不得使用服务器端数据源。
- 不能渲染服务器组件或使用服务器挂钩。
Hydrogen实用程序是执行不同任务以帮助您快速开发的功能。
扁平连接(flattenConnection)
将连接对象转换为平面数组。
语法高亮(gql)
为您的 GraphQL 查询添加语法高亮。
浏览器(isBrowser)
指示代码是否在浏览器中执行。
import {isBrowser} from '@shopify/hydrogen';
export default function Example() {
if (isBrowser()) {
return <p></p>
}
}
服务器(isServer)
指示代码是否在服务器上执行。
import {isServer} from '@shopify/hydrogen';
export default function Example() {
if (isServer()) {
return <p></p>
}
}
日志(log)
记录有关应用程序的调试、警告和错误信息。
import {log} from '@shopify/hydrogen';
log.trace(); // 最低优先级的日志。这些日志非常冗长。
log.debug(); // 正常优先级日志。在内部用于记录路由时序信息。
log.warn(); // 可能会或可能不会导致应用程序失败的高优先级警告。
log.error(); // 用于错误或无效应用程序状态的日志记录。
log.fatal(); // 在进程退出之前使用的日志记录。
解析元字段(parseMetafield)
使用已根据元字段类型解析的值创建一个新的元字段对象。
import {parseMetafield, Metafield} from '@shopify/hydrogen';
解析元字段值(parseMetafieldValue)
将元字段的值从字符串解析为与元字段类型相对应的合理类型。
import {
parseMetafieldValue,
Metafield,
flattenConnection,
useShopQuery,
Metafield,
gql,
} from '@shopify/hydrogen';
查询商店(queryShop)
帮助您查询 Storefront API。
组件与Hooks钩子
帐户(Account)
- AccountActivateForm
- AccountAddressBook
- AccountAddressEdit
- AccountCreateForm
- AccountDeleteAddress
- AccountDetails
- AccountDetailsEdit
- AccountLoginForm
- AccountOrderHistory
- AccountPasswordResetForm
- AccountRecoverForm
卡片(Cards)
- ArticleCard
- CollectionCard
- OrderCard
- ProductCard
购物车(Cart)
- CartDetails
- CartEmpty
- CartLineItem
元素(Elements)
- Button
- Grid
- Heading
- Icon
- Input
- LogoutButton
- Section
- Skeleton
- Text
全局(Global)
- CartDrawer
- Drawer
- Footer
- FooterMenu
- Header
- Layout
- MenuDrawer
- Modal
- NotFound
- PageHeader
产品(Product)
- ProductDetail
- ProductForm
- ProductGallery
- ProductGrid
- ProductOptions
搜索(Search)
- NoResultRecommendations
- SearchPage
内容(Sections)
- FeaturedCollections
- Hero
- ProductCards
- ProductSwimlane
附加组件(Additional components)
- CountrySelector
- CustomFont
- DefaultSeo
- HeaderFallback
路由(Routes)
import {
Link,
Router,
FileRoutes, // 默认情况下,它会在没有传递任何 props 时加载Hydrogen 配置文件中指定的路由。
Route,
useNavigate,
useRouteParams,
useQuery,
useShopQuery,
fetchSync,
} from '@shopify/hydrogen';
export default function Example({request}) {
const {handle} = useRouteParams();
const { pathname } = new URL(request.url);
return (
<section>
<Link prefetch={false} to="/path">Link</Link>
<div>{handle}</div>
</section>
);
}
- Account
- API
- Collections
- Journal
- Pages
- Policies
- Products
- Admin
- Cart
- Index
- Robots
- Search
- Sitemap
Shopify API接口
Shopify 提供了一套 API,允许开发人员扩展平台的内置功能。这些 API 允许合作伙伴读取和写入商家数据,与其他系统和平台进行互操作,并向 Shopify 添加新功能。
- Shopify API:https://shopify.dev/docs/api
@shopify/hydrogen
@shopify/create-hydrogen
@shopify/cli-hydrogen
身份验证和授权(authenticate)
- X-Shopify-Access-Token: {access_token}
- Shopify-Storefront-Private-Token
- Shopify-Storefront-Buyer-IP
OAuth
访问令牌
会话令牌
速率限制(rate limits)
- Compare rate limits by API
API | 限速方法 | 标准限制 | Shopify Plus limit |
---|---|---|---|
Admin API (GraphQL) | 计算查询成本 | 50 点/秒 | 100 点/秒 |
Admin API (REST) | 基于请求的限制 | 2 个请求/秒 | 4 个请求/秒 |
Storefront API | 基于时间的限制 | 每个请求至少 0.5 秒,每个用户 IP 60 秒 | 每个请求至少 0.5 秒,每个用户 IP 120 秒 |
Payments Apps API (GraphQL) | 计算查询成本 | 910 点/秒 | 1820点/秒 |
批量操作
Partner API
合作伙伴 API 用于以编程方式访问合作伙伴仪表板中的数据。
- 相关特征
- 是否需要认证:是
- API格式:GraphQL
Payments Apps API
访问与支付应用程序配置相关的数据。
- 相关特征
- 是否需要认证:是
- API格式:GraphQL
Messaging API
让您的应用向 Shopify Inbox 应用发送消息。
- 相关特征
- 是否需要认证:是
- API格式:REST
Ajax API
Ajax API与Shopify 主题一起使用,无需刷新浏览器即可更新买家的购物车。示例包括获取基本产品信息、将产品添加到购物车或清除购物车内容。
- 相关特征
- 是否需要认证:否
- API格式:REST
Section Rendering API
- 相关特征
- 是否需要认证:否
- API格式:Ajax
Customer Privacy API
允许您读取和写入与客户同意被跟踪相关的 cookie。
- 相关特征
- 是否需要认证:否
- API格式:JavaScript
Oxygen
Oxygen 是Hydrogen 店面的托管平台,可通过 Hydrogen 销售渠道访问。Oxygen 消除了维护服务器基础设施的需要,同时仍使您能够管理和部署 Hydrogen 店面。您可以将不同的 Hydrogen店面部署到 Oxygen 环境,使您能够预览和共享每个店面的不同版本。
常见问题与释疑
- timed out
- 问题致因:
- 解决措施:yarn add -D @shopify/cli@3.0.15 @shopify/cli-hydrogen@3.0.15
- @shopify/hydrogen doesn't appear to be written in CJS, but also doesn't appear to be a valid ES module (i.e. it doesn't have "type": "module" or an .mjs extension for the entry point).
- Error: No ServerRequest Context found
- ERROR: TypeError: Cannot read properties of null (reading 'Head')
性能调优最佳实践
- 页面出现Something's wrong here...故障