本文是对vue-element-admin中,我项目用到的内容进行了一下汇总。
目录结构
├── build # 构建相关
├── mock # 项目mock 模拟数据
├── plop-templates # 基本模板
├── public # 静态资源
│ │── favicon.ico # favicon图标
│ └── index.html # html模板
├── src # 源代码
│ ├── api # 所有请求
│ ├── assets # 主题 字体等静态资源
│ ├── components # 全局公用组件
│ ├── directive # 全局指令
│ ├── filters # 全局 filter
│ ├── icons # 项目所有 svg icons
│ ├── lang # 国际化 language
│ ├── layout # 全局 layout
│ ├── router # 路由
│ ├── store # 全局 store管理
│ ├── styles # 全局样式
│ ├── utils # 全局公用方法
│ ├── vendor # 公用vendor
│ ├── views # views 所有页面
│ ├── App.vue # 入口页面
│ ├── main.js # 入口文件 加载组件 初始化等
│ └── permission.js # 权限管理
├── tests # 测试
├── .env.xxx # 环境变量配置
├── .eslintrc.js # eslint 配置项
├── .babelrc # babel-loader 配置
├── .travis.yml # 自动化CI配置
├── vue.config.js # vue-cli 配置
├── postcss.config.js # postcss 配置
└── package.json # package.json
安装
# 克隆项目
git clone https://github.com/PanJiaChen/vue-element-admin.git
# 进入项目目录
cd vue-element-admin
# 安装依赖
npm install
# 建议不要用 cnpm 安装 会有各种诡异的bug 可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
# 本地开发 启动项目
npm run dev
Windows 用户若安装不成功,很大概率是node-sass安装失败,解决方案。
另外因为 node-sass 是依赖 python环境的,如果你之前没有安装和配置过的话,需要自行查看一下相关安装教程。
启动完成后会自动打开浏览器访问 http://localhost:9527, 你看到下面的页面就代表操作成功了。
接下来你可以修改代码进行业务开发了,本项目内建了典型业务模板、常用业务组件、模拟数据、HMR 实时预览、状态管理、国际化、全局路由等等各种实用的功能来辅助开发,你可以继续阅读和探索左侧的其它文档。
Vue vue-router中必不可少的redirect,meta和hidden三个属性
redirect 重定向
在父子嵌套结构中,父级的redirect指向子级children里的path。//当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
redirect: ‘noRedirect’
meta 元数据
可以作用判断用户是否已登陆
可以通过meta值,展示面包屑
hidden 是否需要展示该路由是否渲染该路由入口,// 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
hidden: true // (默认 false)
更多配置项
// 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
// 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
// 若你想不管路由下面的 children 声明的个数都显示你的根路由
// 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
alwaysShow: true
name: 'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
meta: {
roles: ['admin', 'editor'] // 设置该路由进入的权限,支持多个权限叠加
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标,支持 svg-class,也支持 el-icon-x element-ui 的 icon
noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
affix: true // 如果设置为true,它则会固定在tags-view中(默认 false)
// 当路由设置了该属性,则会高亮相对应的侧边栏。
// 这在某些场景非常有用,比如:一个文章的列表页路由为:/article/list
// 点击文章进入文章详情页,这时候路由为/article/1,但你想在侧边栏高亮文章列表的路由,就可以进行如下设置
activeMenu: '/article/list'
}
示例
{
path: '/permission',
component: Layout,
redirect: '/permission/index', //重定向地址,在面包屑中点击会重定向去的地址
hidden: true, // 不在侧边栏显示
alwaysShow: true, //一直显示根路由
meta: { roles: ['admin','editor'] }, //你可以在根路由设置权限,这样它下面所有的子路由都继承了这个权限
children: [{
path: 'index',
component: ()=>import('permission/index'),
name: 'permission',
meta: {
title: 'permission',
icon: 'lock', //图标
roles: ['admin','editor'], //或者你可以给每一个子路由设置自己的权限
noCache: true // 不会被 <keep-alive> 缓存
}
}]
}
路由
这里的路由分为两种,constantRoutes 和 asyncRoutes。
constantRoutes: 代表那些不需要动态判断权限的路由,如登录页、404、等通用页面。
asyncRoutes: 代表那些需求动态判断权限并通过 addRoutes 动态添加的页面。
这里所有的路由页面都使用 路由懒加载 了
404 页面一定要最后加载
侧边栏
前面也介绍了,侧边栏是通过读取路由并结合权限判断而动态生成的,而且还需要支持路由无限嵌套,所以这里还使用到了递归组件。一般侧边栏有两种形式即:submenu 和 直接 el-menu-item。 一个是嵌套子菜单,另一个则是直接一个链接。
多级目录(嵌套路由)
有三级路由嵌套的情况下,不要忘记还要手动在二级目录的根文件下添加一个 < router-view>。
<!-- 父级路由组件 -->// 原则上有多少级路由嵌套就需要多少个<router-view>
<template>
<div>
<!-- xxx html 内容 -->
<router-view />
</div>
</template>
在用 spa(单页面应用) 这种开发模式之前,用户每次点击侧边栏都会重新请求这个页面,用户渐渐养成了点击侧边栏当前路由来刷新 view 的习惯。但现在 spa 就不一样了,用户点击当前高亮的路由并不会刷新 view,因为 vue-router 会拦截你的路由,它判断你的 url 并没有任何变化,所以它不会触发任何钩子或者是 view 的变化。但可以 通过不断改变 url 的 query 来触发 view 的变化。我们监听侧边栏每个 link 的 click 事件,每次点击都给 router push 一个不一样的 query 来确保会重新刷新 view。
clickLink(path) {
this.$router.push({
path,
query: {
t: +new Date() //保证每次点击路由的query项都是不一样的,确保会重新刷新view
}
})
}
ps:不要忘了在 router-view 加上一个特定唯一的 key,如 , 但这也有一个弊端就是 url 后面有一个很难看的 query 后缀如 xxx.com/article/list?t=1496832345025
侧边栏滚动方案
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'permission_routes',
'sidebar'
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
侧边栏 外链
{
"path": "external-link",
"component": Layout,
"children": [
{
"path": "https://github.com/PanJiaChen/vue-element-admin",
"meta": { "title": "externalLink", "icon": "link" }
}
]
}
侧边栏默认打开
<el-menu
:default-openeds="['/example','/nested']" // 添加本行代码
:text-color="variables.menuText"
:active-text-color="variables.menuActiveText"
>
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
权限验证
该项目中权限的实现方式是:通过获取当前用户的权限去比对路由表,生成当前用户具有的权限可访问的路由表,通过 router.addRoutes 动态挂载到 router 上。
逻辑修改
现在路由层面权限的控制代码都在 @/permission.js 中,如果想修改逻辑,直接在适当的判断逻辑中 next() 释放钩子即可。\src\directive\permission\permission.js
指令权限
封装了一个指令权限,能简单快速的实现按钮级别的权限判断。
<template>
<!-- Admin can see this -->
<el-tag v-permission="['admin']">admin</el-tag>
<!-- Editor can see this -->
<el-tag v-permission="['editor']">editor</el-tag>
<!-- Editor can see this -->
<el-tag v-permission="['admin','editor']">Both admin or editor can see this</el-tag>
</template>
<script>
// 当然你也可以为了方便使用,将它注册到全局
import permission from '@/directive/permission/index.js' // 权限判断指令
export default{
directives: { permission }
}
</script>
在某些情况下,使用v-permission是不合适的,例如只能通过手动设置v-if来实现的元素Tab组件。
<el-tab-pane v-if="checkPermission(['admin'])" label="Admin">Admin can see this</el-tab-pane>
其实 keep-alive 源码不复杂,但逻辑还是蛮绕的,之前尤大自己修复一个 bug 的时候也不小心搞错了,连发两个版本来修复,所以如果没有标签导航栏需求的用户,建议移除此功能。
新增页面
{
path: '/excel',
component: Layout,
redirect: '/excel/export-excel',
name: 'excel',
meta: {
title: 'excel',
icon: 'excel'
},
children: [
{
path: 'export-excel',
component: ()=>import('excel/exportExcel'),
name: 'exportExcel',
meta: { title: 'exportExcel' }
}
]
}
由于 children 下面只声明了一个路由所以不会有展开箭头
如果children下面的路由个数大于 1 就会有展开箭头,如下面所示。 如果你想忽视这个自动判断可以使用 alwaysShow: true,
children: [
{ path: 'export-excel', component: ()=>import('excel/exportExcel'), name: 'exportExcel', meta: { title: 'exportExcel' }},
{ path: 'export-selected-excel', component: ()=>import('excel/selectExcel'), name: 'selectExcel', meta: { title: 'selectExcel' }},
{ path: 'upload-excel', component: ()=>import('excel/uploadExcel'), name: 'uploadExcel', meta: { title: 'uploadExcel' }}
]
新增 api
最后在 @/api 文件夹下创建本模块对应的 api 服务。
新增组件
请记住拆分组件最大的好处不是公用而是可维护性!
新增样式
页面的样式和组件是一个道理,全局的 @/style 放置一下全局公用的样式,每一个页面的样式就写在当前 views下面,请记住加上scoped 或者命名空间,避免造成全局的样式污染。
<style>
/* global styles */
</style>
<style scoped>
/* local styles */
.xxx-container{
/* name scoped */
xxx
}
</style>
CSS Modules
在样式开发过程中,有两个问题比较突出:
全局污染 —— CSS 文件中的选择器是全局生效的,不同文件中的同名选择器,根据 build 后生成文件中的先后顺序,后面的样式会将前面的覆盖;
选择器复杂 —— 为了避免上面的问题,我们在编写样式的时候不得不小心翼翼,类名里会带上限制范围的标示,变得越来越长,多人开发时还很容易导致命名风格混乱,一个元素上使用的选择器个数也可能越来越多,最终导致难以维护。
好在 vue 为我们提供了 scoped 可以很方便的解决上述问题。 它顾名思义给 css 加了一个域的概念。
使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。
目录结构
├── styles
│ ├── btn.scss # 按钮样式
│ ├── element-ui.scss # 全局自定义 element-ui 样式
│ ├── index.scss # 全局通用样式
│ ├── mixin.scss # 全局mixin
│ ├── sidebar.scss # sidebar css
│ ├── transition.scss # vue transition 动画
│ └── variables.scss # 全局变量
常见的工作流程是,全局样式都写在 src/styles 目录下,每个页面自己对应的样式都写在自己的 .vue 文件之中
<style>
/* global styles */
</style>
<style scoped>
/* local styles */
</style>
自定义 element-ui 样式
现在我们来说说怎么覆盖 element-ui 样式。由于 element-ui 的样式我们是在全局引入的,所以你想在某个页面里面覆盖它的样式就不能加 scoped,但你又想只覆盖这个页面的 element 样式,你就可在它的父级加一个 class,用命名空间来解决问题。
.article-page {
/* 你的命名空间 */
.el-tag {
/* element-ui 元素*/
margin-right: 0px;
}
}
当然也可以使用深度作用选择器
当你子组件使用了 scoped 但在父组件又想修改子组件的样式可以 通过 >>> 来实现:
<style scoped>
.a >>> .b { /* ... */ }
</style>
如果你使用了一些预处理的东西,如 sass 你可以通过 /deep/ 来代替 >>> 实现想要的效果。
所以你想覆盖某个特定页面 element 的 button 的样式,你可以这样做:
.xxx-container >>> .el-button{
xxxx
}
Mixin
本项目没有设置自动注入 sass 的 mixin 到全局,所以需要在使用的地方手动引入 mixin
<style rel="stylesheet/scss" lang="scss">
@import "src/styles/mixin.scss";
</style>
和服务端进行交互 前端请求流程
在 vue-element-admin 中,一个完整的前端 UI 交互到服务端处理流程是这样的:
UI 组件交互操作;
调用统一管理的 api service 请求函数;
使用封装的 request.js 发送请求;
获取服务端返回;
更新 data;
从上面的流程可以看出,为了方便管理维护,统一的请求处理都放在 @/api 文件夹中,并且一般按照 model 纬度进行拆分文件,如:
api/
login.js
article.js
remoteSearch.js
...
request.js
其中,@/utils/request.js 是基于 axios 的封装,便于统一处理 POST,GET 等请求参数,请求头,以及错误提示信息等。具体可以参看 request.js。 它封装了全局 request拦截器、response拦截器、统一的错误处理、统一做了超时处理、baseURL设置等。
一个请求文章列表页的例子:
// api/article.js
import request from '../utils/request';
export function fetchList(query) {
return request({
url: '/article/list',
method: 'get',
params: query
})
}
// views/example/list
import { fetchList } from '@/api/article'
export default {
data() {
list: null,
listLoading: true
},
methods: {
fetchData() {
this.listLoading = true
fetchList().then(response => {
this.list = response.data.items
this.listLoading = false
})
}
}
}
设置多个 baseURL
可以通过环境变量设置多个baseURL,从而请求不同的 api 地址。
# .env.development
VUE_APP_BASE_API = '/dev-api' #注入本地 api 的根路径
VUE_APP_BASE_API2 = '/dev-api2' #注入本地 api2 的根路径
之后根据环境变量创建axios实例,让它具有不同的baseURL。 @/utils/request.js
// create an axios instance
const service = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000 // request timeout
})
const service2 = axios.create({
baseURL: process.env.BASE_API2, // api2 的 base_url
timeout: 5000 // request timeout
})
或者
export function fetchList(query) {
return request({
url: '/article/list',
method: 'get',
params: query,
baseURL: 'xxxx' // 直接通过覆盖的方式
})
}