上篇文章中我們成功打包并輸出了多頁文件,而構建一個多頁應用能夠讓我們進一步了解項目配置的可拓展性,可以對學習 Vue 和 webpack 起到強化訓練的效果,本文將在此基礎上主要針對多頁路由及模板的配置進行系列的介紹。
## 路由配置
### 1\. 跳轉
在配置路由前,首先我們要明確一點就是,多頁應用中的每個單頁都是相互隔離的,即如果你想從 page1 下的路由跳到 page2 下的路由,你無法使用 vue-router 中的方法進行跳轉,需要使用原生方法:`location.href` 或 `location.replace`。
此外為了能夠清晰的分辨路由屬于哪個單頁,我們應該給每個單頁路由添加前綴,比如:
* index 單頁:/vue/
* page1 單頁:/vue/page1/
* page2 單頁:/vue/page2/
其中 /vue/ 為項目的二級目錄,其后的目錄代表路由屬于哪個單頁。因此我們每個單頁的路由配置可以像這樣:
```
/* page1 單頁路由配置 */
import Vue from 'vue'
import Router from 'vue-router'
// 首頁
const Home = (resolve => {
require.ensure(['../views/home.vue'], () => {
resolve(require('../views/home.vue'))
})
})
Vue.use(Router)
let base = `${process.env.BASE_URL}` + 'page1'; // 添加單頁前綴
export default new Router({
mode: 'history',
base: base,
routes: [
{
path: '/',
name: 'home',
component: Home
},
]
})
```
我們通過設置路由的 base 值來為每個單頁添加路由前綴,如果是 index 單頁我們無需拼接路由前綴,直接跳轉至二級目錄即可。
那么在單頁間跳轉的地方,我們可以這樣寫:
```
<template>
<div id="app">
<div id="nav">
<a @click="goFn('')">Index</a> |
<a @click="goFn('page1')">Page1</a> |
<a @click="goFn('page2')">Page2</a> |
</div>
<router-view/>
</div>
</template>
<script>
export default {
methods: {
goFn(name) {
location.href = `${process.env.BASE_URL}` + name
}
}
}
</script>
```
但是為了保持和 Vue 路由跳轉同樣的風格,我可以對單頁之間的跳轉做一下封裝,實現一個 `Navigator` 類,類的代碼可以查看本文最后的示例,封裝完成后我們可以將跳轉方法修改為:
```
this.$openRouter({
name: name, // 跳轉地址
query: {
text: 'hello' // 可以進行參數傳遞
},
})
```
使用上述 `$openRouter` 方法我們還需要一個前提條件,便是將其綁定到 Vue 的原型鏈上,我們在所有單頁的入口文件中添加:
```
import { Navigator } from '../../common' // 引入 Navigator
Vue.prototype.$openRouter = Navigator.openRouter; // 添加至 Vue 原型鏈
```
至此我們已經能夠成功模仿 vue-router 進行單頁間的跳轉,但是需要注意的是因為其本質使用的是 location 跳轉,所以必然會產生瀏覽器的刷新與重載。
### 2\. 重定向
當我們完成上述路由跳轉的功能后,可以在本地服務器上來進行一下測試,你會發現 Index 首頁可以正常打開,但是跳轉 Page1、Page2 卻仍然處于 Index 父組件下,這是因為瀏覽器認為你所要跳轉的頁面還是在 Index 根路由下,同時又沒有匹配到 Index 單頁中對應的路由。這時候我們服務器需要做一次重定向,將下方路由指向對應的 html 文件即可:
```
/vue/page1 -> /vue/page1.html
/vue/page2 -> /vue/page2.html
```
在 vue.config.js 中,我們需要對 devServer 進行配置,添加 `historyApiFallback` 配置項,該配置項主要用于解決 HTML5 History API 產生的問題,比如其 rewrites 選項用于重寫路由:
```
/* vue.config.js */
let baseUrl = '/vue/';
module.exports = {
...
devServer: {
historyApiFallback: {
rewrites: [
{ from: new RegExp(baseUrl + 'page1'), to: baseUrl + 'page1.html' },
{ from: new RegExp(baseUrl + 'page2'), to: baseUrl + 'page2.html' },
]
}
}
...
}
```
上方我們通過 rewrites 匹配正則表達式的方式將 `/vue/page1` 這樣的路由替換為訪問服務器下正確 html 文件的形式,如此不同單頁間便可以進行正確跳轉和訪問了。最后需要注意的是如果你的應用發布到正式服務器上,你同樣需要讓服務器或者中間層作出合理解析,參考:[HTML5 History 模式 # 后端配置例子](https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90)
而更多關于 historyApiFallback 的信息可以訪問:[connect-history-api-fallback](https://github.com/bripkens/connect-history-api-fallback)
## 模板配置
上篇文章我們已經介紹了關于多模板的讀取和配置,在配置 html-webpack-plugin 的時候我們提到了自定義配置,這里我將結合模板渲染的功能來進行統一介紹。
### 1\. 模板渲染
這里所說的模板渲染是在我們的 html 模板文件中使用 html-webpack-plugin 提供的 [default template](https://github.com/jaketrent/html-webpack-template/blob/86f285d5c790a6c15263f5cc50fd666d51f974fd/index.html) 語法進行模板編寫,比如:
```
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>模板</title>
<% for (var chunk in htmlWebpackPlugin.files.css) { %>
<% if(htmlWebpackPlugin.files.css[chunk]) {%>
<link href="<%= htmlWebpackPlugin.files.css[chunk] %>" rel="stylesheet" />
<%}%>
<% } %>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<% for (var chunk in htmlWebpackPlugin.files.js) { %>
<% if(htmlWebpackPlugin.files.js[chunk]) {%>
<script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[chunk] %>"></script>
<%}%>
<% } %>
</body>
</html>
```
以上我們使用模板語法手動獲取并遍歷 htmlWebpackPlugin 打包后的文件并生成到模板中,其中的 `htmlWebpackPlugin` 變量是模板提供的可訪問變量,其有以下特定數據:
```
"htmlWebpackPlugin": {
"files": {
"css": [ "main.css" ],
"js": [ "assets/head_bundle.js", "assets/main_bundle.js"],
"chunks": {
"head": {
"entry": "assets/head_bundle.js",
"css": [ "main.css" ]
},
"main": {
"entry": "assets/main_bundle.js",
"css": []
},
}
}
}
```
我們通過 `htmlWebpackPlugin.files` 可以獲取打包輸出的 js 及 css 文件路徑,包括入口文件路徑等。
需要注意的是如果你在模板中編寫了插入對應 js 及 css 的語法,你需要設置 `inject` 的值為 false 來關閉資源的自動注入:
```
/* utils.js */
...
let conf = {
entry: filePath, // page 的入口
template: filePath, // 模板路徑
filename: filename + '.html', // 生成 html 的文件名
chunks: ['manifest', 'vendor', filename],
inject: false, // 關閉資源自動注入
}
...
```
否則在頁面會引入兩次資源,如下圖所示:

### 2\. 自定義配置
在模板渲染中,我們只能夠使用 htmlWebpackPlugin 內部的一些屬性和方法來進行模板的定制化開發,那么如果遇到需要根據不同環境來引入不同資源,同時不同模板間的配置還可能不一樣的需求情況的話,我們使用自定義配置會比較方便。比如我們需要在生產環境模板中引入第三方統計腳本:
```
/* vue.config.js */
module.exports = {
...
pages: utils.setPages({
addScript() {
if (process.env.NODE_ENV === 'production') {
return `
<script src="https://s95.#/z_stat.php?id=xxx&web_id=xxx" language="JavaScript"></script>
`
}
return ''
}
}),
...
}
```
然后在頁面模板中通過 `htmlWebpackPlugin.options` 獲取自定義配置對象并進行輸出:
```
<% if(htmlWebpackPlugin.options.addScript){ %>
<%= htmlWebpackPlugin.options.addScript() %>
<%}%>
```
同時你也可以針對個別模板進行配置,比如我想只在 Index 單頁中添加統計腳本,在 Page1 單頁中添加其他腳本,那么你可以給 addScript 傳入標識符來進行判斷輸出,比如:
```
<% if(htmlWebpackPlugin.options.addScript){ %>
<%= htmlWebpackPlugin.options.addScript('index') %>
<%}%>
```
同時為 addScript 方法添加參數 from:
```
addScript(from) {
if (process.env.NODE_ENV === 'production') {
let url = "https://xxx";
if (from === 'index') {
url = "https://s95.#/z_stat.php?id=xxx&web_id=xxx";
}
return `
<script src=${url} language="JavaScript"></script>
`
}
return ''
}
```
這樣我們就完成了自定義配置中的模板渲染功能。當然根據實際項目需求你的自定義配置項可能會更加復雜和靈活。
## 結語
通過 2 小節的學習,相信大家對 Vue 多頁應用的構建已經有所了解。本文在第 1 節的基礎上重點介紹了多頁路由及模板的配置,闡述了其與單頁應用的不同之處,同時針對模板自定義配置的使用場景給出了簡單的實例,希望大家在了解的基礎上將下方的實例代碼作為參考,進行相應的實戰。
本案例代碼地址:[multi-page-project](https://github.com/luozhihao/vue-project-code/tree/master/multi-page-project)
## 思考 & 作業
* 多頁應用中各自的 `Vuex Store` 信息能實現共享嗎?
* html-webpack-plugin 如何解析非 .html 的模板,比如 .hbs,應該如何配置?
- 開篇: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:嘗試使用外部數據
- 總結篇:寫在最后