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返回的产品名称。
问题根源
- 前端环境变量
process.env.SHOP和process.env.TOKEN未正确配置,导致请求URL出现undefined - 前端直接请求Shopify Admin API会触发跨域(CORS)限制,Shopify不允许前端直接调用Admin API
- 后端自定义的接口存在逻辑错误:内部重新创建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
相关产品推荐
相关产品推荐

