經過對單頁應用配置的了解,相信大家應該對如何構建一個 Vue 單頁應用項目已經有所收獲和體會,在大部分實際場景中,我們都可以構建單頁應用來進行項目的開發和迭代,然而對于項目復雜度過高或者頁面模塊之間差異化較大的項目,我們可以選擇構建多頁應用來實現。那么什么是多頁應用,如何構建一個多頁應用便是本文所要闡述的內容。
## 概念
> 首先我們可以把多頁應用理解為由多個單頁構成的應用,而何謂多個單頁呢?其實你可以把一個單頁看成是一個 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 依賴信息文件
```
根據上方目錄結構我們可以看出其實 pages 下的一個目錄就是一個單頁包含的功能,這里我們包含了 3 個目錄就構成了多頁應用。
除了目錄結構的不同外,其實區別單頁應用,多頁應用在很多配置上都需要進行修改,比如單入口變為多入口、單模板變為多模板等,那么下面我們就來了解一下多頁應用的具體實現。
## 多入口
在單頁應用中,我們的入口文件只有一個,CLI 默認配置的是 main.js,但是到了多頁應用,我們的入口文件便包含了 page1.js、page2.js、index.js等,數量取決于 pages 文件夾下目錄的個數,這時候為了項目的可拓展性,我們需要自動計算入口文件的數量并解析路徑配置到 webpack 中的 entry 屬性上,如:
```
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](https://github.com/isaacs/node-glob) 這一第三方模塊讀取所有 pages 文件夾下的入口文件,其需要進行安裝:`yarn add glob --dev`
讀取并存儲完畢后,我們得到了一個入口文件的對象集合,這個對象我們便可以將其設置到 webpack 的 entry 屬性上,這里我們需要修改 vue.config.js 的配置來間接修改 webpack 的值:
```
/* vue.config.js */
const utils = require('./build/utils')
module.exports = {
...
configureWebpack: config => {
config.entry = utils.getEntries()
},
...
}
```
這樣我們多入口的設置便完成了,當然這并不是 CLI 所希望的操作,后面我們會進行改進。
## 多模板
相對于多入口來說,多模板的配置也是大同小異,這里所說的模板便是每個 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, // 是否注入資源
})
]
...
}
```
以上是單模板的配置,那么如果是多模板只要繼續往 plugins 數組中添加 HtmlWebpackPlugin 即可,但是為了和多入口一樣能夠靈活的獲取 pages 目錄下所有模板文件并進行配置,我們可以在 utils.js 中添加多模板的讀取解析方法:
```
/* utils.js */
// 多頁面輸出配置
// 與上面的多頁面入口配置相同,讀取 page 文件夾下的對應的 html 后綴文件,然后放入數組中
exports.htmlPlugin = configs => {
let entryHtml = glob.sync(PAGE_PATH + '/*/*.html')
let arr = []
entryHtml.forEach(filePath => {
let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
let conf = {
template: filePath, // 模板路徑
filename: filename + '.html', // 生成 html 的文件名
chunks: ['manifest', 'vendor', filename],
inject: true,
}
// 如果有自定義配置可以進行 merge
if (configs) {
conf = merge(conf, configs)
}
// 針對生產環境配置
if (process.env.NODE_ENV === 'production') {
conf = merge(conf, {
minify: {
removeComments: true, // 刪除 html 中的注釋代碼
collapseWhitespace: true, // 刪除 html 中的空白符
// removeAttributeQuotes: true // 刪除 html 元素中屬性的引號
},
chunksSortMode: 'manual' // 按 manual 的順序引入
})
}
arr.push(new HtmlWebpackPlugin(conf))
})
return arr
}
```

以上我們仍然是使用 glob 讀取所有模板文件,然后將其遍歷并設置每個模板的 config,同時針對一些自定義配置和生產環境的配置進行了 merge 處理,其中自定義配置的功能我會在下節進行介紹,這里介紹一下生產環境下 `minify` 配置的作用:**將 html-minifier 的選項作為對象來縮小輸出**。
[html-minifier](https://github.com/kangax/html-minifier) 是一款用于縮小 html 文件大小的工具,其有很多配置項功能,包括上述所列舉的常用的刪除注釋、空白、引號等。
當我們編寫完了多模板的方法后,我們同樣可以在 vue.config.js 中進行配置,與多入口不同的是我們在 configureWebpack 中不能直接替換 plugins 的值,因為它還包含了其他插件,這時候大家還記得第 3 節中講到的使用 return 返回一個對象來進行 merge 操作嗎?
```
/* vue.config.js */
const utils = require('./build/utils')
module.exports = {
...
configureWebpack: config => {
config.entry = utils.getEntries() // 直接覆蓋 entry 配置
// 使用 return 一個對象會通過 webpack-merge 進行合并,plugins 不會置空
return {
plugins: [...utils.htmlPlugin()]
}
},
...
}
```
如此我們多頁應用的多入口和多模板的配置就完成了,這時候我們運行命令 `yarn build` 后你會發現 dist 目錄下生成了 3 個 html 文件,分別是 index.html、page1.html 和 page2.html。
## 使用 pages 配置
其實,在 vue.config.js 中,我們還有一個配置沒有使用,便是 pages。pages 對象允許我們為應用配置多個入口及模板,這就為我們的多頁應用提供了開放的配置入口。官方示例代碼如下:
```
/* vue.config.js */
module.exports = {
pages: {
index: {
// page 的入口
entry: 'src/index/main.js',
// 模板來源
template: 'public/index.html',
// 在 dist/index.html 的輸出
filename: 'index.html',
// 當使用 title 選項時,
// template 中的 title 標簽需要是 <title><%= htmlWebpackPlugin.options.title %></title>
title: 'Index Page',
// 在這個頁面中包含的塊,默認情況下會包含
// 提取出來的通用 chunk 和 vendor chunk。
chunks: ['chunk-vendors', 'chunk-common', 'index']
},
// 當使用只有入口的字符串格式時,
// 模板會被推導為 `public/subpage.html`
// 并且如果找不到的話,就回退到 `public/index.html`。
// 輸出文件名會被推導為 `subpage.html`。
subpage: 'src/subpage/main.js'
}
}
```
我們不難發現,pages 對象中的 key 就是入口的別名,而其 value 對象其實是入口 entry 和模板屬性的合并,這樣我們上述介紹的獲取多入口和多模板的方法就可以合并成一個函數來進行多頁的處理,合并后的 setPages 方法如下:
```
// pages 多入口配置
exports.setPages = configs => {
let entryFiles = glob.sync(PAGE_PATH + '/*/*.js')
let map = {}
entryFiles.forEach(filePath => {
let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
let tmp = filePath.substring(0, filePath.lastIndexOf('\/'))
let conf = {
// page 的入口
entry: filePath,
// 模板來源
template: tmp + '.html',
// 在 dist/index.html 的輸出
filename: filename + '.html',
// 頁面模板需要加對應的js腳本,如果不加這行則每個頁面都會引入所有的js腳本
chunks: ['manifest', 'vendor', filename],
inject: true,
};
if (configs) {
conf = merge(conf, configs)
}
if (process.env.NODE_ENV === 'production') {
conf = merge(conf, {
minify: {
removeComments: true, // 刪除 html 中的注釋代碼
collapseWhitespace: true, // 刪除 html 中的空白符
// removeAttributeQuotes: true // 刪除 html 元素中屬性的引號
},
chunksSortMode: 'manual'// 按 manual 的順序引入
})
}
map[filename] = conf
})
return map
}
```
上述代碼我們 return 出的 map 對象就是 pages 所需要的配置項結構,我們只需在 vue.config.js 中引用即可:
```
/* vue.config.js */
const utils = require('./build/utils')
module.exports = {
...
pages: utils.setPages(),
...
}
```
這樣我們多頁應用基于 pages 配置的改進就大功告成了,當你運行打包命令來查看輸出結果的時候,你會發現和之前的方式相比并沒有什么變化,這就說明這兩種方式都適用于多頁的構建,但是這里還是推薦大家使用更便捷的 pages 配置。
## 結語
本文主要講解了多頁應用開發中多入口和多模板的實現方式,通過針對 webpack 配置的修改我們基本了解了多頁模式與單頁模式的差異性,下篇文章我們將以本文內容為基礎進一步完善我們的多頁應用配置,使其能夠正常適應實際的開發與生產。
本案例代碼地址:[multi-page-project](https://github.com/luozhihao/vue-project-code/tree/master/multi-page-project)
## 思考 & 作業
* 多頁應用相比單頁應用有哪些優點和缺點?
* `chunksSortMode` 除了文中介紹的 `manual` 手動排序外,還有哪些排序方式?
* glob 中 `*` 和 `**` 的區別是什么?
- 開篇:Vue CLI 3 項目構建基礎
- 構建基礎篇 1:你需要了解的包管理工具與配置項
- 構建基礎篇 2:webpack 在 CLI 3 中的應用
- 構建基礎篇 3:env 文件與環境設置
- 構建實戰篇 1:單頁應用的基本配置
- 構建實戰篇 2:使用 pages 構建多頁應用
- 構建實戰篇 3:多頁路由與模板解析
- 構建實戰篇 4:項目整合與優化
- 開發指南篇 1:從編碼技巧與規范開始
- 開發指南篇 2:學會編寫可復用性模塊
- 開發指南篇 3:合理劃分容器組件與展示組件
- 開發指南篇 4:數據驅動與拼圖游戲
- 開發指南篇 5:Vue API 盲點解析
- 開發拓展篇 1:擴充你的開發工具
- 開發拓展篇 2:將 UI 界面交給第三方庫
- 開發拓展篇 3:嘗試使用外部數據
- 總結篇:寫在最后