基于 halo-dev/theme-modern-starter 改进的 Vite 构建配置,让你快速上手现代化主题开发。
🎯 构建目标
把 src/
里的代码打包到 templates/assets/
给 Halo 用。
🚀 快速开始
# 开发模式(热更新)
pnpm dev
# 构建生产版本
pnpm run build
📁 项目结构
src/
├── common/main.js # 公共入口(必须有)
├── pages/ # 页面专用代码
│ ├── index/
│ │ ├── index.js # 首页脚本
│ │ └── index.css # 首页样式
│ └── post/
│ ├── post.js # 文章页脚本
│ └── post.css # 文章页样式
└── static/ # 静态文件(字体、图片等)
├── fonts/
└── images/
⚙️ 构建方案
方案1:公共组件构建(main.js)
作用:全站通用的代码,每个页面都会用到
入口:src/common/main.js
输出:templates/assets/js/main.js
+ templates/assets/css/main.css
// src/common/main.js 示例
import "./css/base.css"; // 基础样式
import "./css/tailwind.css"; // Tailwind CSS
import "./js/utils.js"; // 工具函数
import "./js/theme.js"; // 主题切换
// 全站通用功能
console.log("Sky Theme 加载完成");
包含内容:
- 基础样式(重置、字体、颜色)
- UI框架(Tailwind、DaisyUI)
- 公共脚本(工具函数、主题切换、全局事件)
- 第三方库(图标、动画)
方案2:页面专用构建(动态扫描)
作用:每个页面独有的功能和样式
规则:自动找到 src/pages/*/
下的同名 JS 文件
优势:按需加载,减少首屏体积
// src/pages/index/index.js 示例
import "./index.css"; // 首页专用样式
import "./components/hero.js"; // 首页组件
// 首页专用功能
console.log("首页加载完成");
方案3:静态资源复制
作用:不需要编译的文件直接复制
来源:src/static/
去向:templates/assets/
包含:字体、图片、图标、音频等
Halo 主题构建指南
简单明了的 Vite 构建配置,让你快速上手。
🎯 构建目标
把 src/
里的代码打包到 templates/assets/
给 Halo 用。
📁 项目结构
src/
├── common/main.js # 公共入口(必须有)
├── pages/ # 页面专用代码
│ ├── index/
│ │ ├── index.js # 首页脚本
│ │ └── index.css # 首页样式
│ └── post/
│ ├── post.js # 文章页脚本
│ └── post.css # 文章页样式
└── static/ # 静态文件(字体、图片等)
├── fonts/
└── images/
🚀 使用方法
新增页面(3步搞定)
第1步:创建文件夹
mkdir src/pages/archives
第2步:写JS文件
// src/pages/archives/archives.js
import "./archives.css";
console.log("归档页面加载完成");
第3步:写CSS文件
/* src/pages/archives/archives.css */
.archives {
padding: 20px;
}
构建命令
npm run build
结果
- 生成
templates/assets/js/archives.js
- 生成
templates/assets/css/archives.css
📋 命名规则
✅ 正确的
src/pages/index/index.js → 生成 index.js
src/pages/post/post.js → 生成 post.js
src/pages/archives/archives.js → 生成 archives.js
❌ 错误的
src/pages/tags/tag.js → 不会生成(名字不匹配)
src/pages/about/page.js → 不会生成(名字不匹配)
🔧 核心配置
// vite.config.ts 核心逻辑
function generateEntries() {
const entries = {};
// 1. 公共入口
entries["main"] = "src/common/main.js";
// 2. 扫描页面入口
const jsFiles = glob.sync("src/pages/**/*.js");
jsFiles.forEach((file) => {
// 只要文件夹名 = JS文件名,就生成入口
const matches = file.match(/src\/pages\/([^\/]+)\/\1\.js$/);
if (matches) {
const pageName = matches[1];
entries[pageName] = file;
}
});
return entries;
}
🔧 完整构建流程
第1步:入口扫描
// vite.config.ts 核心逻辑
function generateEntries() {
const entries = {};
// 1. 公共入口(必须)
entries["main"] = "src/common/main.js";
// 2. 页面入口(自动扫描)
const jsFiles = glob.sync("src/pages/**/*.js");
jsFiles.forEach((file) => {
const matches = file.match(/src\/pages\/([^\/]+)\/\1\.js$/);
if (matches) {
const pageName = matches[1];
entries[pageName] = file;
console.log(`📄 ${pageName}: ${file}`);
}
});
console.log(`✅ 生成 ${Object.keys(entries).length} 个入口点`);
return entries;
}
第2步:文件处理
// 输出配置
output: {
entryFileNames: 'js/[name].js', // JS文件放到 js/ 目录
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith(".css")) {
const name = assetInfo.name.replace('.css', '');
return name === 'main' ? 'css/main.css' : `css/${name}.css`;
}
return "assets/[name][extname]";
}
}
第3步:静态资源复制
// 自定义插件
{
name: 'copy-static-assets',
closeBundle() {
copyStaticAssets(); // 复制 src/static/ 到 templates/assets/
}
}
💡 main.js 公共组件详解
标准结构
src/common/
├── main.js # 主入口
├── css/
│ ├── base.css # 基础样式
│ ├── tailwind.css # Tailwind CSS
│ └── components.css # 公共组件样式
├── js/
│ ├── utils.js # 工具函数
│ ├── theme.js # 主题切换
│ └── base.js # 基础脚本
└── fonts/ # 字体文件(可选)
main.js 示例
// src/common/main.js
// 1. 导入基础样式
import "./css/base.css";
import "./css/tailwind.css";
import "./css/components.css";
// 2. 导入公共脚本
import "./js/utils.js";
import "./js/theme.js";
import "./js/base.js";
// 3. 导入第三方库
import "aos/dist/aos.css";
import AOS from "aos";
// 4. 全局初始化
document.addEventListener("DOMContentLoaded", function () {
console.log("Sky Theme 初始化完成");
// 初始化动画库
AOS.init({
duration: 800,
once: true,
});
// 初始化主题
initTheme();
});
// 5. 全局工具函数
window.SkyTheme = {
version: "1.0.0",
utils: window.utils || {},
};
构建结果
templates/assets/
├── js/
│ ├── main.js # 包含所有公共代码
│ ├── index.js # 首页专用代码
│ └── post.js # 文章页专用代码
├── css/
│ ├── main.css # 包含所有公共样式
│ ├── index.css # 首页专用样式
│ └── post.css # 文章页专用样式
├── fonts/ # 字体文件(从 src/static 复制)
├── images/ # 图片文件(从 src/static 复制)
└── icons/ # 图标文件(从 src/static 复制)
🚀 实际使用例子
创建完整页面
# 1. 创建归档页面目录
mkdir -p src/pages/archives
# 2. 创建JS入口
cat > src/pages/archives/archives.js << 'EOF'
import './archives.css';
import './components/timeline.js';
console.log('归档页面加载完成');
// 归档页面专用功能
function initArchives() {
const timeline = document.querySelector('.timeline');
if (timeline) {
// 时间线交互逻辑
}
}
document.addEventListener('DOMContentLoaded', initArchives);
EOF
# 3. 创建CSS样式
cat > src/pages/archives/archives.css << 'EOF'
.archives-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.timeline {
position: relative;
}
.timeline-item {
margin-bottom: 2rem;
padding-left: 2rem;
border-left: 2px solid #e5e7eb;
}
EOF
# 4. 创建组件
mkdir -p src/pages/archives/components
cat > src/pages/archives/components/timeline.js << 'EOF'
// 时间线组件
export function initTimeline() {
console.log('时间线组件初始化');
}
EOF
# 5. 构建
npm run build
构建日志示例
📄 index: src/pages/index/index.js
📄 post: src/pages/post/post.js
📄 archives: src/pages/archives/archives.js
✅ 生成 4 个入口点
构建完成!
- templates/assets/js/main.js (公共代码)
- templates/assets/js/archives.js (归档页面)
- templates/assets/css/main.css (公共样式)
- templates/assets/css/archives.css (归档样式)
- templates/assets/fonts/ (字体文件)
❓ 常见问题
Q: 为什么我的页面没有生成?
A: 检查文件夹名和JS文件名是否一致,比如 tags/tags.js
而不是 tags/tag.js
Q: CSS文件在哪里?
A: 在JS文件里用 import './xxx.css'
导入,构建时会自动提取
Q: 静态文件怎么处理?
A: 放在 src/static/
里,构建时会自动复制到 templates/assets/
🏗️ dist 构建详解
构建流程图
src/ 源码目录
├── common/main.js → templates/assets/js/main.js + css/main.css
├── pages/index/ → templates/assets/js/index.js + css/index.css
├── pages/post/ → templates/assets/js/post.js + css/post.css
└── static/ → templates/assets/ (直接复制)
构建过程:
1. 扫描入口 → 2. Vite打包 → 3. 文件输出 → 4. 静态复制
详细构建步骤
步骤1:入口点扫描 (generateEntries)
// vite.config.ts 第42-56行
function generateEntries() {
const entries = {};
// 1. 固定公共入口
entries["main"] = "src/common/main.js";
// 2. 动态扫描页面入口
const jsFiles = glob.sync("src/pages/**/*.js");
jsFiles.forEach((file) => {
// 匹配规则:src/pages/页面名/页面名.js
const matches = file.match(/src\/pages\/([^\/]+)\/\1\.js$/);
if (matches) {
entries[matches[1]] = file;
}
});
return entries; // 返回所有入口点
}
步骤2:Vite 打包处理
// vite.config.ts 第60-75行
rollupOptions: {
input: generateEntries(), // 使用扫描到的入口
output: {
// JS文件输出规则
entryFileNames: 'js/[name].js',
// CSS文件输出规则
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith(".css")) {
const name = assetInfo.name.replace('.css', '');
return name === 'main' ? 'css/main.css' : `css/${name}.css`;
}
return "assets/[name][extname]";
}
}
}
步骤3:静态资源复制 (copyStaticAssets)
// vite.config.ts 第7-32行
function copyStaticAssets() {
const srcStaticDir = "src/static";
const destAssetsDir = "templates/assets";
// 递归复制所有文件(跳过README.md)
function copyRecursive(src, dest) {
// 创建目标目录
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
// 遍历源目录
const items = readdirSync(src);
items.forEach((item) => {
if (item !== "README.md") {
// 跳过说明文件
// 递归处理子目录,直接复制文件
copyFileSync(srcPath, destPath);
}
});
}
}
构建输出结构
templates/assets/
├── js/
│ ├── main.js # 公共代码包(所有页面都需要)
│ ├── index.js # 首页专用代码
│ ├── post.js # 文章页专用代码
│ └── archives.js # 归档页专用代码
├── css/
│ ├── main.css # 公共样式包(基础样式+Tailwind)
│ ├── index.css # 首页专用样式
│ ├── post.css # 文章页专用样式
│ └── archives.css # 归档页专用样式
├── fonts/ # 字体文件(从src/static复制)
├── images/ # 图片资源(从src/static复制)
└── icons/ # 图标文件(从src/static复制)
与 theme-modern-starter 的改进对比
特性 | theme-modern-starter | Sky Theme 改进版 |
---|---|---|
入口扫描 | 手动配置 | 自动扫描匹配 |
命名规则 | 固定入口 | 文件夹名=文件名 |
静态资源 | 手动处理 | 自动复制插件 |
CSS处理 | 分离配置 | 统一入口导入 |
开发体验 | 基础热更新 | 增强热更新 |
vite.config.ts 代码案例
import { defineConfig } from "vite";
import { glob } from "glob";
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
import { join, dirname } from "path";
/**
* 复制静态资源到构建目录
*/
function copyStaticAssets() {
const srcStaticDir = 'src/static';
const destAssetsDir = 'templates/assets';
if (!existsSync(srcStaticDir)) {
return;
}
function copyRecursive(src: string, dest: string) {
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const items = readdirSync(src);
items.forEach(item => {
const srcPath = join(src, item);
const destPath = join(dest, item);
if (statSync(srcPath).isDirectory()) {
copyRecursive(srcPath, destPath);
} else {
// 跳过 README.md 文件
if (item !== 'README.md') {
copyFileSync(srcPath, destPath);
}
}
});
}
copyRecursive(srcStaticDir, destAssetsDir);
}
/**
* 极简构建配置
* 只处理JS入口,CSS通过JS导入处理
*/
function generateEntries() {
const entries: Record<string, string> = {};
// 公共资源入口
entries['main'] = 'src/common/main.js';
// 扫描页面JS文件
const jsFiles = glob.sync("src/pages/**/*.js");
jsFiles.forEach((file) => {
const matches = file.match(/src\/pages\/([^\/]+)\/\1\.js$/);
if (matches) {
const pageName = matches[1];
entries[pageName] = file;
console.log(`📄 ${pageName}: ${file}`);
}
});
console.log(`✅ 生成 ${Object.keys(entries).length} 个入口点`);
return entries;
}
export default defineConfig({
build: {
outDir: "templates/assets",
minify: 'terser',
rollupOptions: {
input: generateEntries(),
output: {
entryFileNames: 'js/[name].js',
assetFileNames: (assetInfo) => {
if (assetInfo.name && assetInfo.name.endsWith(".css")) {
const name = assetInfo.name.replace('.css', '');
if (name === 'main') {
return 'css/main.css';
}
return `css/${name}.css`;
}
return "assets/[name][extname]";
},
manualChunks: undefined,
},
},
assetsInlineLimit: 0,
},
plugins: [
{
name: 'copy-static-assets',
closeBundle() {
copyStaticAssets();
}
}
]
});