Shopify Functions
Shopify Functions 是 Shopify 平台中最强大的扩展点。它们让你在 Shopify 基础设施内部运行自定义逻辑,以自定义核心商务操作:折扣、运费、支付方式、购物车转换、订单路由等。Functions 作为 WebAssembly 模块在 5 毫秒内执行完毕,速度足以在每次结账时运行而不影响性能。本课程涵盖架构、扩展目标、开发工作流程,以及从已弃用的 Shopify Scripts 的关键迁移。
什么是 Shopify Functions?
可以将 Functions 视为拦截和修改 Shopify 商务逻辑的无服务器微程序。与在你自己的服务器上运行并通过 API 通信的应用不同,Functions 在 Shopify 的服务器上作为沙箱化的 WebAssembly(Wasm)模块运行。
为什么 Functions 很重要
- 无网络延迟:你的代码在 Shopify 的服务器上运行,紧邻数据
- 保证性能:严格的执行限制(5ms、11MB 内存)确保结账速度
- 可靠性:没有外部服务器宕机,没有 Webhook 投递失败
- 可扩展:无论流量多大,每次结账都能运行
- 安全性:沙箱化的 Wasm 无法访问网络、文件系统或其他商店
Shopify Scripts(基于 Ruby 的结账自定义,之前仅限 Plus)已弃用,将于 2026 年 6 月移除。所有使用 Scripts 的商家必须迁移到 Functions。如果你为客户维护 Scripts,现在就应该开始规划迁移。
扩展目标
每个 Function 针对商务流程的特定部分。以下是可用的目标:
| 扩展目标 | 自定义内容 | 使用场景示例 |
|---|---|---|
purchase.product-discount.run | 产品级折扣 | "买2件,享9折" |
purchase.order-discount.run | 订单级折扣 | "订单满$100享8折" |
purchase.shipping-discount.run | 运费折扣 | "订单满$50免运费" |
purchase.payment-customization.run | 支付方式可见性 | 对国际订单隐藏货到付款 |
purchase.delivery-customization.run | 配送选项自定义 | 重命名、重新排序或隐藏运费选项 |
cart-transform.run | 购物车修改 | 自动捆绑产品、合并行项目 |
purchase.validation.run | 购物车/结账验证 | 执行数量限制、地址规则 |
fulfillment-constraints.run | 履行路由 | 将订单路由到特定位置 |
order-routing.run | 订单路由逻辑 | 将订单发送到最近的仓库 |
开发工作流程
创建一个 Function
使用 Shopify CLI 搭建新的 Function 脚手架:
# 导航到你的应用目录
cd ~/shopify-projects/masterclass-app
# 生成 Function 扩展
shopify app generate extension --template product_discounts --name "volume-discount"
这会在你的 extensions/ 文件夹中创建一个新目录:
extensions/volume-discount/
├── src/
│ └── run.js # Function 逻辑(Rust 则为 run.rs)
├── input.graphql # 输入查询定义
├── shopify.extension.toml # 扩展配置
└── package.json # 依赖(仅 JS)
输入查询
每个 Function 都以 GraphQL 输入查询开始,定义 Function 接收的数据。这在你的 Function 执行前运行,决定了可用的信息。
# extensions/volume-discount/input.graphql
query Input {
cart {
lines {
quantity
merchandise {
... on ProductVariant {
id
product {
id
title
hasAnyTag(tags: ["volume-eligible"])
}
}
}
cost {
amountPerQuantity {
amount
currencyCode
}
}
}
}
discountNode {
metafield(namespace: "volume-discount", key: "config") {
value
}
}
}
输入查询在每次结账时运行。保持它最小化——只请求你的 Function 实际需要的数据。不必要的字段会增加延迟和内存使用。
用 JavaScript 编写 Functions
JavaScript Functions 使用 @shopify/shopify_function 包:
// extensions/volume-discount/src/run.js
import { DiscountApplicationStrategy } from "../generated/api";
/**
* 批量折扣:购买 3 件以上符合条件的产品,享受百分比折扣。
*/
export function run(input) {
// 从元字段解析配置
const config = JSON.parse(
input?.discountNode?.metafield?.value ?? '{"quantity": 3, "percentage": 10}'
);
const targets = [];
for (const line of input.cart.lines) {
const variant = line.merchandise;
// 跳过非产品行项目(例如礼品卡)
if (variant.__typename !== "ProductVariant") continue;
// 检查产品是否符合批量折扣条件
if (!variant.product.hasAnyTag) continue;
// 检查数量是否达到阈值
if (line.quantity >= config.quantity) {
targets.push({
productVariant: {
id: variant.id,
},
});
}
}
// 如果没有目标符合条件,返回空折扣
if (targets.length === 0) {
return { discounts: [], discountApplicationStrategy: DiscountApplicationStrategy.First };
}
return {
discounts: [
{
value: {
percentage: {
value: String(config.percentage),
},
},
targets,
message: `${config.percentage}% off when you buy ${config.quantity}+`,
},
],
discountApplicationStrategy: DiscountApplicationStrategy.First,
};
}
用 Rust 编写 Functions
Rust Functions 编译为更小、更快的 Wasm 模块。当你需要最大性能或熟悉 Rust 时,它们是理想选择:
// extensions/volume-discount/src/run.rs
use shopify_function::prelude::*;
use shopify_function::Result;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Config {
quantity: i64,
percentage: f64,
}
impl Default for Config {
fn default() -> Self {
Config {
quantity: 3,
percentage: 10.0,
}
}
}
generate_types!(
query_path = "./input.graphql",
schema_path = "./schema.graphql"
);
#[shopify_function_target(query_path = "./input.graphql", schema_path = "./schema.graphql")]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
let config: Config = input
.discount_node
.metafield
.as_ref()
.and_then(|m| serde_json::from_str(&m.value).ok())
.unwrap_or_default();
let mut targets = vec![];
for line in &input.cart.lines {
if let input::InputCartLinesMerchandise::ProductVariant(variant) = &line.merchandise {
if variant.product.has_any_tag && line.quantity >= config.quantity {
targets.push(output::Target {
product_variant: Some(output::ProductVariantTarget {
id: variant.id.clone(),
}),
});
}
}
}
if targets.is_empty() {
return Ok(output::FunctionRunResult {
discounts: vec![],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
});
}
Ok(output::FunctionRunResult {
discounts: vec![output::Discount {
value: output::Value {
percentage: Some(output::Percentage {
value: config.percentage.to_string(),
}),
fixed_amount: None,
},
targets,
message: Some(format!(
"{}% off when you buy {}+",
config.percentage, config.quantity
)),
conditions: None,
}],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
})
}
选择 JavaScript:当你的团队熟悉 JS、逻辑简单直接、希望快速迭代时。JavaScript Functions 更容易编写和调试。
选择 Rust:当你需要最小的 Wasm 二进制文件、最大执行速度,或者你熟悉 Rust 时。Rust Functions 通常编译为 50-100KB,而 JavaScript 为 200-500KB。
两者都编译为 WebAssembly 并在相同的沙箱中运行。对于典型的折扣/运费逻辑,性能差异通常可以忽略不计。
扩展配置
shopify.extension.toml 文件配置你的 Function:
name = "volume-discount"
type = "product_discounts"
api_version = "2026-04"
[build]
command = "npm exec -- shopify app function build"
path = "dist/function.wasm"
watch = ["src/**/*.js"]
[ui]
enable_create = true
[ui.paths]
create = "/app/volume-discount/:functionId/create"
details = "/app/volume-discount/:functionId/:id"
[ui] 部分很重要——它告诉 Shopify 商家在你的应用 UI 中配置折扣的位置。当商家使用你的 Function 创建新折扣时,Shopify 会在你的嵌入式应用中加载 ui.paths.create 中指定的 URL。
测试 Functions
本地测试
在部署前本地测试 Functions:
# 使用示例输入运行 Function
shopify app function run --path extensions/volume-discount
# 使用特定输入文件测试
echo '{"cart":{"lines":[{"quantity":5,"merchandise":{"__typename":"ProductVariant","id":"gid://shopify/ProductVariant/1","product":{"id":"gid://shopify/Product/1","title":"Widget","hasAnyTag":true}},"cost":{"amountPerQuantity":{"amount":"10.00","currencyCode":"USD"}}}]},"discountNode":{"metafield":{"value":"{\"quantity\":3,\"percentage\":15}"}}}' | shopify app function run --path extensions/volume-discount
单元测试(JavaScript)
// extensions/volume-discount/src/run.test.js
import { describe, it, expect } from "vitest";
import { run } from "./run";
describe("volume-discount function", () => {
it("当数量达到阈值时应用折扣", () => {
const input = {
cart: {
lines: [
{
quantity: 5,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: true },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: {
metafield: {
value: '{"quantity": 3, "percentage": 10}',
},
},
};
const result = run(input);
expect(result.discounts).toHaveLength(1);
expect(result.discounts[0].value.percentage.value).toBe("10");
expect(result.discounts[0].targets).toHaveLength(1);
});
it("当数量低于阈值时不返回折扣", () => {
const input = {
cart: {
lines: [
{
quantity: 1,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: true },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: {
metafield: {
value: '{"quantity": 3, "percentage": 10}',
},
},
};
const result = run(input);
expect(result.discounts).toHaveLength(0);
});
it("跳过不符合条件的产品", () => {
const input = {
cart: {
lines: [
{
quantity: 5,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: false },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: { metafield: null },
};
const result = run(input);
expect(result.discounts).toHaveLength(0);
});
});
Scripts 到 Functions 的迁移
Shopify Scripts(基于 Ruby,仅限 Plus)已弃用,2026 年 6 月为移除截止日期。如果你或你的客户使用 Scripts,迁移是强制性的。
迁移时间线
| 日期 | 里程碑 |
|---|---|
| 2024 | Functions 正式发布,Scripts 弃用公告 |
| 2025 | 无法再创建新的 Scripts |
| 2026 年 1 月 | 管理后台中提供迁移工具 |
| 2026 年 6 月 | Scripts 永久移除 |
变更内容
| Shopify Scripts | Shopify Functions |
|---|---|
| Ruby 语言 | JavaScript 或 Rust(编译为 Wasm) |
| 仅限 Plus | 所有计划可用 |
| Script Editor 应用 | 内置在你的应用中或独立 |
| 仅限于行项目、运费、支付 | 更多扩展目标 |
| 无输入查询 | 显式 GraphQL 输入查询 |
| 可变的购物车对象 | 不可变输入,声明式输出 |
迁移策略
- 审计现有 Scripts:列出所有使用中的 Scripts、它们的类型(行项目、运费或支付)及其逻辑。
- 映射到 Function 目标:行项目脚本映射到
product-discount或order-discount。运费脚本映射到delivery-customization。支付脚本映射到payment-customization。 - 重写逻辑:将 Ruby 逻辑转换为 JavaScript 或 Rust。范式不同——Functions 使用不可变输入和声明式输出,而非修改购物车对象。
- 构建配置 UI:Scripts 使用 Script Editor;Functions 需要在你的应用中构建面向商家的 UI。
- 彻底测试:使用 Function 测试工具,并针对你的 Scripts 处理的相同场景进行验证。
- 部署并激活:在商店上安装 Function 并停用 Script。
迁移需要时间,特别是对于有许多条件规则的复杂 Scripts。现在就开始规划。在开发商店中测试 Functions,并在 2026 年初开始向生产商店推出。等到最后一个月会有结账中断的风险。
迁移示例:行项目 Script 到产品折扣 Function
原始 Ruby Script(Script Editor):
Input.cart.line_items.each do |line_item|
if line_item.quantity >= 3
line_item.change_line_price(
line_item.line_price * 0.9,
message: "10% volume discount"
)
end
end
Output.cart = Input.cart
迁移后的 JavaScript Function:
export function run(input) {
const discounts = [];
for (const line of input.cart.lines) {
if (line.quantity >= 3) {
discounts.push({
value: { percentage: { value: "10" } },
targets: [{ productVariant: { id: line.merchandise.id } }],
message: "10% volume discount",
});
}
}
return {
discounts,
discountApplicationStrategy: "FIRST",
};
}
Function 实现了相同的结果,但使用不可变的输入/输出模式,而不是直接修改购物车。
执行限制
Functions 在受限环境中运行以保护结账性能:
| 限制 | 值 |
|---|---|
| 执行时间 | 5ms |
| 内存 | 11 MB |
| Wasm 二进制大小 | 256 KB |
| 输入大小 | 64 KB |
| 输出大小 | 64 KB |
| 网络访问 | 无 |
| 文件系统访问 | 无 |
保持你的 Function 逻辑简单专注。避免深度嵌套、大型数据结构和复杂的字符串操作。如果你达到了 5ms 限制,请分析你的代码并寻找不必要的迭代。Rust Functions 通常比 JavaScript 更快且使用更少的内存。
要点总结
- Shopify Functions 在 Shopify 基础设施内以 WebAssembly 模块运行自定义逻辑
- 它们快速(低于 5ms)、可靠(无外部服务器)且可扩展(在每次结账时运行)
- 扩展目标涵盖折扣、运费、支付、购物车转换、验证和履行
- 使用 JavaScript(更简单)或 Rust(更小、更快)编写 Functions
- 输入查询定义你的 Function 接收的数据;输出是声明式的
- Scripts 已弃用——在 2026 年 6 月前迁移到 Functions
- 使用
shopify app function run和单元测试进行本地测试
这就完成了 Shopify 基础模块。你现在对平台的架构、API、模板、无头选项和扩展运行时有了深入的理解。在下一个模块中,我们将探索 Claude Code 如何改变你的 Shopify 开发工作流程。