其他章节请看:
使用 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')
}
)
}
}
其他章节请看: