Shopify Webhooks 進行即時資料訂閱

共享數據 webhooks,這是一種將數據再發生特定事件後即時從一個應用程式發送到另一個應用程式的單向通信,當我們需要從應用服務將資料保持同步至其他系統上或是擴展其他現有功能需求時,webhooks 則能很好的解決在開發上的需求。

Webhooks的工作原理

Webhook 通信是通過從應用服務提供的程式向目標發送 HTTP 請求來實現的資料傳遞。當應用服務中發生事件時,將會觸發該事件相關數據至所設定的 HTTP 請求端點。

例如在 Shopify 上為商家註冊一個 orders/create 事件並提供一個 HTTPS URL。每當商家上的訂單有被建立時,Shopify 就會向註冊的 URL 以 JSON 或 XML 格式發送該訂單相關數據。

設置和配置 Webhook

使用 REST API 管理配置 webhook

當我們是一個 Public App 就會需要通過 REST API 的方式來管理配置 webhook,以下是 Shopify 提供 webhook 相關的 Endpoints:

使用 Shopify 商家平台配置 webhook

再 Shopify 商家平台配置 webhook 就比較容易,我們可以從管理平台中點擊 設定 → 通知 → 滑動到最下方就可以找到 webhook 設定的介面。

Webhook 在開發時需要注意及思考的

安全證書

在設定 Shopify webhook 時接收端的 Server 必須是為 SSL 的環境

當丟失數據

在設計系統時考慮可能系統故障或是中斷時,我們該如何處理丟失的數據,在官方文件上也有提到不能僅僅依賴於從 Shopify webhooks 接收數據,所以避免這樣的情境發生,需要也考慮設計一個定期查詢 Shopify API 數據的相關程式作為應對。

數據的重複性

多次接收相同的 webhook 是可能發生的,所以我們在自己的系統上也需要記錄每次傳輸的數據,並使用 Webhook 的唯一 ID X-Shopify-Webhook-Id 來驗證 Webhook 是否已被處理完成。

快速回應狀態碼

如果在 Shopify 發送的五秒內接收端未收到 HTTP 狀態,則會認為它超時視為錯誤響應並重新發送 POST 請求,若持續發生將在 48 小時內總共發送 19 個請求。

在設計系統時,需要思考區分哪些任務是耗時的,再來用 queue 作業方式來將複雜的任務放入背景處理。

接收端的驗證

Shopify webhook 發送資料到接收端時會包含以下 HTTP Header:

  • X-Shopify-Topic — 該值會帶入是由哪個觸發事件的名稱
  • X-Shopify-API-Version — 該值會帶入目前使用的版本
  • X-Shopify-Webhook-Id — 該值會帶入 webhook 的唯一 ID
  • X-Shopify-Shop-Domain — 該值會帶入關聯的商家域名 shop.myshopify.com
  • X-Shopify-Hmac-Sha256 — 該值會帶入 base64 編碼的字串,用於驗證 webhook 的請求

我們可以通過 HTTP Header 將 X-Shopify-Hmac-Sha256 的值與計算出的 HMAC 來進行比較驗證,如果 X-Shopify-Hmac-Sha256 與計算的結果匹配,那麼就可以確定該通知是從 Shopify 發送的。

PHP code example

define('SHOPIFY_APP_SECRET', '');

$hmacHeader = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'];
$data = file_get_contents('php://input');

$calculatedHmac = base64_encode(hash_hmac('sha256', $data, SHOPIFY_APP_SECRET, true));

if ($calculatedHmac === $hmacHeader)
{
    echo 'hmac verified';
}

Shopify App 使用 OAuth 2 進行商家認證和授權

在現在的程式開發語言或框架,可能能夠找到現有的函式庫來快速打造出需要的功能,避免自己編寫過多的程式,但在遭遇問題處理時可能往往會不知道背後的運作流程,所以寫了這篇文章來記錄了解 Shopify 的 OAuth 身份驗證流程的工作原理。

首先建立一個應用程式,獲得 API key 與 API secret key,並在 App 設定中提供 App URL 與允許的重新導向網址。

簡敘什麼是 OAuth2?

根據官方 OAuth 描述:OAuth 2.0 是用於授權的行業標準協議。OAuth 2.0 專注於客戶端開發人員的簡單性,同時為 Web 應用程序、桌面應用程序、移動電話和客廳設備提供特定的授權流程。

它是一種身份驗證和授權訪問權限的方式,網路上的用戶可以在訪問他們在其他網站上的資料,而不需要提供他們的帳戶資料(用戶名/密碼)。

Shopify OAuth 流程

OAuth 流程中處理的步驟:

  1. 商家發送安裝應用程式的請求
  2. 將權限請求導向回 shopify
  3. 商家確認授權頁面
  4. shopify 向應用程式返回確認結果
  5. 將結果再次發送回 Shopify 獲取永久 access token

1.商家發送安裝應用程式的請求

當商家訪問我們的應用程式時,Shopify 會導向並傳遞幾個額外的參數到我們設定的 App URL。

https://app.domain.dev/?hmac=93cf7b063bae0790b74bb0a34e8e5b78e997de8ae771202f3bcd71f504d6d68e&shop=store.myshopify.com&timestamp=1634909927

收到請求時首先進行 hmac 驗證來確保是來自 shopify 的請求。取得 hmac 參數以外的參數組成字串,使用 sha256 加密算法取得 hexdigest 並驗證是否與 hmac 參數一致。

PHP code example

$ary = [];
$hmac = $_GET['hmac'];
unset($_GET['hmac']);

foreach($_GET as $key => $value) {
    $ary[] = $key."=".$value;
}

$str = join('&', $ary);
$ver_hmac =  hash_hmac('sha256', $str, "APP-SECRET-KEY", false);

if($ver_hmac == $hmac)
{
    echo 'hmac verified';
}

2.將權限請求導向回 shopify

現在已經完成請求的真實性,再將該請求的參數信息結合起來構建一個 ****URL 向商家取得授權。

https://store.myshopify.com/admin/oauth/authorize?client_id=080cceb67cdeebfd96390a4d59c66fc1&redirect_uri=https%3A%2F%2Fapp.domain.dev%2Fauth%2Fshopify%2Fcallback&scope=read_products%2Cwrite_products&state=aaf4d88ef34fdc39c951d1864b627fdb65b9e719
  • client_id:應用程式的 API key
  • redirect_uri:在商家授權應用程式的權限請求後,shopify 會將頁面導向該網址。此 URL 需要與App 設定中提供的重新導向網址一致
  • scope:應用程式被授予的權限, shopify 會提示商家查看權限授權範圍
  • state:每個權限 URL 需要創建的隨機值,再 shopify 返回確認結果時必須檢查此值是否一致

3.商家確認授權頁面

當應用程式把所需要的授權訊息導向回去時, shopify 會提示應用程式所需要授予的權限。

4.shopify 向應用程式返回確認結果

商家點擊 "安裝應用程序" 這時候 shopify 會將頁面在重新導向至我們再第二個步驟所帶入的 redirect_uri ,並且傳遞幾個額外的參數讓我們再次驗證。

https://app.domain.dev/auth/shopify/callback?code=04353d50220ae434b065224421480b90&hmac=cfdcc94094bd645d012281c4071dd27b504690732751bdc3187a622e78d08303&host=dGVzdC1lcmljeHh4Lm15c2hvcGlmeS5jb20vYWRtaW4&shop=store.myshopify.com&state=aaf4d88ef34fdc39c951d1864b627fdb65b9e719&timestamp=1634977849
  • code:可以使用它來交換商家權限的永久 API access code
  • hmac:與第一個步驟一樣通過計算 HMAC SHA256 digest,來驗證請求的真實性
  • state:確認在第二個步驟所提供的隨機值是否與返回的請求一致
  • shop:商家資料

5.將結果再次發送回 Shopify 獲取永久 access token

透過 Shopify API 來交換我們最終所需要的永久 access token。

PHP code example

$query = [
  "client_id" => $api_key, 
  "client_secret" => $shared_secret, 
  "code" => $params['code'] 
];

$access_token_url = "https://" . $params['shop'] . "/admin/oauth/access_token";

$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $access_token_url);
curl_setopt($ch, CURLOPT_POST, count($query));
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($query));
$result = curl_exec($ch);
curl_close($ch);

$result = json_decode($result, true);
$access_token = $result['access_token'];

此 access token 則具有我們在第 2 步驟中所請求的權限範圍。我們現在可以使用它代表商店所有者向 Shopify API 發出請求進行查閱或異動資料。

Shopify App Development

Shopify App 允許開發者為在線商店客戶提供增強的服務,擴充既有 Shopify 功能或是與第三方應用程式進行資料串接,定制以滿足商店客戶的特定需求。

Shopify App 可以分為三種模式,來進行增強商家需求

Public App

  • 允許與多個商店合作
  • 需經 Shopify 審核,才能讓商店客戶安裝
  • 可以列於 Shopify App Store

比較 Shopify App 類型時還有技術差異,Public App 需要使用 OAuth 進行身份驗證並獲得用戶的許可,才能訪問 REST API 中的任何資源。

Custom App

  • 限制單一商家使用
  • 不需要經 Shopify 審核
  • 部分 API 有限制

Custom App 也可以嵌入到 Shopify 管理面板中,提供單一商店客戶做使用,但在 Shopify API 的訪問上就有些限制,無法使用更多複雜功能的 API。

Private App

Private App 不能嵌入到 Shopify 管理面板,使用情境會像是在處理商店數據與第三方數據進行處理,將資料導入或導出,在 Shopify API 的訪問權限上也有限制。

存取 Shopify API 的身份驗證是通過商店建立好的 API key 與 Password 來進行簡單的身份驗證。

Shopify 靜態與動態 Sections

Sections 當開發完 Sections 模組最常拿來在 theme 首頁做使用,在深入瞭解後其實還有其他使用方式來搭配 佈景主題編輯器 讓商家在各頁面版面配置外觀內容。

在 Shopify 上的 Sections 有分兩種類型:

靜態區段

頁面上特定位置的區段如頁首、頁尾、以及自定義的區段,使用靜態區段無法移除或重新排序。

Step 1: 新增頁面樣板

在主題上建立新的 liquid file template/page.static.liquid 並添加此標籤使用靜態區段

{% section 'section-file-name' %}

Step 2: 新增一個新頁面

網路商店 > 頁面

在右下角選擇我們剛剛建立的頁面樣板 page.static.liquid

完成此操作後,繼續打開主題編輯器並從下拉列表中選擇新頁面,就可以看到自定義的靜態區段

動態區段

theme editor 上面可讓商家自由的新增、重新排列或移除區段,比較靈活的進行版面配置。

動態區段 Shopify 文件上沒有任何的使用方式說明,原以為只能在首頁上使用,直到有一次在進行功能開發評估時發現在其他頁面上竟然也可以使用,接下來介紹一下使用方法。

Step 1: 新增頁面樣板

在主題上建立新的 json file template/page.dynamic.json

{
  "sections": {
    "list-collections-template": {
      "type": "list-collections-template",
      "disabled": false,
      "settings": {
      }
    }
  },
  "order": [
    "list-collections-template"
  ]
}

Step 2: 在頁面選擇 dynamic 頁面樣板

這次製作的樣板為 json 在這邊的內容就無法使用,所以可能要製作一個 Sections 提供內容上架。

完成此操作後,繼續打開主題編輯器並從下拉列表中選擇頁面,就可以看到動態的區段可讓商家自由的新增或移除。

Shopify 開發 Section 商品輪播組件


設計一個商品輪播的區段,輪播功能使用 Shopify 內的 Slick Carousel 套件

調整 sections/custom-section.liquid

<section class="custom-section" data-section-id="{{ section.id }}" data-section-type="custom-section">
  <div class="box">
    <div class="wrapper">
      <div class="grid grid-spacer slick slick-product-grid">
        {%- for block in section.blocks -%}
          {%- if block.settings.product_id == blank -%}
            <div class="grid__item grid-product flex empty"></div>
          {%- else -%}
            {%- render 'product-grid-item' with all_products[block.settings.product_id] as product -%}
          {%- endif -%}
        {%- endfor -%}
      </div>
    </div>
  </div>
</section>
{% schema %}
{
    "name": "Custom Section",
    "settings": [{
        "id": "custom_text_title",
        "type": "text",
        "label": "Text box heading",
        "default": "Title"
    }],
    "blocks": [{
        "type": "description",
        "name": "Product",
        "settings": [{
            "type": "product",
            "id": "product_id",
            "label": "Product"
        }]
    }],
    "presets": [{
        "name": "Custom Section",
        "category": "Product"
    }]
}
{% endschema %}

HTML

在這個例子中, section 標籤設定兩個 **data-* 屬性,當每個 section 建立時都會有一組 unique ID 可以通過這組 ID 來設定 script 的部分,所以將他儲存至 data-section-id,另外 data-section-type** 則是用來定義 script 的初始化類別。

blocks 的部分是由商家可以自行增加,在 Liquid 中可通過 section.blocks 取得,在使用迴圈的方式渲染每個商品,這邊引用預設 snippets 的 product-grid-item 來建立。

all_products 是一個 Global objects,我們在 schema 定義給商家的選擇欄位是 product_id,在轉換到 Liquid 時只能抓取到商品的 Object handles,所以需要通過 all_products objects 來找出指定的商品

schema

在 settings 標籤屬性:

  • id 指的是定義 Liquid 存取的變數
  • type 定義給商家輸入的類型,類型包括 image_picker、radio、video_url ⋯⋯等
  • label 顯示在主題編輯器上的名稱
  • default 設定輸入欄位預設值

接下來調整前端程式的部分

調整 layout/theme.liquid

<!-- <script src="{{ 'theme.min.js' | asset_url }}" defer="defer"></script> -->
<script src="{{ 'theme.js' | asset_url }}" defer="defer"></script>

這邊因為使用 theme 預設的 script,所以要調整一下載入的路徑

調整 asset/theme.js.liquid

theme.CustomSection = (function() {
  function CustomSection(container) {
    var $container = (this.$container = $(container));
    var sectionId = $container.attr("data-section-id");
    var slider = (this.slider = `#ProductGrid-${sectionId}`);
    var $slider = $container.find(".slick-product-grid");

    if (!$slider.length) {
      return;
    }

    var slickOptions = {
      arrows: true,
      dots: true,
      autoplay: true,
      autoplaySpeed: 1000,
      slidesToShow: 5,
      slidesToScroll: 1,
      swipeToSlide: true,
      infinite: false,
      speed: 200
    };

    var mobileOptions = $.extend({}, slickOptions, {
      slidesToShow: 2,
      centerMode: true,
      focusOnSelect: true,
      autoplay: false,
      infinite: true
    });

    enquire.register(theme.variables.mediaQuerySmallUp, {
      match: function() {
        theme.carousel.init({
          slider: $slider,
          slickOptions: slickOptions
        });
      }
    });

    enquire.register(theme.variables.mediaQuerySmall, {
      match: function() {
        theme.carousel.init({
          slider: $slider,
          slickOptions: mobileOptions
        });
      }
    });
  }

  CustomSection.prototype = _.assignIn({}, CustomSection.prototype, {
    onUnload: function() {
      theme.carousel.destroy($(this.slider));
    }
  });

  return CustomSection;
})();

$(document).ready(function() {
  $("body").addClass("page-loaded");

  theme.init();

  sections.register("custom-section", theme.CustomSection);
});

這邊使用原始架構的 script 進行區塊的擴充,在 jQuery 載入時使用 sections.register 註冊一個我們定義的 object,參數 custom-section 是在 sections/custom-section.liquid 裡設定的 data-section-type

在定義的 object 中所要控制的元素效果就可以寫在這裡,比較要注意的是需要提供一個 prototype.onUnload 方法,當在主題編輯器移除區段時可以清除。

在 JavaScript global variable 中有個 Shopify.designMode 參數,往後需要開發更複雜功能時可以使用,區別目前所執行的程式是否在主題編輯器中。

以下是這個例子中設計的商品輪播 Sections:

Shopify 探索 Theme Sections

Sections 是 Shopify 使用組件方式構成頁面,使開發者更加模組化可以更好地控制自訂義每個單獨組件,商家也能使用 Shopify theme editor 輕鬆自訂內容和商店外觀,以及即時預覽自訂變化。

Shopify 本身已經提供相當多的 Sections 組件可以應用,開發者可以將內建的 Sections 重新設計再定義樣式或功能,當需求遠遠不夠時我們也可以新增 Sections 組件。

開始開發一個新的 Sections 組件

在 sections 資料夾建立一個 liquid 檔案

theme 資料夾結構

shopift-theme
  - assets
  - config
  - layout
  - sections
    - custom-section.liquid
  - templates

custom-section.liquid 文件中架構

<section class="custom-section">

</section>
{% schema %}
{
    "name": "Custom Section",
    "settings": [],
    "blocks": [],
    "presets": [{
        "name": "Custom Section"
    }]
}
{% endschema %}

{% stylesheet %}
{% endstylesheet %}

{% javascript %}
{% endjavascript %}

通過在線商店進入 Shopify 主題編輯器點選左下角新增區段,就可以看到剛剛新增的組件。

schema

定義在 Shopify 主題編輯器裡的組件可設置的內容,例如文字輸入欄位和圖像選擇器、自定義 HTML 和選擇商品 ⋯⋯ 等等,讓商家可以自定內容。

設計的架構屬性:

  • name - 定義新增後區段顯示的名稱

  • settings - 定義區段可讓商家可輸入的內容

  • blocks - 定義可以使用多個子區塊可讓商家重複新增、刪除和排序

    依目前經驗只能設置 1 維數組,目前還無法使用多維數組進行需求開發,可能必須使用其他方法

  • presets - 這裡的 name 屬性是用來定義新增前顯示在可選組件中的名稱

stylesheet

定義在區段新增後渲染的 CSS 樣式,用法與一般開發 CSS 方式相同,需要注意的是因為區段是可以重複新增的在這裡定義的 CSS 就會重複出現造成 HTML 會很肥,如果不需要重複設定的樣式可以略過在這裡定義直接設計在其他共用的 CSS 中。

javascript

定義在區段新增後渲染的 script,這邊的用法與 stylesheet 一樣,需要注意重複新增問題。

Shopify 使用 Git 開發工作流程


一開始接觸 shopify 做主題開發時,用的是 shopify admin 提供的 online code editor 來做簡單的業務需求,不過這樣不太適合做整個主題的更動。

更好開發工作流程

安裝 shopify cli 的命令行工具,它可以快速生成 Node.js 開發環境,在本地進行更改並將這些更改與 shopify 網站同步

brew tap shopify/shopify
brew install shopify-cli

# 初始化專案一個新主題
shopify theme init

# 或者可以初始化已經存在的 git repository 
shopify theme init [NAME] --clone-url=https://github.com/shopify/dawn.git

# 使用 Shopify CLI 進行身份驗證
shopify login --store shoptestex.myshopify.com

在本地環境預覽開發的主題

開啟連結 http://127.0.0.1:9292 就可以觀看本地環境開發的程式,另外也支援 hot reloading

shopify theme serve

安裝 Shopify GitHub 集成

  1. 在 github 建立新的 repository,將開發的主題提交上去
  2. 從 shopify 後台,轉到 網路商店 → 佈景主題
  3. 佈景主題庫 新增佈景主題從 github 連接
  4. 登錄 github 選擇要連接的 repository
  5. 找到連接的主題,然後點擊 動作 → 發佈

Shopify 版本控制最佳實踐

通過 shopify 後台編輯主題時,任何更改都會由 shopify 自動提交到 github,這樣當開發人員在提交到 github 有可能就會遇到衝突和錯誤。

所以應該建立一個 staging branch ,在多個開發人員在同一個商店上開發功能時,都只處理 staging branch,當主題準備上線時,再將 staging branch 合併到 master branch。

安裝 Visual Studio Code plugin

theme check 可以檢查主題中的 liquid 和 json,可以讓我們在開發時快速檢測錯誤

https://marketplace.visualstudio.com/items?itemName=Shopify.theme-check-vscode