Sky
Sky
Published on 2025-08-04 / 19 Visits
1
0

Halo 主题使用 vite 构建样式的一个改进方式

基于 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();
      }
    }
  ]
});


Comment