ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Vue 2.0多页面应用模板:从SPA困境到工程化落地

Vue 2.0多页面应用模板:从SPA困境到工程化落地

简介:这是一套基于 Vue 2.0 打造的多页面应用模板,面向需要在同一工程下管理多个独立页面的前端开发者,有助于避开单页应用在入口扩展、独立构建与路由耦合上的常见问题。压缩包共 32 个文件,整体仅 31KB,核心由 15 个 JavaScript 配置与构建脚本、6 个 Vue 页面/组件构成,并辅以 HTML 入口、ESLint、Babel、editorconfig 等工程化配置,完整覆盖开发调试、生产打包、环境变量与代码规范等环节。目录按 src/module、components、assets 分层组织,可以清晰看出模块划分与可复用组件的位置,便于直接在其上扩展业务功能。目前已有 4660 人学习下载。借助这套模板,既能省去手动搭建 webpack 多页配置的繁琐步骤,也能通过阅读源码理解非 vue-cli 场景下的构建思路,适合团队作为前端多页项目的初始骨架。

1. Vue 2.0 多页面应用模板:为什么 SPA 不是唯一解

接到一个需要 SEO 的官网,或者一个后台系统里登录、报表、配置三个模块要独立部署时,再拿 Vue 2.0 默认的 SPA 单页模板硬扛,就会陷入两难:首屏要等一整包 JS 下载完才能渲染,搜索引擎抓到的只有一个空壳。Vue 2.0(准确说是 Vue 2.x)多页面应用模板解决的正是这个场景——它把 webpack 的多入口、多 HTML 页面、公共依赖抽离都封装好,让你基于 Vue 2 生态一套代码管理多个独立页面。模板本身不神秘:就是约定好目录结构,配置好每个页面的入口和构建产物。适合正在维护 Vue 2 存量项目的团队,也适合新手想快速理解多页应用工程化怎么写。多页面不是倒退,而是 SPA 覆盖不了 SEO 和独立部署诉求时的务实选择。

2. 多页面模板的原理边界:MPA 和 SPA 怎么选,脚手架版本怎么定

2.1 多页面应用的核心机制:多入口、多 Document、独立加载

SPA 的构建产物通常是一个 index.html 加一堆 JS/CSS,浏览器首次只加载一个 Document,之后靠路由切换视图。MPA 则完全不同:每个页面都有独立的 HTML 文件、独立的入口 JS,浏览器访问 /list.html 时加载的是 list 页面自己的 Document 和资源。这是两者最本质的区别,也决定了不同的优化路径。

对比维度SPA(Vue 默认模板)MPA(本文模板)
页面载体单个 index.html每个页面一个 HTML
路由方式vue-router 前端路由浏览器原生导航
SEO 友好度低,首屏内容靠 JS 渲染高,每个 HTML 可独立被抓取
构建配置单入口多 entry + 多 HtmlWebpackPlugin
首屏加载受整个应用体积影响只加载当前页面依赖
公共代码天然共享需要主动抽离

判断标准我一般看三点:有没有 SEO 需求、模块之间是否需要独立部署、团队是否希望“改一个页面不动其他页面”。三条里命中两条,多页面模板就是更好的选择。

2.2 vue-cli 2 与 vue-cli 3/4 的模板差异:先分清你的脚手架版本

“多页面应用模板”这个标题背后,其实是两套完全不同的实现路径。vue-cli 2 时代项目结构是 build/webpack.base.conf.js 手动配置 entry 和 HtmlWebpackPlugin;vue-cli 3 以后引入了 vue.config.js 的 pages 配置,原生支持多页面。这个差异直接决定了模板长什么样,动手前先确认自己的脚手架版本。

对比项vue-cli 2(webpack 3)vue-cli 3 / 4(webpack 4)
配置文件build/webpack.base.conf.jsvue.config.js
多页面写法手写 entry 对象 + new HtmlWebpackPluginpages 对象声明式配置
维护成本中,改动 webpack 配置容易踩坑低,模板 API 收敛
升级难度迁移到 vue-cli 3 需重写配置原生支持,无需额外插件

模板的价值在 vue-cli 2 时代尤其明显:每个页面都要手动 new 一个 HtmlWebpackPlugin,chunks 顺序错了就白屏,这些重复劳动被封装成模板后,新增页面只需要复制一个目录再改两行配置。

2.3 目录结构怎么排:约定优于配置的 pages 组织方式

模板的核心是目录约定。我常用的结构是把每个页面收拢到 src/pages 下独立成文件夹,页面自己的组件、路由、状态都放在这个目录里,避免跨页面互相引用导致构建产物分不开。

src/ ├── pages/ │ ├── index/ │ │ ├── main.js # 页面入口,必须存在 │ │ ├── App.vue │ │ └── index.html # 页面模板,HtmlWebpackPlugin 使用 │ └── list/ │ ├── main.js │ ├── App.vue │ └── index.html ├── common/ # 跨页面的公共组件、工具函数 │ └── utils.js └── styles/ └── common.scss # 公共样式,由各入口 import

这个结构的约束很简单:每个页面目录自包含,公共代码统一放 src 下而不是页面目录里。这样新增一个页面时,复制目录、改 pages 配置、重启开发服务器,三步就能跑起来。公共逻辑如果放错了位置,后面分包和避坑那几章就会频繁碰到问题。

3. 跑通最小多页面模板:vue-cli 3/4 的 pages 配置与目录组织

3.1 最小可复现步骤:初始化项目到第一个双页面

先创建一个基础的 Vue 2 项目,再改造成多页面。命令如下:

vue create my-mpa-template # 选择 Vue 2 preset,不需要额外的 TS 和 Router 选项 cd my-mpa-template

创建完成后删除 src 下的默认单页结构,建立 pages 目录。把原来的 App.vue 和 main.js 复制成两份,分别放到 pages/index 和 pages/list 下:

mkdir -p src/pages/index src/pages/list cp src/App.vue src/pages/index/App.vue cp src/App.vue src/pages/list/App.vue # 分别改写两个 main.js,指向各自的 App.vue

两个 main.js 的内容几乎一样,只是引入路径要保持相对正确。这种刻意重复的好处是页面之间彻底解耦,你可以放心地在 index 页面引入一个重量级图表库,而 list 页面完全不受影响。如果你希望公共逻辑共享,放在 src/common 里让各页面 import 即可,而不是直接改对方的目录。

3.2 vue.config.js 的 pages 完整配置与参数说明

这是整个模板的核心配置。vue-cli 3/4 会在构建时读取 pages 对象,自动为每个页面创建入口和 HtmlWebpackPlugin 实例:

const path = require('path') module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/dist/' : '/', pages: { index: { entry: 'src/pages/index/main.js', template: 'src/pages/index/index.html', filename: 'index.html', title: '首页', chunks: ['chunk-vendors', 'chunk-common', 'index'] }, list: { entry: 'src/pages/list/main.js', template: 'src/pages/list/index.html', filename: 'list.html', title: '列表页', chunks: ['chunk-vendors', 'chunk-common', 'list'] } }, devServer: { port: 8080 } }

pages 配置里每个字段都有明确的用途:entry 是页面入口 JS,决定了该页面会打包哪些依赖;template 是 HTML 模板文件,构建时会把 JS/CSS 注入到这个文件里;filename 是最终生成的文件名,浏览器直接访问的就是它;title 会覆盖模板里的<title>标签;chunks 则声明该页面加载哪些 chunk。chunks 这里我习惯写全,避免页面多了之后公共 chunk 顺序错乱的问题,后面避坑章节会专门讲。

参数调整的经验:title 字段适合静态页标题,如果页面标题需要根据查询参数动态变化,就要在页面代码里用 document.title 覆盖。filename 建议用英文小写,避免部署在 Nginx 等服务器上由大小写引发的 404。

3.3 开发代理、publicPath 与 favicon:模板里的隐性配置

多页面模板接入真实后端接口时,开发环境要配置代理。devServer.proxy 和单页应用没有区别,但注意多页面场景下每个页面都会发起跨域请求,代理配置集中管理更省心:

devServer: { proxy: { '/api': { target: 'http://api.example.com', changeOrigin: true, pathRewrite: { '^/api': '' } } } }

publicPath 是另一个容易忽略的字段。它控制构建产物里 JS/CSS 的引用路径。生产环境部署到域名子目录时,publicPath 要设置为/your-sub-path/;如果部署在站点根目录则用/;如果产物需要放在一个未知路径下,用相对路径'./'最稳妥。这里没有统一的正确答案,只跟着部署方式走。

favicon 的坑比较隐蔽:vue-cli 默认只处理 public 目录下的 favicon.ico,多页面模板里如果每个页面模板都写死<link rel="icon" href="/favicon.ico">,开发环境没问题,生产构建后路径就可能失效。我用一个简单做法:把 favicon 放在 public 下,模板里用<%= BASE_URL %>favicon.ico引用,BASE_URL 会自动适配 publicPath。

4. 上线前的多页面工程化:分包、标题注入与路由取舍

4.1 公共依赖抽离:splitChunks 在 MPA 里的正确姿势

多页面模板运行一段时间后,你会发现问题:每个页面的 bundle 里都打了一份 Vue 核心库,体积急剧膨胀。默认配置下 vue-cli 只把 node_modules 里的依赖抽成 chunk-vendors,但如果两个页面依赖同一个公共工具函数,它就会被重复打包。这时候需要在 vue.config.js 里自定义 splitChunks:

module.exports = { chainWebpack: config => { config.optimization.splitChunks({ chunks: 'all', cacheGroups: { vueCore: { name: 'chunk-vendors', test: /[\\/]node_modules[\\/](vue|vue-router|vuex|axios)[\\/]/, priority: 10, chunks: 'all' }, commonLib: { name: 'chunk-common', test: path.resolve(__dirname, 'src/common'), priority: 5, minChunks: 2, chunks: 'all' } } }) } }

这段配置把 vue、vue-router 这类框架依赖固定抽成 chunk-vendors,把 src/common 下被至少两个页面引用的模块抽成 chunk-common。参数说明:test 是模块匹配规则;priority 决定分组优先级,数值大的先匹配;minChunks 表示最少被几个 chunk 引用才抽离。注意 pages 配置里 chunks 数组要包含这两个名字,否则页面不会加载它们。

一个常见的误用是把所有 node_modules 依赖都打进 chunk-vendors,导致首屏要下载一个极大的 vendor 包。正确的做法是只把真正跨页面共享的框架依赖放进去,其余按页面各自打包。

4.2 每个页面的标题和 Meta 怎么动态控制

pages 配置里的 title 只能搞静态标题,页面内部的动态标题要靠代码覆盖。我习惯写一个小工具函数,放在 src/common 里,每个页面 main.js 初始化时调用:

// src/common/usePageMeta.js export function setPageMeta(title, description) { document.title = title let meta = document.querySelector('meta[name="description"]') if (!meta) { meta = document.createElement('meta') meta.name = 'description' document.head.appendChild(meta) } meta.content = description || '' } // 页面内使用 import { setPageMeta } from '@/common/usePageMeta' setPageMeta('商品列表 - 第 2 页', '按分类浏览全部商品信息')

这个函数在 mounted 或路由守卫里调用均可。注意多页面场景下 document.title 的覆盖是瞬时的,如果页面有异步数据,建议在数据拿到后再调用一次,避免标题闪烁。HTML 模板里也可以注入占位变量,但动态场景用代码覆盖更灵活。

4.3 按页面路由组织组件:MPA 场景下还要不要 vue-router

很多人问:多页面模板里还能用 vue-router 吗?答案是可以,但要分清粒度。页面级别的跳转走 href 或 window.location,浏览器原生导航;页面内部的局部视图切换,比如一个 tab 面板,用 vue-router 的 hash 模式是没有问题的。

场景推荐方案理由
页面间跳转<a href="/list.html">符合 MPA 语义,可被搜索引擎索引
页面内 tab 切换组件 v-if 控制无需引入路由
页面内复杂子视图vue-router hash 模式保留浏览器前进后退

需要强调的是,不要在 MPA 的页面里使用 vue-router 的 history 模式。因为每个页面是独立的 HTML,history 模式的地址会绕过服务器实际文件,刷新时直接 404。

5. 多页面模板避坑:状态百出?先查这五个高频问题

5.1 坑一:页面 A 的 JS 跑到了页面 B 上

现象:打开 list.html 时页面执行了 index 页面的初始化逻辑,弹窗、事件绑定都在 B 页面出现。原因:HtmlWebpackPlugin 的 chunks 没有显式指定,webpack 自动注入时把所有 entry 的 chunk 都塞进了每个 HTML。解决:在 pages 配置里给每个页面写全 chunks 数组,明确只加载自己的业务 chunk 和公共 chunk。排查手段是打开构建后的 HTML 文件看 script 标签,如果出现两个页面的业务 chunk,就是这个原因。

5.2 坑二:打包后资源全部 404

现象:本地 npm run serve 一切正常,build 完上传到服务器后页面白屏,控制台全是资源加载失败。原因:publicPath 配置和生产部署路径不一致。构建生成的引用路径如果是/js/index.js,而项目部署在/sub/子目录下,就必然 404。解决:确认部署路径后设置 publicPath,子目录部署用/sub/,不确定路径时用相对路径'./'。注意相对路径会影响路由懒加载的加载基址,多页面场景下页面级资源没问题,但页面内异步组件要配合 webpack 的publicPath处理。

5.3 坑三:公共样式重复打进每个页面包

现象:一个 20KB 的公共样式文件,在三个页面的产物里各出现一次,构建体积虚高。原因:样式文件被多个入口直接 import,且 splitChunks 配置没有覆盖样式模块。解决:把公共样式放在 src/styles/common.scss,在 pages 配置中让每个页面 entry 都 import 它,同时给 splitChunks 增加 CSS 分组或者依赖 vue-cli 的 css.extract 配置。验证方式:检查 dist/css 下是否只有一个公共样式 chunk,而不是每个页面目录里各有一份。

5.4 坑四:开发环境新增页面后刷新 404

现象:在 pages 配置里加了一个新页面,npm run serve 状态下直接访问新页面地址提示 404,重启后又正常。原因:devServer 的 historyApiFallback 默认只处理单入口,新增 HTML 文件不会热生效。解决:重启开发服务器让 webpack 重新编译入口。如果频繁新增页面嫌重启麻烦,可以配置 devServer.historyApiFallback 的 rewrites 规则,把未匹配路径指到对应模板。但注意 rewrites 要在模板文件生成后才有效。

5.5 坑五:vue-cli 2 老项目改造时 HtmlWebpackPlugin 重复实例

现象:老项目从单页改多页后,构建报错或者页面加载顺序错乱,chunk-vendors 没被正确引用。原因:vue-cli 2 的 webpack.base.conf.js 原本只有一个 HtmlWebpackPlugin 实例,多页需要遍历 pages 数组动态创建,且 plugin 的 chunks 顺序必须把 vendor 放在最前。解决:写一个遍历函数生成 entries 和 plugins,chunks 第一项固定为 vendor,再放公共 chunk 和页面 chunk:

pages.forEach(page => { webpackConfig.plugins.push(new HtmlWebpackPlugin({ filename: page.filename, template: page.template, chunks: ['manifest', 'vendor', 'common', page.name] })) })

这段配置里的 manifest 保存 webpack 运行时,vendor 是公共依赖,common 是业务公共代码,顺序不能乱。老项目迁移到 vue-cli 3/4 是更省心的方案,如果暂时不能迁移,上述写法能帮你撑一阵。

6. 顺着模板做验证:构建产物检查与新项目选型判断

6.1 构建产物里重点检查三个文件

模板搭好后,光跑通还不够,我用这三个检查项判断模板是否健康:

# 1. 检查每个 HTML 引用的 JS 是否只包含自己的 chunk grep -o 'src="[^"]*"' dist/list.html # 2. 检查公共 chunk 是否只有一个 ls dist/js/ # 3. 检查首屏体积,大于 200KB 要警惕 du -sh dist/js/*

正常的多页面产物应该是:每个页面 HTML 引用了 chunk-vendors + chunk-common + 自己的页面 chunk,公共 chunk 只有一个,每个页面 chunk 体积相对可控。我正在接手的某项目模板就是通过第 2 条查到有五个重复的公共 chunk,排查出 splitChunks 配置失效。这套检查能省很多线上排错时间。

6.2 要不要从 Vue 2 模板升级到 Vue 3 多页

每次新建项目,团队都会纠结这个问题。如果维护的是 Vue 2 存量系统,组件库依赖 Vue 2 生态,升级意味着重写大量业务代码,此时这套多页面模板依然能稳定服务。如果是全新项目,建议直接考虑 Vue 3 + Vite 多页配置,原理是相通的:多入口、共享依赖抽离、每个页面独立 HTML。从我自己的操作习惯看,模板的构建配置部分不变,变的是入口文件的写法和构建工具的速度。每接到 SEO 或独立部署需求,我都会先翻出这套模板改目录结构直接落地,省掉重复写 webpack 入口的功夫,把精力留给页面本身。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进