简介Vue 2.0多页面应用模板面向需要同时维护多个独立页面的前端开发场景旨在解决传统单页应用在官网、专题页等多入口项目中重复搭建构建流程、难以统一工程化配置的问题。资源包共32个文件压缩后仅31KB包含15个JavaScript脚本、6个Vue页面/组件以及2个HTML入口并附有Webpack开发与生产构建配置、Babel转译设置、ESLint校验规则、EditorConfig和Git忽略文件等整体轻量且目录结构规范适合快速嵌入现有项目或作为新项目脚手架。已有4660人学习下载可帮助中高级前端开发者深入理解Vue多页面项目的组织方式。通过该模板读者既能掌握多页面项目的目录划分与模块组织方法也能直接复用各环境的构建配置、环境变量和代码检查规则快速启动业务项目减少重复造轮子的成本同时还能借助其清晰的构建脚本理解多页面入口与Vue实例的关联方式。1. 从十几个页面来回跳说起这个 Vue2 多页面应用模板到底是什么接手过一个后台系统页面都是相互独立的业务模块历史原因散落在一套老代码里连构建都没统一。当时第一反应是上 SPA但评估后放弃了——十几条业务线互不依赖共享代码不到两成强改成单页应用只会换来一堆跨模块耦合问题。后来选定了 Vue.js 生态里更合适的方向用一套 Vue2 多页面应用模板按页面拆入口每个页面独立编译、独立部署JavaScript 开发层面几乎没有额外学习成本组件、指令、过滤器照常写。这个模板适合两类人一类是维护传统后台系统、想把老页面渐进替换成 Vue 的开发者另一类是新项目已经明确按页面为单位交付、不需要做单页路由联动。它能解决的核心问题很简单——别再为「只是页面多」的项目背上 SPA 的复杂度。2. 多页应用与单页应用的选型边界什么项目才值得拆多入口2.1 业务独立、共享少的项目多页面是更省事的解法SPA 不是免费的。路由管理、权限状态、菜单联动、组件通信这些在单页里都是全局的页面越多全局状态越难控制。我在评估那套后台系统时列过一份共享度清单公共组件能抽出来的大概只有按钮、表格封装、弹窗这几类而且大多是纯展示逻辑业务模块之间没有任何「从 A 页面打开 B 页面还要带上一堆上下文」的需求每个页面都是独立的「打开 → 操作 → 关掉」流程。这种情况下强行上 SPA反而要把原来不需要考虑的东西都做一遍路由懒加载、keep-alive 策略、全局 store 拆模块、路由守卫与权限的联动。而 Vue2 多页面模板的做法是保留浏览器原生的页面跳转语义每个页面有自己的入口、自己的路由实例、自己的数据请求生命周期。页面之间的隔离是物理级别的JavaScript 开发时不用担心某个全局变量或者某个响应式数据把别的页面带崩。另外一个容易忽略的点是构建产物。多页面的产物是多个 HTML 分别对应的多个 JS 包这对 SEO 友好、对部署友好、对灰度发布也友好——我可以只替换其中一个页面的文件而不是整个 bundle 覆盖上去。单页应用在这三个场景下都要额外做处理。所以在选型时我一般先问三个问题页面之间是否存在必须共享的实时状态是否存在跨页面的复杂联动比如多标签页同步是否所有页面由同一个团队长期维护如果答案多数是否那多页面不是妥协而是更合理的方案。2.2 模板目录怎么摆入口、组件和公共代码的边界模板的目录结构是整个多页方案的地基拿过来改能看到一条清晰的职责线src ├── pages │ ├── dashboard │ │ ├── main.js # 页面入口创建 Vue 实例 │ │ ├── App.vue # 页面根组件 │ │ ├── router │ │ │ └── index.js # 页面内部路由 │ │ └── views │ │ ├── index.vue │ │ └── detail.vue │ ├── order │ │ ├── main.js │ │ ├── App.vue │ │ └── router │ └── user │ ├── main.js │ ├── App.vue │ └── router ├── common │ ├── request.js # 封装 axios 实例 │ ├── auth.js # 登录态检查与跳转 │ └── utils.js └── components # 跨页面复用的纯展示组件 ├── PageTable.vue └── PageDialog.vue每个页面目录自包含一套 Vue 2 的标准结构main.js 负责创建实例和挂载路由App.vue 是页面根组件router 只服务于当前页面内部。有的页面如果有复杂子页面结构列表页 → 详情页就在 router 里配置该页面的子路由如果页面简单连 router 都可以不要App.vue 里直接写模板。common 目录放的是跨页面共享的逻辑层。最典型的是 request.js我在模板里把所有页面的 axios 实例统一封装拦截器里统一处理 token 注入、错误码提示、401 跳转登录页。auth.js 则是登录态检查因为多页面下每个入口都是独立的刷新页面后 Vuex 状态丢失是必然的登录态必须靠 cookie 或 localStorage 来还原——这个坑后面会展开。components 目录只放跨页面复用的纯展示组件业务组件一律放在各自页面目录里。这条边界很重要一旦业务组件放进公共目录很容易出现页面 A 改了一个查询条件页面 B 跟着受影响的情况。2.3 从零搭建和渐进改造两种入场方式这套模板有两种典型的入场姿势。第一种是从零搭建新项目已经确定按多个独立页面交付直接用模板起步把 pages 下的示例页面改成实际业务页面即可。第二种是渐进改造我实际用得更多——老项目是 jQuery 或原生 JS 写的几十个页面不可能一晚上全重写那就一次迁移一个页面新建一个页面目录写 Vue 逻辑在构建配置里加一个入口老页面继续跑新页面逐步替代。这种渐进策略是 SPA 重构很难做到的SPA 重构通常要么一次性切换要么做微前端隔离复杂度高出不少。这里要泼一盆冷水多页面不等于没有前端路由。页面内部如果有二级导航、详情跳转Vue Router 照样用只是它只作用于当前页面内部。所以即使选了多页方案路由相关的知识一个都少学不了只是不需要处理「全局路由和全局状态」那一层。3. 基于 vue.config.js 的多页配置pages 字段与动态入口的完整写法3.1 pages 字段的五个参数entry、template、filename、title、chunksvue-cli 3/4 里多页配置统一收敛在 vue.config.js 的 pages 字段上每个页面就是一个配置对象。先看一份手写的最小配置module.exports { pages: { dashboard: { entry: src/pages/dashboard/main.js, template: public/index.html, filename: dashboard.html, title: 数据中心, chunks: [chunk-vendors, chunk-common, dashboard] }, order: { entry: src/pages/order/main.js, template: public/index.html, filename: order.html, title: 订单管理, chunks: [chunk-vendors, chunk-common, order] } } }逐个说参数。entry 是入口 JS 的路径必填template 是 HTML 模板路径模板里通过% htmlWebpackPlugin.options.title %拿到 title 值filename 是构建产出的 HTML 文件名可以直接内嵌一级目录比如admin/order.html但要注意部署层的路径规划title 会在编译时注入到模板的title标签里chunks 控制这个 HTML 会引用哪些 JS 包。chunks 是最容易理解错的一个。vue-cli 默认生成的工程里会有一个全局的公共包chunk-vendors 装 node_modules 依赖、chunk-common 装被多个页面引用的公共模块所以这里要写全三个 chunk——公共包加当前页面入口。如果只写[dashboard]构建能过但运行时浏览器直接报 Vue 未定义的白屏错误。这一点在 vue-cli 的 pages 机制里是写死的约定模板里保留这个约定以后新增页面时照着写就行。3.2 动态扫描 src/pages手写一段生成 pages 的脚本手动在 pages 里一个页面一个页面地写页面少还行到二十个以上就变成痛苦的重复劳动。模板里带了一个自动扫描脚本运行时扫描 src/pages 下的目录动态生成 pages 对象const path require(path) const fs require(fs) const pageRoot path.resolve(__dirname, src/pages) function resolvePages() { const pages {} const dirs fs.readdirSync(pageRoot) dirs.forEach(dir { const fullPath path.join(pageRoot, dir) if (!fs.statSync(fullPath).isDirectory()) return if (fs.existsSync(path.join(fullPath, main.js)) false) return pages[dir] { entry: src/pages/${dir}/main.js, template: public/index.html, filename: ${dir}.html, title: dir, chunks: [chunk-vendors, chunk-common, dir] } }) return pages } module.exports { pages: resolvePages() }逻辑说明先读取 src/pages 下所有条目过滤掉非目录再检查每个目录里是否有 main.js没有就跳过这样能容忍「目录建了但还没开始写页面」的中间状态。最后以目录名作为页面名。这个脚本有几个隐含约束要遵守页面目录名就是浏览器访问的 HTML 文件名也就是 URL 路径建议用英文小写加连字符title 直接用了目录名如果页面标题要友好可以再加一份映射表。用这个方案之后新增一个页面只需要在 src/pages 下建目录、写 main.js构建配置零改动。删页面也同理删掉目录重新构建即可。我习惯把 title 的映射也放进这个文件里const titleMap { dashboard: 数据中心, order: 订单管理 } // pages[dir].title titleMap[dir] || dir这样既不破坏自动化的便利性又保留了人工干预的入口。3.3 devServer 的多页调试代理和页面跳转开发环境直接跑npm run dev默认访问http://localhost:8080/dashboard.html就能打开对应页面。但有两个地方模板做了特殊处理。第一个是 devServer 的代理。多页面项目里前端只是壳真实数据大概率来自后端接口所有页面的请求都走同一个后端域名的场景很少常见的是按业务线分域名。可以在 vue.config.js 里配多份代理devServer: { proxy: { /api/order: { target: http://order.api.local, changeOrigin: true }, /api/user: { target: http://user.api.local, changeOrigin: true } } }示例说明以 /api/order 开头的请求转发到订单服务以 /api/user 开头的请求转发到用户服务。在实际接项目时代理规则要根据后端网关的实际路径来调整注意接口路径前缀务必和代理匹配规则一致。第二个是页面之间的跳转方式。多页面应用没有 vue-router 的跨页面导航页面间跳转我用原生window.location.href /order.html?orderIdxxx。参数传递有两个选择简单参数直接放 URL query复杂对象放进 sessionStorage目标页面在入口处读取后清理。不建议用 localStorage因为多个页面共用一个 localStorage 命名空间容易互相污染键名。提示多页面开发时每个页面的 Vuex 是不能跨页面共享的。各页面刷新后状态都会重置到初始化值这是多页架构的天然边界设计页面逻辑时要默认「刷新后需要重新拉数据」。4. 手写 webpack 多入口脱离脚手架时四个必须写对的配置4.1 什么情况下需要放下 vue.config.jsvue.config.js 的 pages 封装解决了 90% 的场景但有个边界它对 HtmlWebpackPlugin 的参数支持有限。当页面之间模板长得不一样、需要各自独立的 SEO meta 信息或者需要和存量构建链合并时我就直接改成手写 webpack 配置。这套模板里也保留了一份手写版本作为备选核心逻辑是四个配置块。手写配置不是因为脚手架不好而是脚手架在抽象层做了一些默认假设——比如所有页面共用同一个 HTML 模板。如果你遇到「每个页面要注入不同的 meta 描述」「某个页面要内联一段第三方脚本」「页面 A 用模板 A、页面 B 用模板 B」直接用 vue.config.js 的 pages 就绕了。4.2 entry 与 output多入口对象的写法和 chunk 命名规则const path require(path) const HtmlWebpackPlugin require(html-webpack-plugin) module.exports { entry: { dashboard: ./src/pages/dashboard/main.js, order: ./src/pages/order/main.js, user: ./src/pages/user/main.js }, output: { path: path.resolve(__dirname, dist), filename: js/[name].[contenthash:8].js, chunkFilename: js/[name].[contenthash:8].js, publicPath: ./ } }说明entry 对象的 key 会直接对应 output 里的 [name]这是多入口打包最核心的对应关系——dashboard这个 key 决定产出的 JS 文件名、决定 HTML 插件里 chunks 的引用名三个地方必须严格一致。chunkFilename 管的是异步加载的组件包模板里用了路由懒加载的话每个路由组件会单独拆包名字来自分配规则。publicPath 这里我用的是相对路径./这是多页面部署的一个关键区别。SPA 项目部署在域名根路径时用/没问题但多页面项目经常被部署在某个子目录下比如https://host/admin/此时绝对路径/js/dashboard.js会去根路径找文件直接 404。写成./后浏览器会基于当前 HTML 的 URL 去拼接 JS 路径。要提醒的是publicPath 用相对路径后如果页面本身还有二级路由比如 dashboard 页面内部有/list、/detail这时的相对路径会基于路由路径计算可能出现资源路径错乱。实际项目里我的做法是页面内部统一用 hash 路由规避这个问题页面入口路径保持一级。4.3 HtmlWebpackPlugin 多实例chunks 决定页面引哪些 JS多页面脚手架的自动 HTML 注入在这里失效必须手动为每个页面实例化 HTML 插件plugins: [ new HtmlWebpackPlugin({ template: ./public/dashboard.html, filename: dashboard.html, chunks: [dashboard] }), new HtmlWebpackPlugin({ template: ./public/order.html, filename: order.html, chunks: [order] }) ]这里最常踩的坑是HtmlWebpackPlugin 的 chunks 不写默认会把所有入口 chunk 都注入到每一个 HTML 文件。也就是说页面 A 会引入页面 B 的 JS、页面 C 的 JS这不仅体积浪费还可能因为两个页面入口都修改了全局变量而产生运行时冲突。所以每个插件的 chunks 数组必须显式声明。注意我这里的 chunks 写的是[dashboard]而 vue.config.js 方案里必须写[chunk-vendors, chunk-common, dashboard]。原因在于脚手架方案里公共依赖被独立抽包手写配置里如果不做公共依赖抽离业务代码和 Vue 源码是打在一起的页面的 chunk 本身就带全了依赖。一旦你做了第 4.4 节的 splitChunks 抽离这里的 chunks 就要补上公共包名至少要包含 vendor 相关的 chunk否则同样会在浏览器端报「Vue is not defined」。4.4 splitChunks把公共依赖收进 cacheGroups手写配置不配 splitChunks每个页面的包都会完整地包含一份 Vue、Vue Router、axios三个页面就是三份缓存完全失效。模板里的抽离方案放在 optimization 里optimization: { splitChunks: { cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, name: chunk-vendors, chunks: all, priority: 10 }, common: { name: chunk-common, chunks: all, minChunks: 2, priority: 5 } } } }参数说明vendors 分组把所有来自 node_modules 的代码收敛到一个叫 chunk-vendors 的包里priority 10 保证它在分组竞争时优先接管common 分组把「被至少 2 个页面引用的本地模块」抽成 chunk-commonminChunks: 2 表示引用数达到 2 才抽离。优先级数值越大越先匹配vendors 比 common 高这样公共业务代码不会混进 vendor 包。这两层的顺序决定了页面 HTML 里的脚本引用顺序也决定了浏览器缓存策略。vendor 包的内容相对稳定业务代码频繁改动时vendor 的 contenthash 不会跟着变。我这里 name 用的是固定名称而不是 webpack 默认的自动命名是为了让 HTML 插件的 chunks 引用可以明确定位。固定名称的代价是新增一个大依赖进 node_modules 时这个包本身会变大但多页面场景下所有页面共享同一个 vendor 的缓存收益远大于这点损失。5. 多页面模板的避坑记录四个实测过的常见问题5.1 部署到子目录后白屏publicPath 加没加 ./ 是两种命运现象本地 devServer 一切正常构建产物上传到服务器某个子目录后打开页面白屏控制台一片 404JS、CSS 全部加载失败。原因构建配置里 publicPath 是/。产物里的资源引用是/js/dashboard.abc123.js浏览器会从域名根路径去找资源但文件实际在/admin/js/下自然 404。SPA 项目在根路径部署时这个问题不明显多页面项目经常被挂到各种子路径下问题就暴露了。解决把 publicPath 改成./重新构建。构建产物里的资源引用就变成相对路径浏览器会根据当前访问的 HTML 地址自动拼出正确路径。如果项目需要同时支撑多个部署位置可以用环境变量区分测试环境用./正式环境如果部署在根路径也可以用/。我一般统一用./省心但要注意第 4.2 节提到的页面内部路由层级问题。5.2 devServer 下直接访问页面 URL 404historyApiFallback 的默认行为现象跑开发服务器后打开http://localhost:8080/dashboard.html直接 404但访问首页根地址能正常打开。原因devServer 默认不开启 historyApiFallback也就是说 devServer 在处理 URL 时不回退到 HTML 文件而多页面项目里每个页面对应的 HTML 是编译期动态生成的devServer 启动时如果没有这个文件记录直接访问具体页面 URL 就会 404。解决vue.config.js 里显式开启并配置回退规则devServer: { historyApiFallback: { rewrites: [ { from: /^\/$/, to: /dashboard.html }, { from: /^\/dashboard/, to: /dashboard.html }, { from: /^\/order/, to: /order.html } ] } }配置说明from 是匹配的 URL 路径模式to 是对应回退到的 HTML 文件。实际项目里规则要与 pages 配置一一对应新增页面时这里可能也要加一行所以最好的做法是用第 3.2 节的目录名列表动态生成 rewrites。另外一个更省事的规避方式页面内部路由用 hash 模式跳转 URL 形如http://localhost:8080/dashboard.html#/list不受 historyApiFallback 影响。5.3 页面 A 的 HTML 里混进页面 B 的脚本chunks 配漏了现象打开页面 A功能正常但看 Network 面板发现加载了页面 B 的 JS有时候页面 B 的入口代码在 A 里执行触发意料之外的事件监听或样式污染。原因手写 webpack 配置下HtmlWebpackPlugin 的 chunks 没写插件默认注入所有 chunk。也可能是 chunks 里写的是all或者漏掉了排除逻辑。vue.config.js 方案下如果 pages 配置里手动设置了 chunks 数组但漏了某个页面同样会出现引用不完整或引用过多的情况。解决手写配置里每个页面的 HtmlWebpackPlugin 实例明确写 chunks且只写当前页面需要的 chunk。检查习惯是构建完成后打开 dist 里的 HTML 看 script 标签逐个核对引用的 JS 属于哪个入口。我每次新增页面后构建完都会顺手看一遍产物这是成本最低的验证方式。5.4 每次改业务代码 vendor 的 hash 也变公共依赖抽离失效现象业务页面只改了一个按钮文案构建完发现 chunk-vendors.xxx.js 的文件名 hash 也变了说明缓存策略名存实亡浏览器被迫全部重新下载。原因splitChunks 配置里 name 用了自动命名或者 test 匹配规则写得过宽导致部分业务代码被收进了 vendor 包。更常见的是 minChunks 数值配低了某个只在两个页面用到的模块被抽进 common但这两个页面的改动又会引发这个公共包 hash 变化。解决直接把 vendor 的 name 固定为 chunk-vendors。固定名字的包在内容没变时 hash 保持稳定。另一个检查点是 test 正则必须只匹配 node_modules 路径不要写成test: /[\\/]node_modules[\\/]/以外的宽松规则。minChunks 配 2 是平衡点低于 2 抽离收益低高于 2 公共利用率下降。真正需要验证的是「改动一个页面后vendor 的 hash 保持不变」这个验证写进每次发版前的检查清单。6. 多页应用的按需构建与产物自查最后两公里6.1 只构建指定页面用环境变量过滤入口页面多到一定程度每次 devServer 启动都要编译所有页面CPU 风扇直接起飞。模板里带了一个过滤入口的脚本基于环境变量 PAGES 控制// vue.config.js const pages resolvePages() if (process.env.PAGES) { const onlyPages process.env.PAGES.split(,) Object.keys(pages).forEach(key { if (!onlyPages.includes(key)) { delete pages[key] } }) } module.exports { pages }用法说明命令行里用 cross-env 传递变量只编译 dashboard 和 order 两个页面执行cross-env PAGESdashboard,order npm run dev就行不传 PAGES 时走全量构建。这样本地开发时通常只编译自己在改的页面构建速度的改善是数量级的。注意批量场景下所属业务线的环境变量要以.env文件按环境维护不要在命令行硬编码。6.2 产物自查清单发版前十分钟检查项标准出错信号资源路径HTML 里的 JS/CSS 以./开头子目录部署后白屏页面标题每个 HTML 的 title 不一样所有页面共用默认标题脚本引用页面只引自己的 JS 和公共 chunk引了别的页面 chunk公共包 hash业务改动后 vendor hash 不变vendor hash 跟着变这套清单是我每次构建完固定走一遍的流程。第一次看资源路径第二次看 title第三次抽查两个页面的 script 标签第四次确认 vendor hash。多页面的坑大多集中在这四个点提前看一眼比上线后看错误日志划算得多。按需构建这个习惯是我翻了两次车之后才养成的第一次是全量编译导致本地调试慢到怀疑电脑性能第二次是忘记配环境变量把过滤逻辑带到了正式构建导致线上只发布了两个页面。从那以后我每次做多页面前都强制走一遍「确认入口清单 → 确认部署路径 → 按需构建 → 产物自查」的流程效率高了不少希望帮到你。本文还有配套的精品资源点击获取