项目目录
web/
├── index.html # 首页入口
├── other.html # 其他页面入口(根目录 *.html 均为 MPA 入口)
├── common/ # 公共 HTML 片段(不参与构建入口扫描)
│ ├── common-link.html # 公共 CSS / rem 脚本 / CDN
│ ├── common-script.html # 公共 JS CDN + common.js
│ ├── header.html
│ └── footer.html
├── css/ # 全局样式(全站复用)
│ ├── global.css # 样式入口(@import 聚合)
│ ├── base.css # 重置与基础
│ ├── var.css # CSS 变量(颜色等)
│ ├── font.css # @font-face
│ ├── font-size.css # --font-N 字号变量
│ ├── layout.css # 响应式 --p-x(按需引入)
│ ├── components.css # 通用组件(按钮、section 公共块等)
│ ├── header.scss
│ └── footer.scss
├── styles/ # 页面级 SCSS(按页引入)
│ └── index.scss # 首页区块样式
├── js/
│ ├── common.js # 全站公共逻辑(jQuery)
│ └── home.js # 单页入口(ES Module)
├── assets/
│ ├── images/ # 图片资源
│ └── fonts/ # 字体
├── vite.config.js # vite.config
├── package.json
└── postcss.config.js # postcss
项目依赖 package.json
{
"name": "web-template",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"vite": "^8.0.12",
"vite-plugin-html-inject": "^1.1.2"
},
"devDependencies": {
"sass-embedded": "^1.99.0",
"postcss": "^8.5.0",
"postcss-pxtorem": "^6.1.0"
}
}
创建公共的header.html和footer.html,或者引入其他公共代码html
页面引入
<!-- 引入公共头部 -->
<load src="./common/header.html" />
...
<!-- 引入首页底部 -->
<load src="./common/home-footer.html" />
vite.config.js配置vite-plugin-html-inject处理
import { readdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import htmlInject from 'vite-plugin-html-inject'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig({
plugins: [htmlInject()],
server: {
// 允许外部访问
host: '0.0.0.0',
},
build: {
rollupOptions: {
input:{
index: 'index.html',
job: 'job.html',
},
},
},
)}
还可以自定义插件实现自动获取根目录的 .html文件,新的页面不用手动添加
/** 扫描项目根目录下的 *.html 作为 MPA 构建入口 */
function getPageInputs(rootDir) {
return Object.fromEntries(
readdirSync(rootDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.html'))
.map((entry) => [
path.basename(entry.name, '.html'),
path.resolve(rootDir, entry.name),
])
)
}
export default defineConfig({
plugins: [htmlInject()],
server: {
// 允许外部访问
host: '0.0.0.0',
},
build: {
rollupOptions: {
input: getPageInputs(__dirname),
},
},
})
配置输出目录static/
export default defineConfig({
plugins: [htmlInject()],
server: {
// 允许外部访问
host: '0.0.0.0',
},
build: {
rollupOptions: {
input: getPageInputs(__dirname),
output: {
entryFileNames: 'static/js/[name]-[hash].js',
chunkFileNames: 'static/js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
const name = assetInfo.names[0]
const ext = path.extname(name ?? '')
if (ext === '.css') return 'static/css/[name]-[hash][extname]'
if (/\.(png|jpe?g|gif|svg|webp|ico|avif)$/i.test(ext)) {
return 'static/images/[name]-[hash][extname]'
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(ext)) {
return 'static/fonts/[name]-[hash][extname]'
}
return 'static/assets/[name]-[hash][extname]'
},
},
},
},
})
通过postcss-pxtorem实现rem响应式方案
安装依赖
pnpm i postcss postcss-pxtorem -D
配置文件postcss.config.js
export default {
plugins: [
postcssPxtorem({
rootValue: 16,//基准换算基数 1rem = 16px
propList: ['*'],//需要转换 px → rem 的 CSS 属性列表
selectorBlackList: [],//选择器黑名单:匹配到的 CSS 选择器内 px 不转 rem
minPixelValue: 2,//最小转换像素值:小于该数值的 px 不做转换
mediaQuery: false,//@media (max-width: 576px)<==不处理这个px,不是media内部的px
}),
],
}
如何处理media内部的px不转换?
使用Px代替px
使用自定义插件自动转换 px->Px
/** 将 @media 块内声明值中的 `px` 改为 `Px`,postcss-pxtorem 只匹配小写 `px`,故不转换;浏览器仍按 px 解析 */
function skipPxToRemInsideMedia() {
return {
postcssPlugin: 'skip-px-to-rem-inside-media',
AtRule(atRule) {
if (atRule.name !== 'media') return
atRule.walkDecls((decl) => {
if (!decl.value.includes('px')) return
decl.value = decl.value.replace(/(\d*\.?\d+)px/g, '$1Px')
})
},
}
}
skipPxToRemInsideMedia.postcss = true
export default {
plugins: [
skipPxToRemInsideMedia(),
postcssPxtorem({
rootValue: 16,
propList: ['*'],
selectorBlackList: [],
minPixelValue: 2,
mediaQuery: false,
}),
],
}