hub-web 示例
我们提倡将 hub-web 和 hub 分开
Hub 仓库(图片存储)和 hub-web(画廊网站)应该是 两个独立的仓库:
- Hub 仓库(如
NWTFhub):纯图片存储,Bot 推送/拉取操作的对象 - hub-web(如
NWTFhub-web):画廊网站,构建时从 Hub 仓库拉取图片并展示
这样做的好处:
- 职责分离:Hub 只管图片,web 只管展示,互不干扰
- 部署灵活:web 可以部署到 Cloudflare Pages / Vercel / Netlify,Hub 保持在 GitHub
- 审核不受影响:PR 审核流程在 Hub 仓库中进行,web 仓库不需要参与
- 独立迭代:改网站样式不需要动 Hub,加图片不需要动 web
最小 hub-web 结构
以下是一个最小可用的 hub-web 项目,基于 Vue 3 + vue-waterfall-plugin-next。
hub-web/
├── public/
│ └── imgs/ # 构建时从 Hub 仓库拉取的图片
├── src/
│ ├── assets/
│ │ └── imageList.json # 自动生成的图片路径列表
│ ├── views/
│ │ └── HomeView.vue # 画廊主页面
│ ├── App.vue # 根组件
│ └── main.ts # 入口
├── generateImageList.cjs # 生成 imageList.json 的脚本
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.tspackage.json
json
{
"name": "hub-web",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"getphotos": "rm -rf temp_repo public/imgs && (git clone https://github.com/YOUR-USERNAME/YOUR-HUB-REPO temp_repo && mkdir -p public/imgs && cp -r temp_repo/images/* public/imgs/ && rm -rf temp_repo) || mkdir -p public/imgs",
"dev": "npm run getphotos && node generateImageList.cjs && vite",
"prebuild": "npm run getphotos && node generateImageList.cjs",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-waterfall-plugin-next": "^2.6.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "~5.4.0",
"vite": "^5.3.0",
"vue-tsc": "^2.0.0"
}
}index.html
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Hub</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>vite.config.ts
ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})tsconfig.json
json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"paths": { "@/*": ["./src/*"] }
},
"include": ["src/**/*.ts", "src/**/*.vue"]
}src/main.ts
ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')src/App.vue
vue
<script setup lang="ts">
import HomeView from './views/HomeView.vue'
</script>
<template>
<HomeView />
</template>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
</style>src/views/HomeView.vue
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import imageListData from '@/assets/imageList.json'
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
import 'vue-waterfall-plugin-next/dist/style.css'
const imageList = ref<{ id: string; src: string; name: string }[]>([])
function randomID(length = 6) {
return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36)
}
onMounted(() => {
imageList.value = imageListData.map((imagePath: string) => ({
id: randomID(),
src: imagePath,
name: imagePath.split('/').pop() || ''
}))
})
</script>
<template>
<h1 class="hubname">My Hub</h1>
<Waterfall :list="imageList" :width="320" :gutter="16">
<template #default="{ item }">
<div class="image-wrapper">
<LazyImg :url="item" alt="Image" />
<p class="image-filename">{{ item.name }}</p>
</div>
</template>
</Waterfall>
</template>
<style scoped>
h1 {
text-align: center;
font-size: 24px;
margin-bottom: 20px;
}
.waterfall-list {
background-color: transparent !important;
}
.image-wrapper {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.image-wrapper img {
width: 100%;
height: auto;
display: block;
}
.image-filename {
padding: 8px;
text-align: center;
font-size: 16px;
color: #333;
}
</style>generateImageList.cjs
js
const fs = require('fs')
const path = require('path')
const imagesDir = path.join(__dirname, 'public', 'imgs')
const outputFilePath = path.join(__dirname, 'src', 'assets', 'imageList.json')
fs.readdir(imagesDir, (err, files) => {
if (err) {
console.error('Error reading images directory:', err)
return
}
const imageFiles = files.filter((file) => /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/.test(file))
const imagePaths = imageFiles.map((file) => `./imgs/${file}`)
fs.writeFile(outputFilePath, JSON.stringify(imagePaths, null, 2), (err) => {
if (err) {
console.error('Error writing image list file:', err)
} else {
console.log('Image list generated successfully.')
}
})
})工作原理
npm run getphotos:从 Hub 仓库 clone 图片到public/imgs/generateImageList.cjs:扫描public/imgs/生成imageList.json- Vite 构建:
HomeView.vue读取imageList.json,用瀑布流组件展示图片 - 部署:构建产物可以直接部署到 Cloudflare Pages / Vercel / Netlify
部署到 Cloudflare Pages
| 配置 | 值 |
|---|---|
| 构建命令 | npm run build |
| 输出目录 | dist |
| Node.js 版本 | 18 或更高 |
TIP
getphotos 脚本在构建时需要 git,确保 CI 环境中可用。Cloudflare Pages 默认包含 git。