You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js/React开发Shopify App:API读写及产品名称获取问题

Shopify嵌入应用读取产品名称问题修复

问题说明

我是编码和Shopify纯新手,会用原生JS调用API,但对Node.js和React的操作逻辑不熟悉。已经通过Shopify CLI完成基础搭建,想要实现App读取并更新产品名称,目前第一步只想在控制台打印产品名称,但始终失败。

能直接访问https://[name].myshopify.com/admin/api/2022-10/products.json获取API数据,但前端发起请求时出现404错误:GET https://7172-60-***-***.jp.ngrok.io/undefined/admin/api/2022-10/products.json。期望点击表单按钮时,控制台能打印API返回的产品名称。

问题根源

  1. 前端环境变量process.env.SHOP和process.env.TOKEN未正确配置,导致请求URL出现undefined
  2. 前端直接请求Shopify Admin API会触发跨域(CORS)限制,Shopify不允许前端直接调用Admin API
  3. 后端自定义的接口存在逻辑错误:内部重新创建express实例,未返回数据,也未正确处理请求响应

修复方案

后端代码修改(web/index.js)

  • 全局配置CORS,避免重复创建express实例
  • 新增标准API接口/api/products,用于代理Shopify产品请求
  • 调用Product.all()后将数据返回给前端

前端代码修改(web/pages/index.js)

  • 前端不再直接请求Shopify Admin API,改为请求后端代理接口/api/products
  • 移除无效的环境变量引用,后端会通过会话自动处理Shopify认证

完整修改后代码

后端web/index.js

import { join } from "path";
import { readFileSync } from "fs";
import express from "express";
import cookieParser from "cookie-parser";
import { Shopify, LATEST_API_VERSION } from "@shopify/shopify-api";

import applyAuthMiddleware from "./middleware/auth.js";
import verifyRequest from "./middleware/verify-request.js";
import { setupGDPRWebHooks } from "./gdpr.js";
import productCreator from "./helpers/product-creator.js";
import redirectToAuth from "./helpers/redirect-to-auth.js";
import { BillingInterval } from "./helpers/ensure-billing.js";
import { AppInstallations } from "./app_installations.js";
import { Product } from '@shopify/shopify-api/dist/rest-resources/2022-10/index.js';
import cors from 'cors';


const USE_ONLINE_TOKENS = false;

const PORT = parseInt(process.env.BACKEND_PORT || process.env.PORT, 10);

// TODO: There should be provided by env vars
const DEV_INDEX_PATH = `${process.cwd()}/frontend/`;
const PROD_INDEX_PATH = `${process.cwd()}/frontend/dist/`;

const DB_PATH = `${process.cwd()}/database.sqlite`;

Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SCOPES.split(","),
  HOST_NAME: process.env.HOST.replace(/https?:\/\//, ""),
  HOST_SCHEME: process.env.HOST.split("://")[0],
  API_VERSION: LATEST_API_VERSION,
  IS_EMBEDDED_APP: true,
  // This should be replaced with your preferred storage strategy
  // See note below regarding using CustomSessionStorage with this template.
  SESSION_STORAGE: new Shopify.Session.SQLiteSessionStorage(DB_PATH),
  ...(process.env.SHOP_CUSTOM_DOMAIN && {CUSTOM_SHOP_DOMAINS: [process.env.SHOP_CUSTOM_DOMAIN]}),
});

// NOTE: If you choose to implement your own storage strategy using
// Shopify.Session.CustomSessionStorage, you MUST implement the optional
// findSessionsByShopCallback and deleteSessionsCallback methods.  These are
// required for the app_installations.js component in this template to
// work properly.

Shopify.Webhooks.Registry.addHandler("APP_UNINSTALLED", {
  path: "/api/webhooks",
  webhookHandler: async (_topic, shop, _body) => {
    await AppInstallations.delete(shop);
  },
});

// The transactions with Shopify will always be marked as test transactions, unless NODE_ENV is production.
// See the ensureBilling helper to learn more about billing in this template.
const BILLING_SETTINGS = {
  required: false,
  // This is an example configuration that would do a one-time charge for $5 (only USD is currently supported)
  // chargeName: "My Shopify One-Time Charge",
  // amount: 5.0,
  // currencyCode: "USD",
  // interval: BillingInterval.OneTime,
};

// This sets up the mandatory GDPR webhooks. You’ll need to fill in the endpoint
// in the “GDPR mandatory webhooks” section in the “App setup” tab, and customize
// the code when you store customer data.
setupGDPRWebHooks("/api/webhooks");

// export for test use only
export async function createServer(
  root = process.cwd(),
  isProd = process.env.NODE_ENV === "production",
  billingSettings = BILLING_SETTINGS
) {
  const app = express();

  // 全局配置CORS
  app.use(cors());
  app.set("use-online-tokens", USE_ONLINE_TOKENS);
  app.use(cookieParser(Shopify.Context.API_SECRET_KEY));

  applyAuthMiddleware(app, {
    billing: billingSettings,
  });

  // Do not call app.use(express.json()) before processing webhooks with
  // Shopify.Webhooks.Registry.process().
  app.post("/api/webhooks", async (req, res) => {
    try {
      await Shopify.Webhooks.Registry.process(req, res);
      console.log(`Webhook processed, returned status code 200`);
    } catch (e) {
      console.log(`Failed to process webhook: ${e.message}`);
      if (!res.headersSent) {
        res.status(500).send(e.message);
      }
    }
  });

  // All endpoints after this point will require an active session
  app.use(
    "/api/*",
    verifyRequest(app, {
      billing: billingSettings,
    })
  );

  // 新增产品列表接口
  app.get("/api/products", async (request, response) => {
    try {
      const session = await Shopify.Utils.loadCurrentSession(
        request,
        response,
        app.get("use-online-tokens")
      );
      const products = await Product.all({ session });
      response.status(200).json(products);
    } catch (error) {
      console.error("获取产品失败:", error);
      response.status(500).json({ error: error.message });
    }
  });

  app.get("/api/products/count", async (req, res) => {
    const session = await Shopify.Utils.loadCurrentSession(
      req,
      res,
      app.get("use-online-tokens")
    );
    const { Product } = await import(
      `@shopify/shopify-api/dist/rest-resources/${Shopify.Context.API_VERSION}/index.js`
    );

    const countData = await Product.count({ session });
    res.status(200).send(countData);
  });

  app.get("/api/products/create", async (req, res) => {
    const session = await Shopify.Utils.loadCurrentSession(
      req,
      res,
      app.get("use-online-tokens")
    );
    let status = 200;
    let error = null;

    try {
      await productCreator(session);
    } catch (e) {
      console.log(`Failed to process products/create: ${e.message}`);
      status = 500;
      error = e.message;
    }
    res.status(status).send({ success: status === 200, error });
  });

  // All endpoints after this point will have access to a request.body
  // attribute, as a result of the express.json() middleware
  app.use(express.json());

  app.use((req, res, next) => {
    const shop = Shopify.Utils.sanitizeShop(req.query.shop);
    if (Shopify.Context.IS_EMBEDDED_APP && shop) {
      res.setHeader(
        "Content-Security-Policy",
        `frame-ancestors https://${encodeURIComponent(
          shop
        )} https://admin.shopify.com;`
      );
    } else {
      res.setHeader("Content-Security-Policy", `frame-ancestors 'none';`);
    }
    next();
  });

  if (isProd) {
    const compression = await import("compression").then(
      ({ default: fn }) => fn
    );
    const serveStatic = await import("serve-static").then(
      ({ default: fn }) => fn
    );
    app.use(compression());
    app.use(serveStatic(PROD_INDEX_PATH, { index: false }));
  }

  app.use("/*", async (req, res, next) => {
    if (typeof req.query.shop !== "string") {
      res.status(500);
      return res.send("No shop provided");
    }

    const shop = Shopify.Utils.sanitizeShop(req.query.shop);
    const appInstalled = await AppInstallations.includes(shop);

    if (!appInstalled && !req.originalUrl.match(/^\/exitiframe/i)) {
      return redirectToAuth(req, res, app);
    }

    if (Shopify.Context.IS_EMBEDDED_APP && req.query.embedded !== "1") {
      const embeddedUrl = Shopify.Utils.getEmbeddedAppUrl(req);

      return res.redirect(embeddedUrl + req.path);
    }

    const htmlFile = join(
      isProd ? PROD_INDEX_PATH : DEV_INDEX_PATH,
      "index.html"
    );

    return res
      .status(200)
      .set("Content-Type", "text/html")
      .send(readFileSync(htmlFile));
  });

  return { app };
}

createServer().then(({ app }) => app.listen(PORT));

前端web/pages/index.js

import { React, useState, useCallback } from "react";
import {
  Page,
  Layout,
  Card,
  Button,
  Form,
  FormLayout,
  TextField,
  Stack,
} from "@shopify/polaris";
import { ResourcePicker } from "@shopify/app-bridge-react";
import axios from "axios";

const axiosGetData = () => {
  axios({
    url: "/api/products",
    method: "GET",
  })
    .then((res) => {
      // 遍历打印所有产品名称
      res.data.forEach(product => {
        console.log("产品名称:", product.title);
      });
    })
    .catch((err) => {
      console.error("请求产品数据失败:", err);
    });
};


const index = () => {
  const [value, setValue] = useState("Name");
  const [state, setState] = useState(false);

  const handleChange = useCallback((newValue) => setValue(newValue), []);

  function handleSubmit(e) {
    setState(true);
    e.preventDefault();
    console.log(value);
    axiosGetData();
  }

  function handleCancel() {
    setState(false);
  }

  function handleSelection(resources) {
    setState(false);
    resources.selection.map((name) => (name.handle = value));
    console.log(resources.selection);
  }

  return (
    <Page title="My Shop">
      <Layout>
        <Layout.Section>
          <Card title="Change Product Name" sectioned>
            <Form onSubmit={handleSubmit}>
              <FormLayout>
                <TextField
                  label="Type on the input the new name and select which product name to change"
                  value={value}
                  onChange={handleChange}
                />
                <Button primary submit>
                  Select Product
                </Button>
              </FormLayout>
              <ResourcePicker
                resourceType="Product"
                open={state}
                onCancel={handleCancel}
                onSelection={handleSelection}
              />
            </Form>
          </Card>
        </Layout.Section>
      </Layout>
    </Page>
  );
};

export default index;

内容的提问来源于stack exchange,提问作者Nostalgia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 02:25:53