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 vs 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("applies discount when quantity meets threshold", () => {
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("returns no discounts when quantity is below threshold", () => {
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("skips non-eligible products", () => {
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 GA,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 開發工作流程。