测试和部署
本指南将详细介绍 Shopify 主题开发的测试策略、部署流程和性能优化方法,确保项目的高质量交付。
测试策略
1. 功能测试清单
# Shopify 主题功能测试清单
## 基础功能测试
- [ ] 主页加载正常
- [ ] 导航菜单功能正常
- [ ] 搜索功能正常
- [ ] 产品页面显示正确
- [ ] 集合页面显示正确
- [ ] 购物车功能正常
- [ ] 结账流程无误
## 响应式测试
- [ ] 移动端 (320px - 767px)
- [ ] 平板端 (768px - 1023px)
- [ ] 桌面端 (1024px+)
- [ ] 超大屏幕 (1440px+)
## 浏览器兼容性
- [ ] Chrome (最新版本)
- [ ] Firefox (最新版本)
- [ ] Safari (最新版本)
- [ ] Edge (最新版本)
- [ ] 移动端浏览器
## 性能测试
- [ ] 页面加载速度 < 3秒
- [ ] 图片优化正常
- [ ] JavaScript 无错误
- [ ] CSS 无冲突
## SEO 测试
- [ ] Meta 标签正确
- [ ] 结构化数据有效
- [ ] 图片 Alt 属性完整
- [ ] 链接结构合理
## 可访问性测试
- [ ] 键盘导航正常
- [ ] 屏幕阅读器友好
- [ ] 颜色对比度符合标准
- [ ] ARIA 标签正确
2. 自动化测试脚本
// tests/theme-tests.js
const { test, expect } = require('@playwright/test');
// 基础页面测试
test.describe('基础页面功能', () => {
test('主页加载测试', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/.*首页.*/);
// 检查关键元素
await expect(page.locator('header')).toBeVisible();
await expect(page.locator('main')).toBeVisible();
await expect(page.locator('footer')).toBeVisible();
});
test('导航菜单测试', async ({ page }) => {
await page.goto('/');
// 桌面端导航
await expect(page.locator('.main-navigation')).toBeVisible();
// 移动端导航
await page.setViewportSize({ width: 375, height: 812 });
await page.locator('[data-mobile-menu-toggle]').click();
await expect(page.locator('.navigation-menu')).toHaveClass(/active/);
});
test('搜索功能测试', async ({ page }) => {
await page.goto('/');
// 搜索输入
await page.fill('[data-search-input]', '测试产品');
await page.press('[data-search-input]', 'Enter');
// 验证搜索结果页面
await expect(page).toHaveURL(/.*search.*/);
await expect(page.locator('.search-results')).toBeVisible();
});
});
// 产品页面测试
test.describe('产品页面功能', () => {
test('产品页面基础功能', async ({ page }) => {
await page.goto('/products/test-product');
// 检查产品信息
await expect(page.locator('.product-title')).toBeVisible();
await expect(page.locator('.product-price')).toBeVisible();
await expect(page.locator('.product-gallery')).toBeVisible();
// 检查变体选择器
const variantSelector = page.locator('[data-variant-selector]');
if (await variantSelector.count() > 0) {
await expect(variantSelector).toBeVisible();
}
});
test('添加到购物车功能', async ({ page }) => {
await page.goto('/products/test-product');
// 添加到购物车
await page.click('[data-add-to-cart]');
// 验证购物车抽屉打开
await expect(page.locator('[data-cart-drawer]')).toHaveClass(/is-open/);
// 验证商品已添加
await expect(page.locator('[data-cart-count]')).not.toHaveText('0');
});
});
// 响应式测试
test.describe('响应式设计测试', () => {
const devices = [
{ name: '移动端', width: 375, height: 812 },
{ name: '平板端', width: 768, height: 1024 },
{ name: '桌面端', width: 1280, height: 800 }
];
devices.forEach(device => {
test(`${device.name}布局测试`, async ({ page }) => {
await page.setViewportSize({ width: device.width, height: device.height });
await page.goto('/');
// 检查布局元素
await expect(page.locator('.container')).toBeVisible();
// 检查导航适配
if (device.width < 768) {
await expect(page.locator('.mobile-menu-toggle')).toBeVisible();
} else {
await expect(page.locator('.navigation-list')).toBeVisible();
}
});
});
});
// 性能测试
test.describe('性能测试', () => {
test('页面加载性能', async ({ page }) => {
const start = Date.now();
await page.goto('/');
const loadTime = Date.now() - start;
// 页面加载时间应小于3秒
expect(loadTime).toBeLessThan(3000);
});
test('图片懒加载测试', async ({ page }) => {
await page.goto('/collections/all');
// 检查懒加载图片
const lazyImages = page.locator('img.lazyload');
const imageCount = await lazyImages.count();
if (imageCount > 0) {
// 滚动页面触发懒加载
await page.evaluate(() => window.scrollTo(0, window.innerHeight));
await page.waitForTimeout(1000);
// 验证图片已加载
await expect(lazyImages.first()).toHaveAttribute('src', /.+/);
}
});
});
3. CSS 和 JavaScript 验证
// tests/code-quality.js
const { test, expect } = require('@playwright/test');
test.describe('代码质量测试', () => {
test('JavaScript 错误检查', async ({ page }) => {
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
page.on('pageerror', error => {
errors.push(error.message);
});
await page.goto('/');
// 触发一些交互
await page.click('[data-mobile-menu-toggle]');
await page.fill('[data-search-input]', '测试');
// 验证无 JavaScript 错误
expect(errors).toHaveLength(0);
});
test('CSS 加载验证', async ({ page }) => {
await page.goto('/');
// 检查关键 CSS 样式
const header = page.locator('header');
const headerStyles = await header.evaluate(el => {
return window.getComputedStyle(el);
});
// 验证样式已正确应用
expect(headerStyles.display).not.toBe('none');
});
test('表单验证测试', async ({ page }) => {
// 测试邮件订阅表单
await page.goto('/');
const newsletterForm = page.locator('[data-newsletter-form]');
if (await newsletterForm.count() > 0) {
// 空表单提交
await page.click('button[type="submit"]');
// 验证验证消息
const emailInput = page.locator('input[type="email"]');
const validationMessage = await emailInput.evaluate(el => el.validationMessage);
expect(validationMessage).toBeTruthy();
// 有效邮箱测试
await page.fill('input[type="email"]', '[email protected]');
await page.click('button[type="submit"]');
// 验证提交成功
await expect(page.locator('.newsletter-message')).toBeVisible();
}
});
});
性能优化
1. 图片优化脚本
// scripts/optimize-images.js
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
class ImageOptimizer {
constructor(inputDir, outputDir) {
this.inputDir = inputDir;
this.outputDir = outputDir;
this.supportedFormats = ['.jpg', '.jpeg', '.png', '.webp'];
}
async optimizeDirectory() {
const files = fs.readdirSync(this.inputDir);
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (this.supportedFormats.includes(ext)) {
await this.optimizeImage(file);
}
}
}
async optimizeImage(filename) {
const inputPath = path.join(this.inputDir, filename);
const outputPath = path.join(this.outputDir, filename);
try {
const image = sharp(inputPath);
const metadata = await image.metadata();
// 生成多种尺寸
const sizes = [400, 600, 800, 1200, 1600];
for (const size of sizes) {
if (metadata.width > size) {
const resizedPath = path.join(
this.outputDir,
`${path.parse(filename).name}_${size}w${path.extname(filename)}`
);
await image
.resize(size, null, { withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(resizedPath);
}
}
// 生成 WebP 格式
const webpPath = path.join(
this.outputDir,
`${path.parse(filename).name}.webp`
);
await image
.webp({ quality: 80 })
.toFile(webpPath);
console.log(`✓ 优化完成: ${filename}`);
} catch (error) {
console.error(`✗ 优化失败: ${filename}`, error);
}
}
}
// 使用方法
const optimizer = new ImageOptimizer('./assets/images', './assets/optimized');
optimizer.optimizeDirectory();
2. CSS 和 JavaScript 压缩
// scripts/minify-assets.js
const fs = require('fs');
const path = require('path');
const terser = require('terser');
const CleanCSS = require('clean-css');
class AssetMinifier {
constructor() {
this.cleanCSS = new CleanCSS({
level: 2,
returnPromise: true
});
}
async minifyJavaScript(inputPath, outputPath) {
try {
const code = fs.readFileSync(inputPath, 'utf8');
const result = await terser.minify(code, {
compress: {
drop_console: true,
drop_debugger: true
},
mangle: true
});
if (result.error) {
throw result.error;
}
fs.writeFileSync(outputPath, result.code);
console.log(`✓ JS压缩完成: ${path.basename(inputPath)}`);
} catch (error) {
console.error(`✗ JS压缩失败: ${path.basename(inputPath)}`, error);
}
}
async minifyCSS(inputPath, outputPath) {
try {
const css = fs.readFileSync(inputPath, 'utf8');
const result = await this.cleanCSS.minify(css);
if (result.errors.length > 0) {
throw new Error(result.errors.join('\n'));
}
fs.writeFileSync(outputPath, result.styles);
console.log(`✓ CSS压缩完成: ${path.basename(inputPath)}`);
} catch (error) {
console.error(`✗ CSS压缩失败: ${path.basename(inputPath)}`, error);
}
}
async processDirectory(inputDir, outputDir) {
const files = fs.readdirSync(inputDir);
for (const file of files) {
const inputPath = path.join(inputDir, file);
const outputPath = path.join(outputDir, file);
const ext = path.extname(file);
if (ext === '.js') {
await this.minifyJavaScript(inputPath, outputPath);
} else if (ext === '.css') {
await this.minifyCSS(inputPath, outputPath);
}
}
}
}
// 使用方法
const minifier = new AssetMinifier();
minifier.processDirectory('./assets/dev', './assets/dist');
3. 性能监控脚本
<!-- snippets/performance-monitor.liquid -->
<script>
// 性能监控
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// 页面加载完成后收集性能数据
window.addEventListener('load', () => {
this.collectMetrics();
});
}
collectMetrics() {
if (!window.performance) return;
const navigation = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
this.metrics = {
// 页面加载时间
pageLoadTime: navigation.loadEventEnd - navigation.fetchStart,
// DNS 查询时间
dnsTime: navigation.domainLookupEnd - navigation.domainLookupStart,
// 连接时间
connectTime: navigation.connectEnd - navigation.connectStart,
// 请求响应时间
responseTime: navigation.responseEnd - navigation.requestStart,
// DOM 构建时间
domTime: navigation.domContentLoadedEventEnd - navigation.responseEnd,
// 首次内容绘制
firstContentfulPaint: this.getPaintTime('first-contentful-paint'),
// 最大内容绘制
largestContentfulPaint: this.getLCP()
};
this.reportMetrics();
}
getPaintTime(paintType) {
const paintEntries = performance.getEntriesByType('paint');
const paint = paintEntries.find(entry => entry.name === paintType);
return paint ? paint.startTime : null;
}
getLCP() {
return new Promise((resolve) => {
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });
});
}
reportMetrics() {
// 发送性能数据到分析服务
if (window.gtag) {
gtag('event', 'performance_metrics', {
page_load_time: Math.round(this.metrics.pageLoadTime),
dns_time: Math.round(this.metrics.dnsTime),
response_time: Math.round(this.metrics.responseTime)
});
}
// 控制台输出(开发环境)
{% if settings.debug_mode %}
console.table(this.metrics);
{% endif %}
// 性能警告
this.checkPerformanceThresholds();
}
checkPerformanceThresholds() {
const warnings = [];
if (this.metrics.pageLoadTime > 3000) {
warnings.push('页面加载时间超过3秒');
}
if (this.metrics.firstContentfulPaint > 1500) {
warnings.push('首次内容绘制时间过长');
}
if (warnings.length > 0) {
console.warn('性能警告:', warnings);
}
}
}
// 初始化性能监控
new PerformanceMonitor();
</script>
部署流程
1. 部署前检查清单
#!/bin/bash
# scripts/pre-deploy-check.sh
echo "🔍 开始部署前检查..."
# 1. 代码质量检查
echo "📋 检查代码质量..."
npm run lint
if [ $? -ne 0 ]; then
echo "❌ 代码质量检查失败"
exit 1
fi
# 2. 运行测试
echo "🧪 运行测试..."
npm test
if [ $? -ne 0 ]; then
echo "❌ 测试失败"
exit 1
fi
# 3. 构建资源
echo "🔨 构建资源..."
npm run build
if [ $? -ne 0 ]; then
echo "❌ 构建失败"
exit 1
fi
# 4. 检查文件大小
echo "📏 检查资源文件大小..."
find assets -name "*.js" -size +100k -exec echo "⚠️ JavaScript文件过大: {}" \;
find assets -name "*.css" -size +50k -exec echo "⚠️ CSS文件过大: {}" \;
# 5. 验证关键文件
echo "📂 验证关键文件..."
required_files=(
"layout/theme.liquid"
"templates/index.liquid"
"templates/product.liquid"
"templates/collection.liquid"
"templates/cart.liquid"
)
for file in "${required_files[@]}"; do
if [ ! -f "$file" ]; then
echo "❌ 缺少关键文件: $file"
exit 1
fi
done
echo "✅ 部署前检查通过"
2. 自动化部署脚本
// scripts/deploy.js
const { execSync } = require('child_process');
const fs = require('fs');
class ShopifyDeployer {
constructor(config) {
this.config = config;
this.themeName = `${config.themeName}-${new Date().toISOString().slice(0, 10)}`;
}
async deploy() {
try {
console.log('🚀 开始部署流程...');
// 1. 运行部署前检查
await this.preDeployCheck();
// 2. 创建新主题
await this.createTheme();
// 3. 上传文件
await this.uploadFiles();
// 4. 部署后验证
await this.postDeployValidation();
console.log('✅ 部署完成');
} catch (error) {
console.error('❌ 部署失败:', error);
process.exit(1);
}
}
async preDeployCheck() {
console.log('📋 执行部署前检查...');
try {
execSync('bash scripts/pre-deploy-check.sh', { stdio: 'inherit' });
} catch (error) {
throw new Error('部署前检查失败');
}
}
async createTheme() {
console.log('🎨 创建新主题...');
try {
const command = `shopify theme push --unpublished --theme-name="${this.themeName}"`;
execSync(command, { stdio: 'inherit' });
} catch (error) {
throw new Error('创建主题失败');
}
}
async uploadFiles() {
console.log('📤 上传文件...');
// 获取主题ID
const themeListOutput = execSync('shopify theme list --json').toString();
const themes = JSON.parse(themeListOutput);
const newTheme = themes.find(theme => theme.name === this.themeName);
if (!newTheme) {
throw new Error('找不到新创建的主题');
}
this.themeId = newTheme.id;
// 上传特定文件类型
const fileTypes = ['templates', 'sections', 'snippets', 'assets', 'config'];
for (const type of fileTypes) {
if (fs.existsSync(type)) {
console.log(`📁 上传 ${type} 文件...`);
execSync(`shopify theme push --theme=${this.themeId} --only=${type}/*`, { stdio: 'inherit' });
}
}
}
async postDeployValidation() {
console.log('🔍 部署后验证...');
// 获取主题预览URL
const themeInfoOutput = execSync(`shopify theme info --theme=${this.themeId} --json`).toString();
const themeInfo = JSON.parse(themeInfoOutput);
const previewUrl = themeInfo.preview_url;
console.log(`🔗 主题预览链接: ${previewUrl}`);
// 运行快速验证测试
try {
execSync(`npm run test:smoke -- --url=${previewUrl}`, { stdio: 'inherit' });
console.log('✅ 快速验证通过');
} catch (error) {
console.warn('⚠️ 快速验证失败,请手动检查');
}
}
}
// 配置和执行部署
const config = {
themeName: process.env.THEME_NAME || 'Custom-Theme',
store: process.env.SHOPIFY_STORE,
environment: process.env.NODE_ENV || 'development'
};
const deployer = new ShopifyDeployer(config);
deployer.deploy();
3. CI/CD 配置
# .github/workflows/deploy.yml
name: 部署到Shopify
on:
push:
branches:
- main
- staging
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 设置 Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: 安装依赖
run: npm ci
- name: 运行测试
run: npm test
- name: 代码质量检查
run: npm run lint
- name: 构建资源
run: npm run build
- name: 上传测试结果
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: test-results/
deploy-staging:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/staging'
steps:
- uses: actions/checkout@v3
- name: 设置 Shopify CLI
uses: shopify/cli-action@v1
- name: 部署到测试环境
env:
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_STAGING_TOKEN }}
SHOPIFY_STORE: ${{ secrets.SHOPIFY_STAGING_STORE }}
run: |
shopify theme push --unpublished --theme-name="staging-$(date +%Y%m%d-%H%M%S)"
deploy-production:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v3
- name: 设置 Shopify CLI
uses: shopify/cli-action@v1
- name: 部署到生产环境
env:
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_PRODUCTION_TOKEN }}
SHOPIFY_STORE: ${{ secrets.SHOPIFY_PRODUCTION_STORE }}
run: |
shopify theme push --unpublished --theme-name="production-$(date +%Y%m%d-%H%M%S)"
- name: 发送部署通知
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: '🚀 生产环境部署完成'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
if: always()
监控和维护
1. 错误监控
<!-- snippets/error-monitoring.liquid -->
<script>
// 全局错误监控
class ErrorMonitor {
constructor() {
this.errors = [];
this.init();
}
init() {
// JavaScript 错误监控
window.addEventListener('error', (event) => {
this.logError({
type: 'javascript',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
});
});
// Promise 错误监控
window.addEventListener('unhandledrejection', (event) => {
this.logError({
type: 'promise',
message: event.reason?.message || event.reason,
stack: event.reason?.stack,
timestamp: new Date().toISOString(),
url: window.location.href
});
});
// 定期发送错误报告
setInterval(() => {
this.sendErrorReport();
}, 30000); // 每30秒发送一次
}
logError(error) {
this.errors.push(error);
// 控制台输出
console.error('错误记录:', error);
// 立即发送严重错误
if (this.isCriticalError(error)) {
this.sendErrorReport([error]);
}
}
isCriticalError(error) {
const criticalPatterns = [
/payment/i,
/checkout/i,
/cart/i,
/order/i
];
return criticalPatterns.some(pattern =>
pattern.test(error.message) || pattern.test(error.filename)
);
}
async sendErrorReport(errors = null) {
const errorsToSend = errors || this.errors.splice(0);
if (errorsToSend.length === 0) return;
try {
// 发送到错误监控服务
await fetch('/api/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
shop: {{ shop.permanent_domain | json }},
theme: {{ theme.name | json }},
errors: errorsToSend
})
});
} catch (error) {
console.error('发送错误报告失败:', error);
// 重新加入队列
this.errors.unshift(...errorsToSend);
}
}
}
// 初始化错误监控
if (typeof window !== 'undefined') {
new ErrorMonitor();
}
</script>
2. 性能监控仪表板
// scripts/performance-dashboard.js
class PerformanceDashboard {
constructor() {
this.metrics = new Map();
this.thresholds = {
pageLoadTime: 3000,
firstContentfulPaint: 1500,
largestContentfulPaint: 2500,
cumulativeLayoutShift: 0.1
};
}
async generateReport() {
const pages = [
'/',
'/collections/all',
'/products/sample-product',
'/cart'
];
const results = [];
for (const page of pages) {
const metrics = await this.measurePage(page);
results.push({
url: page,
metrics: metrics,
status: this.evaluatePerformance(metrics)
});
}
this.generateHTML(results);
return results;
}
async measurePage(url) {
// 使用 Lighthouse 或其他性能测试工具
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
const options = { logLevel: 'info', output: 'json', port: chrome.port };
const runnerResult = await lighthouse(url, options);
await chrome.kill();
const audits = runnerResult.lhr.audits;
return {
performanceScore: runnerResult.lhr.categories.performance.score * 100,
firstContentfulPaint: audits['first-contentful-paint'].numericValue,
largestContentfulPaint: audits['largest-contentful-paint'].numericValue,
cumulativeLayoutShift: audits['cumulative-layout-shift'].numericValue,
speedIndex: audits['speed-index'].numericValue,
timeToInteractive: audits['interactive'].numericValue
};
}
evaluatePerformance(metrics) {
const issues = [];
Object.entries(this.thresholds).forEach(([key, threshold]) => {
if (metrics[key] > threshold) {
issues.push(`${key} 超过阈值 (${metrics[key]} > ${threshold})`);
}
});
return {
passed: issues.length === 0,
issues: issues,
score: metrics.performanceScore
};
}
generateHTML(results) {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>性能监控报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.metric-card { border: 1px solid #ddd; margin: 10px 0; padding: 15px; border-radius: 5px; }
.passed { border-color: #4caf50; background-color: #f1f8e9; }
.failed { border-color: #f44336; background-color: #ffebee; }
.metric { margin: 5px 0; }
.score { font-size: 24px; font-weight: bold; }
</style>
</head>
<body>
<h1>性能监控报告</h1>
<p>生成时间: ${new Date().toLocaleString()}</p>
${results.map(result => `
<div class="metric-card ${result.status.passed ? 'passed' : 'failed'}">
<h2>${result.url}</h2>
<div class="score">性能得分: ${result.metrics.performanceScore.toFixed(1)}/100</div>
<div class="metric">首次内容绘制: ${result.metrics.firstContentfulPaint.toFixed(0)}ms</div>
<div class="metric">最大内容绘制: ${result.metrics.largestContentfulPaint.toFixed(0)}ms</div>
<div class="metric">累积布局偏移: ${result.metrics.cumulativeLayoutShift.toFixed(3)}</div>
<div class="metric">交互时间: ${result.metrics.timeToInteractive.toFixed(0)}ms</div>
${result.status.issues.length > 0 ? `
<h3>问题:</h3>
<ul>
${result.status.issues.map(issue => `<li>${issue}</li>`).join('')}
</ul>
` : '<p>✅ 所有指标都在正常范围内</p>'}
</div>
`).join('')}
</body>
</html>
`;
require('fs').writeFileSync('performance-report.html', html);
console.log('性能报告已生成: performance-report.html');
}
}
// 生成性能报告
const dashboard = new PerformanceDashboard();
dashboard.generateReport();
最佳实践总结
1. 开发工作流
- 版本控制: 使用 Git 进行版本管理
- 分支策略: 采用 GitFlow 或 GitHub Flow
- 代码审查: 所有代码必须经过审查
- 自动化测试: 集成持续测试
- 部署流程: 自动化部署到不同环境
2. 质量保证
- 测试覆盖: 确保关键功能测试覆盖
- 性能监控: 持续监控网站性能
- 错误跟踪: 实时监控和处理错误
- 用户反馈: 收集和处理用户反馈
3. 维护策略
- 定期更新: 保持主题和依赖的更新
- 安全检查: 定期进行安全审计
- 备份策略: 定期备份重要文件
- 文档维护: 保持文档的及时更新
通过完善的测试和部署流程,您可以确保 Shopify 主题的高质量交付和稳定运行!
最后更新时间: