(阶段二:设计)面向用户如何集成多模板 + 多模块
🧩 Shopstack 多模板 + 多模块 系统设计文档
📌 宏观目标
为 Shopstack 多租户电商系统构建一套可配置化的主题模板机制,支持:
-
多模板(cool / soft / fashion 等)
-
多模块(Header 模块内部可以选 cart/contact 等 slot)
-
每个租户独立配置模板与模块
-
支持模板懒加载、不影响主系统构建性能
-
模板系统与主系统解耦,支持独立构建和部署
🎯 微观目标
当前 Shopstack 的多模板 + 多模块设计,旨在支持以下两类核心使用场景:
🧾 1. 支持 内容型(content-only)网站
适用于品牌官网、服务介绍、Landing Page、个人展示等以内容呈现为主的站点
✅ 实现方式:
-
ThemeOptions.modules中仅配置如:-
Header: 含 Logo / 联系方式 / Hero -
Main: 含 About / Features / Testimonials / CTA -
Footer: 含 Socials / CopyRight
-
-
不配置商品、购物车、结账等模块
✅ 技术优势:
-
精致排版 + 轻量运行时
-
模板灵活美观,支持动态宣传页生成
-
极简依赖,不需接入电商逻辑模块
🛒 2. 支持 内容 + 电商一体化系统
适用于既需要品牌内容展示,又提供商品销售功能的现代 DTC(直接面向消费者)网站
✅ 实现方式:
-
modules中加入:-
ProductClient: 商品列表 / 商品详情模块 -
Cart: 购物车按钮与浮层 -
Checkout: 可选支付按钮、表单等
-
-
页面通过多段 Section + 产品插入完成内容 + 电商结合
✅ 效果示例:
-
电商品牌首页 + 产品专区
-
自媒体导购页 / Landing page 下单页
-
Shopify / Framer / Webflow 的现代替代方案
✅ 技术优势:
-
与内容系统高度融合,无需跳转至“商店子域名”
-
可动态生成:单商品展示页、限时推广页
-
所有逻辑仍由统一模板渲染层控制,不引入复杂页面路由系统
📦 特别支持能力
| 能力 | 支持方式 |
|---|---|
| 🎨 多模板切换 | themeName 控制风格 |
| 🧩 多模块自由组合 | ThemeOptions.modules 决定 Header/Footer 内容与顺序 |
| 🔁 运行时解耦 | 所有模板通过 props 注入配置,模板层保持纯净 |
| 📤 独立构建部署 | template-system 可本地构建 + 上传 CDN,不影响主系统构建速度 |
| 🧠 未来可接 CMS / 页面构建器 | 模块列表天然适配低代码拖拽生成方式(如 Framer、Builder.io 模型) |
🏗️ 系统组成结构
📁 template-system/ 模板系统目录结构
template-system/ ├─ themes/ │ ├─ cool/ │ ├─ soft/ │ ├─ fashion/ │ └─ shared/ // 公共组件 ├─ build/ // 构建产物,供主系统远程 import │ ├─ theme-cool.js │ ├─ theme-soft.js ├─ vite.config.ts // 统一打包为 UMD / ESM 文件 ├─ scripts/ │ └─ upload-to-cdn.ts // 可选自动上传构建结果到 CDN
📦 模板导出形式(每个主题)
// themes/cool/index.ts export const CoolComponents = { Header: CoolHeader, Footer: CoolFooter, ProductClient: CoolProductClient, } 可统一导出为: export default CoolComponents
进阶思路:模板变化局限于landing,其他场景复用组件
先从landing定基调:dark or light
然后从landing里提取出主题色:注入 props
template-system/ ├─ components/ │ └─ general/ │ ├─ Header.tsx ← 纯组件(不含颜色逻辑) │ ├─ index.ts │ └─ styles/ │ ├─ white.tsx ← 包装白色版组件(基础明亮风格) │ ├─ black.tsx ← 包装黑色版组件(基础暗色风格) ├─ themes/ │ ├─ cool/ ← dark, 紫色系 │ │ └─ CoolComponents.ts ← 使用 blackXXX + purple 变量 │ ├─ calm/ ← dark, 蓝色系 │ ├─ soft/ ← light, 粉色系
1. general 组件(Header 等):
→ 是结构组件,完全不带主题逻辑
→ 所有样式如 dark:text-xl 统统禁止写死
- general component里面所有dark:text-xl 这种都应该改成变量props等待被注入
2. black/white.tsx:
→ 是“视觉基础层包装器”
→ 负责注入布局感 / 字重 / 尺寸 / 明亮 or 暗色的“风格底色”
- blackComponent中注入text:xl 留下text color等待注入;
- 白色Component注入text:2xl,留下text color等待注入;
3. themes/cool.tsx:
→ 是主题顶层注入器
→ 注入颜色、品牌变量、视觉调性(blue/purple)
- themes/cool/里面是注入text color = blue。
结构 + 风格 + 品牌配色三层解耦
1. general 复用结构
type HeaderContentProps = { title: string subtitle: string } type HeaderStyleProps = { darklight?: { bgColor?: string textColor?: string subtitleColor?: string } brand?: { highlightColor?: string accentTextColor?: string } } type HeaderProps = { className?: string style?: React.CSSProperties } & HeaderContentProps & HeaderStyleProps
组件:
export function Header({ title, subtitle, className = '', style, darklight = {}, brand = {}, }: HeaderProps) { const { bgColor = 'bg-white', textColor = 'text-black', subtitleColor = 'text-gray-600', } = darklight const { highlightColor = 'text-purple-300', accentTextColor = 'text-purple-500', } = brand return ( <header className={`p-4 rounded ${bgColor} ${className}`} style={style}> <h1 className={`text-xl font-bold ${textColor}`}>{title}</h1> <p className={`text-sm ${subtitleColor}`}>{subtitle}</p> {/* 示例用法:可以加徽标等 */} <div className={`mt-2 text-xs ${highlightColor}`}> Highlighted by Brand </div> <div className={`text-xs ${accentTextColor}`}> Accent Color by Brand </div> </header> ) }
✅ 使用方式:多层注入示例
black.tsx 明暗包装层注入:
import { Header } from '../Header'
export const blackHeader = (props: any) => (
<Header
{...props}
darklight={{
bgColor: 'bg-slate-900',
textColor: 'text-white',
subtitleColor: 'text-gray-400',
}}
/>
)
CoolComponents.ts 品牌注入层:
import { blackHeader } from '../../components/general/styles/black'
export const CoolComponents = {
Header: (props) =>
blackHeader({
...props,
brand: {
highlightColor: 'text-purple-400',
accentTextColor: 'text-purple-200',
},
}),
}
基于以上设计拆解UI的规划:
✅ 1. 把所有 dark: 开头的类名
→ 迁移到 darklight 字段
-
比如:
className="dark:bg-slate-900 dark:text-white"
拆成:
darklight={{
bgColor: 'bg-slate-900',
textColor: 'text-white',
}}
2. 把所有品牌色(blue、purple、sky、amber)相关的类名
→ 迁移到 brand 字段
比如:
拆成:
✔️ 品牌调性从组件结构中抽离,让它不影响组件逻辑,也方便多主题适配。
template系统输出构建(vite)
// vite.config.ts import { defineConfig } from 'vite' export default defineConfig({ build: { rollupOptions: { input: { 'cool-Landing': 'src/themes/cool/Landing.tsx', 'cool-Shop': 'src/themes/cool/Shop.tsx', 'general-Landing': 'src/components/general/Landing.tsx', 'general-Shop': 'src/components/general/Shop.tsx', // ... }, output: { entryFileNames: '[name].js', format: 'es', }, }, outDir: 'dist', target: 'esnext', minify: true, }, })
你将得到打包产物:
dist/ ├─ cool-Landing.js ├─ cool-Shop.js ├─ general-Landing.js
部署这些到 CDN,即可访问如: https://cdn.example.com/template-system/cool-Landing.js
组件注册表 componentRegistry.json (自动化)
{ "cool": { "Landing": "https://cdn.example.com/template-system/cool-Landing.js", "Shop": "https://cdn.example.com/template-system/cool-Shop.js" }, "general": { "Landing": "https://cdn.example.com/template-system/general-Landing.js" } }
1. 构建前扫描全体目录,自动生成JSON
import fs from 'fs' import path from 'path' const rootDir = path.resolve(__dirname, '../src/themes') const output = {} for (const themeName of fs.readdirSync(rootDir)) { const themePath = path.join(rootDir, themeName) if (!fs.statSync(themePath).isDirectory()) continue output[themeName] = {} for (const file of fs.readdirSync(themePath)) { const name = path.parse(file).name // e.g., Landing from Landing.tsx const url = `https://cdn.example.com/template-system/${themeName}-${name}.js` output[themeName][name] = url } } fs.writeFileSync( path.resolve(__dirname, '../dist/componentRegistry.json'), JSON.stringify(output, null, 2), 'utf-8' )
2. package.json
{ "scripts": { "build": "vite build && ts-node scripts/generateImportMap.ts" } }
3. 主项目中加载这个 import map(JSON):
import importMap from 'https://cdn.example.com/template-system/componentRegistry.json' const url = importMap[themeName][pageName] const Comp = await import(/* @vite-ignore */ url)
🔌 主系统如何加载模板组件
🔥 核心懒加载 Hook:useThemeComponent
// hooks/useThemeComponent.ts import { useContext, useEffect, useState } from "react" import { ThemeContext } from "@/contexts/ThemeContext" type ThemeComponents = { Header?: React.ComponentType<any> Footer?: React.ComponentType<any> ProductClient?: React.ComponentType<any> } export function useThemeComponent(slot: keyof ThemeComponents) { const { themeName } = useContext(ThemeContext) const [Component, setComponent] = useState<React.ComponentType<any> | null>(null) useEffect(() => { if (!themeName) return import(`../themes/${themeName}`).then((mod: { default: ThemeComponents }) => { setComponent(() => mod.default[slot]) }).catch(() => { console.warn(`Theme "${themeName}" or slot "${slot}" not found`) }) }, [themeName, slot]) return Component }
🧠 租户主题配置结构(ThemeOptions)
type SlotConfig = { slot: string props?: Record<string, any> } type ThemeOptions = { themeName: string // 例如 "cool" modules: { [K in keyof ThemeComponents]?: SlotConfig[] } }
示例配置:
const themeOptions: ThemeOptions = { themeName: "cool", modules: { Header: [ { slot: "Logo", props: { size: "lg" } }, { slot: "Cart", props: { showBadge: true } }, { slot: "Contact", props: { emailOnly: true } }, ] } }
🔁 主系统集成流程
1. layout.tsx 初始化时获取租户配置:
const themeOptions = await fetchThemeOptions(tenantName) // SSR or client fetch dispatch(setThemeOptions(themeOptions)) // 写入 Redux
2. 页面组件中使用模板组件:
// ThemedHeader.tsx const StyledHeader = useThemeComponent("Header") const { modules } = useTheme() if (!StyledHeader) return null return <StyledHeader content={modules?.Header ?? []} />
🎨 模板内部如何渲染 slot 模块
每个远程 Header 模板组件结构如下:
// themes/cool/components/Header.tsx import { SlotConfig } from "@/types" export default function Header({ content = [] }: { content: SlotConfig[] }) { return ( <header className="..."> {content.map(({ slot, props }) => { switch (slot) { case "Logo": return <Logo {...props} /> case "Cart": return <Cart {...props} /> case "Contact": return <Contact {...props} /> default: return null } })} </header> ) }
☁️ 部署推荐流程
-
在
template-system/项目中执行构建: -
输出静态文件
build/theme-cool.js等 -
上传到 CDN,例如:
-
Vercel public CDN
-
Cloudflare Pages
-
GitHub Pages
-
自建静态资源服务器
-
-
主系统中通过
import('https://cdn.example.com/theme-cool.js')远程加载
✅ 不会影响主系统构建时间
✅ 模板系统可独立版本管理
✅ 动态拉取即用,利于按需更新与缓存优化
可以把它设置成 GitHub Actions 自动 build + 上传,完全自动化模板发布流程。
✅ 总结
| 能力 | 实现方式 |
|---|---|
| 多模板切换 | useThemeComponent + themeName 控制 |
| 多模块拼装 | ThemeOptions.modules 配置 + props 传递 |
| 运行时解耦 | 模板只接收 props,不依赖 Redux 或 Context |
| 构建优化 | 模板系统独立打包、上传 CDN、主系统懒加载 |
| 可扩展性 | 支持未来加入 Layout、Page 结构、Slot 注册机制 |

浙公网安备 33010602011771号