其他章節請看:
使用 vue-cli 3 搭建一個專案(下)
上篇 我們已經成功引入 element-ui
、axios
、mock
、iconfont
、nprogress
,本篇繼續介紹 許可權控制
、佈局
、多環境(.env)
、跨域
、vue.config.js
,一步一步構建我們自己的架構。
許可權控制
後端系統一開始就得考慮許可權和安全的問題。
大概思路:
-
前端持有一份路由表,表示每個路由可以訪問的許可權(路由表也可以由後端生成,但感覺前端被後端支配,前端的許可權也總是不安全的,所以後端許可權少不了,所以這份路由表做在前端也沒關係)
-
使用者輸入使用者名稱和密碼進行登入,伺服器返回一個
token
(登入標識)。前端將 token 存入 cookie,使用者再次重新整理頁面也能記住登入狀態 -
通過
token
從伺服器取得使用者對應的 role(角色資訊),根據 role 計算出相對應的路由,在用router.addRoute
動態掛載這些路由
最基礎的許可權實現
需求:
- 涉及兩個頁面,登入頁和首頁
- 在登入頁點選登入按鈕,成功則進入首頁
- 在首頁中,點選登出按鈕,則又回到登入頁
- 從其他頁面訪問(/about),如果沒有許可權則會去到登入頁,登入成功後會再次來到 /about
核心思路:
- 建立登入頁,裡面有兩個 input 用於輸入使用者名稱和密碼,點選登入,登入成功,取得 token,並存入 cookie,跳轉到主頁(或 redirect)
- 建立全域性前置守衛,分為已登入和未登入
- 已登入(能取得 token),如果訪問登入頁則直接轉去主頁,如果訪問非登入頁,如果已經獲取過使用者資訊(例如 name),則直接放行(next()),否則就去獲取使用者資訊,獲取成功則放行,獲取失敗則重置 token,並跳轉到登入頁,並攜帶現在的 path
- 未登入,如果是白名單(例如 /login)則直接放行,非白名單,則跳轉到登入頁(/login),並攜帶現在的 path
核心程式碼:
Login.vue
,登入頁store/index.js
,資料和操作都通過 vuex 全域性控制permission.js
,全域性前置守衛
// views/Login.vue
<template>
<div>
<p>name: <input type="text" /></p>
<p>password: <input type="passwored" /></p>
<p><button @click="handleLogin">登入</button></p>
</div>
</template>
<script>
export default {
data() {
return {
redirect: ''
}
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
methods: {
handleLogin(){
// 登入
this.$store.dispatch('login').then(() => {
console.log('頁面跳轉')
// 頁面跳轉
this.$router.push({ path: this.redirect || '/' })
// 解決報錯:Uncaught (in promise) Error: Redirected when going from...
.catch(() => {});
}).catch(() => {
alert('登入失敗')
})
}
},
}
</script>
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
Vue.use(Vuex)
const getDefaultState = () => ({
token: getToken(),
name: ''
})
export default new Vuex.Store({
state: getDefaultState(),
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
SET_NAME: (state, name) => {
state.name = name
},
},
getters: {
name: (state) => state.name
},
actions: {
// user login
login({ commit }, userInfo) {
return new Promise((resolve, reject) => {
// 登入成功(此處應該是 ajax)
Promise.resolve({ code: 200, data: { token: new Date() } }).then(response => {
const { data } = response
// vuex 儲存 token
commit('SET_TOKEN', data.token)
// 將 token 儲存 localStorage 中(你也可以存入 cookie)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 登出
logout({ commit, state }) {
return new Promise((resolve, reject) => {
// 登出(此處應該是 ajax)
Promise.resolve().then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
},
// 獲取使用者資訊
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
// 傳送 token 給後端,取得使用者資訊
Promise.resolve({ code: 200, data: { name: 'ph' } }).then(response => {
const { data } = response
if (!data) {
// 驗證失敗,請重新登入。
return reject('驗證失敗,請重新登入。')
}
const { name } = data
commit('SET_NAME', name)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
// 從 Cookie 中刪除 token
removeToken() // must remove token first
// 重置 vuex 資料,包括重置 token
commit('RESET_STATE')
resolve()
})
},
},
})
// src/permission.js
import router from './router'
import store from './store'
import { getToken } from '@/utils/auth'
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async (to, from, next) => {
// 使用者是否已經登入
const hasToken = getToken()
// 已登入
if (hasToken) {
// 再次訪問登入頁面,則直接進入主頁
if (to.path === '/login') {
next({ path: '/' })
return
}
// 訪問登入頁以外的其他頁面
// 是否已經獲取過使用者資訊
const hasGetUserInfo = store.getters.name
// 有使用者資訊,則直接訪問該頁面
if (hasGetUserInfo) {
next()
return
}
// 前一個非同步操作失敗,也不中斷後面的非同步操作,就可以使用 try...catch
try {
// 獲取使用者資訊
await store.dispatch('getInfo')
next()
} catch (error) {
// 刪除 token 並轉到登入頁面重新登入
await store.dispatch('resetToken')
console.log(error || 'Error')
next(`/login?redirect=${to.path}`)
}
return
}
// 未登入
whiteList.includes(to.path)
// 白名單,直接通過
? next()
// 非白名單,轉去登入頁面
: next(`/login?redirect=${to.path}`)
})
其他相關程式碼:
- main.js,引入 permission.js
- router.js,配置登入頁和主頁的路由
- auth.js,用於將 token 存入本地
- TestHome.vue,主頁
// main.js
import './permission'
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import TestHome from '../views/TestHome'
import Login from '../views/Login'
Vue.use(VueRouter)
const routes = [
{
path: '/login',
component: Login,
},
{
path: '/',
component: TestHome,
// 命名路由
name: 'home-page'
},
]
const createRouter = () => new VueRouter({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: routes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
// src/utils/auth
import Cookies from 'js-cookie'
const TokenKey = 'myself-vue-admin-template-token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
// views/TestHome.vue
<template>
<div class="test-home">
<h2>主頁</h2>
<p><button @click="logout">退出</button></p>
</div>
</template>
<script>
export default {
methods: {
// 登出
async logout(){
await this.$store.dispatch('logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
},
}
</script>
根據角色生成使用者可以訪問的路由
在上一節,我們在路由中定義的只是通用路由,也就是所有角色都可以訪問。
接下來我們定義一些需要許可權訪問的路由,然後根據使用者角色,過濾生成使用者最終可以訪問的路由表。
需求:有如下非通用(需要許可權才能訪問)路由,根據角色(admin 或 editor)生成使用者可以訪問的路由表,將此功能加入 最基礎的許可權實現
一節中的例子裡面。
const asyncRoutes = [
{
path: '/article/:id',
component: Article,
// 用於描述資料的資料
meta: {
roles: ['admin', 'editor']
},
// children 是巢狀路由
children: [
{
// 當 /user/:id/ca 匹配成功,
path: 'ca',
component: ArticleComponentA,
roles: ['admin']
},
{
path: 'cb',
component: ArticleComponentB
}
]
},
]
這裡定義了三個路由,/article/:id
可以被角色 admin 或 editor 訪問,/article/:id/ca
只能被 admin 訪問,而 /article/:id/ca
能被任何角色訪問。
注:對於巢狀路由,如果外面的路由(/article/:id
)不能訪問,但裡面的路由(/article/:id/ca
)卻有許可權訪問,那麼應該都不能訪問。
思路:
- 在
src/router/index.js
中定義非同步路由 - 在
src/store/index.js
中生成使用者最終能訪問的非同步路由 - 在
src/permission.js
中將生成的路由通過addRoute
加入 router 中
核心程式碼如下(在 axios 和 mock 的基礎上寫):
// src/router/index.js
import Article from '../views/Article'
import ArticleComponentA from '../views/ArticleComponentA'
import ArticleComponentB from '../views/ArticleComponentB'
export const asyncRoutes = [
{
path: '/article/:id',
component: Article,
// 用於描述資料的資料
meta: {
roles: ['admin', 'editor']
},
// children 是巢狀路由
children: [
{
// 當 /user/:id/ca 匹配成功,
path: 'ca',
component: ArticleComponentA,
meta: {
roles: ['admin']
}
},
{
path: 'cb',
component: ArticleComponentB
}
]
},
]
Tip: Article.vue
、ArticleComponentA.vue
、ArticleComponentB.vue
只是很簡單的頁面
// views/Article.vue
<template>
<div>
<p>文章列表頁</p>
<!-- 子路由 -->
<router-view></router-view>
</div>
</template>
// views/ArticleComponentA.vue
<template>
<div style="border: 1px solid">
<p>我是A</p>
<p>
這是對應 <em>{{ $route.params.id }}</em> 的文章
</p>
</div>
</template>
// views/ArticleComponentB.vue 與 ArticleComponentA.vue 類似
// src/store/index.js
import { asyncRoutes } from '@/router'
function hasPermission(route, roles) {
return (route.meta && route.meta.roles) // 如果路由定義了角色的限制
? roles.some(role => route.meta.roles.includes(role))
: true
}
function filterRoutes(routes, roles) {
const res = []
routes.forEach(route => {
// 淺拷貝,更安全
const tmp = { ...route }
if (!hasPermission(tmp, roles)) {
return
}
// 先處理孩子
if (tmp.children) {
tmp.children = filterRoutes(tmp.children, roles)
}
res.push(tmp)
})
return res
}
export default new Vuex.Store({
actions: {
// 生成路線
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes = roles.includes('admin') // admin 都能訪問,無需過濾
? (asyncRoutes || [])
: filterRoutes(asyncRoutes, roles)
resolve(accessedRoutes)
})
}
},
})
// src/permission.js
// 修改的程式碼只涉及 try 內
router.beforeEach(async (to, from, next) => {
// 使用者是否已經登入
const hasToken = getToken()
// 已登入
if (hasToken) {
...
try {
// 獲取使用者資訊
let roles = await store.dispatch('getInfo')
// 此處僅模擬
roles = ['editor']
const accessRoutes = await store.dispatch('generateRoutes', roles)
accessRoutes.forEach(route => {
router.addRoute(route)
})
next()
} catch (error) {
...
}
}
})
測試:
訪問:http://localhost:8080/#/article/1/cb
輸出:
文章列表頁
我是B
這是對應 1 的文章
訪問:http://localhost:8080/#/article/1/ca
沒有許可權,無法訪問
404
需求:在上一節的基礎上,訪問 http://localhost:8080/#/article/1/ca 跳轉到 404 頁面,即訪問不存在的路由都轉到 404。
直接上程式碼:
// views/404.vue
<template>
<h1>404 頁面</h1>
</template>
// src/router/index.js
const routes = [
// 注:有人說:404頁面一定要放在最後,這裡放在開頭好似沒有影響
+ { path: '*', redirect: '/404', hidden: true },
+ {
path: '/404',
component: () => import('@/views/404')
},
{
path: '/login',
component: Login,
},
{
path: '/',
component: TestHome,
// 命名路由
name: 'home-page'
}
]
測試:
輸入: http://localhost:8080/#/article/1/cb
顯示正常
輸入: http://localhost:8080/#/article/1/ca
頁面顯示:404 頁面
注:有人說:404頁面一定要放在最後,這裡放在開頭好似沒有影響
佈局
頁面整體佈局是一個產品最外層的框架結構,往往包含導航、側邊欄、麵包屑等等。
模板專案如何佈局
模板專案中絕大部分頁面基於一個 layout.vue,除了個別頁面,如 login、404 沒有使用該 layout。如果需要在專案中有多種不同的 layout 也很方便,只要在一級路由中選擇不同的 layout 即可
加入自己的佈局
需求:一個專案有多種佈局是很正常的,我們建立 2 種佈局,一種上下佈局,一種左右佈局。登入(login.vue
)頁面無需使用佈局,而 page1.vue
和 page2.vue
各應用於一種佈局。
page1.vue 應用於左右佈局
+----------------+
| | |
| | |
| | |
+----------------+
page2.vue 應用於上下佈局
+----------------+
| |
+----------------+
| |
| |
| |
+----------------+
直接上程式碼:
// App.vue(vue-cli 自動生成,無需變動)
<template>
<div id="app">
<router-view />
</div>
</template>
// src/layout/layout1.vue
<template>
<!-- 左右佈局 -->
<div>
<div class="side">導航</div>
<div class="main">
<router-view></router-view>
</div>
</div>
</template>
// src/layout/layout2.vue
<template>
<!-- 上下佈局 -->
<div>
<div>導航</div>
<div class="main">
<router-view></router-view>
</div>
</div>
</template>
// router/index.js
// 略
import Layout1 from '@/layout/layout1'
import Layout2 from '@/layout/layout2'
const routes = [
{
path: '/login',
component: Login,
},
// page1.vue 應用於 Layout1
{
path: '/',
component: Layout1,
// 重定向到 /page1
redirect: '/page1',
children: [{
path: 'page1',
name: 'Page1',
component: () => import('@/views/page1'),
}]
},
// page2.vue 應用於 Layout2
{
path: '/example',
component: Layout2,
redirect: '/example/page2',
children: [{
path: 'page2',
name: 'Page2',
component: () => import('@/views/page2'),
}]
},
]
// views/page1.vue
<template>
<p>主頁</p>
</template>
// views/page2.vue
<template>
<p>page2</p>
</template>
測試:
輸入 http://localhost:8080/#/
跳轉到 http://localhost:8080/#/page1
顯示:
導航
主頁
輸入 http://localhost:8080/#/example
跳轉到 http://localhost:8080/#/example/page2
顯示:
導航
page2
多環境
vue-cli
只提供了兩種環境,開發和生產:
// package.json
{
"name": "myself-vue-admin-template",
"scripts": {
// 開發環境
"serve": "vue-cli-service serve",
// 生產環境
"build": "vue-cli-service build",
},
}
有時我們需要一個預釋出環境,可以這樣:
{
"scripts": {
// 開發環境
"serve": "vue-cli-service serve",
// 生產環境
"build": "vue-cli-service build",
// 預釋出
"build:stage": "vue-cli-service build --mode staging",
},
}
執行 npm run build:stage
會讀取 .env.staging
檔案中的變數。
.env 檔案
在模板專案中,有三個 .env
檔案:
// .env.development
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = '/dev-api'
// .env.production
# just a flag
ENV = 'production'
# base api
VUE_APP_BASE_API = '/prod-api'
// .env.staging
NODE_ENV = production
# just a flag
ENV = 'staging'
# base api
VUE_APP_BASE_API = '/stage-api'
Tip:前兩個 .env 檔案沒有定義 NODE_ENV
變數,那麼 NODE_ENV
的值取決於模式,例如,在 production 模式下被設定為 production
這三個 .env
檔案分別對應著模板專案的這三個命令:
{
"name": "vue-admin-template",
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
},
}
Tip:更多細節請看vue-cli 下 -> 模式和環境變數
base_url
測試環境和線上的 base_url
不相同是很正常的,我們可以通過配置 base_url
解決。
我們直接分析模板專案:
在上一節(.env 檔案
)我們發現,在 3 個 .env
檔案中都定義了一個變數 VUE_APP_BASE_API
:
// .env.development
# base api
VUE_APP_BASE_API = '/dev-api'
// .env.production
# base api
VUE_APP_BASE_API = '/prod-api'
// .env.staging
# base api
VUE_APP_BASE_API = '/stage-api'
在 axios 中有如下一段程式碼:
// src/utils/request.js
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
從註釋得知,我們發出的 url 包含兩部分,而其中的 base url 就會根據不同環境取不同的值,例如在 staging 模式下將等於 /stage-api
Tip: 執行命令(例如 npm run build:prod
)就會替換 process.env.VUE_APP_BASE_API
如果某個請求的 baseURL
和其他的不同,可以這樣:
// 會自動覆蓋你在建立例項時寫的引數
export function getArticle() {
return fetch({
baseURL: https://www.baidu.com
url: '/article',
method: 'get',
});
}
跨域
跨域方式很多,但現在主流的有兩種:
- 開發環境和生成環境都用
cors
- 開發環境用
Proxy
,生成環境用nginx
最推薦的是 cors(跨域資源共享),能決定瀏覽器是否阻止前端 JavaScript 程式碼獲取跨域請求的響應。前端無需變化,工作量在後端。而後端的開發環境和生成環境是一套程式碼,所以也很方便使用。如果後端不願意,前端就用 proxy + nginx
。
proxy
需求:新建一個頁面(Test.vue
),裡面有 2 個按鈕,點選按鈕能發出相應的請求,一個是非跨域請求,一個是跨域請求。
核心程式碼如下:
// views/Test.vue
<template>
<div>
<button @click="request1">非跨域</button>
<button @click="request2">跨域</button>
</div>
</template>
<script>
import { getList, getArticle } from '@/api/table'
export default {
methods: {
request1 () {
getList()
},
request2(){
getArticle()
}
}
}
</script>
// src/api/table.js
import request from '@/utils/request'
// 非跨域請求
export function getList(params) {
return request({
url: '/vue-admin-template/table/list',
})
}
// 跨域請求
export function getArticle(params) {
return request({
url: '/pengjiali/p/14561119.html',
method: 'get',
params
})
}
// vue.config.js
module.exports = {
devServer: {
proxy: {
// 只有 /pengjiali 的請求會被代理
'/pengjiali': {
target: 'https://www.cnblogs.com/',
// changeOrigin: true
},
}
}
}
// src/router/index.js
import Test from '../views/Test'
const routes = [
{
path: '/test',
component: Test
},
]
測試:
訪問:http://localhost:8080/test#/test
顯示兩個按鈕:非跨域 跨域
點選“非跨域”
- 請求 http://localhost:8080/vue-admin-template/table/list
- 響應 {"code":20000,"data":{"total":3,"items":4}}
點選“跨域”
- 請求 http://localhost:8080/pengjiali/p/15419402.html
- 響應 <!DOCTYPE html>...
注:設定 proxy 中的 changeOrigin: true
,在瀏覽器中是看不到效果的。或許瀏覽器只顯示第一層的請求,也就是發給代理的請求,而代理做的事情,瀏覽器就沒顯示了。
nginx
有時我們需要在生成環境下跨域請求,這時就用不了 proxy(proxy 是在本地伺服器中配置的),我們可以使用 nginx
。
Tip: nginx 是一款輕量級的 web 伺服器,能很快的處理靜態資源(js、html、css);同時也是反向代理伺服器;還有負載均衡的作用,並有多種策略以供選擇。
我們模擬一下 nginx
跨域,步驟如下
- 通過
anywhere
(通過 npm 安裝即可) 啟動服務
myself-vue-admin-template\src> anywhere -p 8095
Running at http://192.168.85.1:8095/
Also running at https://192.168.85.1:8096/
- nginx 下載 windows 版本,並啟動 nginx
// 啟動 nginx
nginx-1.21.3> .\nginx.exe
// 關閉 nginx
// nginx-1.21.3> .\nginx.exe -s stop
Tip:筆者使用 powershell 輸入 .\nginx.exe
啟動 nginx,沒有任何提示說明啟動成功,我們可以通過瀏覽器輸入 http://localhost/
來驗證。
- 修改 nginx 配置檔案:
// nginx-1.21.3\conf\nginx.conf
http {
server {
# 修改埠為 8090,用於跨域的實驗
listen 8090;
server_name localhost;
location / {
# 將 nginx 的目錄直接指定到我們的專案
root myself-vue-admin-template\dist;
index index.html index.htm;
}
# 請求 /src 的都被代理到 anywhere 伺服器中
location /src {
proxy_pass http://192.168.85.1:8095/;
}
}
}
- 修改請求 url
// src/aip/table.js
export function getArticle(params) {
return request({
// url: 'pengjiali/p/15419402.html',
url: 'src/main.js',
method: 'get',
params
})
}
- 修改 nginx 配置需要 reload
nginx-1.21.3> .\nginx.exe -s reload
- 驗證
// 打包生成 dist
myself-vue-admin-template> npm run build:prod
瀏覽器進入 http://localhost:8090/#/test
頁面顯示 `非跨域` 和 `跨域` 按鈕
點選`跨域` 按鈕
發出請求 http://localhost:8090/src/main.js,資源 main.js 成功載入
Tip:筆者最初使用 nginx 代理到部落格園,而不是代理到 anywhere 開啟的服務,但是請求卻以 404 而失敗,發出的請求不知道什麼原因,總是給我加了一個 p
期待:
http://localhost:8080/pengjiali/p/15419402.html
實際:
http://localhost:8080/pengjialip/p/15419402.html
vue.config.js
首先我們通過分析模板專案的配置(vue.config.js
),然後在我們的專案中也實現類似效果的配置。
Tip: vue-cli 已經對 webpack 封裝過了,可以通過命令檢視我們配置的是否正確:
// 生成環境的配置
vue-admin-template> vue inspect > output.prod.js --mode=production
// 開發環境的配置
vue-admin-template> vue inspect > output.dev.js
模板專案的配置
// vue-admin-template/vue.config.js
'use strict'
// 載入 node 的 path 模組
const path = require('path')
// 引入 setting.js 模組
const defaultSettings = require('./src/settings.js')
// 返回一個絕對路徑
function resolve(dir) {
/*
每個模組都有 __dirname,表示該檔案的目錄,是一個絕對路徑
path.join() 方法使用特定於平臺的分隔符作為分隔符,將所有給定的路徑段連線在一起,然後對結果路徑進行規範化
> path.join('/foo', 'bar', 'baz/asdf', 'quux');
\\foo\\bar\\baz\\asdf\\quux
*/
return path.join(__dirname, dir)
}
// 定義一個 title,在 public/index.html 中可以獲取
const name = defaultSettings.title || 'vue Admin Template' // page title
/*
定義本地服務的埠,預設是 9528
process 程式,作為全域性可用
process.env.port 會讀取 .env 檔案中的 port 屬性
npm_config_port 可以通過下面命令重置為 9191
vue-admin-template> npm config set port 9191
vue-admin-template> npm config list
*/
const port = process.env.port || process.env.npm_config_port || 9528 // dev port
// 所有配置可以在這裡找到:https://cli.vuejs.org/config/
module.exports = {
// 請始終使用 publicPath 而不要直接修改 webpack 的 output.publicPath
// 預設情況下,Vue CLI 會假設你的應用是被部署在一個域名的根路徑上,例如 `https://www.my-app.com/`。
// 如果應用被部署在一個子路徑上,你就需要用這個選項指定這個子路徑
publicPath: '/',
// 當執行 vue-cli-service build 時生成的生產環境構建檔案的目錄
// 請始終使用 outputDir 而不要修改 webpack 的 output.path
outputDir: 'dist',
// 放置生成的靜態資源 (js、css、img、fonts) 的 (相對於 outputDir 的) 目錄。
assetsDir: 'static',
// 是否在開發環境下通過 eslint-loader 在每次儲存時 lint 程式碼
// 設定為 true 或 'warning' 時,eslint-loader 會將 lint 錯誤輸出為編譯警告。預設情況下,警告僅僅會被輸出到命令列,且不會使得編譯失敗
// 當 `lintOnSave` 是一個 `truthy` 的值時,`eslint-loader` 在開發和生產構建下都會被啟用。這裡在生產構建時禁用 eslint-loader
lintOnSave: process.env.NODE_ENV === 'development',
// 如果你不需要生產環境的 source map,可以將其設定為 false 以加速生產環境構建
productionSourceMap: false,
// 配置開發伺服器
devServer: {
port: port,
// 預設開啟瀏覽器
open: true,
// overlay 只顯示錯誤,不顯示警告
overlay: {
warnings: false,
errors: true
},
// 提供在伺服器內部的所有其他中介軟體之前執行自定義中介軟體的能力。這可用於定義自定義處理程式
// 可以看看:https://www.jianshu.com/p/c4883c04acb3
before: require('./mock/mock-server.js')
},
// 如果這個值是一個物件,則會通過 webpack-merge 合併到最終的配置中
configureWebpack: {
// 在 webpack 的名稱欄位中提供應用程式的標題,以便它可以在 index.html 中訪問以注入正確的標題。
name: name,
resolve: {
alias: {
// 新增 @ 別名
'@': resolve('src')
}
}
},
// Vue CLI 內部的 webpack 配置是通過 webpack-chain 維護的。這個庫提供了一個 webpack 原始配置的上層抽象
chainWebpack(config) {
// 可以提高首屏速度,建議開啟預載入(preload)
// 預設情況下,一個 Vue CLI 應用會為所有初始化渲染需要的檔案自動生成 preload 提示
config.plugin('preload').tap(() => [
{
rel: 'preload',
// 忽略 runtime.js
// runtime.js 在瀏覽器執行過程中,webpack 用來連線模組化應用程式所需的所有程式碼
// https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
// 檔案黑名單
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
include: 'initial'
}
])
// 移除 prefetch 外掛
// 預獲取,告訴瀏覽器在頁面載入完成後,利用空閒時間提前獲取使用者未來可能會訪問的內容
// 預設情況下,一個 Vue CLI 應用會為所有作為 async chunk 生成的 JavaScript 檔案 (通過動態 `import()` 按需 code splitting 的產物) 自動生成 prefetch 提示。
// Prefetch 連結將會消耗頻寬。
config.plugins.delete('prefetch')
// svg 的處理:
// 1. file-loader 不處理 src/icons 資料夾中的 svg 檔案
// 2. 使用 svg-sprite-loader,即 svg 精靈
config.module
.rule('svg') // 取得 svg 規則
.exclude.add(resolve('src/icons')) // 排除 src/icons
.end()
// 新的 loader
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
// 不是 development 才會生效
config
.when(process.env.NODE_ENV !== 'development',
config => {
// script-ext-html-webpack-plugin 是 html-webpack-plugin 的擴充套件外掛
// 此處主要是將 runtime.js 內聯到模板頁面(public/index.html)中
// 正因為要內聯,所以在 preload 中將 runtime.js 加入黑名單
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` 必須與 runtimeChunk 名稱相同。 預設是`執行時`
// 指令碼 'runtime.(.*).js' 是內聯的,而所有其他指令碼都是非同步和預載入的:
inline: /runtime\..*\.js$/
}])
.end()
// 分塊策略,用於生成環境的快取
config
.optimization.splitChunks({
// 設定為 all 可能特別強大,因為這意味著 chunk 可以在非同步和非非同步 chunk 之間共享
chunks: 'all',
cacheGroups: {
// 將依賴於 node_modules 中的第三方庫打包成 chunk-libs.js
// 這部分程式碼是很穩定的
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
// 只打包最初依賴的第三方
chunks: 'initial' // only package third parties that are initially dependent
},
// 將 node_modules 中的 element-ui 庫獨立出來,因為這個庫比較大,600多Kb
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
// 權重需要大於libs和app,否則會打包成libs或app
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
// 為了適應cnpm
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
// 比如自定義了一個元件,有十來個頁面都引入了,分塊策略預設是 30kb 才會拆分
// 加入我們的元件只有20kb,那麼每個頁面都會包括這 20kb,所以,這裡將其拆出來
commons: {
name: 'chunk-commons',
// components 通用的元件?
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
// 值 "single" 會建立一個在所有生成 chunk 之間共享的執行時檔案
// 構建後你會看到 dist/static/js/runtime.xxx.js
config.optimization.runtimeChunk('single')
}
)
}
}
我們專案的配置
下面的配置和模板專案的配置效果幾乎相同。兩者的差異有:
- 有些配置
vue-cli
預設就有,無需修改。例如publicPath
、outputDir
,以及@
別名 - 標題
title
,相對於模板專案,換了一種實現方式 runtime.js
沒有使用內聯,所以無需在preload
外掛中加入黑名單svg
換了一種方式實現,這裡使用的是vue-cli
的svg
外掛
// myself-vue-admin-template/vue.config.js
'use strict'
const path = require('path')
// 返回路徑
function resolve (dir) {
return path.join(__dirname, dir)
}
// 定義標題
const title = 'myself-vue-admin-template'
// 本地伺服器的埠
const port = process.env.port || process.env.npm_config_port || 9528
const assetsDir = 'static'
module.exports = {
// 放置生成的靜態資源 (js、css、img、fonts) 的 (相對於 outputDir 的) 目錄。
assetsDir,
lintOnSave: process.env.NODE_ENV === 'development',
// 如果你不需要生產環境的 source map,可以將其設定為 false 以加速生產環境構建
productionSourceMap: false,
// 配置開發伺服器
devServer: {
port: port,
// 預設開啟瀏覽器
open: true,
// overlay 只顯示錯誤,不顯示警告
overlay: {
warnings: false,
errors: true
},
proxy: {
'/pengjiali': {
target: 'https://www.cnblogs.com/'
}
}
},
// Vue CLI 內部的 webpack 配置是通過 webpack-chain 維護的
chainWebpack: config => {
// 移除 prefetch 外掛
config.plugins.delete('prefetch')
// svg
config.module
.rule('svg-sprite')
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.tap(options => {
// svg 放在 static 目錄下
options.spriteFilename = `${assetsDir}/img/icons.[hash:8].svg`
return options
})
.end()
.use('svgo-loader')
.loader('svgo-loader')
// 定義標題
config
.plugin('html')
.tap(args => {
// 定義title
// 用法:<%= htmlWebpackPlugin.options.title %>
args[0].title = title
return args
})
// 不是 development 才會生效
config
.when(process.env.NODE_ENV !== 'development',
config => {
// 分塊策略,用於生成環境的快取
config
.optimization.splitChunks({
// 設定為 all 可能特別強大,因為這意味著 chunk 可以在非同步和非非同步 chunk 之間共享
chunks: 'all',
cacheGroups: {
// 將依賴於 node_modules 中的第三方庫打包成 chunk-libs.js
// 這部分程式碼是很穩定的
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
// 只打包最初依賴的第三方
chunks: 'initial'
},
// 將 node_modules 中的 element-ui 庫獨立出來,因為這個庫比較大,600 多Kb
elementUI: {
name: 'chunk-elementUI',
priority: 20,
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
// 比如自定義了一個元件,有十來個頁面都引入了,分塊策略預設是 30kb 才會拆分
// 假如我們的元件只有20kb,那麼每個頁面都會包括這 20kb,所以,這裡將其拆出來生成 runtime.xx.js
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3,
priority: 5,
reuseExistingChunk: true
}
}
})
// 構建後你會看到 dist/static/js/runtime.xxx.js
config.optimization.runtimeChunk('single')
}
)
}
}
其他章節請看: