前幾篇文章我們介紹了 Vue 項目構建及運行的前期工作,包括 webpack 的配置、環境變量的使用等,在了解并掌握了這些前期準備工作后,那么接下來我們可以走進 Vue 項目的內部,一探其內部配置的基本構成。

## 配置
### 1\. 路由配置
由于 Vue 這類型的框架都是以一個或多個單頁構成,在單頁內部跳轉并不會重新渲染 HTML 文件,其路由可以由前端進行控制,因此我們需要在項目內部編寫相應的路由文件,Vue 會解析這些文件中的配置并進行對應的跳轉渲染。
我們來看一下 CLI 給我們生成的 router.js 文件的配置:
```
/* router.js */
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue' // 引入 Home 組件
import About from './views/About.vue' // 引入 About 組件
Vue.use(Router) // 注冊路由
export default new Router({
routes: [{
path: '/',
name: 'home',
component: Home
}, {
path: '/about',
name: 'about',
component: About
}]
})
```
這份配置可以算是最基礎的路由配置,有以下幾點需要進行優化:
* 如果路由存在二級目錄,需要添加 base 屬性,否則默認為 "/"
* 默認路由模式是 hash 模式,會攜帶 # 標記,與真實 url 不符,可以改為 history 模式
* 頁面組件沒有進行按需加載,可以使用 `require.ensure()` 來進行優化
下面是我們優化結束的代碼:
```
/* router.js */
import Vue from 'vue'
import Router from 'vue-router'
// 引入 Home 組件
const Home = resolve => {
require.ensure(['./views/Home.vue'], () => {
resolve(require('./views/Home.vue'))
})
}
// 引入 About 組件
const About = resolve => {
require.ensure(['./views/About.vue'], () => {
resolve(require('./views/About.vue'))
})
}
Vue.use(Router)
let base = `${process.env.BASE_URL}` // 動態獲取二級目錄
export default new Router({
mode: 'history',
base: base,
routes: [{
path: '/',
name: 'home',
component: Home
}, {
path: '/about',
name: 'about',
component: About
}]
})
```
改為 history 后我們 url 的路徑就變成了 `http://127.0.0.1:8080/vue/about`,而不是原來的 `http://127.0.0.1:8080/vue/#/about`,但是需要注意頁面渲染 404 的問題,具體可查閱:[HTML5 History 模式](https://router.vuejs.org/zh/guide/essentials/history-mode.html)。
而在異步加載的優化上,我們使用了 webpack 提供的 require.ensure() 進行了代碼拆分,主要區別在于沒有優化前,訪問 Home 頁面會一起加載 About 組件的資源,因為它們打包進了一個 app.js 中:

但是優化過后,它們分別被拆分成了 2.js 和 3.js:


如此,只有當用戶點擊了某頁面,才會加載對應頁面的 js 文件,實現了按需加載的功能。
> webpack 在編譯時,會靜態地解析代碼中的 require.ensure(),同時將模塊添加到一個分開的 chunk 當中。這個新的 chunk 會被 webpack 通過 jsonp 來按需加載。
關于 `require.ensure()` 的知識點可以參考官方文檔:[require.ensure](https://webpack.js.org/api/module-methods/#require-ensure)。
當然,除了使用 require.ensure 來拆分代碼,[Vue Router](https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E6%8A%8A%E7%BB%84%E4%BB%B6%E6%8C%89%E7%BB%84%E5%88%86%E5%9D%97) 官方文檔還推薦使用動態 `import` 語法來進行代碼分塊,比如上述 require.ensure 代碼可以修改為:
```
// 引入 Home 組件
const Home = () => import('./views/Home.vue');
// 引入 About 組件
const About = () => import('./views/About.vue');
```
其余代碼可以保持不變,仍然可以實現同樣的功能。如果你想給拆分出的文件命名,可以嘗試一下 webpack 提供的 `Magic Comments`(魔法注釋):
```
const Home = () => import(/* webpackChunkName:'home'*/ './views/Home.vue');
```
### 2\. Vuex 配置
除了 vue-router,如果你的項目需要用到 [Vuex](https://vuex.vuejs.org/zh/) ,那么你應該對它有一定的了解,Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。這里我們先來看一下使用 CLI 生成的配置文件 store.js 中的內容:
```
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
}
})
```
該配置文件便是 Vuex 的配置文件,主要有 4 個核心點:state、mutations、actions 及 getter,詳細的介紹大家可以參考官方文檔:[核心概念](https://vuex.vuejs.org/zh/guide/state.html),這里我用一句話介紹它們之間的關系就是:**我們可以通過 actions 異步提交 mutations 去 修改 state 的值并通過 getter 獲取**。
需要注意的是不是每一個項目都適合使用 Vuex,如果你的項目是中大型項目,那么使用 Vuex 來管理錯綜復雜的狀態數據是很有幫助的,而為了后期的拓展性和可維護性,這里不建議使用 CLI 生成的一份配置文件來管理所有的狀態操作,我們可以把它拆分為以下目錄:
```
└── store
├── index.js # 我們組裝模塊并導出 store 的地方
├── actions.js # 根級別的 action
├── mutations.js # 根級別的 mutation
└── modules
├── moduleA.js # A模塊
└── moduleB.js # B模塊
```

與單個 store.js 文件不同的是,我們按模塊進行了劃分,每個模塊中都可以包含自己 4 個核心功能。比如模塊 A 中:
```
/* moduleA.js */
const moduleA = {
state: {
text: 'hello'
},
mutations: {
addText (state, txt) {
// 這里的 `state` 對象是模塊的局部狀態
state.text += txt
}
},
actions: {
setText ({ commit }) {
commit('addText', ' world')
}
},
getters: {
getText (state) {
return state.text + '!'
}
}
}
export default moduleA
```
上方我們導出 A 模塊,并在 index.js 中引入:
```
/* index.js */
import Vue from 'vue'
import Vuex from 'vuex'
import moduleA from './modules/moduleA'
import moduleB from './modules/moduleB'
import { mutations } from './mutations'
import actions from './actions'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
groups: [1]
},
modules: {
moduleA, // 引入 A 模塊
moduleB, // 引入 B 模塊
},
actions, // 根級別的 action
mutations, // 根級別的 mutations
// 根級別的 getters
getters: {
getGroups (state) {
return state.groups
}
}
})
```
這樣項目中狀態的模塊劃分就更加清晰,對應模塊的狀態我們只需要修改相應模塊文件即可。詳細的案例代碼可參考文末 github 地址。
### 3\. 接口配置
在項目的開發過程中,我們也少不了與后臺服務器進行數據的獲取和交互,這一般都是通過接口完成的,那么我們如何進行合理的接口配置呢?我們可以在 src 目錄下新建 services 文件夾用于存放接口文件:
```
└── src
└── services
├── http.js # 接口封裝
├── moduleA.js # A模塊接口
└── moduleB.js # B模塊接口
```
為了讓接口便于管理,我們同樣使用不同的文件來配置不同模塊的接口,同時由于接口的調用 ajax 請求代碼重復部分較多,我們可以對其進行簡單的封裝,比如在 http.js 中(fetch為例):
```
/* http.js */
import 'whatwg-fetch'
// HTTP 工具類
export default class Http {
static async request(method, url, data) {
const param = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if (method === 'GET') {
url += this.formatQuery(data)
} else {
param['body'] = JSON.stringify(data)
}
// Tips.loading(); // 可調用 loading 組件
return fetch(url, param).then(response => this.isSuccess(response))
.then(response => {
return response.json()
})
}
// 判斷請求是否成功
static isSuccess(res) {
if (res.status >= 200 && res.status < 300) {
return res
} else {
this.requestException(res)
}
}
// 處理異常
static requestException(res) {
const error = new Error(res.statusText)
error.response = res
throw error
}
// url處理
static formatQuery(query) {
let params = [];
if (query) {
for (let item in query) {
let vals = query[item];
if (vals !== undefined) {
params.push(item + '=' + query[item])
}
}
}
return params.length ? '?' + params.join('&') : '';
}
// 處理 get 請求
static get(url, data) {
return this.request('GET', url, data)
}
// 處理 put 請求
static put(url, data) {
return this.request('PUT', url, data)
}
// 處理 post 請求
static post(url, data) {
return this.request('POST', url, data)
}
// 處理 patch 請求
static patch(url, data) {
return this.request('PATCH', url, data)
}
// 處理 delete 請求
static delete(url, data) {
return this.request('DELETE', url, data)
}
}
```
封裝完畢后我們在 moduleA.js 中配置一個 github 的開放接口:`https://api.github.com/repos/octokit/octokit.rb`
```
/* moduleA.js */
import Http from './http'
// 獲取測試數據
export const getTestData = () => {
return Http.get('https://api.github.com/repos/octokit/octokit.rb')
}
```
然后在項目頁面中進行調用,會成功獲取 github 返回的數據,但是一般我們在項目中配置接口的時候會直接省略項目 url 部分,比如:
```
/* moduleA.js */
import Http from './http'
// 獲取測試數據
export const getTestData = () => {
return Http.get('/repos/octokit/octokit.rb')
}
```
這時候我們再次調用接口的時候會發現其調用地址為本地地址:`http://127.0.0.1:8080/repos/octokit/octokit.rb`,那么為了讓其指向 `https://api.github.com`,我們需要在 vue.config.js 中進行 devServer 的配置:
```
/* vue.config.js */
module.exports = {
...
devServer: {
// string | Object 代理設置
proxy: {
// 接口是 '/repos' 開頭的才用代理
'/repos': {
target: 'https://api.github.com', // 目標地址
changeOrigin: true, // 是否改變源地址
// pathRewrite: {'^/api': ''}
}
},
}
...
}
```
在 devServer 中 我們配置 proxy 進行接口的代理,將我們本地地址轉換為真實的服務器地址,此時我們同樣能順利的獲取到數據,不同點在于接口狀態變成了 304(重定向):

### 4\. 公共設施配置
最后我們項目開發中肯定需要對一些公共的方法進行封裝使用,這里我把它稱之為公共設施,那么我們可以在 src 目錄下建一個 common 文件夾來存放其配置文件:
```
└── src
└── common
├── index.js # 公共配置入口
├── validate.js # 表單驗證配置
└── other.js # 其他配置
```
在入口文件中我們可以向外暴露其他功能配置的模塊,比如:
```
/* index.js */
import Validate from './validate'
import Other from './other'
export {
Validate,
Other,
}
```
這樣我們在頁面中只需要引入一個 index.js 即可。
## 結語
本文介紹了 Vue 單頁應用的一些基本配置,從項目構建層面闡述了各文件的主要配置方式和注意點,由于本文并不是一篇文檔類的配置說明,并不會詳細介紹各配置文件的 API 功能,大家可以訪問文中列出的官方文檔進行查閱。
本案例代碼地址:[single-page-project](https://github.com/luozhihao/vue-project-code/tree/master/single-page-project)
## 思考 & 作業
* devServer 中 proxy 的 key 值代表什么?如果再添加一個 `/reposed` 的配置會產生什么隱患?
* 如何配置 webpack 使得 `require.ensure()` 拆分出的 js 文件具有自定義文件名?
- 開篇: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:嘗試使用外部數據
- 總結篇:寫在最后