后台管理系统相关整理
布局 Layout
除部分 login、404、401 等页面,其他页面都基于 layout,内容在 app-mian 中显示
sidebar、navbar(breadcrumb、tabs-view)、app-main
transform
页面之间切换动画
keep-alive
(缓存 <router-view>
的,配合页面的 tabs-view
标签导航使用,如不需要可自行去除。)
router-view
比如创建和编辑页面,使用同一组件,默认情况下两个页面切换时并不会触发 vue
的 created
和 mounted
钩子。
可以通过
watch
$route
的变化来进行处理在
router-view
上加上唯一的key
,来保证路由切换时都会重新渲染触发钩子js<router-view :key="key"></router-view> // 只要保证 key 唯一性就可以了,保证不同页面的 key 不相同 computed: { key() { return this.$route.fullPath } },
声明两个不同的
view
但引入用一个组件vue<!-- created.vue --> <template> <article-detail is-edit="false" /> </template> <script> import ArticleDetayl from './components/ArticleDetail' </script> <!-- edit.vue --> <template> <article-detail is-edit="true" /> </template> <script> import ArticleDetayl from './components/ArticleDetail' </script>
路由和侧边栏
侧边栏和路由绑定在一起,在 router 中配置好路由,侧边栏会自动生成。
配置项
// 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
hidden: true // (默认 false)
//当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
redirect: 'noRedirect'
// 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
// 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
// 若你想不管路由下面的 children 声明的个数都显示你的根路由
// 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
alwaysShow: true
name: 'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
meta: {
roles: ['admin', 'editor'] // 设置该路由进入的权限,支持多个权限叠加
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标
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 页面一定要最后加载,如果放在 constantRoutes
一同声明了 404 ,后面的所以页面都会被拦截到 404
侧边栏
使用递归组件构建,(UI 元素:el-menu、submenu、el-menu-item)
当你一个路由下面的 children 声明的路由大于>1 个时,自动会变成嵌套的模式。如果子路由正好等于一个就会默认将子路由作为根路由显示在侧边栏中,若不想这样,可以通过设置在根路由中设置 alwaysShow: true 来取消这一特性。
unique-opened 你可以在 Sidebar/index.vue 中设置 unique-opened 来控制侧边栏,是否只保持一个子菜单的展开。
多级目录(嵌套路由)
如果你的路由是多级目录,如本项目 @/views/nested
那样, 有三级路由嵌套的情况下,不要忘记还要手动在二级目录的根文件下添加一个 <router-view>
。
如:@/views/nested/menu1/index.vue
,原则上有多少级路由嵌套就需要多少个<router-view>
。
点击侧边栏 刷新当前路由
clickLink(path) {
this.$router.push({
path,
query: {
t: +new Date() //保证每次点击路由的query项都是不一样的,确保会重新刷新view
}
})
}
ps:不要忘了在 router-view 加上一个特定唯一的 key,如 <router-view :key="$route.path"></router-view>
, 但这也有一个弊端就是 url 后面有一个很难看的 query 后缀如 xxx.com/article/list?t=1496832345025
你可以从前面的 issue 中知道还有很多其它方案。我本人在公司项目中,现在采取的方案是判断当前点击的菜单路由和当前的路由是否一致,但一致的时候,会先跳转到一个专门 Redirect 的页面,它会将路由重定向到我想去的页面,这样就起到了刷新的效果了。
面包屑
通过 watch $route
变化动态生成的。它和 menu
也一样,也可以通过之前那些配置项控制一些路由在面包屑中的展现。大家也可以结合自己的业务需求增改这些自定义属性。比如可以在路由中声明breadcrumb:false
,让其不在 breadcrumb
面包屑显示。
侧边栏滚动问题
之前版本
cssoverflow-y: scroll; ::-webkit-scrollbar { display: none; }
el-scrollbar
处理侧边栏滚动问题 https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/layout/components/Sidebar/index.vue
侧边栏 外链
{
"path": "external-link",
"component": Layout,
"children": [
{
"path": "https://github.com/PanJiaChen/vue-element-admin",
"meta": { "title": "externalLink", "icon": "link" }
}
]
}
侧边栏默认展开
通过default-openeds
来进行设置,首先找到 侧边栏代码
<el-menu
:default-openeds="['/example','/nested']" // 添加本行代码
: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>
注意 :default-openeds="['/example','/nested']" 里面填写的是 submenu 的 route-path
权限验证
该项目中权限的实现方式是:通过获取当前用户的权限去比对路由表,生成当前用户具的权限可访问的路由表,通过 router.addRoutes
动态挂载到 router
上。
但其实很多公司的业务逻辑可能不是这样的,举一个例子来说,很多公司的需求是每个页面的权限是动态配置的,不像本项目中是写死预设的。但其实原理是相同的。如:你可以在后台通过一个 tree 控件或者其它展现形式给每一个页面动态配置权限,之后将这份路由表存储到后端。当用户登录后得到 roles,前端根据 roles 去向后端请求可访问的路由表,从而动态生成可访问页面,之后就是 router.addRoutes 动态挂载到 router 上,你会发现原来是相同的,万变不离其宗。
只是多了一步将后端返回路由表和本地的组件映射到一起。
const map={
login:require('login/index').default // 同步的方式
login:()=>import('login/index') // 异步的方式
}
//你存在服务端的map类似于
const serviceMap=[
{ path: '/login', component: 'login', hidden: true }
]
// 之后遍历这个map,动态生成asyncRoutes
// 并将 component 替换为 map[component]
逻辑修改
现在路由层面权限的控制代码都在 @/permission.js
中,如果想修改逻辑,直接在适当的判断逻辑中 next()
释放钩子即可
指令权限
封装了一个指令权限,能简单快速的实现按钮级别的权限判断。 v-permission
使用
<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>
局限(tab 等)
可以使用全局权限判断函数,用法和指令 v-permission
类似
<template>
<el-tab-pane v-if="checkPermission(['admin'])" label="Admin">
Admin can see this
</el-tab-pane>
<el-tab-pane v-if="checkPermission(['editor'])" label="Editor">
Editor can see this
</el-tab-pane>
<el-tab-pane
v-if="checkPermission(['admin', 'editor'])"
label="Admin-OR-Editor"
>
Both admin or editor can see this
</el-tab-pane>
</template>
<script>
import checkPermission from '@/utils/permission' // 权限判断函数
export default {
methods: {
checkPermission,
},
}
</script>
快捷导航(标题栏导航)
运用 keep-alive
和 router-view
的结合。
<keep-alive :include="cachedViews">
<router-view></router-view>
</keep-alive>
顶部标签栏导航实际作用相当于 nav 的另一种展现形式,其实说白了都是一个个 router-link,点击跳转到相应的页面。然后我们在来监听路由 $route 的变化,来判断当前页面是否需要重新加载或者已被缓存
visitedViews && cachedViews
目前 tags-view 维护了两个数组。
- visitedViews : 用户访问过的页面 就是标签栏导航显示的一个个 tag 数组集合
- cachedViews : 实际 keep-alive 的路由。可以在配置路由的时候通过 meta.noCache 来设置是否需要缓存这个路由 默认都缓存
Affix 固钉
当在声明路由是 添加了 Affix 属性,则当前 tag 会被固定在 tags-view 中(不可被删除)
{
path: '',
component: Layout,
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index'),
name: 'Dashboard',
meta: {
title: 'dashboard',
icon: 'dashboard',
noCache: true,
affix: true
}
}
]
}
移除
如果没有标签导航栏需求的用户,建议移除此功能。
首先找到 @/layout/components/AppMain.vue
然后移除 keep-alive
<template>
<section class="app-main" style="min-height: 100%">
<transition name="fade-transform" mode="out-in">
<router-view></router-view>
</transition>
</section>
</template>
然后移除整个 @/layout/components/TagsView.vue
文件,并在@/layout/components/index
和 @/layout/Layout.vue
移除相应的依赖。最后把 @/store/modules/tagsView
相关的代码删除即可。
新增页面
{
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' }},
{ 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' }}
]
}
多级目录(嵌套路由)
有三级路由嵌套的情况下,不要忘记还要手动在二级目录的根文件下添加一个 <router-view>
。
样式
CSS Modules
只要加上 <style scoped>
这样 css
就只会作用在当前组件内了。详细文档见 vue-loader
├── styles
│ ├── btn.scss # 按钮样式
│ ├── element-ui.scss # 全局自定义 element-ui 样式
│ ├── index.scss # 全局通用样式
│ ├── mixin.scss # 全局mixin
│ ├── sidebar.scss # sidebar css
│ ├── transition.scss # vue transition 动画
│ └── variables.scss # 全局变量
自定义 element-ui 样式
在它的父级加一个 class,用命名空间来解决问题
.article-page {
/* 你的命名空间 */
.el-tag {
/* element-ui 元素*/
margin-right: 0px;
}
}
父组件改变子组件样式 深度选择器
当你子组件使用了 scoped 但在父组件又想修改子组件的样式可以 通过 >>> 来实现
<style scoped>
.a >>> .b { /* ... */ }
</style>
/* 编译陈如下 */
/* .a[data-v-f3f3eg9] .b {
} */
sass
你可以通过 /deep/
来代替 >>>
实现想要的效果。
和服务端进行交互
前端请求流程
完整的前端 UI 交互到服务端处理流程是这样的:
- UI 组件交互操作;
- 调用统一管理的 api service 请求函数;
- 使用封装的 request.js 发送请求;
- 获取服务端返回;
- 更新 data;
为了方便管理维护,统一的请求处理都放在 @/src/api
文件夹中,并且一般按照 model
纬度进行拆分文件
request.js
其中,@/src/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
})
}
}
Mock Data
mock 新方案
在 v4.0
版本之后,在本地会启动一个 mock-server
来模拟数据,线上环境还是继续使用 mockjs
来进行模拟(因为本项目是一个纯前端项目,你也可以自己搭建一个线上 server
来提供数据)。不管是本地还是线上所有的数据模拟都是基于 mockjs
生成的,所以只要写一套 mock
数据,就可以在多环境中使用。
该方案的好处是,在保留 mockjs
的优势的同时,解决之前的痛点。由于我们的 mock
是完全基于 webpack-dev-serve
来实现的,所以在你启动前端服务的同时,mock-server
就会自动启动,而且这里还通过 chokidar
来观察 mock
文件夹内容的变化。在发生变化时会清除之前注册的 mock-api
接口,重新动态挂载新的接口,从而支持热更新。有兴趣的可以自己看一下代码 mock-server.js
。由于是一个真正的 server
,所以你可以通过控制台中的 network
,清楚的知道接口返回的数据结构。并且同时解决了之前 mockjs
会重写 XMLHttpRequest
对象,导致很多第三方库失效的问题。
本项目的所有请求都是通过封装的 request.js
进行发送的,通过阅读源码可以发现所有的请求都设置了一个 baseURL
,而这个 baseURL
又是通过读取 p
这个环境变量来动态设置的,这样方便我们做到不同环境使用不同的 api
地址。
移除
如果你不想使用 mock-server
的话只要在 vue.config.js
中移除 webpack-dev-server
中 proxy
和 after
这个 Middleware
就可以了
proxy: {
// change xxx-api/login => mock/login
// detail: https://cli.vuejs.org/config/#devserver-proxy
[process.env.VUE_APP_BASE_API]: {
target: `http://localhost:${port}/mock`,
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
}
},
after: require('./mock/mock-server.js')
mock-server
只会在开发环境中使用,线上生产环境目前使用 MockJs
进行模拟。如果不需要请移除。具体代码:main.js
import { mockXHR } from '../mock'
if (process.env.NODE_ENV === 'production') {
mockXHR()
}
新增
如果你想添加 mock 数据,只要在根目录下找到 mock 文件,添加对应的路由,对其进行拦截和模拟数据即可。
比如我现在在 src/api/article 中需要添加一个查询某篇文章下面评论数的接口 fetchComments,首先新建接口:
export function fetchComments(id) {
return request({
url: `/article/${id}/comments`,
method: 'get',
})
}
声明完接口之后,我们需要找到对应的 mock 文件夹 mock/article.js,在下面创建一个能拦截路由的 mock 接口
请注意,mock 拦截是基于路由来做的,请确 mock 数据一定能匹配你的 api 保路由,支持正则
// fetchComments 的 mock
{
// url 必须能匹配你的接口路由
// 比如 fetchComments 对应的路由可能是 /article/1/comments 或者 /article/2/comments
// 所以你需要通过正则来进行匹配
url: '/article/[A-Za-z0-9]/comments',
type: 'get', // 必须和你接口定义的类型一样
response: (req, res) => {
// 返回的结果
// req and res detail see
// https://expressjs.com/zh-cn/api.html#req
return {
code: 20000,
data: {
status: 'success'
}
}
}
}
修改
最常见的操作就是:你本地模拟了了一些数据,待后端完成接口后,逐步替换掉原先 mock 的接口。
我们以 src/api/role.js
中的 getRoles
接口为例。它原本是在 mock/role/index.js
中 mock
的数据。现在我们需要将它切换为真实后端数据,只要在 mock/role/index.js
找到对应的路由,之后将它删除即可。这时候你可以在 network
中,查看到真实的数据。
// api 中声明的路由
export function getRoles() {
return request({
url: '/roles',
method: 'get'
})
}
//找到对应的路由,并删除
{
url: '/roles',
type: 'get',
response: _ => {
return {
code: 20000,
data: roles
}
}
},
启用纯前端 Mock
现在在 mock/index.js 也封装了一个纯前端 mock 的方法,你只需要在 src/main.js 中:
import { mockXHR } from '../mock'
mockXHR()
这样就会变成纯前端 mock 数据了和 v4.0 版本之前的 mock 方案是一样的,原理见上文。
引入外部模块
使用第三方库
import moment from 'moment'
Object.defineProperty(Vue.prototype, '$moment', { value: moment })
由于所有的组件都会从 Vue
的原型对象上继承它们的方法, 因此在所有组件/实例中都可以通过 this.$moment
: 的方式访问 Moment
而不需要定义全局变量或者手动的引入。
export default {
created() {
console.log('The time is '.this.$moment().format('HH:mm'))
},
}
在 Vue 中使用第三方库的方式
全局变量
在项目中添加第三方库的最简单方式是讲其作为一个全局变量, 挂载到 window
对象上:
entry.js
:
`window._ = require('lodash');`
MyComponent.vue
:
```js
export default {
created() {
console.log(_.isEmpty() ? 'Lodash everywhere!' : 'Uh oh..')
},
}
```
这种方式不适合于服务端渲染, 因为服务端没有 window
对象, 是 undefined
, 当试图去访问属性时会报错
在每个文件中引入
另一个简单的方式是在每一个需要该库的文件中导入:
MyComponent.vue
:
```js
import _ from 'lodash';
export default {
created() {
console.log(_.isEmpty() ? 'Lodash is available here!' : 'Uh oh..');
}
}
```
这种方式是允许的, 但是比较繁琐, 并且带来的问题是: 你必须记住在哪些文件引用了该库, 如果项目不再依赖这个库时, 得去找到每一个引用该库的文件并删除该库的引用. 如果构建工具没设置正确, 可能导致该库的多份拷贝被引用.
优雅的方式
在 Vuejs
项目中使用 JavaScript
库的一个优雅方式是讲其代理到 Vue
的原型对象上去. 按照这种方式, 我们引入 Moment
库:
entry.js
:
```js
import moment from 'moment';
Object.defineProperty(Vue.prototype, '$moment', { value: moment });
```
由于所有的组件都会从 Vue
的原型对象上继承它们的方法, 因此在所有组件/实例中都可以通过 this.$moment
: 的方式访问 Moment
而不需要定义全局变量或者手动的引入.
MyComponent.vue
:
```js
export default {
created() {
console.log(_.isEmpty() ? 'Lodash everywhere!' : 'Uh oh..')
},
}
```
Object.defineProperty
一般而言, 可以按照下面的方式来给对象设置属性:
Vue.prototype.$moment = moment;
可以这样做, 但是 Object.defineProperty
允许我们通过一个 descriptor
来定义属性. Descriptor
运行我们去设置对象属性的一些底层(low-level)细节, 如是否允许属性可写? 是否允许属性在 for
循环中被遍历.
通常, 我们不会为此感到困扰, 因为大部分时候, 对于属性赋值, 我们不需要考虑这样的细节. 但这有一个明显的优点: 通过 descriptor
创建的属性默认是只读的.
这就意味着, 一些处于迷糊状态的(coffee-deprived)开发者不能在组件内去做一些很愚蠢的事情, 就像这样:
this.$http = 'Assign some random thing to the instance method';
this.$http.get('/'); // TypeError: this.$http.get is not a function
此外, 试图给只读实例的方法重新赋值会得到 TypeError: Cannot assign to read only property
的错误.
$
你可能会注意到, 代理第三库的属性会有一个 $
前缀, 也可能看到其它类似 $refs
, $on
, $mount
的属性和方式, 它们也有这个前缀.
这个不是强制要求, 给属性添加 $
前缀是提供那些处于迷糊状态(coffee-deprived)的开发者这是一个公开的 API
, 和 Vuejs
的一些内部属性和方法区分开来.
this
你还可能注意到, 在组件内是通过 this.libraryName
的方式来使用第三方库的, 当你知道它是一个实例方法时就不会感到意外了. 但与全局变量不同, 通过 this
来使用第三方库时, 必须确保 this
处于正确的作用域. 在回调方法中 this
的作用域会有不同, 但箭头式回调风格能保证 this
的作用域是正确的:
this.$http.get('/').then((res) => {
if (res.status !== 200) {
this.$http.get('/') // etc
// Only works in a fat arrow callback.
}
})
插件
如果你想在多个项目中使用同一个库, 或者想将其分享给其他人, 可以将其写成一个插件:
import MyLibraryPlugin from 'my-library-plugin'
Vue.use(MyLibraryPlugin)
在应用的入口引入插件之后, 就可以在任何一个组件内像使用 Vue Router
, Vuex
一样使用你定义的库了.
写一个插件
首先, 创建一个文件用于编写自己的插件. 在示例中, 我会将 Axios
作为插件添加到项目中, 因而我给文件起名为 axios.js
. 其次, 插件要对外暴露一个 install
方法, 该方法的第一个参数是 Vue
的构造函数:
// 编写插件
import axios from 'axios'
export default {
install: function (Vue) {
// Do stuff
Object.defineProperty(Vue.prototype, '$http', { value: axios })
},
}
// 注册插件
import AxiosPlugin from './axios.js'
Vue.use(AxiosPlugin)
new Vue({
created() {
console.log(this.$http ? 'Axios works!' : 'Uh oh..')
},
})
彩蛋: 插件的可选参数
插件的 install
方法可以接受可选参数. 一些开发可能不喜欢将 Axios
实例命名为 $http
, 因为这是 Vue Resource
提供的一个通用名字. 因而可以提供一个可选的参数允许他们随意命名:
import axios from 'axios'
export default {
// 默认名称
install: function (Vue, name = '$http') {
Object.defineProperty(Vue.prototype, name, { value: axios })
},
}
import AxiosPlugin from './axios.js'
Vue.use(AxiosPlugin, '$axios')
new Vue({
created() {
console.log(this.$axios ? 'Axios works!' : 'Uh oh..')
},
})
构建和发布
环境变量
所有测试环境或者正式环境变量的配置都在 .env.development
等 .env.xxxx
文件中。
它们都会通过 webpack.DefinePlugin
插件注入到全局。
环境变量必须以VUE_APP_
为开头。如:VUE_APP_API
、VUE_APP_TITLE
分析构建文件体积
如果你的构建文件很大,你可以通过 webpack-bundle-analyzer
命令构建并分析依赖模块的体积分布,从而优化你的代码
npm run preview -- --report
发布
对于发布来讲,只需要将最终生成的静态文件,也就是通常情况下 dist 文件夹的静态文件发布到你的 cdn 或者静态服务器即可,需要注意的是其中的 index.html 通常会是你后台服务的入口页面,在确定了 js 和 css 的静态之后可能需要改变页面的引入路径
部署时可能会发现资源路径不对 ,只需修改 vue.config.js
文件资源路径即可
publicPath: './' //请根据自己路径来配置更改
前端路由与服务端的结合
如果你有对应的后台服务器,那么我们推荐采用 browserHistory
,只需要在服务端做一个映射,比如:
Apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
nginx
location / {
try_files $uri $uri/ /index.html;
}
GIT HOOK
husky
最有效的解决方案就是将 Lint
校验放到本地,常见做法是使用 husky
或者 pre-commit
在本地提交之前先做一次 Lint
校验。
npm install husky -D -S
然后修改 package.json
,增加配置:
"husky": {
"hooks": {
"pre-commit": "eslint --ext .js,.vue src"
}
}
最后尝试 Git 提交,你就会很快收到反馈:
git commit -m "Keep calm and commit"
但这样会有一个问题,就是这次提交,我可能只修改了一个文件,比如我就修改了 foo.js
的内容,但它依然会校验所有 src
下面的.js
文件,非常的不友好。导致的问题就是:每次我提交我写的代码,还先要帮人的代码问题解决了才能顺利提交,而且当项目大了之后,检查速度也会变得越来越慢了。
lint-staged
它只会校验检查你提交或者说你修改的部分内容。
npm install lint-staged -D -S
然后,修改 package.json 配置:
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix",
"git add"
]
}
风格规范
Component
所有的 Component
文件都是以大写开头 (PascalCase
)
JS 文件
所有的.js
文件都遵循横线连接 (kebab-case
)。
Views
在 views
文件下,代表路由的.vue
文件都使用横线连接 (kebab-case
),代表路由的文件夹也是使用同样的规则
@/src/views/svg-icons/index.vue
@/src/views/svg-icons/require-icons.js
路由懒加载
当打包构建应用时,Javascript 包会变得非常大,影响页面加载速度。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。如:
const Foo = () => import('./Foo.vue')
当你觉得你的页面热更新速度慢的时候,才需要往下看
方案
使用 babel
的 plugins
babel-plugin-dynamic-import-node
。它只做一件事就是将所有的 import()
转化为 require()
,这样就可以用这个插件将所有异步组件都用同步的方式引入,并结合 BABEL_ENV
这个 babel
环境变量,让它只作用于开发环境下,在开发环境中将所有 import()
转化为 require()
,这种方案解决了之前重复打包的问题,同时对代码的侵入性也很小,你平时写路由的时候只需要按照官方文档路由懒加载的方式就可以了,其它的都交给 babel
来处理,当你不想用这个方案的时候,也只要将它从 babel
的 plugins
中移除就可以了。
具体代码: 首先在 package.json
中增加 BABEL_ENV
"dev": "cross-env BABEL_ENV=development webpack-dev-server --inline --progress --config build/webpack.dev.conf.js"
接着在.babelrc
只能加入 babel-plugin-dynamic-import-node
这个 plugins
,并让它只有在 development
模式中才生效。
{
"env": {
"development": {
"plugins": ["dynamic-import-node"]
}
}
}
图表
Echarts
// 按需引入 引入 ECharts 主模块
var echarts = require('echarts/lib/echarts')
// 引入柱状图
require('echarts/lib/chart/bar')
// 引入提示框和标题组件
require('echarts/lib/component/tooltip')
require('echarts/lib/component/title')
//全部引入
var echarts = require('echarts')
。因为 ECharts
初始化必须绑定 dom
,所以我们只能在 vue
的 mounted
生命周期里进行初始化。
mounted() {
this.initCharts();
},
methods: {
initCharts() {
this.chart = echarts.init(this.$el);
this.setOptions();
},
setOptions() {
this.chart.setOption({
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"]
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
})
}
}
就这样简单,ECharts
就配置完成了,这时候你想说我的 data
是远程获取的,或者说我动态改变 ECharts
的配置该怎么办呢?我们可以通过 watch
来触发 setOptions
方法
//第一种 watch options变化 利用vue的深度 watcher,options 一有变化就重新setOption
watch: {
options: {
handler(options) {
this.chart.setOption(this.options)
},
deep: true
},
}
//第二种 只watch 数据的变化 只有数据变化时触发ECharts
watch: {
seriesData(val) {
this.setOptions({series:val})
}
}
其实都差不多,还是要结合自己业务来封装。后面就和平时使用 ECharts 没有什么区别了。题外话 ECharts 的可配置项真心多,大家使用的时候可能要花一点时间了解它的 api 的。知乎有个问题:百度还有什么比较良心的产品?答案:ECharts,可见 ECharts 的强大与好用。
图标
推荐单独导出 svg
的引入使用方式。
下载完成之后将下载好的 .svg
文件放入 @/icons/svg
文件夹下之后就会自动导入。
使用方式
<svg-icon icon-class="password" /> // icon-class 为 icon 的名字
改变颜色
svg-icon
默认会读取其父级的 color fill: currentColor
;
你可以改变父级的 color
或者直接改变 fill
的颜色即可。
CDN
使用 CDN 外链的方式引入这些第三方库,这样能大大增加构建的速度(通过 CDN 引入的资源不会经 webpack 打包)。如果你的项目没有自己的 CDN 服务的话,使用一些第三方的 CDN 服务,如 unpkg 等是一个很好的选择,它提供过了免费的资源加速,同时提供了缓存优化,由于你的第三方资源是在 html 中通过 script 引入的,它的缓存更新策略都是你自己手动来控制的,省去了你需要优化缓存策略功夫。
采用这种引入方式,或者使用 webpack dll
的方式进行优化。如果你觉得 CDN 引入对于的项目有益处,你可以遵循如下方法进行修改:
使用方式
先找到
vue.config.js
, 添加externals
让webpack
不打包vue
和element
jsexternals: { vue: 'Vue', 'element-ui':'ELEMENT' }
然后配置那些第三方资源的 CDN,请注意先后顺序。
jsconst cdn = { css: [ // element-ui css 'https://unpkg.com/element-ui/lib/theme-chalk/index.css', ], js: [ // vue must at first! 'https://unpkg.com/vue/dist/vue.js', // element-ui js 'https://unpkg.com/element-ui/lib/index.js', ], }
之后通过
html-webpack-plugin
注入到index.html
之中:jsconfig.plugin('html').tap((args) => { args[0].cdn = cdn return args })
找到
public/index.html
。通过你配置的CND Config
依次注入css
和js
。html<head> <!-- 引入样式 --> <% for(var css of htmlWebpackPlugin.options.cdn.css) { %> <link rel="stylesheet" href="<%=css%>" /> <% } %> </head> <!-- 引入JS --> <% for(var js of htmlWebpackPlugin.options.cdn.js) { %> <script src="<%=js%>"></script> <% } %>
之后还有一个小细节是如果你用了全局对象方式引入
vue
,就不需要 手动Vue.use(Vuex)
,它会自动挂载js// src/store/index.js // 在此文件中可删除这两行 import Vue from 'vue' Vue.use(Vuex)
更换主题
样式覆盖
/* 你的命名空间 */
.article-page {
/* element-ui 元素 */
.el-tag {
margin-right: 0px;
}
}
动态换肤
element-ui 2.0 版本之后所有的样式都是基于 SCSS 编写的,所有的颜色都是基于几个基础颜色变量来设置的,所以就不难实现动态换肤了,只要找到那几个颜色变量修改它就可以了。 首先我们需要拿到通过 package.json 拿到 element-ui 的版本号,根据该版本号去请求相应的样式。拿到样式之后将样色,通过正则匹配和替换,将颜色变量替换成你需要的,之后动态添加 style 标签来覆盖原有的 css 样式。
代码地址:@/src/components/ThemePicker
const version = require('element-ui/package.json').version
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
this.getCSSString(url, chalkHandler, 'chalk')
getCSSString(url, callback, variable) {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
callback()
}
}
xhr.open('GET', url)
xhr.send()
}
使用方式
在项目中引入 ThemePicker
组件即可
import ThemePicker from '@/components/ThemePicker'
- 优点:无需准备多套主题,可以自由动态换肤
- 缺点:自定义不够,只支持基础颜色的切换
国际化
国际化 i18n 方案。通过 vue-i18n 而实现。
全局 lang
同时在 @/lang/index.js 中引入了 element-ui 的语言包
// $t 是 vue-i18n 提供的全局方法,更多信息请查看其文档
$t('login.title')
异步 lang
有一些某些特定页面才需要的 lang,比如 @/views/i18n-demo 页面
import local from './local'
this.$i18n.mergeLocaleMessage('en', local.en)
this.$i18n.mergeLocaleMessage('zh', local.zh)
js 中使用 $t
如果你使用如 select 组件,它的值是通过 v-for 而来,如:
<el-select v-model="value">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
this.options = [
{
value: '1',
label: this.$t('i18nView.one'),
},
{
value: '2',
label: this.$t('i18nView.two'),
},
{
value: '3',
label: this.$t('i18nView.three'),
},
]
这种情况下,国际化只会执行一次,因为在 js 中的 this.options 只会在初始化的时候执行一次,它的数据并不会随着你本地 lang 的变化而变化,所以需要你在 lang 变化的时候手动重设 this.options。
export default {
watch: {
lang() {
this.setOptions()
},
},
methods: {
setOptions() {
this.options = [
{
value: '1',
label: this.$t('i18nView.one'),
},
{
value: '2',
label: this.$t('i18nView.two'),
},
{
value: '3',
label: this.$t('i18nView.three'),
},
]
},
},
}
移除国际化
在 src/main.js
中移除 import i18n from './lang'
并且删除 src/lang
文件夹。
并在 src/layout/components/Levelbar
、src/layout/components/SidebarItem
、src/layout/components/TabsView
等文件夹中 移除 this.\$t('route.xxxx')
使用国际化的方式。
在 v4.1.0+
版本之后,默认 master
将不再提供国际化。因为大部分用户其实是用不到国际化的,但移除国际化工作量又相当的大。
如果你有国际化需求的请使用 i18n 分支,它与 master 同步更新。
错误处理
页面
404
页面级的错误处理由
vue-router
统一处理,所有匹配不到正确路由的页面都会进404
页面。{ path: '*', redirect: '/404' }
401
在
@/permission.js
做了权限控制,所有没有权限进入该路由的用户都会被重定向到401
页面
请求
项目里所有的请求都会走@/utils/request.js
里面创建的的 axios
实例,它统一做了错误处理
service.interceptors.response.use(
(response) => {
/**
* code为非20000是抛错 可结合自己业务进行修改
*/
const res = response.data
if (res.code !== 20000) {
Message({
message: res.data,
type: 'error',
duration: 5 * 1000,
})
// 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm(
'你已被登出,可以取消继续留在该页面,或者重新登录',
'确定登出',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject('error')
} else {
return response.data
}
},
(error) => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000,
})
return Promise.reject(error)
}
)
因为所有请求返回的是 promise,所以你也可以对每一个请求通过 catch 错误,从而进行单独的处理。
getInfo()
.then((res) => {})
.catch((err) => {
xxxx
})
代码
- 代码层面的错误处理,如果你开启了 eslint 在编写代码的时候就会提示错误
- 当然还有很多不能被
eslint
检查出来的错误,vue
也提供了全局错误处理钩子errorHandler
,相对应的错误收集
监听错误:@/errorLog.js
错误展示组件:@/components/ErrorLog
相关问题
浏览器兼容性问题
兼容性需求可使用 babel-polyfill
下载依赖
npm install --save babel-polyfill
在入口文件中引入
import 'babel-polyfill'
或者require('babel-polyfill)
在
webpack.config.js
中加入babel-polyfill
到你的入口数组:jsmodule.exports = { entry: ['babel-polyfill', './app/js'], }
我用了 axios , 为什么 IE 浏览器不识别(IE9+)
IE 不支持 promise
,所有需要单独引入 polyfill
npm install es6-promise
// 在 main.js 首行引入即可
require("es6-promise").polyfill();