vite-multipage-demo
项目简介
- 本项目提供了一个
Vite
的多页面配置案例 - 可以在已有项目中快速扩展或拆分路由将应用变更为多页面应用
项目缘由
最近遇到一个需求,项目中的某些偏通用/展示类页面需要供其他系统嵌入,嵌入的方式自然是iframe
,但是目前的项目是基于Vite + Vue3 + Ts
的SPA
,提供给其他系统嵌入必然会加载大量其他系统本不需要的资源,除此之外,当前的系统是有一套layout
的,这意味着其他系统嵌入目标路由时也会加载layout
,尽管一种方式是可以将目标路由调整为不需要layout
,但是这也意味着破坏了目标路由在当前系统中的展示
解决方案
思来想去还是将当前的项目扩展为多页面应用,将需要嵌入其他系统的路由页面全部修改为独立的页面,在当前项目中同样使用iframe
嵌入该独立的页面(也即是修改前的目标路由),当然,由于修改后的独立页面仍然在当前项目中,亦可不通过iframe
嵌入,直接import
组件像正常开发一样使用
配置实现
先来看一下Vite
官网的多页面应用配置文档说明
按照文档所说我组织了如下的项目结构
* 部分文件已忽略
vite-multipage-demo
|-pages // 多页面目录
| |-about // 关于页面
| | |-assets // 关于页面资源
| | |-src // 页面源码
| | | |-App.vue // 页面组件
| | | |-main.ts // 入口文件
| | |-index.html // 页面入口
| |-agreement // 协议页面
| | |-assets // 关于页面资源
| | |-src // 页面源码
| | | |-App.vue // 页面组件
| | | |-main.ts // 入口文件
| | |-index.html // 页面入口
|-src // 公共源码
| |-assets // 公共资源
| |-components // 公共组件
| |- App.vue // 根组件
| |-main.ts // 入口文件
|-index.html // 根入口
|-tsconfig.json // TypeScript 配置
|-package.json // 项目依赖配置
|-vite.config.ts // Vite 配置文件
而后修改了vite.config.ts
配置如下
// @ts-nocheck
import type { UserConfig, ViteDevServer } from "vite";
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";
const root = process.cwd();
function pathResolve(dir: string) {
return resolve(root, ".", dir);
}
// 所有页面
const pages = [
{ name: "index", htmlName: "index.html", htmlPath: "" },
{ name: "about", htmlName: "index.html", htmlPath: "pages/about/" },
{ name: "agreement", htmlName: "index.html", htmlPath: "pages/agreement/" },
];
pages.forEach((page) => {
page.path = pathResolve(page.htmlPath + page.htmlName);
});
export default (): UserConfig => {
return {
plugins: [vue()],
build: {
rollupOptions: {
input: pages.reduce((res: Record<string, string>, cur) => {
res[cur.name] = cur.path;
return res;
}, {}),
},
},
};
};
效果如下:
可以看到效果并不如预期,初步猜测是Vite Server
并没有正确处理响应,通过查阅资料知道Vite
的插件 API
提供了一个configureServer
,是用于配置开发服务器的钩子,关于这一API
源码的注释如下:
大概了解了下,在中间件替换了Vite Server
接收到的请求信息,其实最主要的改动就是修改了请求的url
,配置如下
// .....
// server插件
const multiplePagePlugin = () => ({
name: "multiple-page-plugin",
configureServer(server: ViteDevServer) {
server.middlewares.use((req, res, next) => {
for (let page of pages) {
if (page.name === "index") {
continue;
}
if (req.url.startsWith(`/${page.name}`)) {
req.url = `/${page.htmlPath}${page.htmlName}`;
break;
}
}
next();
});
},
});
export default (): UserConfig => {
return {
plugins: [vue(), multiplePagePlugin()],
// .....
};
};
效果符合预期
本来到这里就结束了,但是在打包之后发现实际上所有的资源都统一被打包到了assets
目录下,如下图:
可以看到about
页面的logo.png
最终输出在根目录下的dist/assets
下,尽管这并不影响我当下的需求,但是考虑到pages
下的产物的灵活性,于是再研究了下rollupOptions
配置项,发现可以通过output
配置实现,新增配置如下:
// .....
// 此处将pages拆分为默认pages和mutltiPages,是因为默认页面的打包资源无需处理
// 在output中只需匹配多页面,没有匹配到的资源仍然放在根目录assets下
// 多页面信息
const mutltiPages = [
{ name: "about", htmlName: "index.html", htmlPath: "pages/about/", outPagePath: "pages/about/" },
{ name: "agreement", htmlName: "index.html", htmlPath: "pages/agreement/", outPagePath: "pages/agreement/" },
];
// 所有页面
const pages = [{ name: "index", htmlName: "index.html", htmlPath: "", outPagePath: "" }, ...mutltiPages];
// ......
export default (): UserConfig => {
return {
// ......
build: {
rollupOptions: {
// ......
output: {
// 自定义输出目录和文件名
entryFileNames: (chunkInfo) => {
// 尝试通过chunk名匹配多页面路径 若匹配到则放置在对应目录 否则放置在根目录
const page = mutltiPages.find((p) => p.name === chunkInfo.name);
return page ? `${page.outPagePath.replace(/^\//, "")}assets/[name].[hash].js` : "assets/[name].[hash].js";
},
chunkFileNames: (chunkInfo) => {
const page = mutltiPages.find((p) => chunkInfo.name.includes(p.name));
return page ? `${page.outPagePath.replace(/^\//, "")}assets/[name].[hash].js` : "assets/[name].[hash].js";
},
assetFileNames: (assetInfo) => {
// 处理 CSS、图片等资源
// 优先按照原始文件名处理 若匹配到多页面路径则放置在对应目录 否则放置在根目录assets
if (assetInfo.originalFileName) {
const page = mutltiPages.find((p) => assetInfo.originalFileName?.includes(p.outPagePath));
return page ? `${page.outPagePath.replace(/^\//, "")}assets/[name].[hash][extname]` : "assets/[name].[hash][extname]";
} else {
// 如果没有原始文件名,通过name匹配
const page = mutltiPages.find((p) => assetInfo.name?.includes(p.name));
return page ? `${page.outPagePath.replace(/^\//, "")}assets/[name].[hash][extname]` : "assets/[name].[hash][extname]";
}
},
},
},
},
};
};
entryFileNames
,chunkFileNames
,assetFileNames
三项配置分别处理入口文件,其他chunk
,静态资源的输出目录,配置好后打包目录符合预期。
本来到这里就结束了,但是有时候多页面的需求可能并不那么纯粹,简单来说,基于我一开始提出的缘由,尽管目标路由变成了独立的页面,但实际上独立出去的页面仍然需要访问目前系统的许多公共资源(根目录/src
下),若是一股脑地全部搬到对应的pages
下,虽然逻辑和结构清晰了,但系统中的公共资源却变成了两份甚至多份,同时后续系统中的公共资源若有变更,则需要维护多处内容,而如果直接在pages
对应的代码中引入公共资源,则会造成结构的破坏,这两个方案都有不足,既然两个方案我都不满意,那么只能将pages
挪到根目录/src
下,或者说至少要支持这样的情况,这样一来,独立出去的页面访问系统公共资源自然是合乎逻辑的。
于是我添加了如下的inner-pages
|-src // 公共源码
| |-assets // 公共资源
| |-components // 公共组件
| |-inner-pages // 内部页面
| | |-releases // 发布页面
| | | |-src // 发布页面源码
| | | | |-App.vue // 发布页面组件
| | | | |-main.ts // 发布页面入口
| | | |-index.html // 发布页面入口
| |- App.vue // 根组件
| |-main.ts // 入口文件
|-index.html // 根入口
修改vite.config.ts
配置如下
// 多页面信息
const mutltiPages = [
{ name: "releases", htmlName: "index.html", htmlPath: "src/inner-pages/releases/", outPagePath: "inner-pages/releases/" },
// ......
];
本以为万事大吉,但是打包后发现inner-pages
的资源符合预期,但是index.html
文件却保留了原来的路径
原因在Vite
的文档中自然提到了
简单来说,HTML
文件路径生成的id
也将作为输出产物的对应路径,但是如果就这样的话,实在有些割裂。
大模型问了半天也没有方案,最后还是仔细看了下rollupOptions
的配置才解决,新增配置如下
// ......
// 处理html输出路径
const htmlPlugin = () => {
return {
name: "html-path-manual",
generateBundle(options, bundle) {
// 对inner-pages下的index.html的输出路径单独进行处理
const innerPages = mutltiPages.filter((page) => page.outPagePath.startsWith("inner-pages"));
for (let page of innerPages) {
const htmlFile = bundle[page.htmlPath + page.htmlName];
if (htmlFile) {
htmlFile.fileName = page.outPagePath + page.htmlName;
}
}
},
};
};
export default (): UserConfig => {
return {
// ......
build: {
rollupOptions: {
// ......
plugins: [htmlPlugin()],
},
},
};
};
打包结果如下
到此,终于结束了。
其他
写了这么多,主要是为了还原我在处理这一需求时的历程,如果是一个全新的项目,我相信自然一开始就有很好的多页面应用组织,但可惜不是,不过掉进了坑里再爬出来,将会拥有更大的自由度,比如此刻,我相信如果你看到了这里,在你的项目中你将可以任意地组织多页面的输入和输出。
Vite
毕竟是新兴的事物,虽然应用越来越多,但是其本身的迭代也很快,相关的需求参考博客都比较少,加上本人对rollup
并不熟悉,所以还是花了大半天时间,所以也希望这篇记录有一些贡献,下次当别人再有需求时直接就能搜到可用的解决方案了,毕竟在项目中,大多数的朋友都不太会参与项目基建或配置的改动。
最后,对Vite
的配置虽然完成了,但生产环境还需要做配置,因为不同的项目背景不同,部署方式也不同,这里只贴一下nginx
的配置参考
// ......
http {
include mime.types;
default_type application/octet-stream;
keepalive_timeout 65;
gzip on;
client_max_body_size 2048m;
server {
listen 8000; # 监听的端口
server_name 0.0.0.0; # 域名或ip
location /about {
alias html/pages/about/;
try_files $uri $uri/ /index.html;
index index.html index.htm;
error_page 405 =200 $uri;
}
location /agreement {
alias html/pages/agreement/;
try_files $uri $uri/ /index.html;
index index.html index.htm;
error_page 405 =200 $uri;
}
location /releases {
alias html/inner-pages/releases/;
try_files $uri $uri/ /index.html;
index index.html index.htm;
error_page 405 =200 $uri;
}
location / {
root html/;# 根目录
try_files $uri $uri/ /index.html;
index index.html index.htm; # 默认页
error_page 405 =200 $uri;
}
}
}
2025.01.11 更新:完善demo
- 新增
vue-router
,about
页面修改为路由模式 pages
下的页面aseets
目录移动到对应页面src
下rollupOptions.output.chunkFileNames
修改实现