(阶段二:设计)面向用户如何集成多模板 + 多模块

🧩 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 字段

比如:

className="text-purple-400 border-indigo-500"

拆成:

brand={{ highlightColor: 'text-purple-400', borderColor: 'border-indigo-500', }}

✔️ 品牌调性从组件结构中抽离,让它不影响组件逻辑,也方便多主题适配。

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>
  )
}

☁️ 部署推荐流程

  1. template-system/ 项目中执行构建:

    pnpm build
  2. 输出静态文件 build/theme-cool.js

  3. 上传到 CDN,例如:

    • Vercel public CDN

    • Cloudflare Pages

    • GitHub Pages

    • 自建静态资源服务器

  4. 主系统中通过 import('https://cdn.example.com/theme-cool.js') 远程加载

✅ 不会影响主系统构建时间
✅ 模板系统可独立版本管理
✅ 动态拉取即用,利于按需更新与缓存优化

可以把它设置成 GitHub Actions 自动 build + 上传,完全自动化模板发布流程。

✅ 总结

能力实现方式
多模板切换 useThemeComponent + themeName 控制
多模块拼装 ThemeOptions.modules 配置 + props 传递
运行时解耦 模板只接收 props,不依赖 Redux 或 Context
构建优化 模板系统独立打包、上传 CDN、主系统懒加载
可扩展性 支持未来加入 Layout、Page 结构、Slot 注册机制

 

posted @ 2025-07-16 06:05  PEAR2020  阅读(26)  评论(0)    收藏  举报