接到一个需求:同一个仓库里要打出三个互不相干的 html,活动页、后台页、落地页各一个,共用一套组件和工具函数,但各自有独立的路由和状态。用三个仓库分开做,公共代码得抄三份;硬塞进一个单页,路由会长成一团乱麻。这就是多页应用要解决的场景。这篇从最土的手写 entry 开始,一步步做到用 glob 自动扫描目录生成配置,最后收敛到 Vue CLI 3 官方的 pages 选项上,新增一个页面只要建个文件夹,构建配置一个字都不用改。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 多页应用的目录该怎么切,每个单页里放什么
- 用
glob自动扫描pages目录生成 webpack 的entry,避免手写入口 html-webpack-plugin的多模板配置,以及minify、chunks、inject各自管什么configureWebpack里为什么entry能直接覆盖而plugins不能- 把多入口和多模板合并成一个
setPages方法,改用 CLI 的pages选项 - 这套 webpack 时代的写法,在 Vite 里对应什么
# 一、多页应用长什么样
首先我们可以把多页应用理解为由多个单页构成的应用。而何谓多个单页呢?其实你可以把一个单页看成是一个 html 文件,那么多个单页便是多个 html 文件,多页应用便是由多个 html 组成的应用。
既然多页应用拥有多个 html,那么同样其应该拥有多个独立的入口文件、组件、路由、vuex 等。说简单一点,多页应用的每个单页都可以拥有单页应用 src 目录下的文件及功能。我们来看一下一个基础多页应用的目录结构:
├── node_modules # 项目依赖包目录
├── build # 项目 webpack 功能目录
├── config # 项目配置项文件夹
├── src # 前端资源目录
│ ├── images # 图片目录
│ ├── components # 公共组件目录
│ ├── pages # 页面目录
│ │ ├── page1 # page1 目录
│ │ │ ├── components # page1 组件目录
│ │ │ ├── router # page1 路由目录
│ │ │ ├── views # page1 页面目录
│ │ │ ├── page1.html # page1 html 模板
│ │ │ ├── page1.vue # page1 vue 配置文件
│ │ │ └── page1.js # page1 入口文件
│ │ ├── page2 # page2 目录
│ │ └── index # index 目录
│ ├── common # 公共方法目录
│ └── store # 状态管理 store 目录
├── .gitignore # git 忽略文件
├── .env # 全局环境配置文件
├── .env.dev # 开发环境配置文件
├── .postcssrc.js # postcss 配置文件
├── babel.config.js # babel 配置文件
├── package.json # 包管理文件
├── vue.config.js # CLI 配置文件
└── yarn.lock # yarn 依赖信息文件
这个结构里有两条边界值得先说清楚。
src/pages/xxx/ 里面的东西是这个单页私有的,别的单页看不见也不该看见。src/components、src/common、src/store 这三个是跨单页共享的,谁都能引。分层一乱,多页的意义就没了,几个页面互相 import 内部组件,最后还是一坨。
另一条边界是运行时。三个 html 是三个独立的浏览器上下文,页面之间的跳转必须走 location.href,vue-router 的 push 只在单页内部有效,跨单页会静默失败。至于单页内部本身的路由、Vuex、接口层怎么组织,跟单页应用是一模一样的,我在 Vue单页应用的基本配置 里写了完整一套,这篇不重复,专门聊构建层。
# 二、多入口,让 entry 自己长出来
在单页应用中,我们的入口文件只有一个,CLI 默认配置的是 main.js。但是到了多页应用,我们的入口文件便包含了 page1.js、page2.js、index.js 等,数量取决于 pages 文件夹下目录的个数。最直白的写法是手写死:
module.exports = {
...
entry: {
page1: '/xxx/pages/page1/page1.js',
page2: '/xxx/pages/page2/page2.js',
index: '/xxx/pages/index/index.js',
},
...
}
能跑,但每加一个页面就要回来改一次配置,还得记得改对路径。这类事情交给代码做比交给人做靠谱。
那么我们如何读取并解析这样的路径呢,这里就需要使用工具和函数来解决了。我们可以在根目录新建 build 文件夹存放 utils.js 这样共用的 webpack 功能性文件,并加入多入口读取解析方法:
/* utils.js */
const path = require('path');
// glob 是 webpack 安装时依赖的一个第三方模块,该模块允许你使用 * 等符号,
// 例如 lib/*.js 就是获取 lib 文件夹下的所有 js 后缀名的文件
const glob = require('glob');
// 取得相应的页面路径,因为之前的配置,所以是 src 文件夹下的 pages 文件夹
const PAGE_PATH = path.resolve(__dirname, '../src/pages');
/*
* 多入口配置
* 通过 glob 模块读取 pages 文件夹下的所有对应文件夹下的 js * 后缀文件,如果该文件存在
* 那么就作为入口处理
*/
exports.getEntries = () => {
let entryFiles = glob.sync(PAGE_PATH + '/*/*.js') // 同步读取所有入口文件
let map = {}
// 遍历所有入口文件
entryFiles.forEach(filePath => {
// 获取文件名
let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
// 以键值对的形式存储
map[filename] = filePath
})
return map
}
这段代码的核心就一句 glob.sync(PAGE_PATH + '/*/*.js')。两层 * 分别对应「任意一个单页目录」和「该目录下任意一个 js 文件」,所以它只会扫到 pages/page1/page1.js 这一层,不会递归进 pages/page1/views/ 里去。这个通配符的精度是有意为之的,写成 **/*.js 就会把所有业务代码都当成入口。
这里有个约定必须遵守:入口文件名要和它所在的文件夹同名。因为 map 的 key 取的是文件名,后面模板、chunk 名全靠这个 key 串起来。你在 page1/ 目录下建一个 utils.js,它会被当成一个新入口打出来。这个坑我第一次配的时候踩过,产物里莫名多了个 html,排查了好一会儿才想明白是 glob 多扫了一个文件。
filePath.lastIndexOf('\/') 这个写法看着奇怪,'\/' 在 JS 字符串里就是 '/',反斜杠是多余的转义,不影响结果。真要写得干净点可以用 path.basename(filePath, '.js') 一步到位。
读取并存储完毕后,我们得到了一个入口文件的对象集合,这个对象我们便可以将其设置到 webpack 的 entry 属性上。这里我们需要修改 vue.config.js 的配置来间接修改 webpack 的值:
/* vue.config.js */
const utils = require('./build/utils')
module.exports = {
...
configureWebpack: config => {
config.entry = utils.getEntries()
},
...
}
这样我们多入口的设置便完成了。当然这并不是 CLI 所希望的操作,后面我们会进行改进。
# 三、多模板,一个页面配一个 html
相对于多入口来说,多模板的配置也是大同小异。这里所说的模板便是每个 page 下的 html 模板文件,而模板文件的作用主要用于 webpack 中 html-webpack-plugin 插件的配置,其会根据模板文件生成一个编译后的 html 文件并自动加入携带 hash 的脚本和样式。基本配置如下:
/* webpack 配置文件 */
const HtmlWebpackPlugin = require('html-webpack-plugin') // 安装并引用插件
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
title: 'My Page', // 生成 html 中的 title
filename: 'demo.html', // 生成 html 的文件名
template: 'xxx/xxx/demo.html', // 模板路径
chunks: ['manifest', 'vendor', 'demo'], // 所要包含的模块
inject: true, // 是否注入资源
})
]
...
}
chunks 这个字段是多页配置里最容易出事的地方。它决定了这个 html 里会插入哪些 script 标签。不写的话,webpack 会把所有 chunk 都塞进每一个页面,三个页面各自加载三份业务代码,多页就白做了。
那为什么每个页面还要带上 manifest 和 vendor 呢?因为这两个是所有页面共用的运行时和第三方依赖,缺了页面直接报错。这里有个版本差异要提醒:manifest 和 vendor 是 webpack 3 时代 Vue CLI 2 模板的默认命名,Vue CLI 3 换成了 chunk-vendors 和 chunk-common。照抄这份配置之前,先去 dist 目录看一眼自己项目实际打出来的 chunk 叫什么名字,对不上的话页面会加载不到公共代码。
以上是单模板的配置,那么如果是多模板只要继续往 plugins 数组中添加 HtmlWebpackPlugin 即可。但是为了和多入口一样能够灵活地获取 pages 目录下所有模板文件并进行配置,我们可以在 utils.js 中添加多模板的读取解析方法: