Shopify Scripts to Functions 迁移:2026 代码教程

Shopify Scripts to Functions 迁移:2026 代码教程

Shopify Scripts to Functions 迁移:2026 代码教程

如何将 Shopify Scripts 迁移至 Functions:完整代码教程(2026版)— Revize 博客文章标题

今天是 2026 年 4 月 16 日。昨天——4 月 15 日——是 Shopify 永久锁定 Script Editor 的日子。你无法再创建或发布新的 Script。执行关停将在 75 天后落地,即 2026 年 6 月 30 日

如果你是 Shopify Plus 开发者,或者是运营 Plus 商店的服务商,并且在过去 12 个月中一直把这个迁移任务推迟到“下个 Sprint”,那么你遇到麻烦了。这不是一个“修不修都行”的问题,而是一个“你的结账页面将在 71 日午夜崩溃”的问题。大多数 Plus 商店多年来累积了 5 到 20 个 Script,每个都在默默运行着没人记得谁写过的折扣规则、隐藏货运方式或支付网关。

本指南是我们在 1 月份梦寐以求的技术迁移手册。 它涵盖了实际代码,而不仅仅是策略。读完本文,你将了解如何使用 Shopify CLI 构建 Function 模板,如何编写 Rust 或 JavaScript 逻辑来处理折扣、配送自定义及支付自定义,如何在标记的高级客户子集上安全地进行测试,并将其推送到生产环境而不损坏现有的结账页面。

让我们把 Scripts 从你的商店中移除,并将 Functions 部署上去。



Developer migrating Shopify Scripts to Shopify Functions modules

快速解答:60 秒内将 Scripts 转换为 Functions

一句话总结迁移: Shopify Scripts(Script Editor 中的 Ruby 代码,仅限 Plus)正被 Shopify Functions(用 Rust 或 JavaScript 编写的 WebAssembly 模块,适用于所有方案)取代。你可以使用 shopify app generate extension 构建 Function,编写一个 run.graphql 查询来获取所需的购物车数据,编写一个 run.rsrun.js 文件来返回操作(折扣、隐藏货运方式等),然后使用 shopify app deploy 进行部署,并通过后台或 GraphQL mutation 进行激活。Functions 作为编译后的 WASM 运行,延迟低于 5 毫秒,适用于所有方案,并且是 Shopify 未来支持的唯一自定义途径。

6 月 30 日到底会发生什么变化

在触碰任何代码之前,先理清日期。有两个关键日期,它们都很重要。


日期

发生事件

你的行动

2026 年 4 月 15 日 (已过)

Script Editor 设为只读。无法新建 Script。无法编辑现有 Script。

现有 Script 仍可执行。立即迁移,否则锁定你的逻辑。

2026 年 6 月 30 日

所有 Shopify Scripts 停止执行。绝无例外。

替代的 Function 必须在此时前上线。

迁移结果是二元的。要么你的 Function 在 6 月 30 日前内部署好且结账页面继续正常工作,要么没有部署——导致每个受阻的购物车默默恢复到标准定价、标准运费,且启用每一个支付方式。没有折中方案。Script 要么运行,要么不运行,而在 6 月 30 日之后它将无法运行。

提示: 在 Shopify 后台中打开 Settings → Checkout → Customizations Report。它列出了你商店中每个激活的 Script、它的作用以及推荐的 Function 替代类型。从这里开始。

Functions 与 Scripts:究竟改变了什么


维度

Shopify Scripts (已废弃)

Shopify Functions (替代方案)

语言

Ruby DSL(Shopify 专用)

Rust, JavaScript, TypeScript

运行时

Shopify 基础设施上的沙盒 Ruby

WebAssembly (WASM) —— 运行延迟低于 5毫秒

方案可用性

仅限 Plus

所有方案(自定义应用需要 Plus;公开应用无限制)

编辑器

后台 Script Editor

本地 IDE + Shopify CLI

版本控制

无 —— 实时编辑

Git 友好 —— 完整的版本控制

测试

在结账页面中手动测试

使用 shopify app dev 进行本地开发和预览链接测试

部署

在后台点击 “保存”

在终端运行 shopify app deploy

对象

行项目,货运,支付

Discounts, Cart Transform, Validation, Delivery Customization, Payment Customization, Order Routing, Fulfillment Constraints 等

架构转变至关重要。Scripts 是“在文本框里微调 Ruby”。Functions 是“编写真正的应用,进行版本控制,本地测试,通过真正的 CI 管道部署”。这门槛更高。但这也是你在可预见的未来中针对结账逻辑进行的最后一次迁移——Functions 是 Shopify 的长期承诺,而不是像 Scripts 那样最终被淘汰的权宜之计。

将你的 Scripts 映射到正确的 Function 类型

你目前的每一个 Script 都完全映射到一个 Function API。这里是需要钉在显示器上的对照表。


旧 Script 类型

功能

新 Function API

Function 运行对象

Line Item Script

对特定的商品/客户/购物车条件应用折扣

Cart & Checkout Discounts API

cart.lines.discounts.generate.run

Shipping Script(折扣)

基于购物车规则的免运费/折扣运费

Cart & Checkout Discounts API

cart.delivery-options.discounts.generate.run

Shipping Script(隐藏/重命名/重排)

隐藏超过指定金额的运费,将 “Standard” 重命名为 “Free over $50”

Delivery Customization API

cart.delivery-options.transform.run

Payment Script

对 B2B 隐藏 PayPal,隐藏超过 500 美元的 COD,重新排序支付方式

Payment Customization API

cart.payment-methods.transform.run

购物车修改 Script(罕见)

组合商品,更换行项目

Cart Transform API

cart.transform.run

阻止结账 Script

如果 SKU 组合无效,则拒绝购物车

Cart & Checkout Validation API

cart.validations.generate.run

如果你有 10 个 Script,你可能只需要构建 3 到 5 个 Function——多个 Script 通常可以通过更清晰的分支逻辑合并为一个 Function。



Shopify Functions unifying discounts, delivery, and payment customizations

前提条件:设置你的本地开发环境

在构建任何 Function 之前,你需要在本地安装三样东西。在终端中运行这些检查。

1. Node.js 18+


node --version
# 必须 >= 18.0.0
node --version
# 必须 >= 18.0.0

如果版本偏旧,通过 nvm 安装或在 nodejs.org 下载。

2. Shopify CLI 3+


npm install -g @shopify/cli@latest
shopify version
# 应当输出 3.x 或更高版本
npm install -g @shopify/cli@latest
shopify version
# 应当输出 3.x 或更高版本

3. Rust 工具链(仅当你打算用 Rust 编写 Functions 时)


curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-wasip1
cargo --version
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-wasip1
cargo --version

JavaScript Functions 不需要 Rust。为你的团队选择一种语言并坚持使用——混用两种语言会增加维护开销。

4. 开发商店

登录你的 Partner 仪表板并创建一个新的开发商店,或者使用现有的商店。在将 Functions 推送到生产环境之前,你将把它们部署到此开发商店中。

构建你的第一个 Function

CLI 负责了绝大部分样板代码。在任意目录下:


# 创建一个新的 Shopify app(如果你已有,请跳过此步)
shopify app init my-checkout-functions
cd my-checkout-functions

# 生成一个 Function 扩展
shopify app generate extension
# 创建一个新的 Shopify app(如果你已有,请跳过此步)
shopify app init my-checkout-functions
cd my-checkout-functions

# 生成一个 Function 扩展
shopify app generate extension

CLI 将引导你完成提示。对于折扣 Function,你需要选择:

  • Type: Function

  • Template: discount(或 cart_checkout_validation, delivery_customization, payment_customization 等)

  • Language: Rust 或 JavaScript

  • Name: 类似于 volume-discount-fn

这会创建 extensions/volume-discount-fn/,包含:


extensions/volume-discount-fn/
├── shopify.extension.toml      # Function 配置 —— 目标、构建、版本
├── src/
├── cart_lines_discounts_generate_run.graphql   # 输入查询
└── cart_lines_discounts_generate_run.rs        # Function 逻辑
├── Cargo.toml                  # Rust 依赖(仅限 Rust)
└── README.md
extensions/volume-discount-fn/
├── shopify.extension.toml      # Function 配置 —— 目标、构建、版本
├── src/
├── cart_lines_discounts_generate_run.graphql   # 输入查询
└── cart_lines_discounts_generate_run.rs        # Function 逻辑
├── Cargo.toml                  # Rust 依赖(仅限 Rust)
└── README.md

你需要经常编辑的三个文件是 .toml(配置)、.graphql(输入)和 .rs / .js(逻辑)。就这些。

教程 1:替换 Line Item Script(数量折扣)

假设你的旧 Script 在购物车包含特定 Collection 的 5 件以上商品时,在订单小计中给予 10% 的折扣。下面是对应的 Function 实现方式。

步骤 1.1:配置(shopify.extension.toml


api_version = "2026-01"

[[extensions]]
name = "volume-discount-fn"
handle = "volume-discount-fn"
type = "function"

[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cart_lines_discounts_generate_run"

[extensions.build]
command = "cargo build --target=wasm32-wasip1 --release"
path = "target/wasm32-wasip1/release/volume-discount-fn.wasm"
watch = ["src/**/*.rs"]
api_version = "2026-01"

[[extensions]]
name = "volume-discount-fn"
handle = "volume-discount-fn"
type = "function"

[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cart_lines_discounts_generate_run"

[extensions.build]
command = "cargo build --target=wasm32-wasip1 --release"
path = "target/wasm32-wasip1/release/volume-discount-fn.wasm"
watch = ["src/**/*.rs"]

步骤 1.2:输入查询(src/cart_lines_discounts_generate_run.graphql


query Input {
  cart {
    lines {
      id
      quantity
      cost {
        subtotalAmount {
          amount
        }
      }
      merchandise {
        ... on ProductVariant {
          product {
            inAnyCollection(ids: ["gid://shopify/Collection/123456789"])
          }
        }
      }
    }
  }
  discount {
    discountClasses
  }
}
query Input {
  cart {
    lines {
      id
      quantity
      cost {
        subtotalAmount {
          amount
        }
      }
      merchandise {
        ... on ProductVariant {
          product {
            inAnyCollection(ids: ["gid://shopify/Collection/123456789"])
          }
        }
      }
    }
  }
  discount {
    discountClasses
  }
}

提示: Functions 只能查询到你请求的数据。保持 GraphQL 最小化——你忽略的每个字段都意味着更快速、更实惠的运行。

步骤 1.3:逻辑(src/cart_lines_discounts_generate_run.rs


use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;

#[shopify_function]
fn cart_lines_discounts_generate_run(
    input: schema::cart_lines_discounts_generate_run::Input,
) -> Result<schema::CartLinesDiscountsGenerateRunResult> {
    // 如果折扣类别不匹配,直接退出
    let has_order_discount = input
        .discount()
        .discount_classes()
        .contains(&schema::DiscountClass::Order);

    if !has_order_discount {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    // 累加目标 Collection 中商品的数量
    let qualifying_qty: i64 = input
        .cart()
        .lines()
        .iter()
        .filter(|line| {
            if let schema::Merchandise::ProductVariant(v) = line.merchandise() {
                *v.product().in_any_collection()
            } else {
                false
            }
        })
        .map(|line| *line.quantity())
        .sum();

    if qualifying_qty < 5 {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    // 在订单小计上应用 10% 折扣
    Ok(schema::CartLinesDiscountsGenerateRunResult {
        operations: vec![schema::CartOperation::OrderDiscountsAdd(
            schema::OrderDiscountsAddOperation {
                selection_strategy: schema::OrderDiscountSelectionStrategy::First,
                candidates: vec![schema::OrderDiscountCandidate {
                    targets: vec![schema::OrderDiscountCandidateTarget::OrderSubtotal(
                        schema::OrderSubtotalTarget {
                            excluded_cart_line_ids: vec![],
                        },
                    )],
                    message: Some("Volume discount: 10% off".to_string()),
                    value: schema::OrderDiscountCandidateValue::Percentage(
                        schema::Percentage { value: Decimal(10.0) }
                    ),
                    conditions: None,
                    associated_discount_code: None,
                }],
            },
        )],
    })
}
use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;

#[shopify_function]
fn cart_lines_discounts_generate_run(
    input: schema::cart_lines_discounts_generate_run::Input,
) -> Result<schema::CartLinesDiscountsGenerateRunResult> {
    // 如果折扣类别不匹配,直接退出
    let has_order_discount = input
        .discount()
        .discount_classes()
        .contains(&schema::DiscountClass::Order);

    if !has_order_discount {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    // 累加目标 Collection 中商品的数量
    let qualifying_qty: i64 = input
        .cart()
        .lines()
        .iter()
        .filter(|line| {
            if let schema::Merchandise::ProductVariant(v) = line.merchandise() {
                *v.product().in_any_collection()
            } else {
                false
            }
        })
        .map(|line| *line.quantity())
        .sum();

    if qualifying_qty < 5 {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    // 在订单小计上应用 10% 折扣
    Ok(schema::CartLinesDiscountsGenerateRunResult {
        operations: vec![schema::CartOperation::OrderDiscountsAdd(
            schema::OrderDiscountsAddOperation {
                selection_strategy: schema::OrderDiscountSelectionStrategy::First,
                candidates: vec![schema::OrderDiscountCandidate {
                    targets: vec![schema::OrderDiscountCandidateTarget::OrderSubtotal(
                        schema::OrderSubtotalTarget {
                            excluded_cart_line_ids: vec![],
                        },
                    )],
                    message: Some("Volume discount: 10% off".to_string()),
                    value: schema::OrderDiscountCandidateValue::Percentage(
                        schema::Percentage { value: Decimal(10.0) }
                    ),
                    conditions: None,
                    associated_discount_code: None,
                }],
            },
        )],
    })
}

步骤 1.4:测试、部署和激活


# 本地开发,支持热重载
shopify app dev

# 准备就绪后进行部署
shopify app deploy

# 在弹出的 GraphiQL 面板(在开发终端按 `g` 键)中,
# 创建运行你 Function 的自动折扣:
# 本地开发,支持热重载
shopify app dev

# 准备就绪后进行部署
shopify app deploy

# 在弹出的 GraphiQL 面板(在开发终端按 `g` 键)中,
# 创建运行你 Function 的自动折扣:
mutation {
  discountAutomaticAppCreate(
    automaticAppDiscount: {
      title: "Volume Discount (5+ collection items)"
      functionHandle: "volume-discount-fn"
      discountClasses: [ORDER]
      startsAt: "2026-04-16T00:00:00Z"
    }
  ) {
    automaticAppDiscount { discountId }
    userErrors { field message }
  }
}
mutation {
  discountAutomaticAppCreate(
    automaticAppDiscount: {
      title: "Volume Discount (5+ collection items)"
      functionHandle: "volume-discount-fn"
      discountClasses: [ORDER]
      startsAt: "2026-04-16T00:00:00Z"
    }
  ) {
    automaticAppDiscount { discountId }
    userErrors { field message }
  }
}

完成了。该 Function 已经上线,纳入了版本控制,且完全替代了旧的 Script。

教程 2:替换 Shipping Script(在购物车金额达到阈值时隐藏配送方式)

常见的 Script 场景:“当购物车小计超过 500 美元时隐藏 Express Shipping,以防止大额订单产生昂贵的隔夜运费。” 对应的 Delivery Customization Function 如下。

步骤 2.1:构建模板


shopify app generate extension --template delivery_customization --name hide-express-fn
shopify app generate extension --template delivery_customization --name hide-express-fn

步骤 2.2:输入查询(src/run.graphql


query Input {
  cart {
    cost {
      subtotalAmount {
        amount
      }
    }
    deliveryGroups {
      deliveryOptions {
        handle
        title
      }
    }
  }
}
query Input {
  cart {
    cost {
      subtotalAmount {
        amount
      }
    }
    deliveryGroups {
      deliveryOptions {
        handle
        title
      }
    }
  }
}

步骤 2.3:逻辑(src/run.js —— JavaScript 版本)


// @ts-check
/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
 */

const NO_CHANGES = { operations: [] };
const THRESHOLD = 500.0;
const HIDE_TITLES = ["Express", "Overnight"];

/**
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function run(input) {
  const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount);
  if (subtotal < THRESHOLD) return NO_CHANGES;

  const operations = input.cart.deliveryGroups.flatMap((group) =>
    group.deliveryOptions
      .filter((opt) => HIDE_TITLES.some((t) => opt.title.includes(t)))
      .map((opt) => ({
        hide: { deliveryOptionHandle: opt.handle },
      }))
  );

  return { operations };
}
// @ts-check
/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
 */

const NO_CHANGES = { operations: [] };
const THRESHOLD = 500.0;
const HIDE_TITLES = ["Express", "Overnight"];

/**
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function run(input) {
  const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount);
  if (subtotal < THRESHOLD) return NO_CHANGES;

  const operations = input.cart.deliveryGroups.flatMap((group) =>
    group.deliveryOptions
      .filter((opt) => HIDE_TITLES.some((t) => opt.title.includes(t)))
      .map((opt) => ({
        hide: { deliveryOptionHandle: opt.handle },
      }))
  );

  return { operations };
}

步骤 2.4:通过后台激活(无需使用 GraphQL)

Delivery Customizations 包含一个内置的后台 UI。运行 shopify app deploy 之后:

  1. 转到 Settings → Shipping and delivery

  2. 向下滚动至底部的 Customizations 部分

  3. 点击 Add customization → 选择你的 Function

  4. 保存并启用

对应的隐藏规则已在生产环境中生效。无需编写 mutation。



Shopify delivery customization Function hiding shipping option at checkout

教程 3:替换 Payment Script(对 B2B 客户隐藏 COD)

旧 Script:“对任何标记为 'B2B' 的客户隐藏货到付款 (COD)。” 下面是 Payment Customization 的版本。

步骤 3.1:构建模板


shopify app generate extension --template payment_customization --name hide-cod-b2b-fn
shopify app generate extension --template payment_customization --name hide-cod-b2b-fn

步骤 3.2:输入查询


query Input {
  cart {
    buyerIdentity {
      customer {
        hasTags(tags: [{ tag: "B2B" }]) {
          tag
          hasTag
        }
      }
    }
  }
  paymentMethods {
    id
    name
  }
}
query Input {
  cart {
    buyerIdentity {
      customer {
        hasTags(tags: [{ tag: "B2B" }]) {
          tag
          hasTag
        }
      }
    }
  }
  paymentMethods {
    id
    name
  }
}

步骤 3.3:逻辑(src/run.js


const NO_CHANGES = { operations: [] };

export function run(input) {
  const tagCheck = input.cart?.buyerIdentity?.customer?.hasTags?.[0];
  const isB2B = tagCheck?.hasTag === true;
  if (!isB2B) return NO_CHANGES;

  const codMethod = input.paymentMethods.find((pm) =>
    pm.name.toLowerCase().includes("cash on delivery")
  );
  if (!codMethod) return NO_CHANGES;

  return {
    operations: [{ hide: { paymentMethodId: codMethod.id } }],
  };
}
const NO_CHANGES = { operations: [] };

export function run(input) {
  const tagCheck = input.cart?.buyerIdentity?.customer?.hasTags?.[0];
  const isB2B = tagCheck?.hasTag === true;
  if (!isB2B) return NO_CHANGES;

  const codMethod = input.paymentMethods.find((pm) =>
    pm.name.toLowerCase().includes("cash on delivery")
  );
  if (!codMethod) return NO_CHANGES;

  return {
    operations: [{ hide: { paymentMethodId: codMethod.id } }],
  };
}

步骤 3.4:激活

Payment Customizations 在后台的 Settings → Payments → Customizations 下也有一个配置 UI。配送的激活步骤相同——选择你的 Function、保存,即可完成。

既然你正在阅读本文 —— 谈谈付款后流程(Post-Purchase)

既然这是 Revize 的博客,那就做个快速披露。Revize 负责处理 Functions 无法触碰的事物——只要订单下达后,客户可能想要增加商品、更换尺码、修改货运地址或应用遗忘的折扣。Functions 适用于结账阶段,而 Revize 适用于付款后。 Functions 决定哪些商品可以留在购物车;Revize 则允许你的客户和客服团队在事后修改订单,无需执行退款再重建订单的操作。这对于两类商店而言十分重要:一类是订单量庞大以至于手动编辑工作会崩溃的商店(Plus 运营商显然是,还有大流量的 Advanced 商家),另一类是视客户体验为生命、发送 “对不起,我们无法修改” 邮件会流失回头客的品牌。

如果你的迁移计划涵盖了 Scripts → Functions,但从未解决过付款后的订单修改问题,那么你大概会在三周内撞上“为什么这会如此艰难?”的下一堵墙。我们刚发布的订单管理指南展示了完美的付款后应对机制。

再回到迁移问题。

测试策略:标记客户模式

Functions 没有在后台可以直接切换的“草稿模式”。专业的流程是基于客户标签来设限新 Function,在系统里并排运行旧 Script 和新 Function,确认它们针对标记用户产生了相同的输出,然后关停旧版、完全切换过去。

步骤 1:标记你的测试用户

Customers 界面,将标签 FN-TESTER 添加到两到三个内部账户中。

步骤 2:对 Function 设置标签判断分支


// 在你的 run 函数的最上方
let is_tester = input
    .cart()
    .buyer_identity()
    .and_then(|bi| bi.customer())
    .map(|c| c.has_any_tag())
    .unwrap_or(&false);

if !*is_tester {
    return Ok(default_result);  // 回退至现有 Script 逻辑
}

// 新 Function 的逻辑仅针对标记用户执行
// 在你的 run 函数的最上方
let is_tester = input
    .cart()
    .buyer_identity()
    .and_then(|bi| bi.customer())
    .map(|c| c.has_any_tag())
    .unwrap_or(&false);

if !*is_tester {
    return Ok(default_result);  // 回退至现有 Script 逻辑
}

// 新 Function 的逻辑仅针对标记用户执行

步骤 3:在输入查询中添加 hasAnyTag


cart {
  buyerIdentity {
    customer {
      hasAnyTag(tags: ["FN-TESTER"])
    }
  }
}
cart {
  buyerIdentity {
    customer {
      hasAnyTag(tags: ["FN-TESTER"])
    }
  }
}

步骤 4:在结账页面中验证

使用已标记的用户身份登录,进行付款结账,确认 Function 正常触发。再换用不含标签的用户身份登录,确保旧的 Script 仍在照常运行。这种平行运行验证几天后,即可移除标签检测代码,让 Function 针对所有用户应用。

步骤 5:停用旧的 Script

在后台转至 Apps → Script Editor → [你的 Script] → Unpublish。一旦取消发布,对应的 Function 就成了唯一的规则来源。

真正实现规模化的部署工作流

不要永远都在开发者的笔记本上提交和部署。一旦完成了两到个 Script 的迁移,应当设立一个正规的 CI 管道。

最小化可行工作流


# .github/workflows/deploy-functions.yml
name: Deploy Shopify Functions
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - uses: dtolnay/rust-toolchain@stable
        with: { targets: wasm32-wasip1 }
      - run: npm install -g @shopify/cli@latest
      - run: shopify app deploy --force
        env:
          SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}
# .github/workflows/deploy-functions.yml
name: Deploy Shopify Functions
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - uses: dtolnay/rust-toolchain@stable
        with: { targets: wasm32-wasip1 }
      - run: npm install -g @shopify/cli@latest
      - run: shopify app deploy --force
        env:
          SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}

在你的 Partner 仪表板下方的 **Settings → Tokens** 页面生成合作伙伴 token。现在,每次向 main 分支的会合提交都会自动将 Functions 部署发布。不需要再通过 Slack 去询问 “Mike 刚才部署上去了吗?”

版本控制与回滚

shopify app deploy 会创建版本快照。执行回滚:


shopify app versions list
shopify app release --version <previous-version-id

shopify app versions list
shopify app release --version <previous-version-id

与早先 Scripts 回滚机制(指“回忆之前的旧代码并重新粘贴回来”)相比,这种流程的改善可谓天翻地覆。



Shopify Functions deployment pipeline across local staging and production environments

大多数团队最容易犯的错

通过在过去的一年中帮助 Plus 商家处理此类迁移事务,我们发现这五个失误不断重复出现。

1. 把 Functions 当作 Scripts 的 1:1 照搬。 它们明显不是。单一的 Function 就能凭借更清晰的代码分支替代掉三个旧的 Scripts。编写代码前应当通盘审查现有的 Scripts 系统。

2. 遗忘了权限读取范围。 精细配置的 Functions 流程往往可能需要 read_customersread_orders 或者 write_discounts。在 shopify.app.tomlscopes 分类下加入权限,并对 app 重新进行赋权授权,不然你的 GraphQL 输入查询将仅仅得到 null。

3. 尚未进行平行对照测试,就直接在生产环境中在没打标签的真实客户群组上运行 Functions。 就算代码看上去完好无异,边缘条件(空购物车、礼品卡、商店储值、B2B 草稿订单等)仍有可能暴露出隐患点。花两天利用标签进行测试,能让你避开后续的 P1 事故灾难。

4. 跳过了 Customizations Report。 这是梳理线上正运行何种逻辑的绝佳资产报告。不要依靠零碎记忆去主导迁移——务必通过这份官方报告对照来进行。

5. 写死了 collection ID 和客户标签名。 如果你需要商家自主调节的可变设置,应该通过 metafields 部署 Function config。CLI 可以快捷构建基于元字段的配置模块——查询有关 Function 设定的 Shopify 官方文档即可。

未来 75 天迁移执行清单

一个务实平稳的周度执行方案,保证在 6 月 30 日气定神闲地收尾。


周计划

执行项

第 1 周 (本周)

导出 Customizations Report。盘清每一个 Script 的实际去向。定夺采用 Function、公共应用还是应该直接作废废弃。

第 2–3 周

配好本地开发环境。搭建第一个 Function 的业务骨架。先对最简单的 Script 进行迁移(通常是支付方式的隐藏规则)。

第 4–6 周

迁移折扣类 Script。这类迁移用时最长,因为 Discounts API 的处理分支最丰富。配合标签测试来反复核实。

第 7–8 周

迁移货运/配送控制的 Script。在 Shopify 后台对应的 Delivery Customizations 中启用这些逻辑。

第 9–10 周

配置好 CI 管道。让部署工作彻底脱离开发者的单机环境。

第 11 周 (6 月中旬)

完成终极大决算测试。把旧的 Ruby Scripts 均转成 unpublish 状态。让店铺靠 Functions 独立跑两周。

6 月 30 日

终点日。因为万事皆备,结账流程不会发生任何故障。

若你本周起步,时间绝对余裕满盈。拖到六月再论,则危若累卵。



Week-by-week migration roadmap from Shopify Scripts to Functions

常见问题解答

我非得加入 Shopify Plus 计划才能启用 Functions 吗?

如果是私有/自定义的 Custom Functions 确实需要 Plus 计划;但在 App Store 上开放给公众的 public-app 类型的 Functions 则在任何计划中都能正常运行。 倘若尚未订购 Plus,你面前有两条出路:去 Shopify 应用商店安装提供对应 Function 功能的外部第三方 app,或是直接升级 Plus 以此来解锁开发自有 Function 功能。考虑到当初用了 Scripts 代码的本就是 Plus 以上层级的商店为主,多数情况下这一点不用担心。

我可以写 TypeScript 面向 Functions 进行开发吗?

完全可以 —— CLI 内核支持生成 TypeScript 对应的脚手架。 在启动 shopify app generate extension 并点选 “JavaScript” 以后,生成的工程目录会把来源于 import("../generated/api") 的代码类型签名一并打好包。你要是中意,把后缀改为 .ts 加上一个 tsconfig.json 即可。它们产出的打包 WASM 二进制没有差异。

与 Scripts 比起来,Functions 的执行耗时处于何种水平?

通常耗时不超过 5ms —— 远超早期的 Ruby 解释器速度。 因为它们直接被预编译到 WebAssembly 层级运行,Shopify 严格制定了最大 5ms 物理限制。若是运算溢出了,逻辑请求便当作超时处理,Function 也不会回传结果。依照日常实践来看,优秀编写的实例大都在 1–2ms 范畴处理妥当。天花板远超早年 Script 性能。

Function 能否调用来自外部的第三方的 API?

完全没办法 —— 系统隔断了网络通信的能力。 算力表现纯粹是无副作用计算:传入购物车 GraphQL 数据 → 演算出目标操作。当遇到强依赖外部输入源(诸如实时查询一套外部 CRM 的结果、做一次即时的物理库存匹配记录)的机制时,需要在前置节点中,把相关数值存储至元字段 metafields 里面,或者采取别样的接口层(比如 App Proxy、Webhooks,或是使用附带后台查询支撑的 Cart Transform 技术)。这就是在重构时不能无脑照搬通常需重新考量设计的最普遍根源。

Cart Transform 与 Discounts 这两个概念有何实质区分?

Discounts 重在改写价格,Cart Transform 重在更改购物车里的客观商品状态。 对于 10% 折扣、免邮费福利或者买一送一 BOGO,依靠 Discounts API 来实行。要将两个独立的 SKU 融合成一个单一的捆绑品类、或者把特定款式打散分拆,就调用 Cart Transform。有很多年长久远的 Scripts 写法常常把这类功能混在一起——在这次迁移作业时,需要将不同的逻辑分拆进专属的 Functions 里去。

如何在没有开发商店的状况下本地调试我的 Function?

你能分别依据语言机制在单体测试跑 cargo test (Rust) 和 npm test (JS),可惜若要达成终极的整体化集成校验,必需要用到开发店铺。 开发命令行 CLI 备有 shopify app function run 便于借助假数据文件入参本地演练。但若希望通盘验明交易支付与支付界面的反应,还是必须在一座实际店铺里构建一辆真的购物车去做验证。

同一个运行节点(Target)支持绑定加载多个 Functions 吗?

可以 —— Shopify 支持在同一个节点加载多个 Functions,它们的执行顺序有确定的规则约束。 关于应用折扣,多者共存的行为由 Shopify 开箱即用的优惠叠加机制和堆叠标准制约。至于配送跟支付自定义模块,它们在流转逻辑中像链条一般衔接走下去。从简单的维度来规划,大部分公司最好在一个节点下仅仅布设一个 Function,尽量避免逻辑纠集。

当我发布了新 Function,先前对应的 Script 后续会怎么发展?

直到你在 Apps → Script Editor 明确将 Script 撤下下线(Unpublish)之前,两者将在生产环境里平行保持起效状态。 这绝对是出于工程安全的意图——它是系统特意给商家安排上的交叉印证校对阶段。当确定了新设的 Function 的准确率无误后,再去用手工将旧 Script 下线。到了 2026 年 6 月 30 日,所有遗留在那运行的 Scripts 均会彻底断连。

一连串的重构会不会打乱前端 SEO 的索引或者是扰乱线上模板代码?

根本不波及 —— 所有的 Functions 实质上全部运行在服务器后端的结账场景,绝不侵扰你的产品页面或模板。 其运作层面仅改变收尾付款阶段的价格、发货选项与可用支付网关组合。前台视觉展现、底层模板和 SEO 不遭受任何负面牵扯。

原 Script 程序会抽取 Input.line_items 自定义字段,对此应该如何处理?

GraphQL 入参的 cart lines 部分,可以用 attribute 结构获取对应的自定义属性。 在对应的 lines 的过滤节点中,把 attribute(key: "your-key") { value } 叠上去。其工作原理和 Scripts 时代识别自定义属性的方式大同小异,只不过是从 Ruby 方法调用转成了 GraphQL 的数据访问形式。

如果是标签、分析类数据,Functions 能够直接写这些数据吗?

Functions 没办法写自定义订单标签、也没办法驱动网络 webhook —— 它唯一的职责在于针对当前的购物车状态返回值。 倘若期望后续步骤添加客户标签或开展下游连锁反馈,应通过 Shopify Flow 监测并触发订单生成事件。大部分常见的处理组合是:靠 Function 处理特定的账目扣减,依靠 Flow 把 "VOLUME-DISCOUNT-APPLIED" 标签自动在最后添加到生成完结的 Order 文件内。

市面上会有现成的通用型 app 支持直接拿来顶上我的自定义开发空缺吗?

当然有 —— 商家应用商店里充斥着一堆将 Functions 包装成开箱即用功能的成熟 app。 搜索关键词 "discount function"、"delivery customization"、或是 "payment customization"。若是用处极通行的(按百分比打折、按照客户标记过滤特定的打款通道、消费满 X 直接免去货运费),靠大厂 app 起效会替你节约掉大把的工期时间。只有面对真正特立独行的私有逻辑,才考虑由团队出马去自研独享的 Functions。

假若万一没在 6 月 30 号前完工,其代价如何承受?

Script 停止执行 —— 绝无降级方案、无过渡缓冲期、也不能递交延期申请。 先前靠 Script 所构建的额外扣费优惠、被悄悄收回的寄送选项、被拉黑的支付手段,皆在 7 月 1 日零点(UTC)被打回原生态默认形式。如果你的业务仰仗这些逻辑,请务必提前做好时间规划。迁移所需的时间通常比开发团队预估的时间要长,尤其是如果考虑到还要加上环境对照测试的时间。

大局完成后,这套 Script Editor 应用能在系统后台卸载吗?

建议于 2026 年 6 月 30 日之后物理卸载,当然 Shopify 极大可能也会统一收束清理掉它。 一旦运行引擎彻底关停,其后台就没有保存意义了。当前若你把全部项目的平顺过渡、测试工序均核对结束,此时就去把该应用清理卸载其实完全可行,Functions 在它的外部独立依存、运作。

本周必须开展的几项工作

千万别读完本贴就把它关在一块当作浮云。在到来的这 7 天内,把这四项实践立刻打好地基。

1. 导出 Customizations Report。Settings → Checkout → Customizations Report。导出它。把这作为你要对齐修改的需求待办(backlog)。

2. 配齐个人及协同组的研发环境。 布置好 Node 18+、Shopify CLI 乃至 Rust 编译包。在终端核实 shopify version 能吐出正确版本。时长:最多 30 分钟。

3. 模板化起步部署:试炼一次把一个至简的功能打通至沙盒小店里去。 抓取你们目前最简单的 Script —— 典型地比如隐藏某样付款形式的需求。从头到尾重演一遍。哪怕这小段功能之后不应用到线上环境,起码这一整串的部署体系与工具链条是被你物理磨合过、踩过坑的。

4. 在工作日历上划出专门的时间段。 迁移工作不应该仅靠敏捷计划会议(sprint)之间的破碎缝隙凑合勉强进行。必须划拨一块专职的黄金时段——例如雷打不动定在每周的周二与周四半天——用极庄重神圣的态度来对待它的发布进度。

在那些见证过圆满度过本轮迁移工作的 Plus 团队眼中,大家的推进节奏都是趋同的:两周的技术拖延,三周的埋头开发,加上一周的安全修复收尾。这正好就是六周时间。你目前的手捏周数是十。冗余还是宽泛的——千万把它划在大面积方案演习和严格的代码走查审计中去,切忌拖延怠工。

当你确保所有的结账扣减机制尘埃落定之后,再往下的重点环节必定是消费者的订单后处理事务 —— 买家的派送坐标写错后的调优、换同款的尺码、下了单后悔后增添折扣凭证之类的事情。如果你手头的路线图也急待解决这摊流程(实际上所有 Plus 规模商家和 Advanced 级别优质商家都必然该把它置于高等级议程),Revize 已经被 Shopify 应用商店收录,完全契合辅助在此期间你们所写出来的每一套 Functions 背后协同运作。

资料参考


h2 id="312">相关博文推荐


今天是 2026 年 4 月 16 日。昨天——4 月 15 日——是 Shopify 永久锁定 Script Editor 的日子。你无法再创建或发布新的 Script。执行关停将在 75 天后落地,即 2026 年 6 月 30 日

如果你是 Shopify Plus 开发者,或者是运营 Plus 商店的服务商,并且在过去 12 个月中一直把这个迁移任务推迟到“下个 Sprint”,那么你遇到麻烦了。这不是一个“修不修都行”的问题,而是一个“你的结账页面将在 71 日午夜崩溃”的问题。大多数 Plus 商店多年来累积了 5 到 20 个 Script,每个都在默默运行着没人记得谁写过的折扣规则、隐藏货运方式或支付网关。

本指南是我们在 1 月份梦寐以求的技术迁移手册。 它涵盖了实际代码,而不仅仅是策略。读完本文,你将了解如何使用 Shopify CLI 构建 Function 模板,如何编写 Rust 或 JavaScript 逻辑来处理折扣、配送自定义及支付自定义,如何在标记的高级客户子集上安全地进行测试,并将其推送到生产环境而不损坏现有的结账页面。

让我们把 Scripts 从你的商店中移除,并将 Functions 部署上去。



Developer migrating Shopify Scripts to Shopify Functions modules

快速解答:60 秒内将 Scripts 转换为 Functions

一句话总结迁移: Shopify Scripts(Script Editor 中的 Ruby 代码,仅限 Plus)正被 Shopify Functions(用 Rust 或 JavaScript 编写的 WebAssembly 模块,适用于所有方案)取代。你可以使用 shopify app generate extension 构建 Function,编写一个 run.graphql 查询来获取所需的购物车数据,编写一个 run.rsrun.js 文件来返回操作(折扣、隐藏货运方式等),然后使用 shopify app deploy 进行部署,并通过后台或 GraphQL mutation 进行激活。Functions 作为编译后的 WASM 运行,延迟低于 5 毫秒,适用于所有方案,并且是 Shopify 未来支持的唯一自定义途径。

6 月 30 日到底会发生什么变化

在触碰任何代码之前,先理清日期。有两个关键日期,它们都很重要。


日期

发生事件

你的行动

2026 年 4 月 15 日 (已过)

Script Editor 设为只读。无法新建 Script。无法编辑现有 Script。

现有 Script 仍可执行。立即迁移,否则锁定你的逻辑。

2026 年 6 月 30 日

所有 Shopify Scripts 停止执行。绝无例外。

替代的 Function 必须在此时前上线。

迁移结果是二元的。要么你的 Function 在 6 月 30 日前内部署好且结账页面继续正常工作,要么没有部署——导致每个受阻的购物车默默恢复到标准定价、标准运费,且启用每一个支付方式。没有折中方案。Script 要么运行,要么不运行,而在 6 月 30 日之后它将无法运行。

提示: 在 Shopify 后台中打开 Settings → Checkout → Customizations Report。它列出了你商店中每个激活的 Script、它的作用以及推荐的 Function 替代类型。从这里开始。

Functions 与 Scripts:究竟改变了什么


维度

Shopify Scripts (已废弃)

Shopify Functions (替代方案)

语言

Ruby DSL(Shopify 专用)

Rust, JavaScript, TypeScript

运行时

Shopify 基础设施上的沙盒 Ruby

WebAssembly (WASM) —— 运行延迟低于 5毫秒

方案可用性

仅限 Plus

所有方案(自定义应用需要 Plus;公开应用无限制)

编辑器

后台 Script Editor

本地 IDE + Shopify CLI

版本控制

无 —— 实时编辑

Git 友好 —— 完整的版本控制

测试

在结账页面中手动测试

使用 shopify app dev 进行本地开发和预览链接测试

部署

在后台点击 “保存”

在终端运行 shopify app deploy

对象

行项目,货运,支付

Discounts, Cart Transform, Validation, Delivery Customization, Payment Customization, Order Routing, Fulfillment Constraints 等

架构转变至关重要。Scripts 是“在文本框里微调 Ruby”。Functions 是“编写真正的应用,进行版本控制,本地测试,通过真正的 CI 管道部署”。这门槛更高。但这也是你在可预见的未来中针对结账逻辑进行的最后一次迁移——Functions 是 Shopify 的长期承诺,而不是像 Scripts 那样最终被淘汰的权宜之计。

将你的 Scripts 映射到正确的 Function 类型

你目前的每一个 Script 都完全映射到一个 Function API。这里是需要钉在显示器上的对照表。


旧 Script 类型

功能

新 Function API

Function 运行对象

Line Item Script

对特定的商品/客户/购物车条件应用折扣

Cart & Checkout Discounts API

cart.lines.discounts.generate.run

Shipping Script(折扣)

基于购物车规则的免运费/折扣运费

Cart & Checkout Discounts API

cart.delivery-options.discounts.generate.run

Shipping Script(隐藏/重命名/重排)

隐藏超过指定金额的运费,将 “Standard” 重命名为 “Free over $50”

Delivery Customization API

cart.delivery-options.transform.run

Payment Script

对 B2B 隐藏 PayPal,隐藏超过 500 美元的 COD,重新排序支付方式

Payment Customization API

cart.payment-methods.transform.run

购物车修改 Script(罕见)

组合商品,更换行项目

Cart Transform API

cart.transform.run

阻止结账 Script

如果 SKU 组合无效,则拒绝购物车

Cart & Checkout Validation API

cart.validations.generate.run

如果你有 10 个 Script,你可能只需要构建 3 到 5 个 Function——多个 Script 通常可以通过更清晰的分支逻辑合并为一个 Function。



Shopify Functions unifying discounts, delivery, and payment customizations

前提条件:设置你的本地开发环境

在构建任何 Function 之前,你需要在本地安装三样东西。在终端中运行这些检查。

1. Node.js 18+


node --version
# 必须 >= 18.0.0

如果版本偏旧,通过 nvm 安装或在 nodejs.org 下载。

2. Shopify CLI 3+


npm install -g @shopify/cli@latest
shopify version
# 应当输出 3.x 或更高版本

3. Rust 工具链(仅当你打算用 Rust 编写 Functions 时)


curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-wasip1
cargo --version

JavaScript Functions 不需要 Rust。为你的团队选择一种语言并坚持使用——混用两种语言会增加维护开销。

4. 开发商店

登录你的 Partner 仪表板并创建一个新的开发商店,或者使用现有的商店。在将 Functions 推送到生产环境之前,你将把它们部署到此开发商店中。

构建你的第一个 Function

CLI 负责了绝大部分样板代码。在任意目录下:


# 创建一个新的 Shopify app(如果你已有,请跳过此步)
shopify app init my-checkout-functions
cd my-checkout-functions

# 生成一个 Function 扩展
shopify app generate extension

CLI 将引导你完成提示。对于折扣 Function,你需要选择:

  • Type: Function

  • Template: discount(或 cart_checkout_validation, delivery_customization, payment_customization 等)

  • Language: Rust 或 JavaScript

  • Name: 类似于 volume-discount-fn

这会创建 extensions/volume-discount-fn/,包含:


extensions/volume-discount-fn/
├── shopify.extension.toml      # Function 配置 —— 目标、构建、版本
├── src/
├── cart_lines_discounts_generate_run.graphql   # 输入查询
└── cart_lines_discounts_generate_run.rs        # Function 逻辑
├── Cargo.toml                  # Rust 依赖(仅限 Rust)
└── README.md

你需要经常编辑的三个文件是 .toml(配置)、.graphql(输入)和 .rs / .js(逻辑)。就这些。

教程 1:替换 Line Item Script(数量折扣)

假设你的旧 Script 在购物车包含特定 Collection 的 5 件以上商品时,在订单小计中给予 10% 的折扣。下面是对应的 Function 实现方式。

步骤 1.1:配置(shopify.extension.toml


api_version = "2026-01"

[[extensions]]
name = "volume-discount-fn"
handle = "volume-discount-fn"
type = "function"

[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cart_lines_discounts_generate_run"

[extensions.build]
command = "cargo build --target=wasm32-wasip1 --release"
path = "target/wasm32-wasip1/release/volume-discount-fn.wasm"
watch = ["src/**/*.rs"]

步骤 1.2:输入查询(src/cart_lines_discounts_generate_run.graphql


query Input {
  cart {
    lines {
      id
      quantity
      cost {
        subtotalAmount {
          amount
        }
      }
      merchandise {
        ... on ProductVariant {
          product {
            inAnyCollection(ids: ["gid://shopify/Collection/123456789"])
          }
        }
      }
    }
  }
  discount {
    discountClasses
  }
}

提示: Functions 只能查询到你请求的数据。保持 GraphQL 最小化——你忽略的每个字段都意味着更快速、更实惠的运行。

步骤 1.3:逻辑(src/cart_lines_discounts_generate_run.rs


use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;

#[shopify_function]
fn cart_lines_discounts_generate_run(
    input: schema::cart_lines_discounts_generate_run::Input,
) -> Result<schema::CartLinesDiscountsGenerateRunResult> {
    // 如果折扣类别不匹配,直接退出
    let has_order_discount = input
        .discount()
        .discount_classes()
        .contains(&schema::DiscountClass::Order);

    if !has_order_discount {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    // 累加目标 Collection 中商品的数量
    let qualifying_qty: i64 = input
        .cart()
        .lines()
        .iter()
        .filter(|line| {
            if let schema::Merchandise::ProductVariant(v) = line.merchandise() {
                *v.product().in_any_collection()
            } else {
                false
            }
        })
        .map(|line| *line.quantity())
        .sum();

    if qualifying_qty < 5 {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    // 在订单小计上应用 10% 折扣
    Ok(schema::CartLinesDiscountsGenerateRunResult {
        operations: vec![schema::CartOperation::OrderDiscountsAdd(
            schema::OrderDiscountsAddOperation {
                selection_strategy: schema::OrderDiscountSelectionStrategy::First,
                candidates: vec![schema::OrderDiscountCandidate {
                    targets: vec![schema::OrderDiscountCandidateTarget::OrderSubtotal(
                        schema::OrderSubtotalTarget {
                            excluded_cart_line_ids: vec![],
                        },
                    )],
                    message: Some("Volume discount: 10% off".to_string()),
                    value: schema::OrderDiscountCandidateValue::Percentage(
                        schema::Percentage { value: Decimal(10.0) }
                    ),
                    conditions: None,
                    associated_discount_code: None,
                }],
            },
        )],
    })
}

步骤 1.4:测试、部署和激活


# 本地开发,支持热重载
shopify app dev

# 准备就绪后进行部署
shopify app deploy

# 在弹出的 GraphiQL 面板(在开发终端按 `g` 键)中,
# 创建运行你 Function 的自动折扣:
mutation {
  discountAutomaticAppCreate(
    automaticAppDiscount: {
      title: "Volume Discount (5+ collection items)"
      functionHandle: "volume-discount-fn"
      discountClasses: [ORDER]
      startsAt: "2026-04-16T00:00:00Z"
    }
  ) {
    automaticAppDiscount { discountId }
    userErrors { field message }
  }
}

完成了。该 Function 已经上线,纳入了版本控制,且完全替代了旧的 Script。

教程 2:替换 Shipping Script(在购物车金额达到阈值时隐藏配送方式)

常见的 Script 场景:“当购物车小计超过 500 美元时隐藏 Express Shipping,以防止大额订单产生昂贵的隔夜运费。” 对应的 Delivery Customization Function 如下。

步骤 2.1:构建模板


shopify app generate extension --template delivery_customization --name hide-express-fn

步骤 2.2:输入查询(src/run.graphql


query Input {
  cart {
    cost {
      subtotalAmount {
        amount
      }
    }
    deliveryGroups {
      deliveryOptions {
        handle
        title
      }
    }
  }
}

步骤 2.3:逻辑(src/run.js —— JavaScript 版本)


// @ts-check
/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
 */

const NO_CHANGES = { operations: [] };
const THRESHOLD = 500.0;
const HIDE_TITLES = ["Express", "Overnight"];

/**
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function run(input) {
  const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount);
  if (subtotal < THRESHOLD) return NO_CHANGES;

  const operations = input.cart.deliveryGroups.flatMap((group) =>
    group.deliveryOptions
      .filter((opt) => HIDE_TITLES.some((t) => opt.title.includes(t)))
      .map((opt) => ({
        hide: { deliveryOptionHandle: opt.handle },
      }))
  );

  return { operations };
}

步骤 2.4:通过后台激活(无需使用 GraphQL)

Delivery Customizations 包含一个内置的后台 UI。运行 shopify app deploy 之后:

  1. 转到 Settings → Shipping and delivery

  2. 向下滚动至底部的 Customizations 部分

  3. 点击 Add customization → 选择你的 Function

  4. 保存并启用

对应的隐藏规则已在生产环境中生效。无需编写 mutation。



Shopify delivery customization Function hiding shipping option at checkout

教程 3:替换 Payment Script(对 B2B 客户隐藏 COD)

旧 Script:“对任何标记为 'B2B' 的客户隐藏货到付款 (COD)。” 下面是 Payment Customization 的版本。

步骤 3.1:构建模板


shopify app generate extension --template payment_customization --name hide-cod-b2b-fn

步骤 3.2:输入查询


query Input {
  cart {
    buyerIdentity {
      customer {
        hasTags(tags: [{ tag: "B2B" }]) {
          tag
          hasTag
        }
      }
    }
  }
  paymentMethods {
    id
    name
  }
}

步骤 3.3:逻辑(src/run.js


const NO_CHANGES = { operations: [] };

export function run(input) {
  const tagCheck = input.cart?.buyerIdentity?.customer?.hasTags?.[0];
  const isB2B = tagCheck?.hasTag === true;
  if (!isB2B) return NO_CHANGES;

  const codMethod = input.paymentMethods.find((pm) =>
    pm.name.toLowerCase().includes("cash on delivery")
  );
  if (!codMethod) return NO_CHANGES;

  return {
    operations: [{ hide: { paymentMethodId: codMethod.id } }],
  };
}

步骤 3.4:激活

Payment Customizations 在后台的 Settings → Payments → Customizations 下也有一个配置 UI。配送的激活步骤相同——选择你的 Function、保存,即可完成。

既然你正在阅读本文 —— 谈谈付款后流程(Post-Purchase)

既然这是 Revize 的博客,那就做个快速披露。Revize 负责处理 Functions 无法触碰的事物——只要订单下达后,客户可能想要增加商品、更换尺码、修改货运地址或应用遗忘的折扣。Functions 适用于结账阶段,而 Revize 适用于付款后。 Functions 决定哪些商品可以留在购物车;Revize 则允许你的客户和客服团队在事后修改订单,无需执行退款再重建订单的操作。这对于两类商店而言十分重要:一类是订单量庞大以至于手动编辑工作会崩溃的商店(Plus 运营商显然是,还有大流量的 Advanced 商家),另一类是视客户体验为生命、发送 “对不起,我们无法修改” 邮件会流失回头客的品牌。

如果你的迁移计划涵盖了 Scripts → Functions,但从未解决过付款后的订单修改问题,那么你大概会在三周内撞上“为什么这会如此艰难?”的下一堵墙。我们刚发布的订单管理指南展示了完美的付款后应对机制。

再回到迁移问题。

测试策略:标记客户模式

Functions 没有在后台可以直接切换的“草稿模式”。专业的流程是基于客户标签来设限新 Function,在系统里并排运行旧 Script 和新 Function,确认它们针对标记用户产生了相同的输出,然后关停旧版、完全切换过去。

步骤 1:标记你的测试用户

Customers 界面,将标签 FN-TESTER 添加到两到三个内部账户中。

步骤 2:对 Function 设置标签判断分支


// 在你的 run 函数的最上方
let is_tester = input
    .cart()
    .buyer_identity()
    .and_then(|bi| bi.customer())
    .map(|c| c.has_any_tag())
    .unwrap_or(&false);

if !*is_tester {
    return Ok(default_result);  // 回退至现有 Script 逻辑
}

// 新 Function 的逻辑仅针对标记用户执行

步骤 3:在输入查询中添加 hasAnyTag


cart {
  buyerIdentity {
    customer {
      hasAnyTag(tags: ["FN-TESTER"])
    }
  }
}

步骤 4:在结账页面中验证

使用已标记的用户身份登录,进行付款结账,确认 Function 正常触发。再换用不含标签的用户身份登录,确保旧的 Script 仍在照常运行。这种平行运行验证几天后,即可移除标签检测代码,让 Function 针对所有用户应用。

步骤 5:停用旧的 Script

在后台转至 Apps → Script Editor → [你的 Script] → Unpublish。一旦取消发布,对应的 Function 就成了唯一的规则来源。

真正实现规模化的部署工作流

不要永远都在开发者的笔记本上提交和部署。一旦完成了两到个 Script 的迁移,应当设立一个正规的 CI 管道。

最小化可行工作流


# .github/workflows/deploy-functions.yml
name: Deploy Shopify Functions
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - uses: dtolnay/rust-toolchain@stable
        with: { targets: wasm32-wasip1 }
      - run: npm install -g @shopify/cli@latest
      - run: shopify app deploy --force
        env:
          SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}

在你的 Partner 仪表板下方的 **Settings → Tokens** 页面生成合作伙伴 token。现在,每次向 main 分支的会合提交都会自动将 Functions 部署发布。不需要再通过 Slack 去询问 “Mike 刚才部署上去了吗?”

版本控制与回滚

shopify app deploy 会创建版本快照。执行回滚:


shopify app versions list
shopify app release --version <previous-version-id

与早先 Scripts 回滚机制(指“回忆之前的旧代码并重新粘贴回来”)相比,这种流程的改善可谓天翻地覆。



Shopify Functions deployment pipeline across local staging and production environments

大多数团队最容易犯的错

通过在过去的一年中帮助 Plus 商家处理此类迁移事务,我们发现这五个失误不断重复出现。

1. 把 Functions 当作 Scripts 的 1:1 照搬。 它们明显不是。单一的 Function 就能凭借更清晰的代码分支替代掉三个旧的 Scripts。编写代码前应当通盘审查现有的 Scripts 系统。

2. 遗忘了权限读取范围。 精细配置的 Functions 流程往往可能需要 read_customersread_orders 或者 write_discounts。在 shopify.app.tomlscopes 分类下加入权限,并对 app 重新进行赋权授权,不然你的 GraphQL 输入查询将仅仅得到 null。

3. 尚未进行平行对照测试,就直接在生产环境中在没打标签的真实客户群组上运行 Functions。 就算代码看上去完好无异,边缘条件(空购物车、礼品卡、商店储值、B2B 草稿订单等)仍有可能暴露出隐患点。花两天利用标签进行测试,能让你避开后续的 P1 事故灾难。

4. 跳过了 Customizations Report。 这是梳理线上正运行何种逻辑的绝佳资产报告。不要依靠零碎记忆去主导迁移——务必通过这份官方报告对照来进行。

5. 写死了 collection ID 和客户标签名。 如果你需要商家自主调节的可变设置,应该通过 metafields 部署 Function config。CLI 可以快捷构建基于元字段的配置模块——查询有关 Function 设定的 Shopify 官方文档即可。

未来 75 天迁移执行清单

一个务实平稳的周度执行方案,保证在 6 月 30 日气定神闲地收尾。


周计划

执行项

第 1 周 (本周)

导出 Customizations Report。盘清每一个 Script 的实际去向。定夺采用 Function、公共应用还是应该直接作废废弃。

第 2–3 周

配好本地开发环境。搭建第一个 Function 的业务骨架。先对最简单的 Script 进行迁移(通常是支付方式的隐藏规则)。

第 4–6 周

迁移折扣类 Script。这类迁移用时最长,因为 Discounts API 的处理分支最丰富。配合标签测试来反复核实。

第 7–8 周

迁移货运/配送控制的 Script。在 Shopify 后台对应的 Delivery Customizations 中启用这些逻辑。

第 9–10 周

配置好 CI 管道。让部署工作彻底脱离开发者的单机环境。

第 11 周 (6 月中旬)

完成终极大决算测试。把旧的 Ruby Scripts 均转成 unpublish 状态。让店铺靠 Functions 独立跑两周。

6 月 30 日

终点日。因为万事皆备,结账流程不会发生任何故障。

若你本周起步,时间绝对余裕满盈。拖到六月再论,则危若累卵。



Week-by-week migration roadmap from Shopify Scripts to Functions

常见问题解答

我非得加入 Shopify Plus 计划才能启用 Functions 吗?

如果是私有/自定义的 Custom Functions 确实需要 Plus 计划;但在 App Store 上开放给公众的 public-app 类型的 Functions 则在任何计划中都能正常运行。 倘若尚未订购 Plus,你面前有两条出路:去 Shopify 应用商店安装提供对应 Function 功能的外部第三方 app,或是直接升级 Plus 以此来解锁开发自有 Function 功能。考虑到当初用了 Scripts 代码的本就是 Plus 以上层级的商店为主,多数情况下这一点不用担心。

我可以写 TypeScript 面向 Functions 进行开发吗?

完全可以 —— CLI 内核支持生成 TypeScript 对应的脚手架。 在启动 shopify app generate extension 并点选 “JavaScript” 以后,生成的工程目录会把来源于 import("../generated/api") 的代码类型签名一并打好包。你要是中意,把后缀改为 .ts 加上一个 tsconfig.json 即可。它们产出的打包 WASM 二进制没有差异。

与 Scripts 比起来,Functions 的执行耗时处于何种水平?

通常耗时不超过 5ms —— 远超早期的 Ruby 解释器速度。 因为它们直接被预编译到 WebAssembly 层级运行,Shopify 严格制定了最大 5ms 物理限制。若是运算溢出了,逻辑请求便当作超时处理,Function 也不会回传结果。依照日常实践来看,优秀编写的实例大都在 1–2ms 范畴处理妥当。天花板远超早年 Script 性能。

Function 能否调用来自外部的第三方的 API?

完全没办法 —— 系统隔断了网络通信的能力。 算力表现纯粹是无副作用计算:传入购物车 GraphQL 数据 → 演算出目标操作。当遇到强依赖外部输入源(诸如实时查询一套外部 CRM 的结果、做一次即时的物理库存匹配记录)的机制时,需要在前置节点中,把相关数值存储至元字段 metafields 里面,或者采取别样的接口层(比如 App Proxy、Webhooks,或是使用附带后台查询支撑的 Cart Transform 技术)。这就是在重构时不能无脑照搬通常需重新考量设计的最普遍根源。

Cart Transform 与 Discounts 这两个概念有何实质区分?

Discounts 重在改写价格,Cart Transform 重在更改购物车里的客观商品状态。 对于 10% 折扣、免邮费福利或者买一送一 BOGO,依靠 Discounts API 来实行。要将两个独立的 SKU 融合成一个单一的捆绑品类、或者把特定款式打散分拆,就调用 Cart Transform。有很多年长久远的 Scripts 写法常常把这类功能混在一起——在这次迁移作业时,需要将不同的逻辑分拆进专属的 Functions 里去。

如何在没有开发商店的状况下本地调试我的 Function?

你能分别依据语言机制在单体测试跑 cargo test (Rust) 和 npm test (JS),可惜若要达成终极的整体化集成校验,必需要用到开发店铺。 开发命令行 CLI 备有 shopify app function run 便于借助假数据文件入参本地演练。但若希望通盘验明交易支付与支付界面的反应,还是必须在一座实际店铺里构建一辆真的购物车去做验证。

同一个运行节点(Target)支持绑定加载多个 Functions 吗?

可以 —— Shopify 支持在同一个节点加载多个 Functions,它们的执行顺序有确定的规则约束。 关于应用折扣,多者共存的行为由 Shopify 开箱即用的优惠叠加机制和堆叠标准制约。至于配送跟支付自定义模块,它们在流转逻辑中像链条一般衔接走下去。从简单的维度来规划,大部分公司最好在一个节点下仅仅布设一个 Function,尽量避免逻辑纠集。

当我发布了新 Function,先前对应的 Script 后续会怎么发展?

直到你在 Apps → Script Editor 明确将 Script 撤下下线(Unpublish)之前,两者将在生产环境里平行保持起效状态。 这绝对是出于工程安全的意图——它是系统特意给商家安排上的交叉印证校对阶段。当确定了新设的 Function 的准确率无误后,再去用手工将旧 Script 下线。到了 2026 年 6 月 30 日,所有遗留在那运行的 Scripts 均会彻底断连。

一连串的重构会不会打乱前端 SEO 的索引或者是扰乱线上模板代码?

根本不波及 —— 所有的 Functions 实质上全部运行在服务器后端的结账场景,绝不侵扰你的产品页面或模板。 其运作层面仅改变收尾付款阶段的价格、发货选项与可用支付网关组合。前台视觉展现、底层模板和 SEO 不遭受任何负面牵扯。

原 Script 程序会抽取 Input.line_items 自定义字段,对此应该如何处理?

GraphQL 入参的 cart lines 部分,可以用 attribute 结构获取对应的自定义属性。 在对应的 lines 的过滤节点中,把 attribute(key: "your-key") { value } 叠上去。其工作原理和 Scripts 时代识别自定义属性的方式大同小异,只不过是从 Ruby 方法调用转成了 GraphQL 的数据访问形式。

如果是标签、分析类数据,Functions 能够直接写这些数据吗?

Functions 没办法写自定义订单标签、也没办法驱动网络 webhook —— 它唯一的职责在于针对当前的购物车状态返回值。 倘若期望后续步骤添加客户标签或开展下游连锁反馈,应通过 Shopify Flow 监测并触发订单生成事件。大部分常见的处理组合是:靠 Function 处理特定的账目扣减,依靠 Flow 把 "VOLUME-DISCOUNT-APPLIED" 标签自动在最后添加到生成完结的 Order 文件内。

市面上会有现成的通用型 app 支持直接拿来顶上我的自定义开发空缺吗?

当然有 —— 商家应用商店里充斥着一堆将 Functions 包装成开箱即用功能的成熟 app。 搜索关键词 "discount function"、"delivery customization"、或是 "payment customization"。若是用处极通行的(按百分比打折、按照客户标记过滤特定的打款通道、消费满 X 直接免去货运费),靠大厂 app 起效会替你节约掉大把的工期时间。只有面对真正特立独行的私有逻辑,才考虑由团队出马去自研独享的 Functions。

假若万一没在 6 月 30 号前完工,其代价如何承受?

Script 停止执行 —— 绝无降级方案、无过渡缓冲期、也不能递交延期申请。 先前靠 Script 所构建的额外扣费优惠、被悄悄收回的寄送选项、被拉黑的支付手段,皆在 7 月 1 日零点(UTC)被打回原生态默认形式。如果你的业务仰仗这些逻辑,请务必提前做好时间规划。迁移所需的时间通常比开发团队预估的时间要长,尤其是如果考虑到还要加上环境对照测试的时间。

大局完成后,这套 Script Editor 应用能在系统后台卸载吗?

建议于 2026 年 6 月 30 日之后物理卸载,当然 Shopify 极大可能也会统一收束清理掉它。 一旦运行引擎彻底关停,其后台就没有保存意义了。当前若你把全部项目的平顺过渡、测试工序均核对结束,此时就去把该应用清理卸载其实完全可行,Functions 在它的外部独立依存、运作。

本周必须开展的几项工作

千万别读完本贴就把它关在一块当作浮云。在到来的这 7 天内,把这四项实践立刻打好地基。

1. 导出 Customizations Report。Settings → Checkout → Customizations Report。导出它。把这作为你要对齐修改的需求待办(backlog)。

2. 配齐个人及协同组的研发环境。 布置好 Node 18+、Shopify CLI 乃至 Rust 编译包。在终端核实 shopify version 能吐出正确版本。时长:最多 30 分钟。

3. 模板化起步部署:试炼一次把一个至简的功能打通至沙盒小店里去。 抓取你们目前最简单的 Script —— 典型地比如隐藏某样付款形式的需求。从头到尾重演一遍。哪怕这小段功能之后不应用到线上环境,起码这一整串的部署体系与工具链条是被你物理磨合过、踩过坑的。

4. 在工作日历上划出专门的时间段。 迁移工作不应该仅靠敏捷计划会议(sprint)之间的破碎缝隙凑合勉强进行。必须划拨一块专职的黄金时段——例如雷打不动定在每周的周二与周四半天——用极庄重神圣的态度来对待它的发布进度。

在那些见证过圆满度过本轮迁移工作的 Plus 团队眼中,大家的推进节奏都是趋同的:两周的技术拖延,三周的埋头开发,加上一周的安全修复收尾。这正好就是六周时间。你目前的手捏周数是十。冗余还是宽泛的——千万把它划在大面积方案演习和严格的代码走查审计中去,切忌拖延怠工。

当你确保所有的结账扣减机制尘埃落定之后,再往下的重点环节必定是消费者的订单后处理事务 —— 买家的派送坐标写错后的调优、换同款的尺码、下了单后悔后增添折扣凭证之类的事情。如果你手头的路线图也急待解决这摊流程(实际上所有 Plus 规模商家和 Advanced 级别优质商家都必然该把它置于高等级议程),Revize 已经被 Shopify 应用商店收录,完全契合辅助在此期间你们所写出来的每一套 Functions 背后协同运作。

资料参考


h2 id="312">相关博文推荐


今天是 2026 年 4 月 16 日。昨天——4 月 15 日——是 Shopify 永久锁定 Script Editor 的日子。你无法再创建或发布新的 Script。执行关停将在 75 天后落地,即 2026 年 6 月 30 日

如果你是 Shopify Plus 开发者,或者是运营 Plus 商店的服务商,并且在过去 12 个月中一直把这个迁移任务推迟到“下个 Sprint”,那么你遇到麻烦了。这不是一个“修不修都行”的问题,而是一个“你的结账页面将在 71 日午夜崩溃”的问题。大多数 Plus 商店多年来累积了 5 到 20 个 Script,每个都在默默运行着没人记得谁写过的折扣规则、隐藏货运方式或支付网关。

本指南是我们在 1 月份梦寐以求的技术迁移手册。 它涵盖了实际代码,而不仅仅是策略。读完本文,你将了解如何使用 Shopify CLI 构建 Function 模板,如何编写 Rust 或 JavaScript 逻辑来处理折扣、配送自定义及支付自定义,如何在标记的高级客户子集上安全地进行测试,并将其推送到生产环境而不损坏现有的结账页面。

让我们把 Scripts 从你的商店中移除,并将 Functions 部署上去。



Developer migrating Shopify Scripts to Shopify Functions modules

快速解答:60 秒内将 Scripts 转换为 Functions

一句话总结迁移: Shopify Scripts(Script Editor 中的 Ruby 代码,仅限 Plus)正被 Shopify Functions(用 Rust 或 JavaScript 编写的 WebAssembly 模块,适用于所有方案)取代。你可以使用 shopify app generate extension 构建 Function,编写一个 run.graphql 查询来获取所需的购物车数据,编写一个 run.rsrun.js 文件来返回操作(折扣、隐藏货运方式等),然后使用 shopify app deploy 进行部署,并通过后台或 GraphQL mutation 进行激活。Functions 作为编译后的 WASM 运行,延迟低于 5 毫秒,适用于所有方案,并且是 Shopify 未来支持的唯一自定义途径。

6 月 30 日到底会发生什么变化

在触碰任何代码之前,先理清日期。有两个关键日期,它们都很重要。


日期

发生事件

你的行动

2026 年 4 月 15 日 (已过)

Script Editor 设为只读。无法新建 Script。无法编辑现有 Script。

现有 Script 仍可执行。立即迁移,否则锁定你的逻辑。

2026 年 6 月 30 日

所有 Shopify Scripts 停止执行。绝无例外。

替代的 Function 必须在此时前上线。

迁移结果是二元的。要么你的 Function 在 6 月 30 日前内部署好且结账页面继续正常工作,要么没有部署——导致每个受阻的购物车默默恢复到标准定价、标准运费,且启用每一个支付方式。没有折中方案。Script 要么运行,要么不运行,而在 6 月 30 日之后它将无法运行。

提示: 在 Shopify 后台中打开 Settings → Checkout → Customizations Report。它列出了你商店中每个激活的 Script、它的作用以及推荐的 Function 替代类型。从这里开始。

Functions 与 Scripts:究竟改变了什么


维度

Shopify Scripts (已废弃)

Shopify Functions (替代方案)

语言

Ruby DSL(Shopify 专用)

Rust, JavaScript, TypeScript

运行时

Shopify 基础设施上的沙盒 Ruby

WebAssembly (WASM) —— 运行延迟低于 5毫秒

方案可用性

仅限 Plus

所有方案(自定义应用需要 Plus;公开应用无限制)

编辑器

后台 Script Editor

本地 IDE + Shopify CLI

版本控制

无 —— 实时编辑

Git 友好 —— 完整的版本控制

测试

在结账页面中手动测试

使用 shopify app dev 进行本地开发和预览链接测试

部署

在后台点击 “保存”

在终端运行 shopify app deploy

对象

行项目,货运,支付

Discounts, Cart Transform, Validation, Delivery Customization, Payment Customization, Order Routing, Fulfillment Constraints 等

架构转变至关重要。Scripts 是“在文本框里微调 Ruby”。Functions 是“编写真正的应用,进行版本控制,本地测试,通过真正的 CI 管道部署”。这门槛更高。但这也是你在可预见的未来中针对结账逻辑进行的最后一次迁移——Functions 是 Shopify 的长期承诺,而不是像 Scripts 那样最终被淘汰的权宜之计。

将你的 Scripts 映射到正确的 Function 类型

你目前的每一个 Script 都完全映射到一个 Function API。这里是需要钉在显示器上的对照表。


旧 Script 类型

功能

新 Function API

Function 运行对象

Line Item Script

对特定的商品/客户/购物车条件应用折扣

Cart & Checkout Discounts API

cart.lines.discounts.generate.run

Shipping Script(折扣)

基于购物车规则的免运费/折扣运费

Cart & Checkout Discounts API

cart.delivery-options.discounts.generate.run

Shipping Script(隐藏/重命名/重排)

隐藏超过指定金额的运费,将 “Standard” 重命名为 “Free over $50”

Delivery Customization API

cart.delivery-options.transform.run

Payment Script

对 B2B 隐藏 PayPal,隐藏超过 500 美元的 COD,重新排序支付方式

Payment Customization API

cart.payment-methods.transform.run

购物车修改 Script(罕见)

组合商品,更换行项目

Cart Transform API

cart.transform.run

阻止结账 Script

如果 SKU 组合无效,则拒绝购物车

Cart & Checkout Validation API

cart.validations.generate.run

如果你有 10 个 Script,你可能只需要构建 3 到 5 个 Function——多个 Script 通常可以通过更清晰的分支逻辑合并为一个 Function。



Shopify Functions unifying discounts, delivery, and payment customizations

前提条件:设置你的本地开发环境

在构建任何 Function 之前,你需要在本地安装三样东西。在终端中运行这些检查。

1. Node.js 18+


node --version
# 必须 >= 18.0.0

如果版本偏旧,通过 nvm 安装或在 nodejs.org 下载。

2. Shopify CLI 3+


npm install -g @shopify/cli@latest
shopify version
# 应当输出 3.x 或更高版本

3. Rust 工具链(仅当你打算用 Rust 编写 Functions 时)


curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-wasip1
cargo --version

JavaScript Functions 不需要 Rust。为你的团队选择一种语言并坚持使用——混用两种语言会增加维护开销。

4. 开发商店

登录你的 Partner 仪表板并创建一个新的开发商店,或者使用现有的商店。在将 Functions 推送到生产环境之前,你将把它们部署到此开发商店中。

构建你的第一个 Function

CLI 负责了绝大部分样板代码。在任意目录下:


# 创建一个新的 Shopify app(如果你已有,请跳过此步)
shopify app init my-checkout-functions
cd my-checkout-functions

# 生成一个 Function 扩展
shopify app generate extension

CLI 将引导你完成提示。对于折扣 Function,你需要选择:

  • Type: Function

  • Template: discount(或 cart_checkout_validation, delivery_customization, payment_customization 等)

  • Language: Rust 或 JavaScript

  • Name: 类似于 volume-discount-fn

这会创建 extensions/volume-discount-fn/,包含:


extensions/volume-discount-fn/
├── shopify.extension.toml      # Function 配置 —— 目标、构建、版本
├── src/
├── cart_lines_discounts_generate_run.graphql   # 输入查询
└── cart_lines_discounts_generate_run.rs        # Function 逻辑
├── Cargo.toml                  # Rust 依赖(仅限 Rust)
└── README.md

你需要经常编辑的三个文件是 .toml(配置)、.graphql(输入)和 .rs / .js(逻辑)。就这些。

教程 1:替换 Line Item Script(数量折扣)

假设你的旧 Script 在购物车包含特定 Collection 的 5 件以上商品时,在订单小计中给予 10% 的折扣。下面是对应的 Function 实现方式。

步骤 1.1:配置(shopify.extension.toml


api_version = "2026-01"

[[extensions]]
name = "volume-discount-fn"
handle = "volume-discount-fn"
type = "function"

[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cart_lines_discounts_generate_run"

[extensions.build]
command = "cargo build --target=wasm32-wasip1 --release"
path = "target/wasm32-wasip1/release/volume-discount-fn.wasm"
watch = ["src/**/*.rs"]

步骤 1.2:输入查询(src/cart_lines_discounts_generate_run.graphql


query Input {
  cart {
    lines {
      id
      quantity
      cost {
        subtotalAmount {
          amount
        }
      }
      merchandise {
        ... on ProductVariant {
          product {
            inAnyCollection(ids: ["gid://shopify/Collection/123456789"])
          }
        }
      }
    }
  }
  discount {
    discountClasses
  }
}

提示: Functions 只能查询到你请求的数据。保持 GraphQL 最小化——你忽略的每个字段都意味着更快速、更实惠的运行。

步骤 1.3:逻辑(src/cart_lines_discounts_generate_run.rs


use super::schema;
use shopify_function::prelude::*;
use shopify_function::Result;

#[shopify_function]
fn cart_lines_discounts_generate_run(
    input: schema::cart_lines_discounts_generate_run::Input,
) -> Result<schema::CartLinesDiscountsGenerateRunResult> {
    // 如果折扣类别不匹配,直接退出
    let has_order_discount = input
        .discount()
        .discount_classes()
        .contains(&schema::DiscountClass::Order);

    if !has_order_discount {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    // 累加目标 Collection 中商品的数量
    let qualifying_qty: i64 = input
        .cart()
        .lines()
        .iter()
        .filter(|line| {
            if let schema::Merchandise::ProductVariant(v) = line.merchandise() {
                *v.product().in_any_collection()
            } else {
                false
            }
        })
        .map(|line| *line.quantity())
        .sum();

    if qualifying_qty < 5 {
        return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    }

    // 在订单小计上应用 10% 折扣
    Ok(schema::CartLinesDiscountsGenerateRunResult {
        operations: vec![schema::CartOperation::OrderDiscountsAdd(
            schema::OrderDiscountsAddOperation {
                selection_strategy: schema::OrderDiscountSelectionStrategy::First,
                candidates: vec![schema::OrderDiscountCandidate {
                    targets: vec![schema::OrderDiscountCandidateTarget::OrderSubtotal(
                        schema::OrderSubtotalTarget {
                            excluded_cart_line_ids: vec![],
                        },
                    )],
                    message: Some("Volume discount: 10% off".to_string()),
                    value: schema::OrderDiscountCandidateValue::Percentage(
                        schema::Percentage { value: Decimal(10.0) }
                    ),
                    conditions: None,
                    associated_discount_code: None,
                }],
            },
        )],
    })
}

步骤 1.4:测试、部署和激活


# 本地开发,支持热重载
shopify app dev

# 准备就绪后进行部署
shopify app deploy

# 在弹出的 GraphiQL 面板(在开发终端按 `g` 键)中,
# 创建运行你 Function 的自动折扣:
mutation {
  discountAutomaticAppCreate(
    automaticAppDiscount: {
      title: "Volume Discount (5+ collection items)"
      functionHandle: "volume-discount-fn"
      discountClasses: [ORDER]
      startsAt: "2026-04-16T00:00:00Z"
    }
  ) {
    automaticAppDiscount { discountId }
    userErrors { field message }
  }
}

完成了。该 Function 已经上线,纳入了版本控制,且完全替代了旧的 Script。

教程 2:替换 Shipping Script(在购物车金额达到阈值时隐藏配送方式)

常见的 Script 场景:“当购物车小计超过 500 美元时隐藏 Express Shipping,以防止大额订单产生昂贵的隔夜运费。” 对应的 Delivery Customization Function 如下。

步骤 2.1:构建模板


shopify app generate extension --template delivery_customization --name hide-express-fn

步骤 2.2:输入查询(src/run.graphql


query Input {
  cart {
    cost {
      subtotalAmount {
        amount
      }
    }
    deliveryGroups {
      deliveryOptions {
        handle
        title
      }
    }
  }
}

步骤 2.3:逻辑(src/run.js —— JavaScript 版本)


// @ts-check
/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
 */

const NO_CHANGES = { operations: [] };
const THRESHOLD = 500.0;
const HIDE_TITLES = ["Express", "Overnight"];

/**
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function run(input) {
  const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount);
  if (subtotal < THRESHOLD) return NO_CHANGES;

  const operations = input.cart.deliveryGroups.flatMap((group) =>
    group.deliveryOptions
      .filter((opt) => HIDE_TITLES.some((t) => opt.title.includes(t)))
      .map((opt) => ({
        hide: { deliveryOptionHandle: opt.handle },
      }))
  );

  return { operations };
}

步骤 2.4:通过后台激活(无需使用 GraphQL)

Delivery Customizations 包含一个内置的后台 UI。运行 shopify app deploy 之后:

  1. 转到 Settings → Shipping and delivery

  2. 向下滚动至底部的 Customizations 部分

  3. 点击 Add customization → 选择你的 Function

  4. 保存并启用

对应的隐藏规则已在生产环境中生效。无需编写 mutation。



Shopify delivery customization Function hiding shipping option at checkout

教程 3:替换 Payment Script(对 B2B 客户隐藏 COD)

旧 Script:“对任何标记为 'B2B' 的客户隐藏货到付款 (COD)。” 下面是 Payment Customization 的版本。

步骤 3.1:构建模板


shopify app generate extension --template payment_customization --name hide-cod-b2b-fn

步骤 3.2:输入查询


query Input {
  cart {
    buyerIdentity {
      customer {
        hasTags(tags: [{ tag: "B2B" }]) {
          tag
          hasTag
        }
      }
    }
  }
  paymentMethods {
    id
    name
  }
}

步骤 3.3:逻辑(src/run.js


const NO_CHANGES = { operations: [] };

export function run(input) {
  const tagCheck = input.cart?.buyerIdentity?.customer?.hasTags?.[0];
  const isB2B = tagCheck?.hasTag === true;
  if (!isB2B) return NO_CHANGES;

  const codMethod = input.paymentMethods.find((pm) =>
    pm.name.toLowerCase().includes("cash on delivery")
  );
  if (!codMethod) return NO_CHANGES;

  return {
    operations: [{ hide: { paymentMethodId: codMethod.id } }],
  };
}

步骤 3.4:激活

Payment Customizations 在后台的 Settings → Payments → Customizations 下也有一个配置 UI。配送的激活步骤相同——选择你的 Function、保存,即可完成。

既然你正在阅读本文 —— 谈谈付款后流程(Post-Purchase)

既然这是 Revize 的博客,那就做个快速披露。Revize 负责处理 Functions 无法触碰的事物——只要订单下达后,客户可能想要增加商品、更换尺码、修改货运地址或应用遗忘的折扣。Functions 适用于结账阶段,而 Revize 适用于付款后。 Functions 决定哪些商品可以留在购物车;Revize 则允许你的客户和客服团队在事后修改订单,无需执行退款再重建订单的操作。这对于两类商店而言十分重要:一类是订单量庞大以至于手动编辑工作会崩溃的商店(Plus 运营商显然是,还有大流量的 Advanced 商家),另一类是视客户体验为生命、发送 “对不起,我们无法修改” 邮件会流失回头客的品牌。

如果你的迁移计划涵盖了 Scripts → Functions,但从未解决过付款后的订单修改问题,那么你大概会在三周内撞上“为什么这会如此艰难?”的下一堵墙。我们刚发布的订单管理指南展示了完美的付款后应对机制。

再回到迁移问题。

测试策略:标记客户模式

Functions 没有在后台可以直接切换的“草稿模式”。专业的流程是基于客户标签来设限新 Function,在系统里并排运行旧 Script 和新 Function,确认它们针对标记用户产生了相同的输出,然后关停旧版、完全切换过去。

步骤 1:标记你的测试用户

Customers 界面,将标签 FN-TESTER 添加到两到三个内部账户中。

步骤 2:对 Function 设置标签判断分支


// 在你的 run 函数的最上方
let is_tester = input
    .cart()
    .buyer_identity()
    .and_then(|bi| bi.customer())
    .map(|c| c.has_any_tag())
    .unwrap_or(&false);

if !*is_tester {
    return Ok(default_result);  // 回退至现有 Script 逻辑
}

// 新 Function 的逻辑仅针对标记用户执行

步骤 3:在输入查询中添加 hasAnyTag


cart {
  buyerIdentity {
    customer {
      hasAnyTag(tags: ["FN-TESTER"])
    }
  }
}

步骤 4:在结账页面中验证

使用已标记的用户身份登录,进行付款结账,确认 Function 正常触发。再换用不含标签的用户身份登录,确保旧的 Script 仍在照常运行。这种平行运行验证几天后,即可移除标签检测代码,让 Function 针对所有用户应用。

步骤 5:停用旧的 Script

在后台转至 Apps → Script Editor → [你的 Script] → Unpublish。一旦取消发布,对应的 Function 就成了唯一的规则来源。

真正实现规模化的部署工作流

不要永远都在开发者的笔记本上提交和部署。一旦完成了两到个 Script 的迁移,应当设立一个正规的 CI 管道。

最小化可行工作流


# .github/workflows/deploy-functions.yml
name: Deploy Shopify Functions
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - uses: dtolnay/rust-toolchain@stable
        with: { targets: wasm32-wasip1 }
      - run: npm install -g @shopify/cli@latest
      - run: shopify app deploy --force
        env:
          SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}

在你的 Partner 仪表板下方的 **Settings → Tokens** 页面生成合作伙伴 token。现在,每次向 main 分支的会合提交都会自动将 Functions 部署发布。不需要再通过 Slack 去询问 “Mike 刚才部署上去了吗?”

版本控制与回滚

shopify app deploy 会创建版本快照。执行回滚:


shopify app versions list
shopify app release --version <previous-version-id

与早先 Scripts 回滚机制(指“回忆之前的旧代码并重新粘贴回来”)相比,这种流程的改善可谓天翻地覆。



Shopify Functions deployment pipeline across local staging and production environments

大多数团队最容易犯的错

通过在过去的一年中帮助 Plus 商家处理此类迁移事务,我们发现这五个失误不断重复出现。

1. 把 Functions 当作 Scripts 的 1:1 照搬。 它们明显不是。单一的 Function 就能凭借更清晰的代码分支替代掉三个旧的 Scripts。编写代码前应当通盘审查现有的 Scripts 系统。

2. 遗忘了权限读取范围。 精细配置的 Functions 流程往往可能需要 read_customersread_orders 或者 write_discounts。在 shopify.app.tomlscopes 分类下加入权限,并对 app 重新进行赋权授权,不然你的 GraphQL 输入查询将仅仅得到 null。

3. 尚未进行平行对照测试,就直接在生产环境中在没打标签的真实客户群组上运行 Functions。 就算代码看上去完好无异,边缘条件(空购物车、礼品卡、商店储值、B2B 草稿订单等)仍有可能暴露出隐患点。花两天利用标签进行测试,能让你避开后续的 P1 事故灾难。

4. 跳过了 Customizations Report。 这是梳理线上正运行何种逻辑的绝佳资产报告。不要依靠零碎记忆去主导迁移——务必通过这份官方报告对照来进行。

5. 写死了 collection ID 和客户标签名。 如果你需要商家自主调节的可变设置,应该通过 metafields 部署 Function config。CLI 可以快捷构建基于元字段的配置模块——查询有关 Function 设定的 Shopify 官方文档即可。

未来 75 天迁移执行清单

一个务实平稳的周度执行方案,保证在 6 月 30 日气定神闲地收尾。


周计划

执行项

第 1 周 (本周)

导出 Customizations Report。盘清每一个 Script 的实际去向。定夺采用 Function、公共应用还是应该直接作废废弃。

第 2–3 周

配好本地开发环境。搭建第一个 Function 的业务骨架。先对最简单的 Script 进行迁移(通常是支付方式的隐藏规则)。

第 4–6 周

迁移折扣类 Script。这类迁移用时最长,因为 Discounts API 的处理分支最丰富。配合标签测试来反复核实。

第 7–8 周

迁移货运/配送控制的 Script。在 Shopify 后台对应的 Delivery Customizations 中启用这些逻辑。

第 9–10 周

配置好 CI 管道。让部署工作彻底脱离开发者的单机环境。

第 11 周 (6 月中旬)

完成终极大决算测试。把旧的 Ruby Scripts 均转成 unpublish 状态。让店铺靠 Functions 独立跑两周。

6 月 30 日

终点日。因为万事皆备,结账流程不会发生任何故障。

若你本周起步,时间绝对余裕满盈。拖到六月再论,则危若累卵。



Week-by-week migration roadmap from Shopify Scripts to Functions

常见问题解答

我非得加入 Shopify Plus 计划才能启用 Functions 吗?

如果是私有/自定义的 Custom Functions 确实需要 Plus 计划;但在 App Store 上开放给公众的 public-app 类型的 Functions 则在任何计划中都能正常运行。 倘若尚未订购 Plus,你面前有两条出路:去 Shopify 应用商店安装提供对应 Function 功能的外部第三方 app,或是直接升级 Plus 以此来解锁开发自有 Function 功能。考虑到当初用了 Scripts 代码的本就是 Plus 以上层级的商店为主,多数情况下这一点不用担心。

我可以写 TypeScript 面向 Functions 进行开发吗?

完全可以 —— CLI 内核支持生成 TypeScript 对应的脚手架。 在启动 shopify app generate extension 并点选 “JavaScript” 以后,生成的工程目录会把来源于 import("../generated/api") 的代码类型签名一并打好包。你要是中意,把后缀改为 .ts 加上一个 tsconfig.json 即可。它们产出的打包 WASM 二进制没有差异。

与 Scripts 比起来,Functions 的执行耗时处于何种水平?

通常耗时不超过 5ms —— 远超早期的 Ruby 解释器速度。 因为它们直接被预编译到 WebAssembly 层级运行,Shopify 严格制定了最大 5ms 物理限制。若是运算溢出了,逻辑请求便当作超时处理,Function 也不会回传结果。依照日常实践来看,优秀编写的实例大都在 1–2ms 范畴处理妥当。天花板远超早年 Script 性能。

Function 能否调用来自外部的第三方的 API?

完全没办法 —— 系统隔断了网络通信的能力。 算力表现纯粹是无副作用计算:传入购物车 GraphQL 数据 → 演算出目标操作。当遇到强依赖外部输入源(诸如实时查询一套外部 CRM 的结果、做一次即时的物理库存匹配记录)的机制时,需要在前置节点中,把相关数值存储至元字段 metafields 里面,或者采取别样的接口层(比如 App Proxy、Webhooks,或是使用附带后台查询支撑的 Cart Transform 技术)。这就是在重构时不能无脑照搬通常需重新考量设计的最普遍根源。

Cart Transform 与 Discounts 这两个概念有何实质区分?

Discounts 重在改写价格,Cart Transform 重在更改购物车里的客观商品状态。 对于 10% 折扣、免邮费福利或者买一送一 BOGO,依靠 Discounts API 来实行。要将两个独立的 SKU 融合成一个单一的捆绑品类、或者把特定款式打散分拆,就调用 Cart Transform。有很多年长久远的 Scripts 写法常常把这类功能混在一起——在这次迁移作业时,需要将不同的逻辑分拆进专属的 Functions 里去。

如何在没有开发商店的状况下本地调试我的 Function?

你能分别依据语言机制在单体测试跑 cargo test (Rust) 和 npm test (JS),可惜若要达成终极的整体化集成校验,必需要用到开发店铺。 开发命令行 CLI 备有 shopify app function run 便于借助假数据文件入参本地演练。但若希望通盘验明交易支付与支付界面的反应,还是必须在一座实际店铺里构建一辆真的购物车去做验证。

同一个运行节点(Target)支持绑定加载多个 Functions 吗?

可以 —— Shopify 支持在同一个节点加载多个 Functions,它们的执行顺序有确定的规则约束。 关于应用折扣,多者共存的行为由 Shopify 开箱即用的优惠叠加机制和堆叠标准制约。至于配送跟支付自定义模块,它们在流转逻辑中像链条一般衔接走下去。从简单的维度来规划,大部分公司最好在一个节点下仅仅布设一个 Function,尽量避免逻辑纠集。

当我发布了新 Function,先前对应的 Script 后续会怎么发展?

直到你在 Apps → Script Editor 明确将 Script 撤下下线(Unpublish)之前,两者将在生产环境里平行保持起效状态。 这绝对是出于工程安全的意图——它是系统特意给商家安排上的交叉印证校对阶段。当确定了新设的 Function 的准确率无误后,再去用手工将旧 Script 下线。到了 2026 年 6 月 30 日,所有遗留在那运行的 Scripts 均会彻底断连。

一连串的重构会不会打乱前端 SEO 的索引或者是扰乱线上模板代码?

根本不波及 —— 所有的 Functions 实质上全部运行在服务器后端的结账场景,绝不侵扰你的产品页面或模板。 其运作层面仅改变收尾付款阶段的价格、发货选项与可用支付网关组合。前台视觉展现、底层模板和 SEO 不遭受任何负面牵扯。

原 Script 程序会抽取 Input.line_items 自定义字段,对此应该如何处理?

GraphQL 入参的 cart lines 部分,可以用 attribute 结构获取对应的自定义属性。 在对应的 lines 的过滤节点中,把 attribute(key: "your-key") { value } 叠上去。其工作原理和 Scripts 时代识别自定义属性的方式大同小异,只不过是从 Ruby 方法调用转成了 GraphQL 的数据访问形式。

如果是标签、分析类数据,Functions 能够直接写这些数据吗?

Functions 没办法写自定义订单标签、也没办法驱动网络 webhook —— 它唯一的职责在于针对当前的购物车状态返回值。 倘若期望后续步骤添加客户标签或开展下游连锁反馈,应通过 Shopify Flow 监测并触发订单生成事件。大部分常见的处理组合是:靠 Function 处理特定的账目扣减,依靠 Flow 把 "VOLUME-DISCOUNT-APPLIED" 标签自动在最后添加到生成完结的 Order 文件内。

市面上会有现成的通用型 app 支持直接拿来顶上我的自定义开发空缺吗?

当然有 —— 商家应用商店里充斥着一堆将 Functions 包装成开箱即用功能的成熟 app。 搜索关键词 "discount function"、"delivery customization"、或是 "payment customization"。若是用处极通行的(按百分比打折、按照客户标记过滤特定的打款通道、消费满 X 直接免去货运费),靠大厂 app 起效会替你节约掉大把的工期时间。只有面对真正特立独行的私有逻辑,才考虑由团队出马去自研独享的 Functions。

假若万一没在 6 月 30 号前完工,其代价如何承受?

Script 停止执行 —— 绝无降级方案、无过渡缓冲期、也不能递交延期申请。 先前靠 Script 所构建的额外扣费优惠、被悄悄收回的寄送选项、被拉黑的支付手段,皆在 7 月 1 日零点(UTC)被打回原生态默认形式。如果你的业务仰仗这些逻辑,请务必提前做好时间规划。迁移所需的时间通常比开发团队预估的时间要长,尤其是如果考虑到还要加上环境对照测试的时间。

大局完成后,这套 Script Editor 应用能在系统后台卸载吗?

建议于 2026 年 6 月 30 日之后物理卸载,当然 Shopify 极大可能也会统一收束清理掉它。 一旦运行引擎彻底关停,其后台就没有保存意义了。当前若你把全部项目的平顺过渡、测试工序均核对结束,此时就去把该应用清理卸载其实完全可行,Functions 在它的外部独立依存、运作。

本周必须开展的几项工作

千万别读完本贴就把它关在一块当作浮云。在到来的这 7 天内,把这四项实践立刻打好地基。

1. 导出 Customizations Report。Settings → Checkout → Customizations Report。导出它。把这作为你要对齐修改的需求待办(backlog)。

2. 配齐个人及协同组的研发环境。 布置好 Node 18+、Shopify CLI 乃至 Rust 编译包。在终端核实 shopify version 能吐出正确版本。时长:最多 30 分钟。

3. 模板化起步部署:试炼一次把一个至简的功能打通至沙盒小店里去。 抓取你们目前最简单的 Script —— 典型地比如隐藏某样付款形式的需求。从头到尾重演一遍。哪怕这小段功能之后不应用到线上环境,起码这一整串的部署体系与工具链条是被你物理磨合过、踩过坑的。

4. 在工作日历上划出专门的时间段。 迁移工作不应该仅靠敏捷计划会议(sprint)之间的破碎缝隙凑合勉强进行。必须划拨一块专职的黄金时段——例如雷打不动定在每周的周二与周四半天——用极庄重神圣的态度来对待它的发布进度。

在那些见证过圆满度过本轮迁移工作的 Plus 团队眼中,大家的推进节奏都是趋同的:两周的技术拖延,三周的埋头开发,加上一周的安全修复收尾。这正好就是六周时间。你目前的手捏周数是十。冗余还是宽泛的——千万把它划在大面积方案演习和严格的代码走查审计中去,切忌拖延怠工。

当你确保所有的结账扣减机制尘埃落定之后,再往下的重点环节必定是消费者的订单后处理事务 —— 买家的派送坐标写错后的调优、换同款的尺码、下了单后悔后增添折扣凭证之类的事情。如果你手头的路线图也急待解决这摊流程(实际上所有 Plus 规模商家和 Advanced 级别优质商家都必然该把它置于高等级议程),Revize 已经被 Shopify 应用商店收录,完全契合辅助在此期间你们所写出来的每一套 Functions 背后协同运作。

资料参考


h2 id="312">相关博文推荐


重构你的 Shopify 店。以用户体验为主导。

© Copyright 2024, 保留所有权利

重构你的 Shopify 店。以用户体验为主导。

© Copyright 2024, 保留所有权利

重构你的 Shopify 店。以用户体验为主导。

© Copyright 2024, 保留所有权利

重构你的 Shopify 店。以用户体验为主导。

© Copyright 2024, 保留所有权利

The Universal Commerce Protocol UCP Guide How to Start a Shopify Store in 2026 The True Cost of Returns Guide How to Change Shipping Address on Shopify Best Shopify Customer Service Apps 10 Advanced Shopify Flow Workflows How to Add Discount on Shopify After Checkout How to Edit an Order on Shopify Shopify Draft Orders Complete Guide How to Do a Partial Refund on Shopify Shopify Social Login Complete Guide Post Purchase Email Marketing Automating E-commerce with Shopify Flow Customize Shopify Login Redirect Shopify New Customer Accounts Migration Guide How Poor Customer Support Can Sabotage Your Business How Refunds Work on Shopify In-House Warranty Management vs Shopify Apps Shopify Checkout Extensibility 2026: Deadline, Migration, and What's Broken How to Let Customers Cancel Orders on Shopify Shopify Legacy Customer Accounts Are Deprecated How to Edit Your Shopify Order Confirmation Email How to Do an Exchange on Shopify How to Sell on ChatGPT with Shopify Agentic Storefronts Shopify Functions Migration Tutorial Shopify AI Toolkit Guide 2026: Agents, MCP, and UCP Explained Shopify Sidekick vs Your Agency: The 2026 Scorecard for Plus Stores Shopify B2B 2026 Complete Guide Shopify Order Management Guide 2026 Shopify Advanced to Plus 2026 Migration Playbook 20 Shopify Flow AI Prompts Plus Operators Copy Shopify MCP Developer Guide 2026 EU Withdrawal Button for Shopify 2026 Best Shopify Order Editing Apps 2026