Vue3+Vite单页面应用(SPA)改造为多页面应用(MPA)
在 Vue 开发中,默认创建的项目通常是单页面应用(SPA)。然而,在某些特定场景下,我们可能需要将其改造成多页面应用(MPA)以满足不同的业务需求。以下是详细的改造步骤,以通过 Vue CLI 创建的项目为例进行说明。
创建项目
在开始之前,请确保你已经正确安装了 npm(推荐 node.js v20+)。安装完成后,我们可以使用以下命令创建一个新的 Vue 项目:
npm create vue@latest
# 按照脚手架的提示信息,创建一个名称为 my-mpa-project 的项目,创建完成后进入该项目目录
cd my-mpa-project
2
3
配置多页面
新增目录结构
为了支持多个静态页面,我们需要创建一个新的目录 src/pages/
。接着,将 index.html
、src/App.vue
和 src/main.js
文件移动到 src/pages/index/
目录下。在移动文件后,需要调整页面中的组件引用地址,确保所有引用的地址都是正确的。以下是修改后的 index.html
文件示例:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/pages/index/main.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
创建一个新页面
为了实现多页面应用,我们可以复制 src/pages/index
目录到 src/pages/about
目录。复制完成后,需要调整 about
目录下 index.html
文件中的 script
地址,使其指向正确的 main.js
文件。调整后的 index.html
文件内容如下:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>关于我们</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/pages/about/main.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
集成多页面插件
为了更好地管理多页面应用,我们需要集成一个多页面插件。这里我们使用 vite-plugin-mpa
插件,可以通过以下命令进行安装:
npm install -D vite-plugin-mpa
安装完成后,需要修改 vite.config.js
文件,添加与多页面应用相关的配置代码。具体代码如下:
import mpa from 'vite-plugin-mpa';
export default defineConfig({
base: '/',
plugins: [
...
mpa.default({
open: 'index/index.html',
scanDir: 'src/pages',
scanFile: 'main.js',
filename: 'index.html',
}),
],
})
2
3
4
5
6
7
8
9
10
11
12
13
14
多页面路由支持(可选)
如果你的项目需要不同的页面使用不同的路由,可以将 src/router/index.js
文件复制为 indexRouter.js
和 aboutRouter.js
,并分别用于不同的页面。复制完成后,需要同步调整页面 main.js
文件中的路由引用地址,确保路由的正确使用。
运行和构建项目
运行项目
使用以下命令运行项目:
npm run dev
项目运行后,你可以通过 http://localhost:5173/index/
和 http://localhost:5173/about/
访问不同的页面,检查多页面应用是否正常工作。
构建项目
当项目开发完成后,需要进行构建以生成生产环境所需的文件。使用以下命令进行项目构建:
npm run build
构建完成后,在 dist
目录下会生成多个 HTML 文件目录,这些目录包含了多页面应用的静态文件。
通过以上步骤,你就可以将 Vue 的单页面应用成功改造成多页面应用。
项目简介
本项目提供了一个 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 配置文件
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
而后修改了 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;
}, {}),
},
},
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
效果并不如预期,初步猜测是 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()],
// .....
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
效果符合预期
本来到这里就结束了,但是在打包之后发现实际上所有的资源都统一被打包到了
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]";
}
},
},
},
},
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 // 根入口
2
3
4
5
6
7
8
9
10
11
12
修改 vite.config.ts
配置如下
// 多页面信息
const mutltiPages = [
{ name: "releases", htmlName: "index.html", htmlPath: "src/inner-pages/releases/", outPagePath: "inner-pages/releases/" },
// ......
];
2
3
4
5
本以为万事大吉,但是打包后发现 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()],
},
},
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
到此,终于结束了。
其他
写了这么多,主要是为了还原我在处理这一需求时的历程,如果是一个全新的项目,我相信自然一开始就有很好的多页面应用组织,但可惜不是,不过掉进了坑里再爬出来,将会拥有更大的自由度,比如此刻,我相信如果你看到了这里,在你的项目中你将可以任意地组织多页面的输入和输出。
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;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43