开发应用
本教程是「用 Shoplazza CLI 构建应用」系列的第一步:先用 shoplazza app 命令脚手架一个 Node.js + Express.js 应用并跑通 OAuth,再继续为同一工程添加 主题扩展 和 结账扩展。
在本教程中,您将构建一个应用程序,通过调用 Shoplazza OpenAPI 接口,随机生成一个商品内容。
你将学到什么
在本教程中,你将学习如何执行以下任务:
-
如何快速开发一个示例 Shoplazza App
-
如何实现 OAuth 授权流程获取店铺访问权限
-
如何通过 HMAC 验证确保请求安全性
-
如何调用 Shoplazza OpenAPI 创建商品
前置需求
-
已在 Shoplazza Partners 创建了应用(查看如何创建应用)
-
已在 Partners 创建了测试店铺
-
Node.js 版本 ≥ 18(Node 14 已于 2023-04 EOL,建议使用 Volta / nvm 锁定版本)
-
基础的 JavaScript 和 Express 框架知识
第一步:搭建项目基础
1.1 创建项目文件夹
mkdir shoplazza-app-demo
cd shoplazza-app-demo
1.2 初始化 npm 项目
npm init -y
这将创建一个 package.json 文件,包含项目的基本信息。
1.3 安装依赖
npm install express axios dotenv sqlite3
依赖说明:
-
express: Web 应用框架,用于构建 HTTP 服务 -
axios: HTTP 客户端,用于调用 Shoplazza OpenAPI -
dotenv: 环境变量管理工具 -
sqlite3: 轻量级数据库,用于存储访问令牌
crypto是 Node.js 内置模块(用于 HMAC 验证),无需也不要通过 npm 安装。npm 上同名的crypto包是废弃/无关包,存在 typosquatting 供应链风险。
1.4 创建基础 Express 服务
在项目根目录创建 index.js 文件:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Shoplazza App is running!");
});
app.listen(3000, () => {
console.log("Server is listening on port 3000");
});
1.5 启动服务
node index.js
访问 http://localhost:3000,你应该看到 "Shoplazza App is running!" 的提示。
第二步:配置 ngrok 实现内网穿透
由于 Shoplazza 需要通过公网 URL 访问你的应用,我们需要使用 ngrok 将本地服务暴露到公网。
2.1 下载并安装 ngrok
访问 ngrok 官网 下载对应系统的版本。
macOS 用户可以使用 Homebrew 安装:
brew install ngrok/ngrok/ngrok
或者直接下载二进制文件:
-
下载对应系统的版本
-
解压并将 ngrok 移动到系统 PATH 中
2.2 注册并获取 authtoken
-
登录后访问 https://dashboard.ngrok.com/get-started/your-authtoken
-
复制你的 authtoken
-
在终端执行:
ngrok config add-authtoken YOUR_AUTHTOKEN
2.3 启动 ngrok
确保你的 Express 服务正在运行(端口 3000),然后在新的终端窗口中执行:
ngrok http 3000
你将看到类似以下的输出:
Session Status online
Account [email protected]
Version 3.x.x
Region United States (us)
Forwarding https://xxx.ngrok-free.dev -> http://localhost:3000
访问 https://xxx.ngrok-free.dev,你应该看到 "Shoplazza App is running!" 的提示。
第三步:配置环境变量
3.1 创建 .env 文件
项目通常会提供
.env.example模板,直接cp .env.example .env后填入实际值即可。
在项目根目录创建 .env 文件:
CLIENT_ID=your_client_id_here
CLIENT_SECRET=your_client_secret_here
BASE_URL=https://xxx.ngrok-free.dev
SCOPES=write_product
SCOPES 是应用申请的权限范围,多个权限用空格分隔(例如 write_product read_order)。完整权限列表见 Shoplazza API 权限文档。
3.2 获取 CLIENT_ID 和 CLIENT_SECRET
-
进入你创建的应用详情页
-
在"应用凭证"或"App credentials"部分找到:Client ID(客户端 ID),Client Secret(客户端密钥)
-
将这些值复制到
.env文件中
3.3 配置 BASE_URL
将第二步中 ngrok 生成的 HTTPS 地址填入 BASE_URL。
BASE_URL=https://xxx.ngrok-free.dev
3.4 配置应用回调地址
在 Shoplazza Partners 应用设置中,设置下面两个地址:
-
App URL设置为:https://xxx.ngrok-free.dev/auth -
Redirect URL设置为:https://xxx.ngrok-free.dev/auth/callback
3.5 创建 .gitignore 文件
为了保护敏感信息,创建 .gitignore 文件:
# Dependencies
node_modules/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Build outputs
dist/
build/
.env
.env.*
*.db
安全提示:
-
永远不要将
.env文件提交到代码仓库 -
建议在生产环境使用环境变量管理服务(如 AWS Secrets Manager、Azure Key Vault 等)
第四步:实现 OAuth 授权流程
本步骤实现 OAuth 2.0 授权码流程。完整的流程原理、HMAC 算法、安全防护与最佳实践,见 App 授权参考。
4.1 创建工具函数和 HMAC 验证
在实现具体的授权路由之前,我们需要准备一些工具函数。
这些函数包括:环境变量检查中间件、HMAC 验证中间件等。HMAC 验证是 OAuth 流程中的关键安全措施,用于确保请求确实来自 Shoplazza 平台,而不是被恶意第三方伪造或篡改。
创建 utils/index.js 文件:
require("dotenv").config();
const crypto = require("crypto");
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const BASE_URL = process.env.BASE_URL;
const SCOPES = process.env.SCOPES;
const REDIRECT_URI = `${BASE_URL}/auth/callback`;
// 校验 shop 域名(防止 SSRF):允许 *.myshoplaza.com / *.dev.myshoplaza.com / *.stg.myshoplaza.com
const SHOP_DOMAIN_REGEXP = /^[a-z0-9][a-z0-9-]*(\.dev|\.stg)?\.myshoplaza\.com$/i;
function isValidShop(shop) {
return typeof shop === "string" && SHOP_DOMAIN_REGEXP.test(shop);
}
// 检查必需的环境变量
function checkEnvMiddleware(req, res, next) {
if (!CLIENT_ID || !CLIENT_SECRET || !BASE_URL) {
const missingVars = [];
if (!CLIENT_ID) missingVars.push("CLIENT_ID");
if (!CLIENT_SECRET) missingVars.push("CLIENT_SECRET");
if (!BASE_URL) missingVars.push("BASE_URL");
return res.status(500).send(`环境变量配置缺失: ${missingVars.join(", ")}`);
}
next();
}
// 安全比对函数(防止时序攻击)
function secureCompare(a, b) {
try {
const A = Buffer.from(a, "hex");
const B = Buffer.from(b, "hex");
if (A.length !== B.length) return false;
return crypto.timingSafeEqual(A, B);
} catch (e) {
return false;
}
}
// HMAC 验证中间件
function hmacValidator(req, res, next) {
const { hmac } = req.query;
const map = Object.assign({}, req.query);
delete map["hmac"]; // 移除 hmac 参数
// 按字母顺序排序参数
const sortedKeys = Object.keys(map).sort();
const message = sortedKeys.map((key) => `${key}=${map[key]}`).join("&");
// 使用 CLIENT_SECRET 生成 HMAC
const generated_hash = crypto
.createHmac("sha256", CLIENT_SECRET)
.update(message)
.digest("hex");
if (!secureCompare(generated_hash, hmac)) {
return res.status(400).send("HMAC validation failed");
}
next();
}
// 简易的 state 临时存储(演示用,生产建议改用 Redis / session)
const stateStore = new Map();
const STATE_TTL_MS = 10 * 60 * 1000; // 10 分钟
function saveState(state, shop) {
stateStore.set(state, { shop, expireAt: Date.now() + STATE_TTL_MS });
}
function consumeState(state, shop) {
const entry = stateStore.get(state);
if (!entry) return false;
stateStore.delete(state); // 一次性使用
if (entry.expireAt < Date.now()) return false;
return entry.shop === shop;
}
module.exports = {
checkEnvMiddleware,
hmacValidator,
isValidShop,
saveState,
consumeState,
CLIENT_ID,
CLIENT_SECRET,
BASE_URL,
SCOPES,
REDIRECT_URI,
};
代码说明:
-
checkEnvMiddleware: 中间件函数,确保所有必需的环境变量都已配置 -
secureCompare: 使用时间安全的比对方式,防止时序攻击 -
hmacValidator: HMAC 验证中间件,确保请求来自 Shoplazza -
isValidShop: 校验shop参数是合法的*.myshoplaza.com域名,防止 SSRF 攻击——攻击者可能传入evil.com让服务端把 token 换取请求发到第三方域。 -
saveState/consumeState: OAuthstate临时存储,发起授权时写入、回调时一次性消费并校验,防止 CSRF 攻击。 -
REDIRECT_URI: 自动拼接授权回调地址
HMAC 验证算法与"为什么需要 HMAC"见 App 授权参考。
4.2 实现授权入口路由
现在我们来实现授权流程的第一步:构建授权页面的 URL 并重定向用户。
当用户点击安装应用时,应用需要将用户重定向到 Shoplazza 的授权页面,在 URL 中携带应用 ID、权限范围、回调地址等参数。
在项目根目录 index.js 中添加 /auth 路由:
const crypto = require("crypto");
const {
checkEnvMiddleware,
hmacValidator,
isValidShop,
saveState,
CLIENT_ID,
SCOPES,
REDIRECT_URI,
} = require("./utils/index");
app.get("/auth", checkEnvMiddleware, hmacValidator, (req, res) => {
const { shop } = req.query;
// 校验 shop 必须是合法的 *.myshoplaza.com 域名(防止 SSRF)
if (!isValidShop(shop)) {
return res.status(400).send("Invalid shop domain");
}
const state = crypto.randomBytes(16).toString("hex"); // 随机值,防止 CSRF 攻击
saveState(state, shop); // 存储 state,回调时校验
// URL 编码参数
const scopesEncoded = encodeURIComponent(SCOPES);
const redirectUriEncoded = encodeURIComponent(REDIRECT_URI);
// 重定向用户到 Shoplazza 授权页面
res.redirect(
`https://${shop}/admin/oauth/authorize?client_id=${CLIENT_ID}&scope=${scopesEncoded}&redirect_uri=${redirectUriEncoded}&response_type=code&state=${state}`
);
});
参数说明:
-
shop: 店铺域名(如example.myshoplaza.com),从查询参数中获取,必须校验是合法的*.myshoplaza.com域名 -
client_id: 应用的客户端 ID -
scope: 请求的权限范围,从环境变量SCOPES读取;多个权限用空格分隔(如write_product read_order) -
redirect_uri: 授权完成后的回调地址 -
response_type: 固定值code,表示使用授权码模式 -
state: 随机字符串,用于防止 CSRF 攻击;必须在/auth中存储、/auth/callback中校验(不一致即拒绝)
更多权限范围请参考 Shoplazza API 权限文档。
4.3 实现授权回调路由
当用户在 Shoplazza 授权页面点击同意后,Shoplazza 会将用户重定向回我们的应用,并在 URL 中携带授权码(code)。
我们需要实现回调路由来接收这个授权码(code),然后用它换取访问令牌(access_token)。获取到的 access_token 需要妥善存储,因为后续所有的 API 调用都需要使用它来进行身份验证。
在 index.js 中添加 /auth/callback 路由:
const axios = require("axios");
const {
hmacValidator,
isValidShop,
consumeState,
CLIENT_ID,
CLIENT_SECRET,
REDIRECT_URI,
} = require("./utils/index");
const { saveToken } = require("./utils/sqlite");
app.get("/auth/callback", hmacValidator, async (req, res) => {
const { code, hmac, state, shop } = req.query;
if (!shop || !hmac || !code || !state) {
return res.status(400).send("Required parameters missing");
}
// 校验 shop 域名(防止 SSRF)
if (!isValidShop(shop)) {
return res.status(400).send("Invalid shop domain");
}
// 校验 state(防止 CSRF):从存储取出,与本次 shop 一致才放行
if (!consumeState(state, shop)) {
return res.status(400).send("Invalid or expired state");
}
try {
// 使用 code 获取 access_token
const { data } = await axios.post(`https://${shop}/admin/oauth/token`, {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code,
grant_type: "authorization_code",
redirect_uri: REDIRECT_URI,
});
// 验证响应结构
if (!data || !data.access_token) {
console.error("Token exchange failed: missing access_token", data);
return res.status(502).send("Token exchange failed");
}
// 存储 access_token
await saveToken(shop, data.access_token);
// 跳转到首页
res.redirect(`/?shop=${shop}`);
} catch (err) {
console.error("OAuth callback error:", err.response?.data || err.message);
res.status(500).send("OAuth callback failed");
}
});
Token 接口请求参数:
-
client_id: 应用的客户端 ID -
client_secret: 应用的客户端密钥 -
code: 授权码 -
grant_type: 固定值authorization_code -
redirect_uri: 必须与授权时的回调地址一致
Token 接口响应:
{
"token_type": "Bearer",
"expires_at": 1550546245,
"access_token": "{YOUR_ACCESS_TOKEN}",
"refresh_token": "{YOUR_REFRESH_TOKEN}",
"store_id": "2",
"store_name": "xxx"
}
4.4 实现 Token 存储
获取到 access_token 后,我们需要将它持久化存储。
在示例项目中,我们使用 SQLite 数据库来存储每个店铺的访问令牌,以店铺域名作为主键(生产环境建议使用 PostgreSQL、MySQL 等)。这样当店铺再次访问应用时,我们可以直接从数据库中读取令牌,无需重新授权。
创建 utils/sqlite.js 文件:
const path = require("path");
const sqlite3 = require("sqlite3").verbose();
const DB_PATH = path.resolve(__dirname, "../database/tokens.db");
const db = new sqlite3.Database(DB_PATH);
// 初始化数据库表
const initPromise = new Promise((resolve, reject) => {
db.run(
`CREATE TABLE IF NOT EXISTS shop_tokens (
shop_domain TEXT PRIMARY KEY,
access_token TEXT NOT NULL,
updated_time INTEGER NOT NULL
)`,
(err) => {
if (err) return reject(err);
resolve();
}
);
});
// 保存或更新 token
function saveToken(shopDomain, accessToken) {
return initPromise.then(
() =>
new Promise((resolve, reject) => {
const now = Date.now();
// UPSERT:shop_domain 已存在则更新 token 与时间,否则插入新行(excluded 指本次待插入的值)
db.run(
`INSERT INTO shop_tokens (shop_domain, access_token, updated_time)
VALUES (?, ?, ?)
ON CONFLICT(shop_domain) DO UPDATE SET
access_token = excluded.access_token,
updated_time = excluded.updated_time`,
[shopDomain, accessToken, now],
(err) => {
if (err) return reject(err);
resolve();
}
);
})
);
}
// 获取 token
function getShopToken(shopDomain) {
return initPromise.then(
() =>
new Promise((resolve, reject) => {
db.get(
`SELECT access_token FROM shop_tokens WHERE shop_domain = ?`,
[shopDomain],
(err, row) => {
if (err) return reject(err);
resolve(row?.access_token || null);
}
);
})
);
}
module.exports = {
saveToken,
getShopToken,
};
数据库设计说明:
-
shop_domain: 店铺域名(主键) -
access_token: 访问令牌 -
updated_time: 更新时间戳
并且创建 database 文件夹:
mkdir database
当应用启动时,会自动创建 tokens.db 文件,用于存储每个店铺的 access_token。
生产环境建议:
- 不要使用
require("sqlite3").verbose(),verbose会输出额外的调试日志,影响性能与可读性;生产换成require("sqlite3")即可。- 数据库路径建议通过环境变量配置(例如
DB_PATH),避免相对路径在不同部署环境下出问题。- SQLite 仅适合演示,正式项目建议使用 PostgreSQL / MySQL 等独立数据库。
第五步:创建应用首页
5.1 实现首页路由
应用首页是用户授权成功后看到的第一个页面,我们使用该页面调用后端接口创建随机商品。后端接口在收到前端请求后,会先检查该店铺是否已经完成授权(即数据库中是否存在该店铺的 access_token)。如果未授权,则重定向到授权流程;如果已授权,则调用 Shoplazza OpenAPI 创建随机商品。
在项目根目录下 index.js 中,改造原始首页路由:
const path = require("path");
const { getShopToken } = require("./utils/sqlite");
const { isValidShop } = require("./utils/index");
app.get("/", checkEnvMiddleware, async (req, res) => {
const { shop } = req.query;
if (!shop || !isValidShop(shop)) {
return res.send("请提供合法的 shop 参数。");
}
// 检查是否已授权
const token = await getShopToken(shop);
if (!token) {
return res.redirect(`/auth?shop=${shop}`);
}
// 返回应用首页(用 sendFile 避免每次请求都同步读文件)
res.sendFile(path.resolve(__dirname, "index.html"));
});
用
res.sendFile替代fs.readFileSync+res.send,避免每次请求都同步读盘;如果首页是纯静态资源,也可以挂app.use(express.static(__dirname))由 Express 托管。
逻辑说明:
-
从查询参数获取
shop参数 -
查询数据库检查该店铺是否已授权
-
如果未授权,重定向到
/auth开始授权流程 -
如果已授权,返回应用首页 HTML
5.2 创建 HTML 页面
现在我们来创建应用的前端界面。
这个页面包含一个简单的按钮,用户点击后会调用后端接口创建随机商品。页面使用原生 HTML 和 JavaScript 实现,通过 fetch API 与后端通信,并实时展示创建结果。
在项目根目录创建 index.html 文件:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shoplazza App Demo</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f5f7;
color: #111;
}
.page {
max-width: 860px;
margin: 24px auto;
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 16px 18px;
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.card h2 { margin: 0 0 8px; font-size: 18px; }
.card p { margin: 6px 0; color: #444; }
.card pre {
background: #f8f8fb;
padding: 12px;
border-radius: 8px;
overflow: auto;
}
.btn {
border: none;
background: #111;
color: #fff;
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
}
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.result { margin-top: 12px; color: #333; font-size: 14px; }
</style>
</head>
<body>
<div class="page">
<div class="card">
<h2>恭喜创建 Shoplazza App 🎉</h2>
<p>这是一个简化的演示页面,技术栈如下:</p>
<ul>
<li>框架: Express</li>
<li>界面: 简单 HTML</li>
<li>API: <a href="https://www.shoplazza.dev/reference/api-usage" target="_blank">Shoplazza OpenAPI</a></li>
<li>数据库: SQLite</li>
</ul>
</div>
<div class="card">
<h2>开始创建商品</h2>
<p>点击按钮创建一个随机商品。</p>
<button class="btn" id="createBtn">生成商品</button>
<div class="result" id="createResult"></div>
<pre id="productResult" style="display: none;"></pre>
</div>
</div>
<script>
function getShop() {
const params = new URLSearchParams(window.location.search);
return params.get("shop");
}
const shop = getShop();
const btn = document.getElementById("createBtn");
const result = document.getElementById("createResult");
const productResult = document.getElementById("productResult");
btn.addEventListener("click", async () => {
if (!shop) {
result.textContent = "请提供 shop 参数。";
return;
}
btn.disabled = true;
result.textContent = "创建中...";
productResult.textContent = "";
try {
const resp = await fetch(`/api/create-product?shop=${encodeURIComponent(shop)}`, {
method: "POST",
});
const data = await resp.json();
if (!data.ok) {
result.textContent = "创建失败:" + JSON.stringify(data.error);
} else {
const product = data.product || {};
result.textContent = "创建成功:" + (product.title || product.id || "OK");
productResult.textContent = JSON.stringify(product, null, 2);
productResult.style.display = "block";
}
} catch (e) {
result.textContent = "创建失败:" + e.message;
productResult.style.display = "none";
} finally {
btn.disabled = false;
}
});
</script>
</body>
</html>
页面功能:
-
显示应用基本信息
-
提供"生成商品"按钮
-
点击按钮时调用
/api/create-product接口 -
展示创建结果和商品详情
第六步:调用 OpenAPI 创建商品
6.1 创建随机商品数据生成函数
为了演示商品创建功能,我们需要一个能够生成随机商品数据的函数。这个函数会随机组合商品标题、生成随机价格,并构造符合 Shoplazza OpenAPI 要求的数据结构。
在 utils/index.js 中添加:
function randomPick(list) {
return list[Math.floor(Math.random() * list.length)];
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function buildRandomProductPayload() {
const prefix = ["春日", "轻盈", "舒适", "简约", "清新", "自然", "柔软"];
const material = ["棉麻", "纯棉", "雪纺", "牛仔", "针织", "丝滑"];
const category = ["T恤", "衬衫", "连衣裙", "外套", "卫衣", "长裤"];
const suffix = ["款", "风", "系列", "限定", "基础款"];
const title = `${randomPick(prefix)}${randomPick(material)}${randomPick(category)}${randomPick(suffix)}`;
const price = parseFloat((randomInt(79, 299) + randomInt(0, 99) / 100).toFixed(2));
return {
title,
images: [
{
src: "https://placehold.co/800x800",
},
],
variants: [
{
price,
position: 1,
},
],
};
}
module.exports = {
// ... 其他导出
buildRandomProductPayload,
};
数据结构说明:
-
title: 商品标题(随机组合生成) -
images: 商品图片数组src: 图片 URL
-
variants: 商品规格数组-
price: 价格 -
position: 规格排序位置
-
6.2 实现创建商品接口
现在我们来实现核心功能:调用 Shoplazza OpenAPI 创建商品。这个接口会从数据库获取店铺的 access_token,生成随机商品数据,然后调用 Shoplazza 的商品创建 API。接口需要处理各种可能的错误情况,并返回友好的错误信息。
在 index.js 中添加 /api/create-product 路由:
const { buildRandomProductPayload, isValidShop } = require("./utils/index");
app.post("/api/create-product", checkEnvMiddleware, async (req, res) => {
const { shop } = req.query;
if (!shop || !isValidShop(shop)) {
return res.status(400).json({ ok: false, error: "请提供合法的 shop 参数。" });
}
const token = await getShopToken(shop);
if (!token) {
return res.status(401).json({ ok: false, error: "未授权,请先完成安装。" });
}
try {
const payload = buildRandomProductPayload();
const { data } = await axios.post(
`https://${shop}/openapi/2025-06/products`,
{ product: payload },
{
headers: {
"Access-Token": token,
},
}
);
res.json({ ok: true, product: data?.product || data });
} catch (err) {
const status = err.response?.status || 500;
const detail = err.response?.data || err.message || "创建失败";
res.status(status).json({ ok: false, error: detail });
}
});
创建商品是写操作,应使用
app.post(...)(前端fetch也需带method: "POST")。app.get在 RESTful 语义中代表只读,且部分浏览器/代理可能缓存 GET 请求或在重定向时重发。
OpenAPI 接口说明:
关于 Shoplazza OpenAPI 的详细参数说明、请求格式、响应结构等,请参考 Shoplazza 商品 API 文档。
6.3 添加全局错误处理中间件
为防止异步路由中未捕获的异常导致进程崩溃,在 index.js 所有路由注册之后追加一个错误处理中间件:
// 注意:错误处理中间件必须是 4 个参数 (err, req, res, next),且放在所有路由之后
app.use((err, req, res, next) => {
console.error("Unhandled error:", err);
if (res.headersSent) return next(err);
res.status(500).json({ ok: false, error: "Internal server error" });
});
如果还存在不经过 Express 路由的异步代码(如全局事件回调),建议同时监听 process.on("unhandledRejection", ...) 与 process.on("uncaughtException", ...) 做进程级兜底。
第七步:测试应用
7.1 启动应用
确保以下服务都在运行:
终端 1 - Express 服务:
node index.js
终端 2 - ngrok 服务:
ngrok http 3000
7.2 安装应用到测试店铺
-
通过 Partners 后台安装测试应用:登录 Shoplazza Partners,点击“应用详情页 -- 应用测试 -- 选择店铺 -- 安装App”

-
你将被重定向到 Shoplazza 授权页面

-
点击"安装App"按钮
-
授权成功后,你将被重定向到应用首页

7.3 创建商品
-
在应用首页点击"生成商品"按钮
-
等待几秒后,你将看到创建成功的提示和商品详情

-
登录 Shoplazza 店铺后台,在"商品"页面查看刚创建的商品

下一步
恭喜你!你成功使用 Node.js、Express.js、SQLite 创建了一个 Shoplazza App。接下来你可以:
- 发布应用:将应用发布到 App Store,请参考 Shoplazza App 发布
- 构建嵌入式应用:用 App Bridge + Session Token 让应用在店铺后台内运行,请参考 使用 Node.js 和 Express.js 构建嵌入式应用
- Theme Extension 开发:开发 Theme Extension 应用,请参考 Shoplazza App Theme Extension 开发
- Checkout Extension 开发:开发 Checkout Extension 应用,请参考 Shoplazza App Checkout Extension 开发
- 深入授权与最佳实践:见 App 授权参考