Skip to main content

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 无法访问网络、文件系统或其他商店
Functions 替代 Scripts

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 与 Rust 的选择

选择 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,迁移是强制性的。

迁移时间线

日期里程碑
2024Functions 正式发布,Scripts 弃用公告
2025无法再创建新的 Scripts
2026 年 1 月管理后台中提供迁移工具
2026 年 6 月Scripts 永久移除

变更内容

Shopify ScriptsShopify Functions
Ruby 语言JavaScript 或 Rust(编译为 Wasm)
仅限 Plus所有计划可用
Script Editor 应用内置在你的应用中或独立
仅限于行项目、运费、支付更多扩展目标
无输入查询显式 GraphQL 输入查询
可变的购物车对象不可变输入,声明式输出

迁移策略

  1. 审计现有 Scripts:列出所有使用中的 Scripts、它们的类型(行项目、运费或支付)及其逻辑。
  2. 映射到 Function 目标:行项目脚本映射到 product-discountorder-discount。运费脚本映射到 delivery-customization。支付脚本映射到 payment-customization
  3. 重写逻辑:将 Ruby 逻辑转换为 JavaScript 或 Rust。范式不同——Functions 使用不可变输入和声明式输出,而非修改购物车对象。
  4. 构建配置 UI:Scripts 使用 Script Editor;Functions 需要在你的应用中构建面向商家的 UI。
  5. 彻底测试:使用 Function 测试工具,并针对你的 Scripts 处理的相同场景进行验证。
  6. 部署并激活:在商店上安装 Function 并停用 Script。
不要等到 2026 年 6 月

迁移需要时间,特别是对于有许多条件规则的复杂 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 开发工作流程。