如何在 Shopify 中使用 Prisma Postgres
簡介
Shopify 是一個熱門的電子商務開店平台。本指南將向您展示如何將 Shopify 應用程式連接至 Prisma Postgres 資料庫,以便為商品建立內部筆記。
先決條件
1. 設定您的專案
如果您尚未安裝 Shopify CLI,可以使用 npm install -g @shopify/cli 進行安裝。
首先,使用 Shopify CLI 初始化一個新的 Shopify 應用程式:
shopify app init
在設定過程中,系統會提示您自訂應用程式。別擔心,只需遵循以下建議選項即可快速上手,並確保您的應用程式設定順利:
- 開始建立您的應用程式:
Build a Remix app (推薦) - 對於您的 Remix 模板,您想要使用哪種語言:
JavaScript - 應用程式名稱:
prisma-store(名稱不能包含shopify)
導覽至 prisma-store 目錄
cd prisma-store
2. 設定 Prisma
Prisma 已預先安裝在您的專案中,但我們先花點時間將其更新至最新版本。這能確保您在建構應用程式時,能夠使用最新的功能、改進項目並獲得最佳體驗。
由於您將切換至 Prisma Postgres 資料庫,請刪除 prisma 目錄中的 migrations 資料夾以及 dev.sqlite 檔案。
您需要更新 schema.prisma 檔案中的幾項內容,以便使其與 Remix 和 Prisma Postgres 配合運作。
- 切換至新的
prisma-client產生器。 - 將 provider 更新為
postgresql。 - 將 url 更新為新的資料庫連結。
generator client {
provider = "prisma-client-js"
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "sqlite"
provider = "postgresql"
url = "file:../dev.db"
}
model Session {
// ... existing model
}
建立 prisma.config.ts 檔案以設定 Prisma:
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});
由於 Shopify 應用程式通常預先安裝了 dotenv,因此您應該已經可以存取它。如果沒有,請使用以下指令安裝:
npm install dotenv
為了讓您的應用程式能夠儲存每個商品的筆記,讓我們在 Prisma schema 中新增一個 ProductNote 模型。
此模型將允許您透過 productGid 欄位,在資料庫中儲存並整理連結至特定商品的筆記。
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model Session {
// ... existing model
}
model ProductNote {
id String @id @default(uuid())
productGid String
body String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
接下來,需要將 Prisma 更新至最新版本。執行:
npm install prisma @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg pg
如果您使用的是不同的資料庫提供者(MySQL、SQL Server、SQLite),請安裝相應的驅動程式適配器套件,而不是 @prisma/adapter-pg。如需更多資訊,請參閱資料庫驅動程式。
Prisma Postgres 允許您即時建立新的資料庫,您可以在初始化專案時加入 --db 旗標來同時建立新的資料庫。
npx prisma init --db
完成提示後,是時候存取您的新資料庫了。
-
開啟:
- 登入並選擇您剛建立的資料庫專案。
-
取得您的資料庫連線字串。
- 點擊 Connect 按鈕。
- 複製出現的連線字串。它看起來應該像這樣:
DATABASE_URL="postgresql://user:password@host:5432/database?sslmode=require" -
設定您的環境
- 在專案根目錄中建立一個新的
.env檔案。 - 將您剛剛複製的
DATABASE_URL貼到此檔案中。
- 在專案根目錄中建立一個新的
-
套用您的資料庫 schema
- 執行以下指令來建立資料表並準備好您的資料庫:
npx prisma migrate dev --name init接著生成 Prisma Client
npx prisma generate
現在,在繼續之前,讓我們更新 db.server.ts 檔案,以使用帶有驅動程式轉接器 (driver adapter) 的新產生 Prisma 客戶端。
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from "./generated/prisma/client.js";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});
if (process.env.NODE_ENV !== "production") {
if (!global.prismaGlobal) {
global.prismaGlobal = new PrismaClient();
global.prismaGlobal = new PrismaClient({ adapter });
}
}
const prisma = global.prismaGlobal ?? new PrismaClient();
const prisma = global.prismaGlobal ?? new PrismaClient({ adapter });
export default prisma;
建議將 app/generated/prisma 加入到您的 .gitignore 檔案中。
3. 建立您的 Remix 模型
為了保持專案結構整潔,讓我們建立一個新的 models/ 資料夾。在此資料夾內,新增一個名為 notes.server.js 的檔案。這將是您所有筆記相關邏輯的存放處,隨著應用程式擴展,這會使您的程式碼庫更易於管理。
notes.server.js 檔案將包含兩個函式:
getNotes- 用於取得特定商品的所有筆記。createNote- 用於為特定商品建立新筆記。
首先,從 db.server.ts 匯入 Prisma 客戶端,並建立 getNotes 函式:
import prisma from "../db.server";
export const getNotes = async (productGid) => {
const notes = await prisma.productNote.findMany({
where: { productGid: productGid.toString() },
orderBy: { createdAt: "desc" },
});
return notes;
};
為了讓使用者能夠新增筆記到您的資料庫,讓我們在 notes.server.js 中建立一個使用 prisma.productNote.create 的函式。
import prisma from "../db.server";
export const getNotes = async (productGid) => {
const notes = await prisma.productNote.findMany({
where: { productGid: productGid.toString() },
orderBy: { createdAt: "desc" },
});
return notes;
};
export const createNote = async (note) => {
const newNote = await prisma.productNote.create({
data: {
body: note.body,
productGid: note.productGid,
},
});
return newNote;
};
4. 建立您的版面配置路由 (Layout Route)
在呼叫這些函式之前,我們的路由需要一個版面配置來承載。此版面配置路由將包含一個用於選擇商品的按鈕,並作為 ProductNotes 路由的父層,保持應用程式結構清晰且便於使用。
4.1. 建立 ProductNotesLayout 元件
首先建立 routes/app.product-notes.jsx 資料夾,並在其中新增 ProductNotesLayout 元件。
import { Page, Layout } from "@shopify/polaris";
export default function ProductNotesLayout() {
return (
<Page title="Product Notes">
<Layout>
<Layout.Section></Layout.Section>
</Layout>
</Page>
);
}
接下來,建立 selectProduct 函式以及一個 Button,讓使用者可以挑選商品。
import { useNavigate } from "@remix-run/react";
import { Page, Layout } from "@shopify/polaris";
import { Button, Page, Layout } from "@shopify/polaris";
export default function ProductNotesLayout() {
const navigate = useNavigate();
async function selectProduct() {
const products = await window.shopify.resourcePicker({
type: "product",
action: "select",
});
const selectedGid = products[0].id;
navigate(`/app/product-notes/${encodeURIComponent(selectedGid)}`);
}
return (
<Page title="Product Notes">
<Layout>
<Layout.Section>
<Button onClick={selectProduct} fullWidth size="large">
Select Product
</Button>
</Layout.Section>
</Layout>
</Page>
);
}
Remix 提供了渲染巢狀路由的能力。在 routes/app.product-notes.jsx 檔案中新增一個 <Outlet />,這裡將會渲染 ProductNotes 路由。
import { useNavigate } from "@remix-run/react";
import { Outlet, useNavigate } from "@remix-run/react";
import { Page, Button, Layout } from "@shopify/polaris";
export default function ProductNotesLayout() {
const navigate = useNavigate();
async function selectProduct() {
const products = await window.shopify.resourcePicker({
type: "product",
action: "select",
});
const selectedGid = products[0].id;
navigate(`/app/product-notes/${encodeURIComponent(selectedGid)}`);
}
return (
<Page title="Product Notes">
<Layout>
<Layout.Section>
<Button onClick={selectProduct} fullWidth size="large">
Select Product
</Button>
</Layout.Section>
<Outlet />
</Layout>
</Page>
);
}
4.2. 將 ProductNotesLayout 加入側邊欄
如果您執行 npm run dev,您將無法看到 Product Notes 路由。為了解決此問題,您需要將 ProductNotesLayout 加入到 app.jsx 檔案中,使其顯示在側邊欄。
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
import { boundary } from "@shopify/shopify-app-remix/server";
import { AppProvider } from "@shopify/shopify-app-remix/react";
import { NavMenu } from "@shopify/app-bridge-react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
import { authenticate } from "../shopify.server";
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
export const loader = async ({ request }) => {
await authenticate.admin(request);
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
};
export default function App() {
const { apiKey } = useLoaderData();
return (
<AppProvider isEmbeddedApp apiKey={apiKey}>
<NavMenu>
<Link to="/app" rel="home">
Home
</Link>
<Link to="/app/product-notes">Product Notes</Link>
</NavMenu>
<Outlet />
</AppProvider>
);
}
// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
export function ErrorBoundary() {
return boundary.error(useRouteError());
}
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};
5. 建立您的商品筆記路由
目前,如果您執行 npm run dev 並前往 Product Notes 路由,選擇商品後會看到一片空白。
請按照以下步驟建立商品筆記路由:
建立一個新的 routes/app/app.notes.$productGid.jsx 檔案,該檔案將接收 productGid 作為參數,並傳回與該商品相關聯的商品筆記,以及一個用於建立新筆記的表單。
export default function ProductNotes() {
return (
<></>
);
}
5.1. 渲染筆記
載入時,路由需要獲取該商品的筆記並顯示出來。
為該路由新增一個 loader 函式。
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getNotes } from "../models/note.server";
export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};
export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
return (
<></>
);
}
使用 Polaris 元件,在 ProductNotes 元件中映射 (Map) 出筆記。
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getNotes } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";
export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};
export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
return (
<>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}
您應該會看到「No notes yet.」(尚無筆記)。如果是這樣,那表示您進度正確。
5.2. 新增表單
為了建立新筆記,需要在路由中加入幾件事:
- 為該路由新增一個
action函式。 - 建立筆記時顯示
Toast通知。 - 從
models/note.server.js匯入createNote函式。 - 匯入
useActionData和useAppBridge。
import { json, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { useLoaderData, useActionData } from "@remix-run/react";
import { getNotes } from "../models/note.server";
import { getNotes, createNote } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";
import { useAppBridge } from "@shopify/app-bridge-react";
export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};
export const action = async ({ request, params }) => {
const formData = await request.formData();
const body = formData.get("body")?.toString() || null;
const { productGid } = params;
await createNote({ productGid, body });
return redirect(`/app/product-notes/${encodeURIComponent(productGid)}`);
};
export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
const actionData = useActionData();
const app = useAppBridge();
useEffect(() => {
if (actionData?.ok) {
app.toast.show("Note saved", { duration: 3000 });
setBody("");
}
}, [actionData, app]);
return (
<>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}
現在,您可以構建將會呼叫 action 函式的表單。
import { json, redirect } from "@remix-run/node";
import { useLoaderData, useActionData } from "@remix-run/react";
import { getNotes, createNote } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";
import { Card, Layout, Text, BlockStack, Form, FormLayout, TextField, Button } from "@shopify/polaris";
import { useAppBridge } from "@shopify/app-bridge-react";
export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};
export const action = async ({ request, params }) => {
const formData = await request.formData();
const body = formData.get("body")?.toString() || null;
const { productGid } = params;
await createNote({ productGid, body });
return redirect(`/app/product-notes/${encodeURIComponent(productGid)}`);
};
export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
const actionData = useActionData();
const app = useAppBridge();
useEffect(() => {
if (actionData?.ok) {
app.toast.show("Note saved", { duration: 3000 });
setBody("");
}
}, [actionData, app]);
return (
<>
<Layout.Section>
<Card sectioned>
<Form method="post">
<FormLayout>
<BlockStack gap="200">
<input type="hidden" name="productGid" value={productGid} />
<TextField
label="Note"
value={body}
onChange={setBody}
name="body"
autoComplete="off"
multiline={4}
/>
<Button submit primary>
Add Note
</Button>
</BlockStack>
</FormLayout>
</Form>
</Card>
</Layout.Section>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}
您現在應該可以為商品新增筆記並看到它顯示出來。
6. 測試您的路由
執行 npm run dev 並導覽至 Product Notes 路由。
- 在側邊欄導覽至 Product Notes。
- 選擇一個商品。
- 新增一則筆記。
- 確認筆記已正確顯示並儲存。
後續步驟
既然您已經擁有一個連接到 Prisma Postgres 資料庫且運作正常的 Shopify 應用程式,您可以:
- 使用更多模型與關係來擴充您的 Prisma schema
- 加入建立/更新/刪除 (CRUD) 的路由與表單
- 啟用 Prisma Postgres 的查詢快取以獲得更好的效能
更多資訊
與 Prisma 保持聯繫
透過以下方式與我們聯繫,繼續您的 Prisma 旅程: 我們的活躍社群。保持資訊靈通、參與其中,並與其他開發者合作
- 在 X 上關注我們 以獲取公告、現場活動和實用技巧。
- 加入我們的 Discord 提出問題、與社群對話,並透過對話獲得積極支援。
- 在 YouTube 上訂閱 查看教學、演示和直播。
- 在 GitHub 上交流 透過為存放庫加星標、報告問題或為 issue 做出貢獻。